OrderManage.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <template>
  2. <div class="order-manage admin-page">
  3. <div class="header">
  4. <h2>订单管理</h2>
  5. <div class="filters">
  6. <el-input v-model="keyword" placeholder="搜索..." prefix-icon="el-icon-search" style="width:200px" clearable @keyup.enter.native="handleSearch" @clear="handleSearch" />
  7. <el-select v-model="statusFilter" placeholder="订单状态" @change="loadList" style="width: 180px;">
  8. <el-option label="全部状态" value="" />
  9. <el-option label="待支付" value="pending" />
  10. <el-option label="已支付(待发货)" value="paid" />
  11. <el-option label="待收货" value="pending_receipt" />
  12. <el-option label="已完成" value="completed" />
  13. <el-option label="已取消" value="cancelled" />
  14. <el-option label="退款中" value="refunding" />
  15. <el-option label="已退款" value="refunded" />
  16. <el-option label="已关闭" value="closed" />
  17. </el-select>
  18. </div>
  19. </div>
  20. <el-table :data="list" v-loading="loading" border stripe>
  21. <el-table-column prop="orderNo" label="订单号" width="180" />
  22. <el-table-column label="商品" min-width="160">
  23. <template slot-scope="{ row }">
  24. <div style="display:flex;align-items:center;gap:8px;">
  25. <img v-if="row.coverImage" :src="row.coverImage" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
  26. <span>{{ row.productName || '-' }}</span>
  27. </div>
  28. </template>
  29. </el-table-column>
  30. <el-table-column prop="quantity" label="数量" width="60" align="center" />
  31. <el-table-column label="金额" width="100">
  32. <template slot-scope="{ row }">
  33. {{ formatPrice(row.totalAmount) }}
  34. </template>
  35. </el-table-column>
  36. <el-table-column label="状态" width="100">
  37. <template slot-scope="{ row }">
  38. <el-tag :type="statusType(row.status)">{{ statusLabel(row.status) }}</el-tag>
  39. </template>
  40. </el-table-column>
  41. <el-table-column prop="paymentMethod" label="支付方式" width="100">
  42. <template slot-scope="{ row }">
  43. {{ payMethodLabel(row.paymentMethod) }}
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="下单时间" width="155">
  47. <template slot-scope="{ row }">
  48. {{ formatTime(row.createdAt) }}
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="操作" width="300" fixed="right">
  52. <template slot-scope="{ row }">
  53. <el-button type="primary" size="small" @click="viewDetail(row)">详情</el-button>
  54. <el-button type="success" size="small" @click="showShipDialog(row)" v-if="row.status === 'paid'">发货</el-button>
  55. <el-button type="danger" size="small" @click="handleClose(row)" v-if="row.status === 'paid' || row.status === 'refunding'">关闭</el-button>
  56. <el-button type="warning" size="small" @click="showRefundDialog(row)" v-if="row.status === 'refunding'">退款审核</el-button>
  57. </template>
  58. </el-table-column>
  59. </el-table>
  60. <el-pagination
  61. @size-change="onSizeChange"
  62. @current-change="onPageChange"
  63. :current-page="page"
  64. :page-size="size"
  65. :total="total"
  66. layout="total, sizes, prev, pager, next"
  67. :page-sizes="[10, 20, 50]"
  68. style="margin-top: 20px; text-align: right;"
  69. />
  70. <!-- Detail Dialog -->
  71. <el-dialog title="订单详情" :visible.sync="detailDialogVisible" width="700px" top="5vh">
  72. <template v-if="detail">
  73. <el-descriptions :column="2" border>
  74. <el-descriptions-item label="订单编号" :span="2">{{ detail.orderNo }}</el-descriptions-item>
  75. <el-descriptions-item label="商品名称">{{ detail.productName || '-' }}</el-descriptions-item>
  76. <el-descriptions-item label="商品类型">{{ typeLabel(detail.productType) }}</el-descriptions-item>
  77. <el-descriptions-item label="数量">{{ detail.quantity }}</el-descriptions-item>
  78. <el-descriptions-item label="单价">{{ formatPrice(detail.unitPrice) }}</el-descriptions-item>
  79. <el-descriptions-item label="总金额">{{ formatPrice(detail.totalAmount) }}</el-descriptions-item>
  80. <el-descriptions-item label="优惠金额">{{ detail.discountAmount ? formatPrice(detail.discountAmount) : '-' }}</el-descriptions-item>
  81. <el-descriptions-item label="订单状态">
  82. <el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
  83. </el-descriptions-item>
  84. <el-descriptions-item label="支付方式">{{ payMethodLabel(detail.paymentMethod) }}</el-descriptions-item>
  85. <el-descriptions-item label="支付时间">{{ detail.paidAt ? formatTime(detail.paidAt) : '-' }}</el-descriptions-item>
  86. <el-descriptions-item label="买家备注">{{ detail.remark || '-' }}</el-descriptions-item>
  87. <el-descriptions-item label="下单时间">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
  88. </el-descriptions>
  89. </template>
  90. <span slot="footer">
  91. <el-button @click="detailDialogVisible = false">关闭</el-button>
  92. </span>
  93. </el-dialog>
  94. <!-- Ship Dialog -->
  95. <el-dialog title="发货" :visible.sync="shipDialogVisible" width="450px">
  96. <el-alert title="确认给以下订单发货" type="info" :closable="false" style="margin-bottom:15px;">
  97. <template slot>
  98. 订单号:<strong>{{ shipTarget?.orderNo }}</strong>
  99. </template>
  100. </el-alert>
  101. <el-form :model="shipForm" label-width="90px">
  102. <el-form-item label="物流公司">
  103. <el-input v-model="shipForm.expressCompany" placeholder="如:顺丰、中通" />
  104. </el-form-item>
  105. <el-form-item label="物流单号">
  106. <el-input v-model="shipForm.trackingNumber" placeholder="请输入物流单号" />
  107. </el-form-item>
  108. </el-form>
  109. <span slot="footer">
  110. <el-button @click="shipDialogVisible = false">取消</el-button>
  111. <el-button type="primary" @click="confirmShip" :loading="shipLoading">确认发货</el-button>
  112. </span>
  113. </el-dialog>
  114. <!-- Refund Dialog -->
  115. <el-dialog title="退款审核" :visible.sync="refundDialogVisible" width="450px">
  116. <template v-if="refundTarget">
  117. <el-alert title="退款申请" type="warning" :closable="false" style="margin-bottom:15px;">
  118. <template slot>
  119. 订单号:<strong>{{ refundTarget.orderNo }}</strong><br>
  120. 退款原因:{{ refundTarget.refundReason || '-' }}
  121. </template>
  122. </el-alert>
  123. <el-input v-model="refundRejectReason" type="textarea" placeholder="拒绝原因(选填)" :rows="3" />
  124. </template>
  125. <span slot="footer">
  126. <el-button @click="refundDialogVisible = false">取消</el-button>
  127. <el-button type="danger" @click="handleRejectRefund" :loading="refundLoading">拒绝退款</el-button>
  128. <el-button type="success" @click="handleApproveRefundConfirm" :loading="refundLoading">同意退款</el-button>
  129. </span>
  130. </el-dialog>
  131. </div>
  132. </template>
  133. <script>
  134. import { getOrderList, shipOrder, closeOrder, approveRefund, rejectRefund } from '@/api/admin'
  135. export default {
  136. data() {
  137. return {
  138. list: [],
  139. loading: false,
  140. page: 1,
  141. size: 20,
  142. total: 0,
  143. statusFilter: '',
  144. keyword: '',
  145. detailDialogVisible: false,
  146. detail: null,
  147. shipDialogVisible: false,
  148. shipTarget: null,
  149. shipLoading: false,
  150. shipForm: { expressCompany: '', trackingNumber: '' },
  151. refundDialogVisible: false,
  152. refundTarget: null,
  153. refundLoading: false,
  154. refundRejectReason: ''
  155. }
  156. },
  157. mounted() {
  158. this.loadList()
  159. },
  160. methods: {
  161. handleSearch() {
  162. this.page = 1
  163. this.loadList()
  164. },
  165. async loadList() {
  166. this.loading = true
  167. try {
  168. var params = { page: this.page, size: this.size }
  169. if (this.statusFilter) {
  170. params.status = this.statusFilter
  171. }
  172. if (this.keyword) {
  173. params.keyword = this.keyword
  174. }
  175. var res = await getOrderList(params)
  176. this.list = res.data.records || []
  177. this.total = res.data.total || 0
  178. this.page = res.data.page || this.page
  179. this.size = res.data.size || this.size
  180. } catch (e) {
  181. this.$message.error('加载订单列表失败')
  182. } finally {
  183. this.loading = false
  184. }
  185. },
  186. onPageChange(val) {
  187. this.page = val
  188. this.loadList()
  189. },
  190. onSizeChange(val) {
  191. this.size = val
  192. this.page = 1
  193. this.loadList()
  194. },
  195. viewDetail(row) {
  196. this.detail = row
  197. this.detailDialogVisible = true
  198. },
  199. showShipDialog(row) {
  200. this.shipTarget = row
  201. this.shipForm = { expressCompany: '', trackingNumber: '' }
  202. this.shipDialogVisible = true
  203. },
  204. async confirmShip() {
  205. if (!this.shipForm.trackingNumber.trim()) {
  206. return this.$message.warning('请填写物流单号')
  207. }
  208. this.shipLoading = true
  209. try {
  210. await shipOrder(this.shipTarget.orderNo, this.shipForm.trackingNumber, this.shipForm.expressCompany)
  211. this.$message.success('发货成功')
  212. this.shipDialogVisible = false
  213. this.loadList()
  214. } catch (e) {
  215. this.$message.error(e.message || '发货失败')
  216. } finally {
  217. this.shipLoading = false
  218. }
  219. },
  220. async handleClose(row) {
  221. try {
  222. await this.$confirm('确定关闭订单 ' + row.orderNo + '?', '提示', { type: 'warning' })
  223. await closeOrder(row.orderNo)
  224. this.$message.success('订单已关闭')
  225. this.loadList()
  226. } catch (e) {
  227. if (e !== 'cancel') {
  228. this.$message.error(e.message || '关闭失败')
  229. }
  230. }
  231. },
  232. showRefundDialog(row) {
  233. this.refundTarget = row
  234. this.refundRejectReason = ''
  235. this.refundDialogVisible = true
  236. },
  237. async handleApproveRefundConfirm() {
  238. this.refundLoading = true
  239. try {
  240. await this.$confirm('确认同意退款?', '提示', { type: 'warning' })
  241. await approveRefund(this.refundTarget.orderNo)
  242. this.$message.success('已同意退款')
  243. this.refundDialogVisible = false
  244. this.loadList()
  245. } catch (e) {
  246. if (e !== 'cancel') {
  247. this.$message.error(e.message || '操作失败')
  248. }
  249. } finally {
  250. this.refundLoading = false
  251. }
  252. },
  253. async handleRejectRefund() {
  254. this.refundLoading = true
  255. try {
  256. await this.$confirm('确认拒绝退款?', '提示', { type: 'warning' })
  257. await rejectRefund(this.refundTarget.orderNo, this.refundRejectReason)
  258. this.$message.success('已拒绝退款')
  259. this.refundDialogVisible = false
  260. this.loadList()
  261. } catch (e) {
  262. if (e !== 'cancel') {
  263. this.$message.error(e.message || '操作失败')
  264. }
  265. } finally {
  266. this.refundLoading = false
  267. }
  268. },
  269. canClose(status) {
  270. return ['pending', 'paid', 'pending_receipt', 'refunding'].includes(status)
  271. },
  272. statusType(status) {
  273. var map = { pending: 'warning', paid: 'primary', pending_receipt: '', completed: 'success', cancelled: 'info', refunding: 'danger', refunded: 'danger', closed: 'info' }
  274. return map[status] || 'info'
  275. },
  276. statusLabel(status) {
  277. var map = { pending: '待支付', paid: '待发货', pending_receipt: '待收货', completed: '已完成', cancelled: '已取消', refunding: '退款中', refunded: '已退款', closed: '已关闭' }
  278. return map[status] || status
  279. },
  280. payMethodLabel(method) {
  281. var map = { wechat: '微信支付', balance: '余额支付', points: '积分兑换', test: '测试支付' }
  282. return map[method] || method || '-'
  283. },
  284. typeLabel(type) {
  285. var map = { physical: '实物商品', virtual: '虚拟商品', coupon: '优惠券' }
  286. return map[type] || type || '-'
  287. },
  288. formatPrice(cents) {
  289. if (cents == null) return '-'
  290. return '¥' + (cents / 100).toFixed(2)
  291. },
  292. formatTime(time) {
  293. if (!time) return '-'
  294. return new Date(time).toLocaleString()
  295. }
  296. }
  297. }
  298. </script>
  299. <style scoped>
  300. .order-manage { padding: 20px; }
  301. .header {
  302. display: flex;
  303. justify-content: space-between;
  304. align-items: center;
  305. margin-bottom: 20px;
  306. flex-wrap: wrap;
  307. }
  308. .header h2 { margin: 0; }
  309. .filters {
  310. display: flex;
  311. gap: 10px;
  312. align-items: center;
  313. }
  314. </style>