TransferList.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. <script setup lang="ts">
  2. import { ref, onMounted, nextTick } from 'vue'
  3. import { ElMessage, ElMessageBox } from 'element-plus'
  4. import request from '@/utils/request'
  5. import { userApi } from '@/api/user'
  6. import { Refresh, ArrowRight } from '@element-plus/icons-vue'
  7. import { imgUrl } from '@/utils/request'
  8. // 定义微信转账记录类型
  9. interface TransferRecord {
  10. id: number
  11. user_id: number
  12. mch_id: string
  13. out_bill_no: string
  14. transfer_bill_no: string
  15. appid: string
  16. state: 'APPLYING' | 'ACCEPTED' | 'PROCESSING' | 'WAIT_USER_CONFIRM' | 'TRANSFERING' | 'SUCCESS' | 'FAIL' | 'CANCELING' | 'CANCELLED'
  17. state_text: string
  18. transfer_amount: number
  19. transfer_remark: string
  20. fail_reason: string | null
  21. openid: string | null
  22. user_name: string | null
  23. create_time: string
  24. update_time: string
  25. fee: number
  26. user?: {
  27. nickname?: string
  28. username?: string
  29. mobile?: string
  30. avatar?: string
  31. }
  32. }
  33. // 定义余额记录类型
  34. interface BalanceRecord {
  35. id: number
  36. user_id: number
  37. type: number
  38. type_text: string
  39. amount: number
  40. balance: number
  41. description: string
  42. create_time: string
  43. }
  44. // 定义统计数据类型
  45. interface StatisticsData {
  46. state: string
  47. count: number
  48. amount: number
  49. }
  50. // 响应式数据
  51. const transferList = ref<TransferRecord[]>([])
  52. const statisticsData = ref<StatisticsData[]>([])
  53. const loading = ref(false)
  54. const pagination = ref({
  55. page: 1,
  56. limit: 10,
  57. total: 0
  58. })
  59. const tableRef = ref()
  60. const showScrollHint = ref(false)
  61. const balanceRecordDialogVisible = ref(false)
  62. const balanceRecordLoading = ref(false)
  63. const balanceRecords = ref<BalanceRecord[]>([])
  64. const balanceRecordPagination = ref({
  65. page: 1,
  66. limit: 10,
  67. total: 0
  68. })
  69. const currentUserId = ref<number|null>(null)
  70. // 搜索表单
  71. const searchForm = ref({
  72. state: '',
  73. out_bill_no: ''
  74. })
  75. // 组件挂载时获取数据
  76. onMounted(() => {
  77. fetchTransfers()
  78. nextTick(() => {
  79. checkTableScroll()
  80. })
  81. })
  82. // 检查表格是否需要水平滚动
  83. const checkTableScroll = () => {
  84. nextTick(() => {
  85. const tableElement = tableRef.value?.$el
  86. if (tableElement) {
  87. const tableBodyWrapper = tableElement.querySelector('.el-table__body-wrapper')
  88. if (tableBodyWrapper) {
  89. showScrollHint.value = tableBodyWrapper.scrollWidth > tableBodyWrapper.clientWidth
  90. }
  91. }
  92. })
  93. }
  94. // 处理表格滚动事件
  95. const handleTableScroll = (event: Event) => {
  96. const target = event.target as HTMLElement
  97. // 可以在这里添加滚动相关的处理逻辑
  98. }
  99. // 获取转账记录列表
  100. const fetchTransfers = async () => {
  101. loading.value = true
  102. try {
  103. const response = await request({
  104. url: '/wechatpay/transfer_bill',
  105. method: 'get',
  106. params: {
  107. page: pagination.value.page,
  108. limit: pagination.value.limit,
  109. state: searchForm.value.state || undefined,
  110. out_bill_no: searchForm.value.out_bill_no || undefined
  111. }
  112. })
  113. if (response.code === 200) {
  114. transferList.value = response.page.data || []
  115. pagination.value.total = response.page.total || 0
  116. // 更新统计数据
  117. statisticsData.value = response.data || []
  118. // 检查是否需要显示滚动提示
  119. nextTick(() => {
  120. checkTableScroll()
  121. })
  122. } else {
  123. ElMessage.error(response.msg || '获取微信转账记录失败')
  124. }
  125. } catch (error) {
  126. ElMessage.error('获取微信转账记录失败')
  127. console.error('获取微信转账记录失败:', error)
  128. } finally {
  129. loading.value = false
  130. }
  131. }
  132. // 重置搜索
  133. const resetSearch = () => {
  134. searchForm.value.state = ''
  135. searchForm.value.out_bill_no = ''
  136. pagination.value.page = 1
  137. fetchTransfers()
  138. }
  139. // 搜索
  140. const handleSearch = () => {
  141. pagination.value.page = 1
  142. fetchTransfers()
  143. }
  144. // 格式化金额(从分转换为元)
  145. const formatAmount = (amount: number) => {
  146. return (amount / 100).toFixed(2)
  147. }
  148. // 格式化余额金额(直接保留两位小数)
  149. const formatBalanceAmount = (amount: number) => {
  150. return parseFloat(amount.toString()).toFixed(2)
  151. }
  152. // 状态标签类型
  153. const getStateType = (state: string) => {
  154. switch (state) {
  155. case 'SUCCESS':
  156. return 'success'
  157. case 'FAIL':
  158. return 'danger'
  159. case 'PROCESSING':
  160. case 'TRANSFERING':
  161. return 'warning'
  162. case 'APPLYING':
  163. case 'ACCEPTED':
  164. case 'WAIT_USER_CONFIRM':
  165. return 'info'
  166. default:
  167. return 'info'
  168. }
  169. }
  170. // 处理分页变化
  171. const handlePageChange = (page: number) => {
  172. pagination.value.page = page
  173. fetchTransfers()
  174. }
  175. // 处理页面大小变化
  176. const handleSizeChange = (size: number) => {
  177. pagination.value.limit = size
  178. pagination.value.page = 1
  179. fetchTransfers()
  180. }
  181. // 同意转账申请
  182. const approveTransfer = async (id: number) => {
  183. try {
  184. await ElMessageBox.confirm('确定要同意此转账申请吗?', '确认操作', {
  185. confirmButtonText: '确定',
  186. cancelButtonText: '取消',
  187. type: 'warning'
  188. })
  189. const response = await request({
  190. url: `/wechatpay/transfer_bill/${id}/approve`,
  191. method: 'post'
  192. })
  193. if (response.code === 200) {
  194. ElMessage.success('操作成功')
  195. fetchTransfers()
  196. } else {
  197. ElMessage.error(response.msg || '操作失败')
  198. }
  199. } catch (error) {
  200. if (error !== 'cancel') {
  201. ElMessage.error('操作失败')
  202. console.error('同意转账申请失败:', error)
  203. }
  204. }
  205. }
  206. // 驳回转账申请
  207. const rejectTransfer = async (id: number) => {
  208. try {
  209. await ElMessageBox.prompt('请输入驳回原因', '驳回操作', {
  210. confirmButtonText: '确定',
  211. cancelButtonText: '取消',
  212. inputPlaceholder: '请输入驳回原因',
  213. inputPattern: /\S/,
  214. inputErrorMessage: '驳回原因不能为空'
  215. }).then(async ({ value }) => {
  216. const response = await request({
  217. url: `/wechatpay/transfer_bill/${id}/reject`,
  218. method: 'post',
  219. data: {
  220. fail_reason: value
  221. }
  222. })
  223. if (response.code === 200) {
  224. ElMessage.success('操作成功')
  225. fetchTransfers()
  226. } else {
  227. ElMessage.error(response.msg || '操作失败')
  228. }
  229. }).catch(() => {
  230. // 用户取消操作
  231. })
  232. } catch (error) {
  233. ElMessage.error('操作失败')
  234. console.error('驳回转账申请失败:', error)
  235. }
  236. }
  237. // 刷新转账状态
  238. const refreshTransferStatus = async (id: number) => {
  239. try {
  240. const response = await request({
  241. url: `/wechatpay/transfer_bill/${id}/refresh`,
  242. method: 'get'
  243. })
  244. if (response.code === 200) {
  245. ElMessage.success('刷新成功')
  246. // 重新获取列表数据以更新状态
  247. fetchTransfers()
  248. } else {
  249. ElMessage.error(response.msg || '刷新失败')
  250. }
  251. } catch (error) {
  252. ElMessage.error('刷新失败')
  253. console.error('刷新转账状态失败:', error)
  254. }
  255. }
  256. // 显示余额记录对话框
  257. const showBalanceRecordDialog = (userId: number) => {
  258. currentUserId.value = userId
  259. balanceRecordDialogVisible.value = true
  260. getBalanceRecords()
  261. }
  262. // 关闭余额记录对话框
  263. const closeBalanceRecordDialog = () => {
  264. balanceRecordDialogVisible.value = false
  265. balanceRecords.value = []
  266. balanceRecordPagination.value.page = 1
  267. balanceRecordPagination.value.total = 0
  268. currentUserId.value = null
  269. }
  270. // 获取余额记录
  271. const getBalanceRecords = async () => {
  272. try {
  273. balanceRecordLoading.value = true
  274. const params = {
  275. user_id: currentUserId.value,
  276. page: balanceRecordPagination.value.page,
  277. limit: balanceRecordPagination.value.limit
  278. }
  279. const response = await userApi.getUserBalanceList(params)
  280. if (response.code === 200) {
  281. balanceRecords.value = response.page?.data || []
  282. balanceRecordPagination.value.total = response.page?.total || 0
  283. } else {
  284. ElMessage.error(response.msg || '获取余额记录失败')
  285. }
  286. } catch (error) {
  287. console.error('获取余额记录失败:', error)
  288. ElMessage.error('获取余额记录失败')
  289. } finally {
  290. balanceRecordLoading.value = false
  291. }
  292. }
  293. // 余额记录分页处理
  294. const handleBalanceRecordSizeChange = (size: number) => {
  295. balanceRecordPagination.value.limit = size
  296. balanceRecordPagination.value.page = 1
  297. getBalanceRecords()
  298. }
  299. const handleBalanceRecordCurrentChange = (page: number) => {
  300. balanceRecordPagination.value.page = page
  301. getBalanceRecords()
  302. }
  303. </script>
  304. <template>
  305. <div class="wechatpay-transfer">
  306. <el-card>
  307. <template #header>
  308. <div class="card-header">
  309. <span>微信支付转账记录</span>
  310. <div class="header-actions">
  311. <el-button type="primary" :icon="Refresh" @click="fetchTransfers" circle />
  312. </div>
  313. </div>
  314. </template>
  315. <!-- 搜索表单 -->
  316. <div class="search-bar">
  317. <el-form :model="searchForm" inline>
  318. <el-form-item label="商户单号">
  319. <el-input
  320. v-model="searchForm.out_bill_no"
  321. placeholder="请输入商户单号"
  322. clearable
  323. style="width: 200px;"
  324. />
  325. </el-form-item>
  326. <el-form-item label="状态">
  327. <el-select
  328. v-model="searchForm.state"
  329. placeholder="请选择状态"
  330. clearable
  331. style="width: 150px;"
  332. >
  333. <el-option label="申请中" value="APPLYING" />
  334. <el-option label="转账已受理" value="ACCEPTED" />
  335. <el-option label="转账锁定资金中" value="PROCESSING" />
  336. <el-option label="待收款用户确认" value="WAIT_USER_CONFIRM" />
  337. <el-option label="转账中" value="TRANSFERING" />
  338. <el-option label="转账成功" value="SUCCESS" />
  339. <el-option label="转账失败" value="FAIL" />
  340. <el-option label="转账撤销中" value="CANCELING" />
  341. <el-option label="转账撤销完成" value="CANCELLED" />
  342. </el-select>
  343. </el-form-item>
  344. <el-form-item>
  345. <el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
  346. <el-button @click="resetSearch">重置</el-button>
  347. </el-form-item>
  348. </el-form>
  349. </div>
  350. <!-- 统计数据展示 -->
  351. <div class="stats-cards">
  352. <el-row :gutter="20" style="margin-bottom: 20px;">
  353. <el-col :span="6">
  354. <el-card class="stats-card">
  355. <div class="stats-item">
  356. <div class="stats-label">转账成功</div>
  357. <div class="stats-value success">
  358. <div class="count">{{ statisticsData.find(item => item.state === 'SUCCESS')?.count || 0 }} 笔</div>
  359. <div class="amount">¥{{ formatAmount(statisticsData.find(item => item.state === 'SUCCESS')?.amount || 0) }}</div>
  360. </div>
  361. </div>
  362. </el-card>
  363. </el-col>
  364. <el-col :span="6">
  365. <el-card class="stats-card">
  366. <div class="stats-item">
  367. <div class="stats-label">转账失败</div>
  368. <div class="stats-value failed">
  369. <div class="count">{{ statisticsData.find(item => item.state === 'FAIL')?.count || 0 }} 笔</div>
  370. <div class="amount">¥{{ formatAmount(statisticsData.find(item => item.state === 'FAIL')?.amount || 0) }}</div>
  371. </div>
  372. </div>
  373. </el-card>
  374. </el-col>
  375. <el-col :span="6">
  376. <el-card class="stats-card">
  377. <div class="stats-item">
  378. <div class="stats-label">处理中</div>
  379. <div class="stats-value processing">
  380. <div class="count">{{ (statisticsData.find(item => item.state === 'PROCESSING')?.count || 0) + (statisticsData.find(item => item.state === 'TRANSFERING')?.count || 0) }} 笔</div>
  381. <div class="amount">¥{{ formatAmount((statisticsData.find(item => item.state === 'PROCESSING')?.amount || 0) + (statisticsData.find(item => item.state === 'TRANSFERING')?.amount || 0)) }}</div>
  382. </div>
  383. </div>
  384. </el-card>
  385. </el-col>
  386. <el-col :span="6">
  387. <el-card class="stats-card">
  388. <div class="stats-item">
  389. <div class="stats-label">待处理</div>
  390. <div class="stats-value pending">
  391. <div class="count">{{ (statisticsData.find(item => item.state === 'APPLYING')?.count || 0) + (statisticsData.find(item => item.state === 'ACCEPTED')?.count || 0) + (statisticsData.find(item => item.state === 'WAIT_USER_CONFIRM')?.count || 0) }} 笔</div>
  392. <div class="amount">¥{{ formatAmount((statisticsData.find(item => item.state === 'APPLYING')?.amount || 0) + (statisticsData.find(item => item.state === 'ACCEPTED')?.amount || 0) + (statisticsData.find(item => item.state === 'WAIT_USER_CONFIRM')?.amount || 0)) }}</div>
  393. </div>
  394. </div>
  395. </el-card>
  396. </el-col>
  397. </el-row>
  398. </div>
  399. <div class="table-wrapper">
  400. <div class="table-container">
  401. <div class="scroll-hint" v-if="showScrollHint">
  402. <el-icon><ArrowRight /></el-icon>
  403. <span>左右滑动查看更多</span>
  404. </div>
  405. <el-table
  406. ref="tableRef"
  407. v-loading="loading"
  408. :data="transferList"
  409. style="width: 100%"
  410. border
  411. @scroll="handleTableScroll"
  412. >
  413. <el-table-column prop="id" label="ID" width="80" />
  414. <el-table-column label="用户信息" width="240">
  415. <template #default="scope">
  416. <div class="user-info-cell">
  417. <div class="avatar-container">
  418. <el-avatar
  419. v-if="scope.row.user && scope.row.user.avatar"
  420. :src="scope.row.user.avatar.startsWith('http') ? scope.row.user.avatar : `${imgUrl}${scope.row.user.avatar}`"
  421. :size="30"
  422. />
  423. <el-avatar
  424. v-else
  425. :size="30"
  426. >
  427. {{ scope.row.user?.nickname?.charAt(0) || scope.row.user?.username?.charAt(0) || 'U' }}
  428. </el-avatar>
  429. </div>
  430. <div class="user-details">
  431. <div class="nickname">{{ scope.row.user?.nickname || '-' }}</div>
  432. <div class="username"><span class="label">用户名:</span> {{ scope.row.user?.username || '-' }}</div>
  433. <div class="mobile"><span class="label">手机:</span> {{ scope.row.user?.mobile || '-' }}</div>
  434. <div class="user-name"><span class="label">姓名:</span> {{ scope.row.user_name || '-' }}</div>
  435. <div class="openid"><span class="label">OpenID:</span> {{ scope.row.openid || '-' }}</div>
  436. </div>
  437. </div>
  438. </template>
  439. </el-table-column>
  440. <el-table-column label="转账金额" width="240">
  441. <template #default="scope">
  442. <div class="amount-group">
  443. <div class="total-amount">
  444. <span class="label">总金额:</span>
  445. <span class="amount-text">¥{{ formatAmount(scope.row.transfer_amount + scope.row.fee) }}</span>
  446. </div>
  447. <div class="amount-details">
  448. <div class="detail-item">
  449. <span class="label">实际到账:</span>
  450. <span class="value">¥{{ formatAmount(scope.row.transfer_amount) }}</span>
  451. </div>
  452. <div class="detail-item">
  453. <span class="label">手续费:</span>
  454. <span class="value">¥{{ formatAmount(scope.row.fee) }}</span>
  455. </div>
  456. <div class="detail-item">
  457. <el-button
  458. type="primary"
  459. size="small"
  460. link
  461. style="padding: 0"
  462. @click="showBalanceRecordDialog(scope.row.user_id)"
  463. >
  464. 资金明细
  465. </el-button>
  466. </div>
  467. </div>
  468. </div>
  469. </template>
  470. </el-table-column>
  471. <el-table-column prop="out_bill_no" label="商户单号" min-width="180" />
  472. <el-table-column prop="transfer_bill_no" label="微信转账订单号" min-width="180" />
  473. <el-table-column prop="state_text" label="状态" width="150">
  474. <template #default="scope">
  475. <div style="display: flex; align-items: center;">
  476. <el-tag :type="getStateType(scope.row.state)">
  477. {{ scope.row.state_text }}
  478. </el-tag>
  479. <!-- 当状态为处理中时显示刷新图标 -->
  480. <el-button
  481. v-if="['APPLYING', 'ACCEPTED', 'PROCESSING', 'TRANSFERING', 'WAIT_USER_CONFIRM'].includes(scope.row.state)"
  482. type="text"
  483. :icon="Refresh"
  484. size="small"
  485. @click.stop="refreshTransferStatus(scope.row.id)"
  486. title="刷新状态"
  487. style="margin-left: 4px; color: #606266;"
  488. />
  489. </div>
  490. </template>
  491. </el-table-column>
  492. <el-table-column prop="transfer_remark" label="转账备注" min-width="120" />
  493. <el-table-column prop="fail_reason" label="失败原因" min-width="120" />
  494. <el-table-column prop="create_time" label="创建时间" width="180" />
  495. <el-table-column prop="update_time" label="更新时间" width="180" />
  496. <el-table-column label="操作" width="150" fixed="right">
  497. <template #default="scope">
  498. <div v-if="scope.row.state === 'APPLYING'" style="display: flex; gap: 8px;">
  499. <el-button size="small" type="primary" @click.stop="approveTransfer(scope.row.id)">同意</el-button>
  500. <el-button size="small" type="danger" @click.stop="rejectTransfer(scope.row.id)">驳回</el-button>
  501. </div>
  502. <div v-else>-</div>
  503. </template>
  504. </el-table-column>
  505. </el-table>
  506. </div>
  507. </div>
  508. <!-- 余额记录对话框 -->
  509. <el-dialog
  510. v-model="balanceRecordDialogVisible"
  511. title="资金明细"
  512. width="800px"
  513. :before-close="closeBalanceRecordDialog"
  514. >
  515. <el-table
  516. v-loading="balanceRecordLoading"
  517. :data="balanceRecords"
  518. style="width: 100%"
  519. border
  520. stripe
  521. >
  522. <el-table-column prop="id" label="ID" width="80" />
  523. <el-table-column prop="type_text" label="类型" width="120">
  524. <template #default="scope">
  525. <el-tag v-if="scope.row.type === 1" type="success">收入</el-tag>
  526. <el-tag v-else type="danger">支出</el-tag>
  527. </template>
  528. </el-table-column>
  529. <el-table-column prop="amount" label="金额" width="120">
  530. <template #default="scope">
  531. <span :class="scope.row.type === 1 ? 'income-amount' : 'expense-amount'">
  532. {{ scope.row.type === 1 ? '+' : '-' }}¥{{ formatBalanceAmount(Math.abs(scope.row.amount)) }}
  533. </span>
  534. </template>
  535. </el-table-column>
  536. <el-table-column prop="balance" label="余额" width="120">
  537. <template #default="scope">
  538. ¥{{ formatBalanceAmount(scope.row.balance) }}
  539. </template>
  540. </el-table-column>
  541. <el-table-column prop="description" label="备注" min-width="150" />
  542. <el-table-column prop="create_time" label="创建时间" width="180" />
  543. </el-table>
  544. <div class="pagination-container" style="margin-top: 20px; display: flex; justify-content: flex-end;">
  545. <el-pagination
  546. v-model:current-page="balanceRecordPagination.page"
  547. v-model:page-size="balanceRecordPagination.limit"
  548. :page-sizes="[10, 20, 50, 100]"
  549. :total="balanceRecordPagination.total"
  550. layout="total, sizes, prev, pager, next, jumper"
  551. @current-change="handleBalanceRecordCurrentChange"
  552. @size-change="handleBalanceRecordSizeChange"
  553. />
  554. </div>
  555. <template #footer>
  556. <div class="dialog-footer">
  557. <el-button @click="closeBalanceRecordDialog">关闭</el-button>
  558. </div>
  559. </template>
  560. </el-dialog>
  561. <div class="pagination-container">
  562. <el-pagination
  563. v-model:current-page="pagination.page"
  564. v-model:page-size="pagination.limit"
  565. :page-sizes="[10, 20, 50, 100]"
  566. :total="pagination.total"
  567. layout="total, sizes, prev, pager, next, jumper"
  568. @current-change="handlePageChange"
  569. @size-change="handleSizeChange"
  570. />
  571. </div>
  572. </el-card>
  573. </div>
  574. </template>
  575. <style scoped lang="scss">
  576. .wechatpay-transfer {
  577. padding: 20px;
  578. .card-header {
  579. display: flex;
  580. justify-content: space-between;
  581. align-items: center;
  582. font-weight: 600;
  583. font-size: 16px;
  584. }
  585. .header-actions {
  586. display: flex;
  587. gap: 10px;
  588. }
  589. .search-bar {
  590. background: #f5f7fa;
  591. padding: 15px;
  592. border-radius: 4px;
  593. margin-bottom: 20px;
  594. }
  595. .table-container {
  596. position: relative;
  597. .scroll-hint {
  598. position: absolute;
  599. top: 50%;
  600. right: 10px;
  601. transform: translateY(-50%);
  602. background: rgba(64, 158, 255, 0.8);
  603. color: white;
  604. padding: 5px 10px;
  605. border-radius: 4px;
  606. z-index: 10;
  607. display: flex;
  608. align-items: center;
  609. gap: 5px;
  610. font-size: 12px;
  611. pointer-events: none;
  612. animation: fadeInOut 2s infinite;
  613. }
  614. @keyframes fadeInOut {
  615. 0%, 100% { opacity: 0.7; }
  616. 50% { opacity: 1; }
  617. }
  618. }
  619. .amount-group {
  620. .total-amount {
  621. display: flex;
  622. justify-content: space-between;
  623. font-weight: 600;
  624. margin-bottom: 4px;
  625. .label {
  626. color: #606266;
  627. }
  628. .amount-text {
  629. font-size: 16px;
  630. color: #e74c3c;
  631. }
  632. }
  633. .amount-details {
  634. background-color: #f5f7fa;
  635. border-radius: 4px;
  636. padding: 6px 8px;
  637. .detail-item {
  638. display: flex;
  639. justify-content: space-between;
  640. font-size: 12px;
  641. margin-bottom: 2px;
  642. &:last-child {
  643. margin-bottom: 0;
  644. }
  645. .label {
  646. color: #909399;
  647. }
  648. .value {
  649. color: #606266;
  650. }
  651. }
  652. }
  653. }
  654. .stats-cards {
  655. .stats-card {
  656. text-align: center;
  657. .stats-item {
  658. .stats-label {
  659. color: #909399;
  660. font-size: 14px;
  661. margin-bottom: 8px;
  662. }
  663. .stats-value {
  664. font-weight: bold;
  665. .count {
  666. font-size: 20px;
  667. margin-bottom: 5px;
  668. }
  669. .amount {
  670. font-size: 16px;
  671. }
  672. &.success {
  673. .count, .amount {
  674. color: #67c23a;
  675. }
  676. }
  677. &.failed {
  678. .count, .amount {
  679. color: #f56c6c;
  680. }
  681. }
  682. &.processing {
  683. .count, .amount {
  684. color: #e6a23c;
  685. }
  686. }
  687. &.pending {
  688. .count, .amount {
  689. color: #409eff;
  690. }
  691. }
  692. }
  693. }
  694. }
  695. }
  696. .amount-text {
  697. font-size: 16px;
  698. font-weight: 600;
  699. color: #e74c3c;
  700. }
  701. .income-amount {
  702. color: #67c23a;
  703. font-weight: 600;
  704. }
  705. .expense-amount {
  706. color: #f56c6c;
  707. font-weight: 600;
  708. }
  709. :deep(.el-form--inline .el-form-item) {
  710. margin-right: 15px;
  711. margin-bottom: 10px;
  712. }
  713. :deep(.el-table) {
  714. .el-table__cell {
  715. padding: 8px 0;
  716. }
  717. // 添加更好的滚动条样式
  718. &::-webkit-scrollbar {
  719. height: 8px;
  720. width: 8px;
  721. }
  722. &::-webkit-scrollbar-thumb {
  723. background-color: #c1c1c1;
  724. border-radius: 4px;
  725. &:hover {
  726. background-color: #a8a8a8;
  727. }
  728. }
  729. &::-webkit-scrollbar-track {
  730. background-color: #f1f1f1;
  731. border-radius: 4px;
  732. }
  733. }
  734. // 添加阴影效果提示还有更多内容可滚动
  735. .table-wrapper {
  736. position: relative;
  737. &::before,
  738. &::after {
  739. content: '';
  740. position: absolute;
  741. top: 0;
  742. bottom: 0;
  743. width: 30px;
  744. pointer-events: none;
  745. transition: box-shadow 0.3s;
  746. z-index: 1;
  747. }
  748. &::before {
  749. left: 0;
  750. box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.15);
  751. }
  752. &::after {
  753. right: 0;
  754. box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.15);
  755. }
  756. }
  757. .pagination-container {
  758. display: flex;
  759. justify-content: flex-end;
  760. margin-top: 20px;
  761. }
  762. .user-info-cell {
  763. display: flex;
  764. align-items: flex-start;
  765. .avatar-container {
  766. margin-right: 8px;
  767. margin-top: 2px;
  768. }
  769. .user-details {
  770. flex: 1;
  771. overflow: hidden;
  772. .nickname {
  773. font-weight: 500;
  774. color: #303133;
  775. overflow: hidden;
  776. text-overflow: ellipsis;
  777. white-space: nowrap;
  778. margin-bottom: 4px;
  779. }
  780. .username, .mobile, .openid, .user-name {
  781. font-size: 12px;
  782. color: #909399;
  783. overflow: hidden;
  784. text-overflow: ellipsis;
  785. white-space: nowrap;
  786. margin-bottom: 2px;
  787. .label {
  788. color: #606266;
  789. font-weight: 500;
  790. margin-right: 4px;
  791. }
  792. }
  793. .openid, .user-name {
  794. font-size: 11px;
  795. color: #c0c4cc;
  796. }
  797. }
  798. }
  799. }
  800. </style>