index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <template>
  2. <view class="finance-container">
  3. <view class="finance-stats">
  4. <view class="stat-card">
  5. <text class="stat-title">今日营收</text>
  6. <text class="stat-value">¥{{ formatAmount(todayRevenue || 0) }}</text>
  7. <view class="stat-change" :class="getChangeClass(todayRevenueChange)">
  8. <AppIcon :name="todayRevenueChange >= 0 ? 'arrow-up' : 'arrow-down'" :size="22" :color="todayRevenueChange >= 0 ? '#52C41A' : '#F44336'" />
  9. <text>{{ Math.abs(todayRevenueChange) }}%</text>
  10. </view>
  11. </view>
  12. <view class="stat-card">
  13. <text class="stat-title">本月营收</text>
  14. <text class="stat-value">¥{{ formatAmount(monthRevenue || 0) }}</text>
  15. <view class="stat-change" :class="getChangeClass(monthRevenueChange)">
  16. <AppIcon :name="monthRevenueChange >= 0 ? 'arrow-up' : 'arrow-down'" :size="22" :color="monthRevenueChange >= 0 ? '#52C41A' : '#F44336'" />
  17. <text>{{ Math.abs(monthRevenueChange) }}%</text>
  18. </view>
  19. </view>
  20. <view class="stat-card">
  21. <text class="stat-title">待提现金额</text>
  22. <text class="stat-value">¥{{ formatAmount(pendingWithdraw || 0) }}</text>
  23. <text class="stat-footnote">站点待提现</text>
  24. </view>
  25. <view class="stat-card">
  26. <text class="stat-title">累计分账</text>
  27. <text class="stat-value">¥{{ formatAmount(totalSplitAmount || 0) }}</text>
  28. <text class="stat-footnote">平台分账总额</text>
  29. </view>
  30. </view>
  31. <!-- 快捷功能入口 -->
  32. <view class="finance-menu">
  33. <view class="menu-item" @click="navigateTo('/pages/finance/withdraw')">
  34. <AppIcon name="credit-card" :size="40" color="#C6171E" class="menu-icon" />
  35. <text class="menu-title">提现管理</text>
  36. <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
  37. </view>
  38. <view class="menu-item" @click="navigateTo('/pages/finance/refund')">
  39. <AppIcon name="rotate-ccw" :size="40" color="#C6171E" class="menu-icon" />
  40. <text class="menu-title">退款清单</text>
  41. <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
  42. </view>
  43. <view class="menu-item" @click="navigateTo('/pages/finance/settlement')">
  44. <AppIcon name="clipboard" :size="40" color="#C6171E" class="menu-icon" />
  45. <text class="menu-title">结算记录</text>
  46. <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
  47. </view>
  48. <view class="menu-item" @click="navigateTo('/pages/finance/split-record')">
  49. <AppIcon name="trending-up" :size="40" color="#C6171E" class="menu-icon" />
  50. <text class="menu-title">分账记录</text>
  51. <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
  52. </view>
  53. </view>
  54. <view class="finance-tabs">
  55. <view class="segmented-control">
  56. <view
  57. v-for="(option, index) in tabOptions"
  58. :key="index"
  59. class="segment-item"
  60. :class="{ 'active': activeTab === index }"
  61. @click="activeTab = index; handleTabChange(index)"
  62. >
  63. <text>{{ option }}</text>
  64. </view>
  65. </view>
  66. </view>
  67. <view class="finance-content">
  68. <!-- 站点账户列表 -->
  69. <view v-if="activeTab === 0" class="account-list">
  70. <view
  71. v-for="account in stationAccounts"
  72. :key="account.id"
  73. class="account-item"
  74. >
  75. <view class="account-header">
  76. <view class="account-info">
  77. <text class="station-name">{{ account.stationName }}</text>
  78. <text class="station-id">站点ID: {{ account.stationId }}</text>
  79. </view>
  80. <view class="account-balance">
  81. <text class="balance-label">账户余额:</text>
  82. <text class="balance-value">¥{{ formatAmount(account.balance || 0) }}</text>
  83. </view>
  84. </view>
  85. <view class="account-content">
  86. <view class="account-stats">
  87. <view class="stat-column">
  88. <text class="stat-period">今日营收</text>
  89. <text class="stat-amount">¥{{ formatAmount(account.todayRevenue || 0) }}</text>
  90. </view>
  91. <view class="stat-column">
  92. <text class="stat-period">本月营收</text>
  93. <text class="stat-amount">¥{{ formatAmount(account.monthRevenue || 0) }}</text>
  94. </view>
  95. <view class="stat-column">
  96. <text class="stat-period">累计营收</text>
  97. <text class="stat-amount">¥{{ formatAmount(account.totalRevenue || 0) }}</text>
  98. </view>
  99. </view>
  100. <button class="withdraw-btn" @click="navigateToWithdraw(account)">
  101. 提现管理
  102. </button>
  103. </view>
  104. </view>
  105. </view>
  106. <!-- 分账记录列表 -->
  107. <view v-else-if="activeTab === 1" class="split-list">
  108. <view
  109. v-for="record in splitRecords"
  110. :key="record.id"
  111. class="split-item"
  112. >
  113. <view class="split-header">
  114. <view class="split-info">
  115. <text class="split-no">分账编号: {{ record.splitNo }}</text>
  116. <text class="split-time">{{ formatTime(record.createTime) }}</text>
  117. </view>
  118. <view class="split-amount">
  119. <text class="amount-label">分账金额:</text>
  120. <text class="amount-value">¥{{ formatAmount(record.amount || 0) }}</text>
  121. </view>
  122. </view>
  123. <view class="split-content">
  124. <view class="split-detail">
  125. <text class="detail-label">分账类型:</text>
  126. <text class="detail-value">{{ getSplitTypeText(record.splitType) }}</text>
  127. </view>
  128. <view class="split-detail">
  129. <text class="detail-label">站点名称:</text>
  130. <text class="detail-value">{{ record.stationName || '平台' }}</text>
  131. </view>
  132. <view class="split-detail">
  133. <text class="detail-label">分账状态:</text>
  134. <text class="detail-value" :style="getSplitStatusStyle(record.status)">
  135. {{ getSplitStatusText(record.status) }}
  136. </text>
  137. </view>
  138. </view>
  139. </view>
  140. </view>
  141. </view>
  142. <view class="empty-state" v-if="(stationAccounts.length === 0 && activeTab === 0) || (splitRecords.length === 0 && activeTab === 1)">
  143. <view class="empty-icon-wrapper"><AppIcon name="inbox" :size="80" color="#B0B0B0" /></view>
  144. <text>暂无数据</text>
  145. </view>
  146. <!-- 加载指示器 -->
  147. <view class="loading-overlay" v-if="loading">
  148. <view class="loading-spinner"></view>
  149. </view>
  150. </view>
  151. </template>
  152. <script setup>
  153. import { ref, onMounted } from 'vue'
  154. import { getStationAccounts, getSplitRecords } from '../../api/finance.js'
  155. import { getDashboardData } from '../../api/stat.js'
  156. import { formatTime, showToast, formatAmount, storage, fmtDictName, getDictColor } from '../../utils/index.js'
  157. import { loadDicts } from '../../utils/dict.js'
  158. const activeTab = ref(0)
  159. const tabOptions = ['站点账户', '分账记录']
  160. // 真实数据
  161. const todayRevenue = ref(0.00)
  162. const monthRevenue = ref(0.00)
  163. const pendingWithdraw = ref(0.00)
  164. const totalSplitAmount = ref(0.00)
  165. // 环比变化
  166. const todayRevenueChange = ref(0.0)
  167. const monthRevenueChange = ref(0.0)
  168. const loading = ref(false)
  169. // 数据列表
  170. const stationAccounts = ref([])
  171. const splitRecords = ref([])
  172. onMounted(async () => {
  173. await loadDicts()
  174. loadFinanceData()
  175. loadStationAccounts()
  176. loadSplitRecords()
  177. })
  178. // 加载财务概览数据
  179. const loadFinanceData = async () => {
  180. loading.value = true
  181. try {
  182. // 使用仪表盘API获取财务数据
  183. const res = await getDashboardData()
  184. if (res && res.code === 200) {
  185. const data = res.data
  186. todayRevenue.value = data.todayIncome || 0.00
  187. monthRevenue.value = data.monthIncome || 0.00
  188. // 其他财务数据可能需要调用专门的API获取
  189. }
  190. } catch (error) {
  191. showToast('加载财务数据失败')
  192. } finally {
  193. loading.value = false
  194. }
  195. }
  196. const loadStationAccounts = async () => {
  197. loading.value = true
  198. try {
  199. const res = await getStationAccounts({ page: 1, pageSize: 10 })
  200. if (res && res.code === 200) {
  201. const data = res.data
  202. // 适配不同的数据结构
  203. stationAccounts.value = data.records || data.list || data
  204. }
  205. } catch (error) {
  206. showToast('获取站点账户失败')
  207. } finally {
  208. loading.value = false
  209. }
  210. }
  211. const loadSplitRecords = async () => {
  212. loading.value = true
  213. try {
  214. const res = await getSplitRecords({ page: 1, pageSize: 10 })
  215. if (res && res.code === 200) {
  216. const data = res.data
  217. // 适配不同的数据结构
  218. splitRecords.value = data.records || data.list || data
  219. }
  220. } catch (error) {
  221. showToast('获取分账记录失败')
  222. } finally {
  223. loading.value = false
  224. }
  225. }
  226. const handleTabChange = (index) => {
  227. activeTab.value = index
  228. }
  229. const navigateToWithdraw = (account) => {
  230. uni.navigateTo({
  231. url: `/pages/finance/withdraw?stationId=${account.stationId}&stationName=${account.stationName}`
  232. })
  233. }
  234. const navigateTo = (url) => {
  235. uni.navigateTo({ url })
  236. }
  237. const getChangeClass = (change) => {
  238. return change >= 0 ? 'change-up' : 'change-down'
  239. }
  240. const getSplitTypeText = (type) => {
  241. return fmtDictName('SplitRecord.type', type)
  242. }
  243. // 从字典获取分账状态文本
  244. const getSplitStatusText = (status) => {
  245. return fmtDictName('SplitRecord.status', status)
  246. }
  247. // 从字典获取分账状态内联样式
  248. const getSplitStatusStyle = (status) => {
  249. const color = getDictColor('SplitRecord.status', status)
  250. if (color) {
  251. return {
  252. color: color,
  253. backgroundColor: `${color}1A`
  254. }
  255. }
  256. return {}
  257. }
  258. </script>
  259. <style scoped>
  260. .finance-container {
  261. padding: 30rpx;
  262. background-color: #F5F7FA;
  263. min-height: 100vh;
  264. box-sizing: border-box;
  265. padding-bottom: 120rpx;
  266. }
  267. /* 财务概览卡片 */
  268. .finance-stats {
  269. display: grid;
  270. grid-template-columns: repeat(2, 1fr);
  271. gap: 20rpx;
  272. margin-bottom: 30rpx;
  273. }
  274. .stat-card {
  275. background-color: #FFFFFF;
  276. padding: 30rpx;
  277. border-radius: 24rpx;
  278. text-align: center;
  279. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  280. transition: all 0.3s;
  281. }
  282. .stat-card:active {
  283. transform: translateY(-4rpx);
  284. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
  285. }
  286. .stat-title {
  287. font-size: 24rpx;
  288. color: #999999;
  289. margin-bottom: 16rpx;
  290. display: block;
  291. }
  292. .stat-value {
  293. font-size: 40rpx;
  294. font-weight: 700;
  295. color: #1A1A1A;
  296. margin-bottom: 12rpx;
  297. display: block;
  298. }
  299. .stat-change {
  300. font-size: 24rpx;
  301. font-weight: 600;
  302. display: flex;
  303. align-items: center;
  304. justify-content: center;
  305. gap: 4rpx;
  306. }
  307. .change-up {
  308. color: #52C41A;
  309. }
  310. .change-down {
  311. color: #F44336;
  312. }
  313. .stat-footnote {
  314. font-size: 22rpx;
  315. color: #B0B0B0;
  316. display: block;
  317. }
  318. /* 快捷功能入口 */
  319. .finance-menu {
  320. background-color: #FFFFFF;
  321. border-radius: 24rpx;
  322. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  323. margin-bottom: 30rpx;
  324. overflow: hidden;
  325. }
  326. .menu-item {
  327. display: flex;
  328. align-items: center;
  329. padding: 28rpx 30rpx;
  330. border-bottom: 1rpx solid #F0F0F0;
  331. }
  332. .menu-item:last-child {
  333. border-bottom: none;
  334. }
  335. .menu-item:active {
  336. background-color: #F5F7FA;
  337. }
  338. .menu-icon {
  339. margin-right: 20rpx;
  340. }
  341. .menu-title {
  342. flex: 1;
  343. font-size: 28rpx;
  344. color: #1A1A1A;
  345. font-weight: 500;
  346. }
  347. /* 切换标签 */
  348. .finance-tabs {
  349. background-color: #FFFFFF;
  350. padding: 20rpx;
  351. border-radius: 24rpx;
  352. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  353. margin-bottom: 30rpx;
  354. }
  355. .segmented-control {
  356. display: flex;
  357. width: 100%;
  358. border-radius: 16rpx;
  359. overflow: hidden;
  360. background-color: #F5F5F5;
  361. padding: 6rpx;
  362. gap: 6rpx;
  363. }
  364. .segment-item {
  365. flex: 1;
  366. text-align: center;
  367. padding: 16rpx 0;
  368. font-size: 26rpx;
  369. color: #666666;
  370. transition: all 0.3s;
  371. border-radius: 12rpx;
  372. font-weight: 500;
  373. }
  374. .segment-item.active {
  375. color: #FFFFFF;
  376. font-weight: 600;
  377. background: #C6171E;
  378. box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.3);
  379. }
  380. /* 内容区域 */
  381. .finance-content {
  382. background-color: #FFFFFF;
  383. border-radius: 24rpx;
  384. padding: 30rpx;
  385. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  386. }
  387. /* 空状态 */
  388. .empty-state {
  389. display: flex;
  390. flex-direction: column;
  391. align-items: center;
  392. justify-content: center;
  393. padding: 120rpx 0;
  394. color: #999999;
  395. text-align: center;
  396. }
  397. .empty-state text {
  398. margin-top: 16rpx;
  399. font-size: 28rpx;
  400. display: block;
  401. }
  402. /* 加载状态 */
  403. .loading-overlay {
  404. position: fixed;
  405. top: 0;
  406. left: 0;
  407. right: 0;
  408. bottom: 0;
  409. background-color: rgba(0, 0, 0, 0.4);
  410. display: flex;
  411. align-items: center;
  412. justify-content: center;
  413. z-index: 999;
  414. }
  415. .loading-spinner {
  416. width: 60rpx;
  417. height: 60rpx;
  418. border: 4rpx solid rgba(255, 255, 255, 0.3);
  419. border-top-color: #FFFFFF;
  420. border-radius: 50%;
  421. animation: spin 0.8s linear infinite;
  422. }
  423. /* ===== 站点账户列表 ===== */
  424. .account-list {
  425. max-height: 1000rpx;
  426. overflow-y: auto;
  427. }
  428. .account-item {
  429. padding: 30rpx 0;
  430. border-bottom: 2rpx solid #F0F0F0;
  431. }
  432. .account-item:last-child {
  433. border-bottom: none;
  434. }
  435. .account-header {
  436. display: flex;
  437. justify-content: space-between;
  438. align-items: flex-start;
  439. margin-bottom: 24rpx;
  440. }
  441. .account-info {
  442. flex: 1;
  443. }
  444. .station-name {
  445. font-size: 32rpx;
  446. font-weight: 600;
  447. color: #1A1A1A;
  448. margin-bottom: 12rpx;
  449. display: block;
  450. }
  451. .station-id {
  452. font-size: 24rpx;
  453. color: #999999;
  454. display: block;
  455. }
  456. .account-balance {
  457. text-align: right;
  458. }
  459. .balance-label {
  460. font-size: 24rpx;
  461. color: #999999;
  462. margin-bottom: 12rpx;
  463. display: block;
  464. }
  465. .balance-value {
  466. font-size: 40rpx;
  467. font-weight: 700;
  468. color: #52C41A;
  469. display: block;
  470. }
  471. .account-content {
  472. display: flex;
  473. align-items: center;
  474. gap: 20rpx;
  475. }
  476. .account-stats {
  477. flex: 1;
  478. display: grid;
  479. grid-template-columns: repeat(3, 1fr);
  480. gap: 20rpx;
  481. padding: 20rpx;
  482. background: rgba(0, 0, 0, 0.02);
  483. border-radius: 12rpx;
  484. }
  485. .stat-column {
  486. text-align: center;
  487. display: flex;
  488. flex-direction: column;
  489. align-items: center;
  490. gap: 8rpx;
  491. }
  492. .stat-period {
  493. font-size: 22rpx;
  494. color: #999999;
  495. display: block;
  496. }
  497. .stat-amount {
  498. font-size: 28rpx;
  499. font-weight: 600;
  500. color: #1A1A1A;
  501. display: block;
  502. }
  503. .withdraw-btn {
  504. padding: 20rpx 32rpx;
  505. background: #C6171E;
  506. color: #FFFFFF;
  507. border: none;
  508. border-radius: 16rpx;
  509. font-size: 26rpx;
  510. font-weight: 500;
  511. white-space: nowrap;
  512. transition: all 0.3s;
  513. }
  514. .withdraw-btn:active {
  515. transform: scale(0.95);
  516. opacity: 0.9;
  517. }
  518. /* ===== 分账记录列表 ===== */
  519. .split-list {
  520. max-height: 1000rpx;
  521. overflow-y: auto;
  522. }
  523. .split-item {
  524. padding: 30rpx 0;
  525. border-bottom: 2rpx solid #F0F0F0;
  526. }
  527. .split-item:last-child {
  528. border-bottom: none;
  529. }
  530. .split-header {
  531. display: flex;
  532. justify-content: space-between;
  533. align-items: flex-start;
  534. margin-bottom: 24rpx;
  535. }
  536. .split-info {
  537. flex: 1;
  538. }
  539. .split-no {
  540. font-size: 28rpx;
  541. font-weight: 600;
  542. color: #1A1A1A;
  543. margin-bottom: 12rpx;
  544. display: block;
  545. }
  546. .split-time {
  547. font-size: 24rpx;
  548. color: #999999;
  549. }
  550. .split-amount {
  551. text-align: right;
  552. }
  553. .amount-label {
  554. font-size: 24rpx;
  555. color: #999999;
  556. margin-bottom: 12rpx;
  557. display: block;
  558. }
  559. .amount-value {
  560. font-size: 40rpx;
  561. font-weight: 700;
  562. color: #C6171E;
  563. }
  564. .split-content {
  565. display: flex;
  566. flex-direction: column;
  567. gap: 12rpx;
  568. padding-top: 16rpx;
  569. }
  570. .split-detail {
  571. display: flex;
  572. align-items: center;
  573. font-size: 26rpx;
  574. }
  575. .detail-label {
  576. color: #999999;
  577. width: 160rpx;
  578. flex-shrink: 0;
  579. }
  580. .detail-value {
  581. color: #1A1A1A;
  582. font-weight: 500;
  583. }
  584. </style>