DanSettlement.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <template>
  2. <div class="dan-settlement admin-page">
  3. <!-- 统计卡片 -->
  4. <el-row :gutter="20" style="margin-bottom: 20px;">
  5. <el-col :span="8">
  6. <el-card shadow="hover">
  7. <div class="stat-card">
  8. <div class="stat-icon-wrap total">
  9. <i class="el-icon-money"></i>
  10. </div>
  11. <div class="stat-info">
  12. <div class="stat-label">总结算金额</div>
  13. <div class="stat-value">¥{{ (stats.totalAmount / 100).toFixed(2) }}</div>
  14. <div class="stat-desc">共 {{ stats.totalCount }} 笔</div>
  15. </div>
  16. </div>
  17. </el-card>
  18. </el-col>
  19. <el-col :span="8">
  20. <el-card shadow="hover">
  21. <div class="stat-card">
  22. <div class="stat-icon-wrap pending">
  23. <i class="el-icon-time"></i>
  24. </div>
  25. <div class="stat-info">
  26. <div class="stat-label">待结算金额</div>
  27. <div class="stat-value pending">¥{{ (stats.pendingAmount / 100).toFixed(2) }}</div>
  28. <div class="stat-desc">共 {{ stats.pendingCount }} 笔</div>
  29. </div>
  30. </div>
  31. </el-card>
  32. </el-col>
  33. <el-col :span="8">
  34. <el-card shadow="hover">
  35. <div class="stat-card">
  36. <div class="stat-icon-wrap settled">
  37. <i class="el-icon-circle-check"></i>
  38. </div>
  39. <div class="stat-info">
  40. <div class="stat-label">已结算金额</div>
  41. <div class="stat-value settled">¥{{ (stats.settledAmount / 100).toFixed(2) }}</div>
  42. <div class="stat-desc">共 {{ stats.settledCount }} 笔</div>
  43. </div>
  44. </div>
  45. </el-card>
  46. </el-col>
  47. </el-row>
  48. <!-- 结算列表 -->
  49. <el-card>
  50. <div slot="header" class="admin-page-header">
  51. <span class="admin-page-title">结算记录管理</span>
  52. <div class="filter-bar">
  53. <el-select v-model="statusFilter" placeholder="结算状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
  54. <el-option label="全部状态" value="" />
  55. <el-option label="待结算" value="pending" />
  56. <el-option label="已结算" value="settled" />
  57. <el-option label="结算失败" value="failed" />
  58. </el-select>
  59. <el-select v-model="providerTypeFilter" placeholder="服务类型" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
  60. <el-option label="全部类型" value="" />
  61. <el-option label="评估师" value="assessor" />
  62. <el-option label="规划师" value="planner" />
  63. </el-select>
  64. <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
  65. </div>
  66. </div>
  67. <el-table :data="list" v-loading="loading" border stripe empty-text="暂无结算记录">
  68. <el-table-column label="ID" prop="id" width="80" />
  69. <el-table-column label="执行ID" prop="executionId" width="100" />
  70. <el-table-column label="订单ID" prop="assessmentOrderId" width="100" />
  71. <el-table-column label="服务方ID" prop="providerId" width="100" />
  72. <el-table-column label="服务类型" width="110">
  73. <template slot-scope="{ row }">
  74. <div class="status-cell">
  75. <i :class="row.providerType === 'assessor' ? 'el-icon-user' : 'el-icon-s-custom'"
  76. :style="{ color: row.providerType === 'assessor' ? '#409EFF' : '#67c23a' }"></i>
  77. <el-tag :type="row.providerType === 'assessor' ? '' : 'success'" size="mini">
  78. {{ row.providerType === 'assessor' ? '评估师' : '规划师' }}
  79. </el-tag>
  80. </div>
  81. </template>
  82. </el-table-column>
  83. <el-table-column label="服务费" width="100">
  84. <template slot-scope="{ row }">
  85. ¥{{ (row.serviceFee / 100).toFixed(2) }}
  86. </template>
  87. </el-table-column>
  88. <el-table-column label="佣金比例" width="100">
  89. <template slot-scope="{ row }">
  90. {{ row.commissionRate }}%
  91. </template>
  92. </el-table-column>
  93. <el-table-column label="结算金额" width="120">
  94. <template slot-scope="{ row }">
  95. <span style="font-weight: bold; color: #409EFF;">
  96. ¥{{ (row.commissionAmount / 100).toFixed(2) }}
  97. </span>
  98. </template>
  99. </el-table-column>
  100. <el-table-column label="结算状态" width="130">
  101. <template slot-scope="{ row }">
  102. <div class="status-cell">
  103. <i :class="statusIconClass(row.status)" :style="{ color: statusIconColor(row.status) }"></i>
  104. <el-tag :type="statusType(row.status)" size="mini">
  105. {{ statusLabel(row.status) }}
  106. </el-tag>
  107. </div>
  108. </template>
  109. </el-table-column>
  110. <el-table-column label="结算时间" width="160">
  111. <template slot-scope="{ row }">{{ formatTime(row.settledAt) }}</template>
  112. </el-table-column>
  113. <el-table-column label="交易ID" prop="transactionId" min-width="150" show-overflow-tooltip />
  114. <el-table-column label="创建时间" width="160">
  115. <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
  116. </el-table-column>
  117. <el-table-column label="操作" width="150" fixed="right">
  118. <template slot-scope="{ row }">
  119. <el-button
  120. v-if="row.status === 'pending'"
  121. size="mini"
  122. type="success"
  123. icon="el-icon-check"
  124. plain
  125. @click="confirmSettlement(row)"
  126. >确认结算</el-button>
  127. <el-button size="mini" type="primary" icon="el-icon-view" plain @click="viewDetail(row)">详情</el-button>
  128. </template>
  129. </el-table-column>
  130. </el-table>
  131. <el-pagination
  132. @size-change="handleSizeChange"
  133. @current-change="handleCurrentChange"
  134. :current-page="page"
  135. :page-sizes="[10, 20, 50]"
  136. :page-size="size"
  137. :total="total"
  138. layout="total, sizes, prev, pager, next, jumper"
  139. class="pagination-wrap"
  140. />
  141. </el-card>
  142. <!-- 详情 Dialog -->
  143. <el-dialog title="结算详情" :visible.sync="detailDialogVisible" width="600px">
  144. <div v-if="currentRecord" v-loading="detailLoading">
  145. <el-descriptions :column="2" border size="small">
  146. <el-descriptions-item label="结算ID">{{ currentRecord.id }}</el-descriptions-item>
  147. <el-descriptions-item label="执行ID">{{ currentRecord.executionId }}</el-descriptions-item>
  148. <el-descriptions-item label="订单ID">{{ currentRecord.assessmentOrderId }}</el-descriptions-item>
  149. <el-descriptions-item label="服务方ID">{{ currentRecord.providerId }}</el-descriptions-item>
  150. <el-descriptions-item label="服务类型">
  151. <div class="status-cell">
  152. <i :class="currentRecord.providerType === 'assessor' ? 'el-icon-user' : 'el-icon-s-custom'"
  153. :style="{ color: currentRecord.providerType === 'assessor' ? '#409EFF' : '#67c23a' }"></i>
  154. <el-tag :type="currentRecord.providerType === 'assessor' ? '' : 'success'" size="mini">
  155. {{ currentRecord.providerType === 'assessor' ? '评估师' : '规划师' }}
  156. </el-tag>
  157. </div>
  158. </el-descriptions-item>
  159. <el-descriptions-item label="服务费">¥{{ (currentRecord.serviceFee / 100).toFixed(2) }}</el-descriptions-item>
  160. <el-descriptions-item label="佣金比例">{{ currentRecord.commissionRate }}%</el-descriptions-item>
  161. <el-descriptions-item label="结算金额">
  162. <span style="font-weight: bold; color: #409EFF;">
  163. ¥{{ (currentRecord.commissionAmount / 100).toFixed(2) }}
  164. </span>
  165. </el-descriptions-item>
  166. <el-descriptions-item label="结算状态">
  167. <div class="status-cell">
  168. <i :class="statusIconClass(currentRecord.status)" :style="{ color: statusIconColor(currentRecord.status) }"></i>
  169. <el-tag :type="statusType(currentRecord.status)" size="mini">
  170. {{ statusLabel(currentRecord.status) }}
  171. </el-tag>
  172. </div>
  173. </el-descriptions-item>
  174. <el-descriptions-item label="结算时间">{{ formatTime(currentRecord.settledAt) }}</el-descriptions-item>
  175. <el-descriptions-item label="交易ID">{{ currentRecord.transactionId || '-' }}</el-descriptions-item>
  176. <el-descriptions-item label="创建时间">{{ formatTime(currentRecord.createdAt) }}</el-descriptions-item>
  177. <el-descriptions-item label="更新时间">{{ formatTime(currentRecord.updatedAt) }}</el-descriptions-item>
  178. </el-descriptions>
  179. </div>
  180. </el-dialog>
  181. </div>
  182. </template>
  183. <script>
  184. export default {
  185. name: 'DanSettlement',
  186. data() {
  187. return {
  188. list: [],
  189. loading: false,
  190. page: 1,
  191. size: 10,
  192. total: 0,
  193. statusFilter: '',
  194. providerTypeFilter: '',
  195. stats: {
  196. totalAmount: 0,
  197. pendingAmount: 0,
  198. settledAmount: 0,
  199. totalCount: 0,
  200. pendingCount: 0,
  201. settledCount: 0
  202. },
  203. detailDialogVisible: false,
  204. detailLoading: false,
  205. currentRecord: null
  206. }
  207. },
  208. created() {
  209. this.loadData()
  210. this.loadStats()
  211. },
  212. methods: {
  213. async loadData() {
  214. this.loading = true
  215. try {
  216. const res = await this.$axios.post('/api/admin/dan-settlement/list', {
  217. status: this.statusFilter,
  218. providerType: this.providerTypeFilter
  219. })
  220. if (res.data.code === 200) {
  221. this.list = res.data.data || []
  222. this.total = this.list.length
  223. } else {
  224. this.$message.error(res.data.message || '加载失败')
  225. }
  226. } catch (e) {
  227. console.error('加载结算记录失败', e)
  228. this.$message.error('加载失败')
  229. } finally {
  230. this.loading = false
  231. }
  232. },
  233. async loadStats() {
  234. try {
  235. const res = await this.$axios.post('/api/admin/dan-settlement/stats')
  236. if (res.data.code === 200) {
  237. this.stats = res.data.data || this.stats
  238. }
  239. } catch (e) {
  240. console.error('加载统计数据失败', e)
  241. }
  242. },
  243. async viewDetail(row) {
  244. this.detailDialogVisible = true
  245. this.detailLoading = true
  246. try {
  247. const res = await this.$axios.get(`/api/admin/dan-settlement/detail/${row.id}`)
  248. if (res.data.code === 200) {
  249. this.currentRecord = res.data.data
  250. } else {
  251. this.$message.error(res.data.message || '加载失败')
  252. }
  253. } catch (e) {
  254. console.error('加载详情失败', e)
  255. this.$message.error('加载失败')
  256. } finally {
  257. this.detailLoading = false
  258. }
  259. },
  260. async confirmSettlement(row) {
  261. try {
  262. await this.$confirm('确认将此结算记录标记为已结算?', '提示', {
  263. confirmButtonText: '确定',
  264. cancelButtonText: '取消',
  265. type: 'warning'
  266. })
  267. const res = await this.$axios.post(`/api/admin/dan-settlement/confirm/${row.id}`)
  268. if (res.data.code === 200) {
  269. this.$message.success('确认成功')
  270. this.loadData()
  271. this.loadStats()
  272. } else {
  273. this.$message.error(res.data.message || '操作失败')
  274. }
  275. } catch (e) {
  276. if (e !== 'cancel') {
  277. console.error('确认结算失败', e)
  278. this.$message.error('操作失败')
  279. }
  280. }
  281. },
  282. handleSearch() {
  283. this.page = 1
  284. this.loadData()
  285. },
  286. handleSizeChange(val) {
  287. this.size = val
  288. this.loadData()
  289. },
  290. handleCurrentChange(val) {
  291. this.page = val
  292. this.loadData()
  293. },
  294. formatTime(time) {
  295. if (!time) return '-'
  296. return new Date(time).toLocaleString('zh-CN')
  297. },
  298. statusType(status) {
  299. if (status === 'settled') return 'success'
  300. if (status === 'pending') return 'warning'
  301. if (status === 'failed') return 'danger'
  302. return 'info'
  303. },
  304. statusLabel(status) {
  305. if (status === 'settled') return '已结算'
  306. if (status === 'pending') return '待结算'
  307. if (status === 'failed') return '失败'
  308. return status || '-'
  309. },
  310. statusIconClass(status) {
  311. if (status === 'settled') return 'el-icon-circle-check'
  312. if (status === 'pending') return 'el-icon-time'
  313. if (status === 'failed') return 'el-icon-circle-close'
  314. return 'el-icon-info'
  315. },
  316. statusIconColor(status) {
  317. if (status === 'settled') return '#67c23a'
  318. if (status === 'pending') return '#e6a23c'
  319. if (status === 'failed') return '#f56c6c'
  320. return '#909399'
  321. }
  322. }
  323. }
  324. </script>
  325. <style scoped>
  326. .admin-page {
  327. padding: 20px;
  328. }
  329. .admin-page-header {
  330. display: flex;
  331. justify-content: space-between;
  332. align-items: center;
  333. }
  334. .admin-page-title {
  335. font-size: 18px;
  336. font-weight: bold;
  337. }
  338. .filter-bar {
  339. display: flex;
  340. align-items: center;
  341. }
  342. .stat-card {
  343. display: flex;
  344. align-items: center;
  345. padding: 10px 0;
  346. }
  347. .stat-icon-wrap {
  348. width: 56px;
  349. height: 56px;
  350. border-radius: 14px;
  351. display: flex;
  352. align-items: center;
  353. justify-content: center;
  354. margin-right: 16px;
  355. flex-shrink: 0;
  356. }
  357. .stat-icon-wrap i {
  358. font-size: 28px;
  359. color: #fff;
  360. }
  361. .stat-icon-wrap.total {
  362. background: linear-gradient(135deg, #409EFF, #2d8cf0);
  363. }
  364. .stat-icon-wrap.pending {
  365. background: linear-gradient(135deg, #e6a23c, #d4941c);
  366. }
  367. .stat-icon-wrap.settled {
  368. background: linear-gradient(135deg, #67c23a, #4fa62c);
  369. }
  370. .stat-info {
  371. flex: 1;
  372. }
  373. .stat-label {
  374. font-size: 13px;
  375. color: #909399;
  376. margin-bottom: 6px;
  377. }
  378. .stat-value {
  379. font-size: 24px;
  380. font-weight: bold;
  381. color: #303133;
  382. margin-bottom: 4px;
  383. }
  384. .stat-value.pending {
  385. color: #e6a23c;
  386. }
  387. .stat-value.settled {
  388. color: #67c23a;
  389. }
  390. .stat-desc {
  391. font-size: 12px;
  392. color: #909399;
  393. }
  394. .status-cell {
  395. display: flex;
  396. align-items: center;
  397. gap: 6px;
  398. }
  399. .status-cell i {
  400. font-size: 16px;
  401. }
  402. .pagination-wrap {
  403. margin-top: 20px;
  404. text-align: right;
  405. }
  406. </style>