You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

536 lines
16 KiB

2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
1 week ago
2 weeks ago
1 week ago
1 week ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
1 week ago
1 week ago
2 weeks ago
1 week ago
1 week ago
2 weeks ago
1 week ago
  1. <script setup>
  2. import { ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
  3. import { useRoute } from 'vue-router'
  4. import { ElMessage } from 'element-plus'
  5. import API from '@/util/http.js'
  6. import moment from 'moment'
  7. import { useI18n } from 'vue-i18n'
  8. import { useAdminStore } from "@/store/index.js"
  9. import { storeToRefs } from "pinia"
  10. import { selectWalletRecords } from "@/api/cash/cash.js"
  11. const props = defineProps({
  12. type: {
  13. type: Number,
  14. required: true
  15. }
  16. })
  17. const { t } = useI18n()
  18. const route = useRoute()
  19. const adminStore = useAdminStore()
  20. const { flag } = storeToRefs(adminStore)
  21. const tableData = ref([])
  22. const total = ref(0)
  23. const loading = ref(false)
  24. const marketLists = ref([])
  25. const selectedMarketPath = ref([])
  26. const selectData = ref({
  27. jwcode: '',
  28. walletId: '',
  29. market: ''
  30. })
  31. const getObj = ref({
  32. pageNum: 1,
  33. pageSize: 20
  34. })
  35. const tableRef = ref(null)
  36. const scrollTableTop = () => {
  37. tableRef.value?.setScrollTop?.(0)
  38. }
  39. // 精网号去空格
  40. const trimJwCode = () => {
  41. if (selectData.value.jwcode) {
  42. selectData.value.jwcode = selectData.value.jwcode.replace(/\s/g, '');
  43. }
  44. }
  45. // 获取类型文本
  46. const getWalletRecordTypeText = (item) => {
  47. const type = Number(item.type)
  48. if (type === 0) {
  49. return t('clientCount.recharge')
  50. }
  51. if (type === 1) {
  52. return t('clientCount.consume')
  53. }
  54. if (type === 2) {
  55. return t('clientCount.refund')
  56. }
  57. return item.typeText || t('clientCount.other')
  58. }
  59. // 格式化金额
  60. const format3 = (num) => {
  61. return num.toLocaleString('en-US')
  62. }
  63. // 统一获取数据的方法
  64. const getWalletData = async () => {
  65. console.log('walletId:', selectData.value.walletId)
  66. if (!selectData.value.walletId) return;
  67. if (selectData.value.jwcode) {
  68. const numberRegex = /^\d{1,9}$/;
  69. if (!numberRegex.test(selectData.value.jwcode)) {
  70. ElMessage.error(t('elmessage.checkJwcodeFormat'))
  71. return
  72. }
  73. }
  74. loading.value = true
  75. try {
  76. const params = {
  77. pageNum: getObj.value.pageNum,
  78. pageSize: getObj.value.pageSize,
  79. userWalletRecord: {
  80. walletId: selectData.value.walletId,
  81. market: selectData.value.market,
  82. jwcode: selectData.value.jwcode
  83. }
  84. }
  85. const result = await selectWalletRecords(params)
  86. if (result.code === 200) {
  87. tableData.value = result.data.list.map(item => ({
  88. ...item,
  89. time: moment(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
  90. typeText: getWalletRecordTypeText(item),
  91. amount: Number(item.amount) || 0,
  92. desc: item.description || '',
  93. orderNo: item.orderCode,
  94. status: item.status === 0 ? 1 : 2,
  95. userName: item.userName || '-',
  96. jwcode: item.jwcode || '-'
  97. }))
  98. total.value = result.data.total
  99. await nextTick()
  100. scrollTableTop()
  101. } else {
  102. ElMessage.error(result.msg || t('elmessage.getDataFailed'))
  103. }
  104. } catch (error) {
  105. console.error('获取钱包明细失败:', error)
  106. } finally {
  107. loading.value = false
  108. }
  109. }
  110. // 搜索
  111. const search = function () {
  112. trimJwCode()
  113. getObj.value.pageNum = 1
  114. getWalletData()
  115. }
  116. // 重置
  117. const reset = function () {
  118. selectData.value.jwcode = ''
  119. selectData.value.market = ''
  120. selectedMarketPath.value = []
  121. getObj.value.pageNum = 1
  122. getWalletData()
  123. }
  124. // 分页改变
  125. const handlePageSizeChange = function (val) {
  126. getObj.value.pageSize = val
  127. getWalletData()
  128. }
  129. const handleCurrentChange = function (val) {
  130. getObj.value.pageNum = val
  131. getWalletData()
  132. }
  133. // 监听全局flag变化重新请求数据
  134. watch(flag, (newFlag, oldFlag) => {
  135. if (newFlag !== oldFlag) {
  136. getWalletData()
  137. }
  138. })
  139. // 核心:监听父组件传入的 props 参数变化
  140. watch(
  141. () => props.type,
  142. (newType) => {
  143. if (newType) {
  144. selectData.value.walletId = newType
  145. getObj.value.pageNum = 1
  146. getWalletData()
  147. }
  148. },
  149. { immediate: true }
  150. )
  151. // ==================== 导出逻辑 ====================
  152. const exportListVisible = ref(false)
  153. const EXPORT_LIST_POLL_INTERVAL = 3000
  154. let exportListPollingTimer = null
  155. const exportList = ref([])
  156. const exportListLoading = ref(false)
  157. const exportListRequesting = ref(false)
  158. const sortExportList = (list = []) => {
  159. return [...list].sort((a, b) => {
  160. return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
  161. })
  162. }
  163. const hasPendingExportTask = (list = []) => {
  164. const latestTask = sortExportList(list)[0]
  165. return latestTask ? latestTask.state === 0 || latestTask.state === 1 : false
  166. }
  167. const stopExportListPolling = () => {
  168. if (exportListPollingTimer) {
  169. clearInterval(exportListPollingTimer)
  170. exportListPollingTimer = null
  171. }
  172. }
  173. const startExportListPolling = () => {
  174. if (exportListPollingTimer) {
  175. return
  176. }
  177. exportListPollingTimer = setInterval(() => {
  178. if (!exportListVisible.value) {
  179. stopExportListPolling()
  180. return
  181. }
  182. getExportList({ showLoading: false, silentError: true })
  183. }, EXPORT_LIST_POLL_INTERVAL)
  184. }
  185. const exportExcelOnlyOne = async function () {
  186. if (!selectData.value.walletId) {
  187. ElMessage.error(t('elmessage.selectCompanyWallet'))
  188. return
  189. }
  190. const params = {
  191. pageNum: 1, // 导出通常从第一页开始
  192. pageSize: 10000, // 导出大量数据
  193. userWalletRecord: {
  194. walletId: selectData.value.walletId,
  195. jwcode: selectData.value.jwcode ? Number(selectData.value.jwcode) : null
  196. }
  197. }
  198. try {
  199. const res = await API({
  200. url: '/export/exportUserWalletRecord',
  201. method: 'post',
  202. data: params
  203. })
  204. if (res.code === 200) {
  205. ElMessage.success(t('elmessage.exportSuccess'))
  206. } else {
  207. ElMessage.error(res.msg || t('elmessage.exportFailed'))
  208. }
  209. } catch (error) {
  210. console.error('导出失败:', error)
  211. ElMessage.error(t('elmessage.exportFailed'))
  212. }
  213. }
  214. // 打开导出列表弹窗
  215. const openExportList = () => {
  216. exportListVisible.value = true
  217. }
  218. // 获取导出列表
  219. const getExportList = async ({ showLoading = true, silentError = false } = {}) => {
  220. if (exportListRequesting.value) {
  221. return
  222. }
  223. if (showLoading) {
  224. exportListLoading.value = true
  225. }
  226. exportListRequesting.value = true
  227. try {
  228. const result = await API({ url: '/export/export' })
  229. if (result.code === 200) {
  230. // 过滤只显示 type 为 16 和 17 的导出记录(保持和 WalletBalance 页面一致)
  231. const filteredData = result.data.filter(item => {
  232. return item.type === 16;
  233. });
  234. exportList.value = sortExportList(filteredData)
  235. if (exportListVisible.value && hasPendingExportTask(exportList.value)) {
  236. startExportListPolling()
  237. } else {
  238. stopExportListPolling()
  239. }
  240. } else {
  241. stopExportListPolling()
  242. if (!silentError) {
  243. ElMessage.error(result.msg || t('elmessage.getExportListError'))
  244. }
  245. }
  246. } catch (error) {
  247. console.error('获取导出列表出错:', error)
  248. stopExportListPolling()
  249. if (!silentError) {
  250. ElMessage.error(t('elmessage.getExportListError'))
  251. }
  252. } finally {
  253. exportListRequesting.value = false
  254. if (showLoading) {
  255. exportListLoading.value = false
  256. }
  257. }
  258. }
  259. // 下载导出文件
  260. const downloadExportFile = (item) => {
  261. if (item.state === 2) {
  262. const link = document.createElement('a')
  263. link.href = item.url
  264. link.download = item.fileName
  265. link.click()
  266. } else {
  267. ElMessage.warning(t('elmessage.exportingInProgress'))
  268. }
  269. }
  270. //根据状态返回对应的标签类型
  271. const getTagType = (state) => {
  272. switch (state) {
  273. case 0: return 'info';
  274. case 1: return 'primary';
  275. case 2: return 'success';
  276. case 3: return 'danger';
  277. default: return 'info';
  278. }
  279. }
  280. //根据状态返回对应的标签文案
  281. const getTagText = (state) => {
  282. switch (state) {
  283. case 0: return t('elmessage.pendingExecution');
  284. case 1: return t('elmessage.executing');
  285. case 2: return t('elmessage.executed');
  286. case 3: return t('elmessage.errorExecution');
  287. default: return t('elmessage.unknownStatus');
  288. }
  289. }
  290. const transformTree = (nodes) => {
  291. // 直接处理第一级节点的子节点
  292. const allChildren = nodes.flatMap(node => node.children || []);
  293. return allChildren.map(child => {
  294. const grandchildren = child.children && child.children.length
  295. ? transformTree([child]) // 递归处理子节点
  296. : null;
  297. return {
  298. value: child.id,
  299. label: child.name,
  300. children: grandchildren
  301. };
  302. });
  303. };
  304. const selectMarket = async function () {
  305. try {
  306. const selectMarketResult = await API({ url: '/market/selectMarket' });
  307. marketLists.value = transformTree(selectMarketResult.data)
  308. console.log('转换后的地区树==============:', marketLists.value);
  309. } catch (error) {
  310. console.error('获取地区树失败:', error);
  311. return {};
  312. }
  313. };
  314. const handleMarketChange = (value) => {
  315. if (value && value.length > 0) {
  316. // 级联选择器绑定了 id,直接取数组最后一级(叶子节点)的 id 即可
  317. const lastValueId = value[value.length - 1];
  318. selectData.value.market = lastValueId;
  319. } else {
  320. selectData.value.market = '';
  321. }
  322. };
  323. onMounted(() => {
  324. selectData.value.walletId = props.type
  325. selectMarket()
  326. })
  327. watch(exportListVisible, (visible) => {
  328. if (visible) {
  329. getExportList()
  330. } else {
  331. stopExportListPolling()
  332. }
  333. })
  334. onBeforeUnmount(() => {
  335. stopExportListPolling()
  336. })
  337. </script>
  338. <template>
  339. <div style="display: flex; flex-direction: column; height: 95vh;">
  340. <el-card class="card1" style="margin-bottom: 1vh;">
  341. <div class="head-card">
  342. <div class="head-card-element">
  343. <el-text class="mx-1" size="large">{{ $t('common_list.jwcode') }}</el-text>
  344. <el-input v-model="selectData.jwcode" style="width: 12.5vw" :placeholder="$t('common.jwcodePlaceholder')" clearable />
  345. </div>
  346. <div class="head-card-element">
  347. <el-text class="mx-1" size="large">{{ $t('common_list.market') }}</el-text>
  348. <el-cascader v-model="selectedMarketPath" :options="marketLists" :placeholder="$t('common_list.marketPlaceholder')"
  349. clearable style="width:180px" @change="handleMarketChange" />
  350. </div>
  351. <div class="head-card-btn">
  352. <el-button type="success" @click="reset">{{ $t('common.reset') }}</el-button>
  353. <el-button type="primary" @click="search">{{ $t('common.search') }}</el-button>
  354. <el-button type="primary" @click="exportExcelOnlyOne">{{ $t('common.exportExcel') }}</el-button>
  355. <el-button type="primary" @click="openExportList">{{ $t('common.viewExportList') }}</el-button>
  356. </div>
  357. </div>
  358. </el-card>
  359. <el-card class="card2">
  360. <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
  361. <el-table ref="tableRef" :data="tableData" v-loading="loading" style="width: 100%; flex: 1"
  362. :row-style="{ height: '50px' }">
  363. <el-table-column type="index" :label="$t('common_list.id')" width="80px" fixed="left">
  364. <template #default="scope">
  365. <span>{{ scope.$index + 1 + (getObj.pageNum - 1) * getObj.pageSize }}</span>
  366. </template>
  367. </el-table-column>
  368. <el-table-column prop="jwcode" :label="$t('common_list.jwcode')" width="140" />
  369. <el-table-column prop="userName" :label="$t('common_list.name')" width="140" />
  370. <el-table-column prop="marketName" :label="$t('common_list.marketName')" width="140" />
  371. <el-table-column prop="typeText" :label="$t('clientCount.transactionType')" align="center" width="120" />
  372. <el-table-column prop="transactionCurrency" :label="$t('clientCount.transactionCurrency')" align="center"
  373. width="120" />
  374. <el-table-column prop="amount" :label="$t('common_list.money')" align="center" width="120">
  375. <template #default="scope">
  376. <span :style="{ color: scope.row.amount >= 0 ? '#67C23A' : '#F56C6C', fontWeight: 'bold' }">
  377. {{ scope.row.amount > 0 ? '+' + format3(scope.row.amount) : format3(scope.row.amount) }}
  378. </span>
  379. </template>
  380. </el-table-column>
  381. <el-table-column prop="orderNo" :label="$t('clientCount.transactionOrderNo')" align="center" min-width="320" />
  382. <el-table-column prop="desc" :label="$t('clientCount.transactionDesc')" align="center" min-width="150" />
  383. <el-table-column prop="status" :label="$t('clientCount.transactionStatus')" align="center" width="150">
  384. <template #default="scope">
  385. <el-tag :type="scope.row.status === 1 ? 'success' : scope.row.status === 2 ? 'danger' : 'info'"
  386. :effect="scope.row.status === 1 ? 'light' : 'plain'">
  387. {{ scope.row.status === 1 ? t('common_list.normal') : scope.row.status === 2 ? t('common_list.refunded')
  388. : t('clientCount.exceptionData') }}
  389. </el-tag>
  390. </template>
  391. </el-table-column>
  392. <el-table-column prop="time" :label="$t('clientCount.time')" align="center" width="180">
  393. <template #default="scope">{{ scope.row.time }}</template>
  394. </el-table-column>
  395. </el-table>
  396. </div>
  397. <!-- 分页 -->
  398. <div class="pagination" style="margin-top: 20px">
  399. <el-pagination background :current-page="getObj.pageNum" :page-size="getObj.pageSize"
  400. :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="total"
  401. @size-change="handlePageSizeChange" @current-change="handleCurrentChange"></el-pagination>
  402. </div>
  403. </el-card>
  404. <!-- 导出列表弹窗 -->
  405. <el-dialog v-model="exportListVisible" :title="$t('common_export.exportList')" width="80%">
  406. <el-table :data="exportList" style="width: 100% ;height: 60vh;" :loading="exportListLoading">
  407. <el-table-column prop="fileName" :label="$t('common_export.fileName')" />
  408. <el-table-column prop="state" :label="$t('common_export.status')">
  409. <template #default="scope">
  410. <el-tag :type="getTagType(scope.row.state)" :effect="scope.row.state === 3 ? 'light' : 'plain'">
  411. {{ getTagText(scope.row.state) }}
  412. </el-tag>
  413. </template>
  414. </el-table-column>
  415. <el-table-column prop="createTime" :label="$t('common_export.createTime')">
  416. <template #default="scope">
  417. {{ moment(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
  418. </template>
  419. </el-table-column>
  420. <el-table-column :label="$t('common_export.operation')">
  421. <template #default="scope">
  422. <el-button type="primary" size="small" @click="downloadExportFile(scope.row)"
  423. :disabled="scope.row.state !== 2">
  424. {{ $t('common_export.download') }}
  425. </el-button>
  426. </template>
  427. </el-table-column>
  428. </el-table>
  429. <template #footer>
  430. <div class="dialog-footer">
  431. <el-button text @click="exportListVisible = false">{{ $t('common_export.close') }}</el-button>
  432. </div>
  433. </template>
  434. </el-dialog>
  435. </div>
  436. </template>
  437. <style scoped lang="scss">
  438. .card1 {
  439. background: #F3FAFE;
  440. }
  441. .card2 {
  442. background: #E7F4FD;
  443. flex: 1;
  444. display: flex;
  445. flex-direction: column;
  446. :deep(.el-card__body) {
  447. padding: 20px;
  448. flex: 1;
  449. display: flex;
  450. flex-direction: column;
  451. overflow: hidden;
  452. }
  453. }
  454. :deep(.el-table__header-wrapper),
  455. :deep(.el-table__body-wrapper),
  456. :deep(.el-table__cell),
  457. :deep(.el-table__body td) {
  458. background-color: #F3FAFE !important;
  459. }
  460. :deep(.el-table__header th) {
  461. background-color: #F3FAFE !important;
  462. }
  463. :deep(.el-table__row:hover > .el-table__cell) {
  464. background-color: #E5EBFE !important;
  465. }
  466. .pagination {
  467. display: flex;
  468. }
  469. .head-card {
  470. display: flex;
  471. }
  472. .head-card-element {
  473. margin-right: 20px;
  474. white-space: nowrap;
  475. .mx-1 {
  476. font-size: 16px;
  477. margin: 0 10px;
  478. }
  479. }
  480. .head-card-btn {
  481. margin-left: 100px;
  482. }
  483. </style>