PendingRefund.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <template>
  2. <div class="pending-refund admin-page">
  3. <div class="header admin-page-header">
  4. <h2 class="admin-page-title">待退款列表</h2>
  5. <div class="flex items-center gap-sm">
  6. <el-select v-model="statusFilter" placeholder="退款状态" @change="loadList">
  7. <el-option label="全部状态" value="" />
  8. <el-option label="待处理" value="pending" />
  9. <el-option label="处理中" value="processing" />
  10. <el-option label="已成功" value="success" />
  11. <el-option label="已失败" value="failed" />
  12. </el-select>
  13. <el-select v-model="typeFilter" placeholder="订单类型" @change="loadList">
  14. <el-option label="全部类型" value="" />
  15. <el-option label="提现退款" value="withdrawal" />
  16. <el-option label="商品退款" value="product" />
  17. </el-select>
  18. </div>
  19. </div>
  20. <div v-if="summary.totalPendingAmount > 0" class="summary-bar">
  21. <el-alert
  22. :title="'待退款总金额:¥' + (summary.totalPendingAmount / 100).toFixed(2) + '(' + summary.pendingCount + ' 笔待处理)'"
  23. type="warning"
  24. :closable="false"
  25. show-icon
  26. />
  27. </div>
  28. <div class="adm-table-wrap">
  29. <el-table :data="list" v-loading="loading" border stripe height="100%">
  30. <el-table-column prop="id" label="ID" width="70">
  31. <template slot="header">
  32. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
  33. </template>
  34. </el-table-column>
  35. <el-table-column label="类型" width="100">
  36. <template slot-scope="{ row }">
  37. <el-tag :type="row.orderType === 'product' ? 'primary' : 'warning'" size="small">
  38. {{ row.orderType === 'product' ? '商品退款' : '提现退款' }}
  39. </el-tag>
  40. </template>
  41. </el-table-column>
  42. <el-table-column label="订单号" width="180">
  43. <template slot-scope="{ row }">
  44. {{ row.orderNo || '-' }}
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="提现申请ID" width="100">
  48. <template slot-scope="{ row }">
  49. {{ row.withdrawalRequestId || '-' }}
  50. </template>
  51. </el-table-column>
  52. <el-table-column label="用户" width="120">
  53. <template slot-scope="{ row }">
  54. {{ row.userName || '用户' + row.userId }}
  55. </template>
  56. </el-table-column>
  57. <el-table-column label="退款金额" width="120">
  58. <template slot-scope="{ row }">
  59. <span class="amount">{{ formatPriceWithSymbol(row.amount) }}</span>
  60. </template>
  61. </el-table-column>
  62. <el-table-column label="实退金额" width="120">
  63. <template slot-scope="{ row }">
  64. <span class="amount">{{ row.refundedAmount ? formatPriceWithSymbol(row.refundedAmount) : '-' }}</span>
  65. </template>
  66. </el-table-column>
  67. <el-table-column label="状态" width="100">
  68. <template slot-scope="{ row }">
  69. <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
  70. </template>
  71. </el-table-column>
  72. <el-table-column label="尝试次数" width="80">
  73. <template slot-scope="{ row }">
  74. {{ row.attemptCount }}/{{ row.maxAttempts }}
  75. </template>
  76. </el-table-column>
  77. <el-table-column label="失败原因" min-width="200" show-overflow-tooltip>
  78. <template slot-scope="{ row }">
  79. {{ row.failureReason || '-' }}
  80. </template>
  81. </el-table-column>
  82. <el-table-column label="创建时间" width="160">
  83. <template slot-scope="{ row }">
  84. {{ formatTime(row.createdAt) }}
  85. </template>
  86. <template slot="header">
  87. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">创建时间{{ sortIcon('createdAt') }}</span>
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="退款时间" width="160">
  91. <template slot-scope="{ row }">
  92. {{ row.refundedAt ? formatTime(row.refundedAt) : '-' }}
  93. </template>
  94. </el-table-column>
  95. <el-table-column label="操作" width="120" fixed="right">
  96. <template slot-scope="{ row }">
  97. <el-button v-if="row.status === 'pending' || row.status === 'processing'" type="danger" size="mini" @click="handleCancel(row)">
  98. 中止退款
  99. </el-button>
  100. </template>
  101. </el-table-column>
  102. </el-table>
  103. </div>
  104. <div class="pagination-wrap">
  105. <el-pagination
  106. background
  107. layout="total, prev, pager, next"
  108. :total="total"
  109. :page-size="pageSize"
  110. :current-page.sync="currentPage"
  111. @current-change="handlePageChange"
  112. />
  113. </div>
  114. </div>
  115. </template>
  116. <script>
  117. import { getPendingRefundList, getPendingRefundSummary, cancelPendingRefund, searchUsers } from '@/api/admin'
  118. import SortMixin from '@/mixins/SortMixin'
  119. export default {
  120. name: 'PendingRefund',
  121. mixins: [SortMixin],
  122. computed: {
  123. tableHeight() {
  124. return window.innerHeight - 300
  125. }
  126. },
  127. data() {
  128. return {
  129. list: [],
  130. sortableColumns: { id: 'ID', createdAt: '创建时间' },
  131. loading: false,
  132. statusFilter: '',
  133. typeFilter: '',
  134. currentPage: 1,
  135. pageSize: 20,
  136. total: 0,
  137. summary: {
  138. totalPendingAmount: 0,
  139. pendingCount: 0
  140. }
  141. }
  142. },
  143. created() {
  144. this.loadSummary()
  145. this.loadList()
  146. },
  147. methods: {
  148. async loadSummary() {
  149. try {
  150. const res = await getPendingRefundSummary()
  151. if (res.code === 200) {
  152. this.summary = res.data || { totalPendingAmount: 0 }
  153. }
  154. } catch (e) {
  155. console.error('加载待退款摘要失败', e)
  156. }
  157. },
  158. async loadList() {
  159. this.loading = true
  160. try {
  161. const reqParams = {
  162. status: this.statusFilter || undefined,
  163. orderType: this.typeFilter || undefined,
  164. page: this.currentPage,
  165. size: this.pageSize
  166. }
  167. if (this.sortState.length > 0) reqParams.sort = this.sortState
  168. const res = await getPendingRefundList(reqParams)
  169. if (res.code === 200) {
  170. this.list = res.data.records || []
  171. this.total = res.data.total || 0
  172. await this.resolveUserNames(this.list, 'userId')
  173. }
  174. } catch (e) {
  175. console.error('加载待退款列表失败', e)
  176. } finally {
  177. this.loading = false
  178. }
  179. },
  180. resolveUserNames(items, idField) {
  181. if (!items || items.length === 0) return Promise.resolve()
  182. var ids = []
  183. items.forEach(function(item) {
  184. if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
  185. })
  186. var nameMap = {}
  187. return Promise.all(ids.map(function(id) {
  188. return searchUsers({ keyword: String(id) })
  189. .then(function(res) {
  190. if (res.data && res.data.length > 0) {
  191. var u = res.data[0]
  192. nameMap[id] = u.nickname || u.realName || '用户' + id
  193. } else {
  194. nameMap[id] = '用户' + id
  195. }
  196. })
  197. .catch(function() {
  198. nameMap[id] = '用户' + id
  199. })
  200. })).then(function() {
  201. items.forEach(function(item) {
  202. item.userName = nameMap[item[idField]] || '用户' + item[idField]
  203. })
  204. })
  205. },
  206. handlePageChange(page) {
  207. this.currentPage = page
  208. this.loadList()
  209. },
  210. statusType(status) {
  211. const map = { pending: 'warning', processing: 'primary', success: 'success', failed: 'danger', cancelled: 'info' }
  212. return map[status] || 'info'
  213. },
  214. statusLabel(status) {
  215. const map = { pending: '待处理', processing: '处理中', success: '已成功', failed: '已失败', cancelled: '已中止' }
  216. return map[status] || status
  217. },
  218. async handleCancel(row) {
  219. try {
  220. await this.$confirm('确认中止该退款?订单将回到退款审核状态。', '确认中止', { type: 'warning' })
  221. const res = await cancelPendingRefund({ refundId: row.id })
  222. if (res.code === 200) {
  223. this.$message.success('已中止退款')
  224. this.loadList()
  225. this.loadSummary()
  226. } else {
  227. this.$message.error(res.message || '操作失败')
  228. }
  229. } catch (e) {
  230. if (e !== 'cancel') {
  231. console.error('中止退款失败', e)
  232. }
  233. }
  234. },
  235. formatTime(time) {
  236. if (!time) return '-'
  237. const d = new Date(time)
  238. const pad = n => n < 10 ? '0' + n : n
  239. return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
  240. ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
  241. },
  242. formatPriceWithSymbol(amount) {
  243. if (amount == null) return '-'
  244. return '¥' + (amount / 100).toFixed(2)
  245. }
  246. }
  247. }
  248. </script>
  249. <style scoped>
  250. .summary-bar {
  251. margin-bottom: 16px;
  252. }
  253. .sort-header {
  254. cursor: pointer;
  255. user-select: none;
  256. font-weight: 500;
  257. font-size: 12px;
  258. }
  259. .sort-header:hover {
  260. color: #409eff;
  261. }
  262. </style>