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.

452 lines
13 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. <script setup>
  2. import { ref, reactive, onMounted, toRefs } from 'vue'
  3. import { ElMessage, ElMessageBox } from 'element-plus'
  4. import request from '@/util/http.js'
  5. import dayjs from 'dayjs'
  6. import { useI18n } from 'vue-i18n'
  7. import { refundOnline,performanceSelect,exportPerformance } from '@/api/cash/financialAccount.js'
  8. import {useAdminStore} from '@/store/index.js'
  9. import { storeToRefs } from 'pinia'
  10. const adminStore = useAdminStore()
  11. const {adminData} = storeToRefs(adminStore)
  12. const { t } = useI18n()
  13. const paytypeList = [
  14. t('cash.payMethods.stripe'),
  15. t('cash.payMethods.paymentAsia'),
  16. t('cash.payMethods.ipay88'),
  17. t('cash.payMethods.bankTransfer'),
  18. t('cash.payMethods.card'),
  19. t('cash.payMethods.cash'),
  20. t('cash.payMethods.check'),
  21. t('cash.payMethods.grabpay'),
  22. t('cash.payMethods.nets'),
  23. t('cash.payMethods.transfer'),
  24. t('cash.payMethods.paypal'),
  25. ]
  26. const payPlatformOptions = ref([...paytypeList])
  27. const statusOptions = [
  28. { label: t('cash.statusList.received'), value: 4 },
  29. { label: t('cash.statusList.refunded'), value: 6 }
  30. ]
  31. // 地区树
  32. const marketOptions = ref([])
  33. // 查询参数
  34. const queryParams = reactive({
  35. jwcode: '',
  36. adminMarket: [], // 下拉多选
  37. timeRange: [], // [startTime, endTime]
  38. customerMarket: [], // 客户地区
  39. pageNum: 1,
  40. pageSize: 20
  41. })
  42. const total = ref(0)
  43. const tableData = ref([])
  44. const loading = ref(false)
  45. // 转换树形结构(参考 coinConsumeDetail.vue)
  46. const transformTree = (nodes) => {
  47. const allChildren = nodes.flatMap(node => node.children || []);
  48. return allChildren.map(child => {
  49. const grandchildren = child.children && child.children.length
  50. ? transformTree([child])
  51. : null;
  52. return {
  53. value: child.id,
  54. label: child.name,
  55. children: grandchildren
  56. };
  57. });
  58. };
  59. // 获取地区数据
  60. const getMarket = async () => {
  61. try {
  62. const result = await request({ url: '/market/selectMarket' });
  63. if (result && result.data) {
  64. marketOptions.value = transformTree(result.data)
  65. console.log('地区树:', marketOptions.value)
  66. }
  67. } catch (error) {
  68. console.error('获取地区失败', error)
  69. }
  70. }
  71. // 查询列表
  72. const fetchData = async () => {
  73. loading.value = true
  74. try {
  75. // 构建请求参数
  76. const params = {
  77. pageNum: queryParams.pageNum,
  78. pageSize: queryParams.pageSize,
  79. performanceDTO:{
  80. jwcode: queryParams.jwcode,
  81. adminMarket: adminData.value.markets.split(',').filter(item => item.trim() !== '') || [],
  82. customerMarket: queryParams.customerMarket,
  83. startTime: queryParams.timeRange?.[0] ? dayjs(queryParams.timeRange[0]).format('YYYY-MM-DD HH:mm:ss') : '',
  84. endTime: queryParams.timeRange?.[1] ? dayjs(queryParams.timeRange[1]).format('YYYY-MM-DD HH:mm:ss') : '',
  85. }
  86. }
  87. console.log('查询参数:', params)
  88. const res = await performanceSelect(params)
  89. if (res.code == 200) {
  90. tableData.value = res.data.list || []
  91. total.value = res.data.total || 0
  92. loading.value = false
  93. } else {
  94. ElMessage.error(res.msg || t('elmessage.getDataFailed'))
  95. loading.value = false
  96. }
  97. } catch (error) {
  98. console.error(error)
  99. loading.value = false
  100. ElMessage.error(t('elmessage.getDataFailed'))
  101. }
  102. }
  103. const handleSearch = () => {
  104. queryParams.pageNum = 1
  105. fetchData()
  106. }
  107. const handleReset = () => {
  108. queryParams.jwcode = ''
  109. queryParams.adminMarket = []
  110. queryParams.timeRange = null
  111. queryParams.customerMarket = []
  112. handleSearch()
  113. }
  114. const handlePageSizeChange = (val) => {
  115. queryParams.pageSize = val
  116. fetchData()
  117. }
  118. const handleCurrentChange = (val) => {
  119. queryParams.pageNum = val
  120. fetchData()
  121. }
  122. // 退款操作
  123. const handleRefund = (row) => {
  124. ElMessageBox.confirm(t('elmessage.refundConfirmContent', { orderNo: row.systemTradeNo }), t('elmessage.refundConfirmTitle'), {
  125. confirmButtonText: t('common.confirm'),
  126. cancelButtonText: t('common.cancel'),
  127. type: 'warning'
  128. }).then(() => {
  129. ElMessage.success(t('elmessage.refundSubmitSuccess'))
  130. // 刷新列表
  131. fetchData()
  132. }).catch(() => {})
  133. }
  134. // ==================== 导出相关逻辑 ====================
  135. const exportListVisible = ref(false)
  136. const exportList = ref([])
  137. const exportListLoading = ref(false)
  138. // 导出Excel
  139. const handleExport = async () => {
  140. try {
  141. const params = {
  142. pageNum: queryParams.pageNum,
  143. pageSize: queryParams.pageSize,
  144. performanceDTO:{
  145. jwcode: queryParams.jwcode,
  146. adminMarket: queryParams.adminMarket,
  147. startTime: queryParams.timeRange?.[0] ? dayjs(queryParams.timeRange[0]).format('YYYY-MM-DD HH:mm:ss') : '',
  148. endTime: queryParams.timeRange?.[1] ? dayjs(queryParams.timeRange[1]).format('YYYY-MM-DD HH:mm:ss') : '',
  149. customerMarket: queryParams.customerMarket,
  150. }
  151. }
  152. // TODO: 确认导出接口 URL
  153. const res = await exportPerformance(params)
  154. if(res.code == 200){
  155. console.log('导出参数', params)
  156. ElMessage.success(t('elmessage.exportSuccess'))
  157. }
  158. } catch (error) {
  159. console.error(error)
  160. ElMessage.error(t('elmessage.exportError'))
  161. }
  162. }
  163. // 打开导出列表弹窗
  164. const openExportList = () => {
  165. getExportList()
  166. exportListVisible.value = true
  167. }
  168. // 获取导出列表
  169. const getExportList = async () => {
  170. exportListLoading.value = true
  171. try {
  172. const result = await request({ url: '/export/export' })
  173. if (result.code === 200) {
  174. const filteredData = result.data.filter(item => item.type == 13);
  175. exportList.value = filteredData || []
  176. } else {
  177. ElMessage.error(result.msg || t('elmessage.getExportListError'))
  178. }
  179. } catch (error) {
  180. console.error('获取导出列表出错:', error)
  181. ElMessage.error(t('elmessage.getExportListError'))
  182. } finally {
  183. exportListLoading.value = false
  184. }
  185. }
  186. // 下载导出文件
  187. const downloadExportFile = (item) => {
  188. if (item.state === 2) {
  189. const link = document.createElement('a')
  190. link.href = item.url
  191. link.download = item.fileName
  192. link.click()
  193. } else {
  194. ElMessage.warning(t('elmessage.exportingInProgress'))
  195. }
  196. }
  197. // 根据状态返回对应的标签类型
  198. const getTagType = (state) => {
  199. switch (state) {
  200. case 0: return 'info';
  201. case 1: return 'primary';
  202. case 2: return 'success';
  203. case 3: return 'danger';
  204. default: return 'info';
  205. }
  206. }
  207. // 根据状态返回对应的标签文案
  208. const getTagText = (state) => {
  209. switch (state) {
  210. case 0: return t('elmessage.pendingExecution');
  211. case 1: return t('elmessage.executing');
  212. case 2: return t('elmessage.executed');
  213. case 3: return t('elmessage.errorExecution');
  214. default: return t('elmessage.unknownStatus');
  215. }
  216. }
  217. onMounted(() => {
  218. getMarket()
  219. fetchData()
  220. })
  221. </script>
  222. <template>
  223. <div class="cash-flow-container">
  224. <!-- 搜索区域 -->
  225. <el-card class="search-card">
  226. <div class="search-bar">
  227. <!-- 第一行 -->
  228. <div class="search-row">
  229. <div class="search-item">
  230. <span class="label">{{ t('common.jwcode') }}</span>
  231. <el-input v-model="queryParams.jwcode" :placeholder="t('common.jwcodePlaceholder')" clearable />
  232. </div>
  233. <div class="search-item">
  234. <span class="label">{{ t('common.market') }}</span>
  235. <!-- 下拉多选使用 el-cascader 匹配地区树结构 -->
  236. <el-cascader
  237. v-model="queryParams.customerMarket"
  238. :options="marketOptions"
  239. :props="{ multiple: true, emitPath: false }"
  240. collapse-tags
  241. collapse-tags-tooltip
  242. :placeholder="t('common.marketPlaceholder')"
  243. clearable
  244. style="width: 8vw;"
  245. />
  246. </div>
  247. <div class="search-item" style="width: auto;">
  248. <span class="label">{{ t('common.payTime2') }}</span>
  249. <el-date-picker
  250. v-model="queryParams.timeRange"
  251. type="datetimerange"
  252. :range-separator="t('common.to')"
  253. :start-placeholder="t('common.startTime')"
  254. :end-placeholder="t('common.endTime')"
  255. :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
  256. style="width: 18vw;"
  257. />
  258. </div>
  259. <el-button type="primary" @click="handleSearch">{{ t('common.search') }}</el-button>
  260. <el-button type="primary" @click="handleExport">{{ t('common.exportExcel') }}</el-button>
  261. <el-button type="primary" @click="openExportList">{{ t('common.viewExportList') }}</el-button>
  262. <el-button type="success" @click="handleReset">{{ t('common.reset') }}</el-button>
  263. </div>
  264. </div>
  265. </el-card>
  266. <!-- 表格区域 -->
  267. <el-card class="table-card">
  268. <el-table :data="tableData" v-loading="loading" style="width: 100%; flex: 1;" :cell-style="{ textAlign: 'center' }" :header-cell-style="{ background: '#F3FAFE', color: '#333',textAlign: 'center' }">
  269. <el-table-column type="index" :label="t('common_list.id')" min-width="60" align="center" fixed="left" />
  270. <el-table-column prop="jwcode" :label="t('common_list.jwcode')" min-width="120" fixed="left" />
  271. <el-table-column prop="name" :label="t('common_list.name')" min-width="150" show-overflow-tooltip />
  272. <el-table-column prop="marketName" :label="t('common_list.market')" min-width="120" show-overflow-tooltip />
  273. <el-table-column prop="orderCode" :label="t('common_list.orderCode')" min-width="280" show-overflow-tooltip />
  274. <el-table-column prop="paymentAmount" :label="t('common_list.payAmount')" min-width="150" align="right">
  275. <!-- <template #default="{ row }">
  276. {{ row.paymentAmount }} {{ row.paymentCurrency }}
  277. </template> -->
  278. </el-table-column>
  279. <el-table-column prop="paymentCurrency" :label="t('common_list.payCurrency')" min-width="180" show-overflow-tooltip />
  280. <el-table-column prop="receivedAmount" :label="t('common_list.receiveAmount')" min-width="150" align="right">
  281. <!-- <template #default="{ row }">
  282. {{ row.receivedAmount }} {{ row.receivedCurrency }}
  283. </template> -->
  284. </el-table-column>
  285. <el-table-column prop="receivedCurrency" :label="t('common_list.receiveCurrency')" min-width="180" show-overflow-tooltip />
  286. <el-table-column prop="handlingCharge" :label="t('common_list.fee')" min-width="100" align="right" />
  287. </el-table>
  288. <!-- 分页 -->
  289. <div class="pagination-container">
  290. <el-pagination
  291. background
  292. layout="total, sizes, prev, pager, next, jumper"
  293. :total="total"
  294. :current-page="queryParams.pageNum"
  295. :page-size="queryParams.pageSize"
  296. :page-sizes="[10, 20, 50, 100]"
  297. @size-change="handlePageSizeChange"
  298. @current-change="handleCurrentChange"
  299. />
  300. </div>
  301. </el-card>
  302. <!-- 导出列表弹窗 -->
  303. <el-dialog v-model="exportListVisible" :title="t('common_export.exportList')" width="80%">
  304. <el-table :data="exportList" style="width: 100% ;height: 60vh;" :loading="exportListLoading">
  305. <el-table-column prop="fileName" :label="t('common_export.fileName')" />
  306. <el-table-column prop="state" :label="t('common_export.status')">
  307. <template #default="scope">
  308. <el-tag :type="getTagType(scope.row.state)" :effect="scope.row.state === 3 ? 'light' : 'plain'">
  309. {{ getTagText(scope.row.state) }}
  310. </el-tag>
  311. </template>
  312. </el-table-column>
  313. <el-table-column prop="createTime" :label="t('common_export.createTime')">
  314. <template #default="scope">
  315. {{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
  316. </template>
  317. </el-table-column>
  318. <el-table-column :label="t('common_export.operation')">
  319. <template #default="scope">
  320. <el-button type="primary" size="small" @click="downloadExportFile(scope.row)"
  321. :disabled="scope.row.state !== 2">
  322. {{ t('common_export.download') }}
  323. </el-button>
  324. </template>
  325. </el-table-column>
  326. </el-table>
  327. <template #footer>
  328. <div class="dialog-footer">
  329. <el-button text @click="exportListVisible = false">{{ t('common_export.close') }}</el-button>
  330. </div>
  331. </template>
  332. </el-dialog>
  333. </div>
  334. </template>
  335. <style scoped lang="scss">
  336. .cash-flow-container {
  337. display: flex;
  338. flex-direction: column;
  339. height: 100%;
  340. }
  341. .search-card {
  342. margin-bottom: 10px;
  343. background: #F3FAFE; // 浅蓝背景
  344. border: none;
  345. :deep(.el-card__body) {
  346. padding: 15px;
  347. }
  348. }
  349. .search-bar {
  350. display: flex;
  351. flex-direction: column;
  352. gap: 15px;
  353. }
  354. .search-row {
  355. display: flex;
  356. flex-wrap: wrap;
  357. gap: 20px;
  358. align-items: center;
  359. }
  360. .search-item {
  361. display: flex;
  362. align-items: center;
  363. .label {
  364. white-space: nowrap;
  365. margin-right: 8px;
  366. min-width: 60px;
  367. text-align: right;
  368. }
  369. .el-input, .el-select {
  370. width: 8vw;
  371. }
  372. }
  373. .search-btn-group {
  374. margin-left: auto;
  375. display: flex;
  376. gap: 10px;
  377. }
  378. .table-card {
  379. flex: 1;
  380. display: flex;
  381. flex-direction: column;
  382. overflow: hidden;
  383. border: none;
  384. :deep(.el-card__body) {
  385. height: 100%;
  386. display: flex;
  387. flex-direction: column;
  388. padding: 0;
  389. }
  390. }
  391. .pagination-container {
  392. padding: 15px;
  393. display: flex;
  394. justify-content: flex-end;
  395. background: #fff;
  396. border-top: 1px solid #ebeef5;
  397. }
  398. .dialog-footer {
  399. text-align: right;
  400. }
  401. </style>