withdraw.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. <template>
  2. <view class="withdraw-container">
  3. <!-- 账户概览 -->
  4. <view class="section">
  5. <view class="section-title">账户信息</view>
  6. <view class="info-cards">
  7. <view class="info-card">
  8. <text class="info-label">可提现余额</text>
  9. <text class="info-value accent-blue">¥{{ formatAmount(availableBalance || 0) }}</text>
  10. </view>
  11. <view class="info-card">
  12. <text class="info-label">待审核金额</text>
  13. <text class="info-value accent-warning">¥{{ formatAmount(pendingAmount || 0) }}</text>
  14. </view>
  15. <view class="info-card">
  16. <text class="info-label">累计提现</text>
  17. <text class="info-value accent-success">¥{{ formatAmount(totalWithdrawn || 0) }}</text>
  18. </view>
  19. </view>
  20. </view>
  21. <!-- 申请提现表单 -->
  22. <view class="section">
  23. <view class="section-title">申请提现</view>
  24. <view class="apply-form">
  25. <view class="form-item">
  26. <text class="form-label">提现金额</text>
  27. <view class="form-input">
  28. <input
  29. type="number"
  30. v-model="withdrawForm.amount"
  31. placeholder="请输入提现金额"
  32. step="0.01"
  33. min="0.01"
  34. :max="formatAmount(availableBalance, true)"
  35. />
  36. <text class="balance-tip">最小提现金额:0.01元</text>
  37. </view>
  38. </view>
  39. <view class="form-item">
  40. <text class="form-label">提现账户</text>
  41. <view class="form-input">
  42. <input
  43. type="text"
  44. v-model="withdrawForm.account"
  45. placeholder="请输入提现账户"
  46. />
  47. </view>
  48. </view>
  49. <view class="form-item">
  50. <text class="form-label">备注(选填)</text>
  51. <view class="form-input">
  52. <input
  53. type="text"
  54. v-model="withdrawForm.remark"
  55. placeholder="请输入备注信息"
  56. maxlength="50"
  57. />
  58. </view>
  59. </view>
  60. <button class="apply-btn" @click="handleApplyWithdraw" :disabled="!canApply">
  61. 提交提现申请
  62. </button>
  63. </view>
  64. </view>
  65. <!-- 提现记录 -->
  66. <view class="section">
  67. <view class="section-title">提现记录</view>
  68. <view class="records-filter">
  69. <view class="segmented-control">
  70. <view
  71. v-for="(option, index) in filterOptions"
  72. :key="index"
  73. class="segment-item"
  74. :class="{ 'active': activeFilter === index }"
  75. @click="activeFilter = index; handleFilterChange(index)"
  76. >
  77. <text>{{ option.label }}</text>
  78. </view>
  79. </view>
  80. </view>
  81. <view class="records-list">
  82. <view
  83. v-for="record in withdrawRecords"
  84. :key="record.id"
  85. class="record-item"
  86. >
  87. <view class="record-header">
  88. <view class="record-info">
  89. <text class="record-no">提现编号: {{ record.withdrawNo }}</text>
  90. <text class="record-time">{{ formatTime(record.createTime) }}</text>
  91. </view>
  92. <text class="record-status" :style="getStatusStyle(record.status)">
  93. {{ getStatusText(record.status) }}
  94. </text>
  95. </view>
  96. <view class="record-content">
  97. <view class="record-amount">
  98. <text class="amount-label">提现金额:</text>
  99. <text class="amount-value">¥{{ formatAmount(record.amount || 0) }}</text>
  100. </view>
  101. <view class="record-account">
  102. <text class="account-label">提现账户:</text>
  103. <text class="account-value">{{ record.account || '未填写' }}</text>
  104. </view>
  105. </view>
  106. <view class="record-footer" v-if="record.remark">
  107. <view class="record-remark">
  108. <text class="remark-label">备注:</text>
  109. <text class="remark-value">{{ record.remark }}</text>
  110. </view>
  111. </view>
  112. <view class="record-actions" v-if="isPendingStatus(record.status)">
  113. <button class="approve-btn" @click="handleApprove(record.id)">
  114. 审核通过
  115. </button>
  116. <button class="reject-btn" @click="handleReject(record.id)">
  117. 拒绝
  118. </button>
  119. </view>
  120. </view>
  121. </view>
  122. <view class="empty-state" v-if="withdrawRecords.length === 0">
  123. <view class="empty-icon-wrapper"><AppIcon name="inbox" :size="80" color="#B0B0B0" /></view>
  124. <text>暂无提现记录</text>
  125. </view>
  126. </view>
  127. </view>
  128. </template>
  129. <script setup>
  130. import { ref, computed, onMounted } from 'vue'
  131. import { onLoad } from '@dcloudio/uni-app'
  132. import { getWithdrawnRecords, reviewWithdrawn, applyWithdrawn, getStationAccounts } from '../../api/finance.js'
  133. import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
  134. import dictUtil, { loadDicts } from '../../utils/dict.js'
  135. const stationId = ref('')
  136. const stationName = ref('')
  137. // 真实数据
  138. const availableBalance = ref(0.00)
  139. const pendingAmount = ref(0.00)
  140. const totalWithdrawn = ref(0.00)
  141. const loading = ref(false)
  142. const page = ref(1)
  143. const withdrawForm = ref({
  144. amount: '',
  145. account: '',
  146. remark: ''
  147. })
  148. const activeFilter = ref(0)
  149. const filterOptions = computed(() => dictUtil.getDictFilterOptions('WithdrawnRecord.status'))
  150. const withdrawRecords = ref([])
  151. onLoad(async (options) => {
  152. await loadDicts()
  153. stationId.value = options.stationId || ''
  154. stationName.value = options.stationName || '站点'
  155. loadAccountData()
  156. loadWithdrawRecords()
  157. })
  158. // 加载账户数据
  159. const loadAccountData = async () => {
  160. loading.value = true
  161. try {
  162. // 获取站点账户详情
  163. const res = await getStationAccounts({ stationId: stationId.value, page: 1, pageSize: 1 })
  164. if (res && res.code === 200 && res.data) {
  165. const records = res.data.records || res.data.list || (Array.isArray(res.data) ? res.data : [])
  166. if (records.length > 0) {
  167. const account = records[0]
  168. availableBalance.value = account.balance || 0
  169. } else {
  170. availableBalance.value = 0
  171. }
  172. } else {
  173. availableBalance.value = 0
  174. }
  175. // 获取提现统计数据
  176. const withdrawRes = await getWithdrawnRecords({ stationId: stationId.value })
  177. if (withdrawRes && withdrawRes.code === 200 && withdrawRes.data) {
  178. const records = withdrawRes.data.records || withdrawRes.data.list || (Array.isArray(withdrawRes.data) ? withdrawRes.data : [])
  179. if (Array.isArray(records)) {
  180. pendingAmount.value = records
  181. .filter(record => isPendingStatus(record.status))
  182. .reduce((sum, record) => sum + (record.amount || 0), 0)
  183. totalWithdrawn.value = records
  184. .filter(record => isApprovedStatus(record.status))
  185. .reduce((sum, record) => sum + (record.amount || 0), 0)
  186. } else {
  187. pendingAmount.value = 0
  188. totalWithdrawn.value = 0
  189. }
  190. } else {
  191. pendingAmount.value = 0
  192. totalWithdrawn.value = 0
  193. }
  194. } catch (error) {
  195. showToast('加载账户数据失败')
  196. availableBalance.value = 0
  197. pendingAmount.value = 0
  198. totalWithdrawn.value = 0
  199. } finally {
  200. loading.value = false
  201. }
  202. }
  203. const canApply = computed(() => {
  204. const amountYuan = parseFloat(withdrawForm.value.amount)
  205. if (isNaN(amountYuan)) return false
  206. // 将用户输入的元转换为分,与 availableBalance(分)进行比较
  207. const amountCent = amountYuan * 100
  208. return amountYuan > 0 && amountCent <= availableBalance.value && withdrawForm.value.account
  209. })
  210. const loadWithdrawRecords = async (isLoadMore = false) => {
  211. loading.value = true
  212. try {
  213. const params = {
  214. stationId: stationId.value,
  215. status: activeFilter.value === 0 ? '' : getStatusValue(activeFilter.value),
  216. page: isLoadMore ? page.value + 1 : 1,
  217. pageSize: 10
  218. }
  219. const res = await getWithdrawnRecords(params)
  220. if (res && res.code === 200) {
  221. const records = res.data.records || res.data.list || res.data
  222. if (isLoadMore) {
  223. withdrawRecords.value = [...withdrawRecords.value, ...records]
  224. } else {
  225. withdrawRecords.value = records
  226. }
  227. page.value = params.page
  228. }
  229. } catch (error) {
  230. showToast('获取提现记录失败')
  231. } finally {
  232. loading.value = false
  233. }
  234. }
  235. const handleApplyWithdraw = async () => {
  236. try {
  237. await applyWithdrawn({
  238. stationId: stationId.value,
  239. amount: parseFloat(withdrawForm.value.amount) * 100, // 将元转换为分
  240. account: withdrawForm.value.account,
  241. remark: withdrawForm.value.remark
  242. })
  243. showToast('提现申请已提交,等待审核', 'success')
  244. // 重置表单
  245. withdrawForm.value = {
  246. amount: '',
  247. account: '',
  248. remark: ''
  249. }
  250. // 刷新数据
  251. loadAccountData()
  252. loadWithdrawRecords()
  253. } catch (error) {
  254. showToast('提交提现申请失败')
  255. }
  256. }
  257. const handleFilterChange = (index) => {
  258. activeFilter.value = index
  259. loadWithdrawRecords()
  260. }
  261. const handleApprove = async (recordId) => {
  262. try {
  263. await reviewWithdrawn({ id: recordId, status: 1 })
  264. showToast('审核通过', 'success')
  265. loadWithdrawRecords()
  266. } catch (error) {
  267. showToast('审核失败')
  268. }
  269. }
  270. const handleReject = async (recordId) => {
  271. try {
  272. await reviewWithdrawn({ id: recordId, status: 2 })
  273. showToast('审核失败', 'success')
  274. loadWithdrawRecords()
  275. } catch (error) {
  276. showToast('拒绝失败')
  277. }
  278. }
  279. // 从字典获取提现状态文本
  280. const getStatusText = (status) => {
  281. return fmtDictName('WithdrawnRecord.status', status)
  282. }
  283. // 从字典获取提现状态内联样式
  284. const getStatusStyle = (status) => {
  285. const color = getDictColor('WithdrawnRecord.status', status)
  286. if (color) {
  287. return {
  288. color: color,
  289. backgroundColor: `${color}1A`
  290. }
  291. }
  292. return {}
  293. }
  294. // 从字典获取提现状态值(用于过滤)
  295. const getStatusValue = (filterIndex) => {
  296. if (filterIndex === 0) return ''
  297. const options = dictUtil.getDictOptions('WithdrawnRecord.status')
  298. const idx = filterIndex - 1
  299. return options[idx] ? options[idx].value : ''
  300. }
  301. const isPendingStatus = (status) => {
  302. return status == dictUtil.getDictValue('WithdrawnRecord.status', '待审核')
  303. }
  304. const isApprovedStatus = (status) => {
  305. return status == dictUtil.getDictValue('WithdrawnRecord.status', '已通过')
  306. }
  307. </script>
  308. <style scoped>
  309. /* 全局容器 */
  310. .withdraw-container {
  311. min-height: 100vh;
  312. background: #F5F7FA;
  313. padding-bottom: 60rpx;
  314. }
  315. /* 区域样式 */
  316. .section {
  317. padding: 32rpx 30rpx 20rpx;
  318. }
  319. .section-title {
  320. font-size: 30rpx;
  321. font-weight: 600;
  322. color: #1A1A1A;
  323. margin-bottom: 20rpx;
  324. padding-left: 24rpx;
  325. position: relative;
  326. }
  327. .section-title::before {
  328. content: '';
  329. position: absolute;
  330. left: 0;
  331. top: 50%;
  332. transform: translateY(-50%);
  333. width: 16rpx;
  334. height: 16rpx;
  335. background: #C6171E;
  336. border-radius: 50%;
  337. }
  338. /* 信息卡片 */
  339. .info-cards {
  340. display: grid;
  341. grid-template-columns: repeat(3, 1fr);
  342. gap: 20rpx;
  343. }
  344. .info-card {
  345. background: #FFFFFF;
  346. padding: 24rpx 16rpx;
  347. border-radius: 24rpx;
  348. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  349. text-align: center;
  350. }
  351. .info-label {
  352. font-size: 22rpx;
  353. font-weight: 500;
  354. color: #999999;
  355. margin-bottom: 8rpx;
  356. display: block;
  357. }
  358. .info-value {
  359. font-size: 36rpx;
  360. font-weight: 700;
  361. color: #1A1A1A;
  362. display: block;
  363. }
  364. .info-value.accent-blue {
  365. color: #2196F3;
  366. }
  367. .info-value.accent-warning {
  368. color: #FF9800;
  369. }
  370. .info-value.accent-success {
  371. color: #52C41A;
  372. }
  373. /* 申请提现表单 */
  374. .apply-form {
  375. background: #FFFFFF;
  376. padding: 20rpx;
  377. border-radius: 24rpx;
  378. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  379. }
  380. .form-item {
  381. display: flex;
  382. flex-direction: column;
  383. gap: 8rpx;
  384. margin-bottom: 16rpx;
  385. }
  386. .form-item:last-child {
  387. margin-bottom: 0;
  388. }
  389. .form-label {
  390. font-size: 28rpx;
  391. color: #666666;
  392. font-weight: 500;
  393. }
  394. .form-input input,
  395. .form-input textarea {
  396. width: 100%;
  397. padding: 20rpx 28rpx;
  398. border: none;
  399. border-radius: 16rpx;
  400. font-size: 28rpx;
  401. color: #1A1A1A;
  402. background-color: #F5F5F5;
  403. box-sizing: border-box;
  404. transition: all 0.25s ease;
  405. line-height: 1.5;
  406. min-height: 80rpx;
  407. }
  408. .form-input input:focus,
  409. .form-input textarea:focus {
  410. outline: none;
  411. box-shadow: 0 0 0 6rpx rgba(198, 23, 30, 0.2);
  412. }
  413. .balance-tip {
  414. font-size: 22rpx;
  415. color: #999999;
  416. margin-top: 8rpx;
  417. display: block;
  418. text-align: center;
  419. }
  420. .apply-btn {
  421. width: 100%;
  422. padding: 20rpx;
  423. background: #C6171E;
  424. color: white;
  425. border: none;
  426. border-radius: 44rpx;
  427. font-size: 28rpx;
  428. font-weight: 600;
  429. transition: all 0.25s ease;
  430. box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.3);
  431. margin-top: 12rpx;
  432. }
  433. .apply-btn:active:not(:disabled) {
  434. transform: scale(0.97);
  435. box-shadow: 0 2rpx 10rpx rgba(198, 23, 30, 0.25);
  436. }
  437. .apply-btn:disabled {
  438. background: #CCCCCC;
  439. opacity: 0.5;
  440. box-shadow: none;
  441. }
  442. /* 记录过滤 */
  443. .records-filter {
  444. margin-bottom: 20rpx;
  445. }
  446. .segmented-control {
  447. display: flex;
  448. background: #F5F5F5;
  449. border-radius: 16rpx;
  450. padding: 6rpx;
  451. }
  452. .segment-item {
  453. flex: 1;
  454. text-align: center;
  455. padding: 18rpx 0;
  456. font-size: 26rpx;
  457. color: #666666;
  458. border-radius: 12rpx;
  459. transition: all 0.25s ease;
  460. font-weight: 400;
  461. }
  462. .segment-item.active {
  463. color: #FFFFFF;
  464. background: #C6171E;
  465. font-weight: 500;
  466. box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.3);
  467. }
  468. /* 记录列表 */
  469. .records-list {
  470. display: flex;
  471. flex-direction: column;
  472. gap: 16rpx;
  473. }
  474. .record-item {
  475. padding: 24rpx;
  476. border-radius: 24rpx;
  477. background-color: #FFFFFF;
  478. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  479. transition: all 0.3s;
  480. }
  481. .record-header {
  482. display: flex;
  483. justify-content: space-between;
  484. align-items: flex-start;
  485. margin-bottom: 12rpx;
  486. }
  487. .record-info {
  488. flex: 1;
  489. }
  490. .record-no {
  491. font-size: 28rpx;
  492. font-weight: 600;
  493. color: #1A1A1A;
  494. margin-bottom: 6rpx;
  495. display: block;
  496. }
  497. .record-time {
  498. font-size: 24rpx;
  499. color: #999999;
  500. }
  501. .record-status {
  502. font-size: 22rpx;
  503. font-weight: 500;
  504. padding: 6rpx 24rpx;
  505. border-radius: 100rpx;
  506. }
  507. .record-content {
  508. margin-bottom: 16rpx;
  509. }
  510. .record-amount,
  511. .record-account,
  512. .record-remark {
  513. display: flex;
  514. align-items: center;
  515. gap: 12rpx;
  516. margin-bottom: 12rpx;
  517. }
  518. .record-amount:last-child,
  519. .record-account:last-child,
  520. .record-remark:last-child {
  521. margin-bottom: 0;
  522. }
  523. .amount-label,
  524. .account-label,
  525. .remark-label {
  526. font-size: 26rpx;
  527. color: #666666;
  528. width: 120rpx;
  529. }
  530. .amount-value {
  531. font-size: 32rpx;
  532. font-weight: 600;
  533. color: #52C41A;
  534. }
  535. .account-value,
  536. .remark-value {
  537. font-size: 26rpx;
  538. color: #1A1A1A;
  539. flex: 1;
  540. }
  541. .record-footer {
  542. margin-top: 16rpx;
  543. padding-top: 16rpx;
  544. border-top: 1rpx solid #F0F0F0;
  545. }
  546. .record-actions {
  547. display: flex;
  548. gap: 16rpx;
  549. margin-top: 16rpx;
  550. padding-top: 16rpx;
  551. border-top: 1rpx solid #F0F0F0;
  552. }
  553. .approve-btn {
  554. flex: 1;
  555. padding: 18rpx;
  556. background: #52C41A;
  557. color: white;
  558. border: none;
  559. border-radius: 16rpx;
  560. font-size: 26rpx;
  561. cursor: pointer;
  562. transition: all 0.3s ease;
  563. font-weight: 500;
  564. box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.25);
  565. }
  566. .approve-btn:active {
  567. transform: scale(0.98);
  568. box-shadow: 0 2rpx 6rpx rgba(82, 196, 26, 0.3);
  569. }
  570. .reject-btn {
  571. flex: 1;
  572. padding: 18rpx;
  573. background: #C6171E;
  574. color: white;
  575. border: none;
  576. border-radius: 16rpx;
  577. font-size: 26rpx;
  578. cursor: pointer;
  579. transition: all 0.3s ease;
  580. font-weight: 500;
  581. box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.25);
  582. }
  583. .reject-btn:active {
  584. transform: scale(0.98);
  585. box-shadow: 0 2rpx 6rpx rgba(198, 23, 30, 0.35);
  586. }
  587. /* 空状态 */
  588. .empty-state {
  589. display: flex;
  590. flex-direction: column;
  591. align-items: center;
  592. justify-content: center;
  593. padding: 80rpx 0;
  594. color: #999999;
  595. }
  596. .empty-state text {
  597. font-size: 28rpx;
  598. color: #666666;
  599. margin-top: 20rpx;
  600. }
  601. </style>