| | |
| | | <script setup> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { ElMessage } from 'element-plus' |
| | | import { Refresh, Search } from '@element-plus/icons-vue' |
| | | import { getOrders, getOrderScenicOptions, refundOrder } from '@/api/orders' |
| | | import { |
| | |
| | | const total = ref(0) |
| | | const scenicOptions = ref([]) |
| | | const dateRange = ref([]) |
| | | const refundDialogVisible = ref(false) |
| | | const refundSubmitting = ref(false) |
| | | const refundOrderRecord = ref(null) |
| | | const refundForm = reactive({ amount: '' }) |
| | | const visitorId = Number(route.query.visitorId) || undefined |
| | | const guideId = Number(route.query.guideId) || undefined |
| | | const query = reactive({ page: 1, size: 10, keyword: '', scenicName: null, status: null }) |
| | | const paymentMethodMap = { WECHAT: '微信支付', ALIPAY: '支付宝' } |
| | | const refundStatusMap = { |
| | | REFUNDING: '\u9000\u6b3e\u5904\u7406\u4e2d', |
| | | PARTIAL: '\u90e8\u5206\u9000\u6b3e', |
| | | REFUND_FAILED: '\u9000\u6b3e\u5931\u8d25', |
| | | } |
| | | const refundStatusTypeMap = { REFUNDING: 'warning', PARTIAL: 'warning', REFUND_FAILED: 'danger' } |
| | | |
| | | const formatDateTime = (value) => value ? value.replace('T', ' ').slice(0, 19) : '-' |
| | | const formatMoney = (value) => Number(value || 0).toFixed(2) |
| | | const canRefund = (order) => refundableOrderStatuses.includes(order.status) && order.refundStatus !== 'REFUNDED' |
| | | const getRefundableAmount = (order) => Number( |
| | | order.refundableAmount |
| | | ?? (Number(order.paymentAmount ?? order.amount ?? 0) - Number(order.refundedAmount ?? 0)), |
| | | ) |
| | | const canRefund = (order) => (refundableOrderStatuses.includes(order.status) |
| | | || (order.status === 'CANCELLED' && ['PARTIAL', 'REFUND_FAILED'].includes(order.refundStatus))) |
| | | && !['REFUNDING', 'REFUNDED'].includes(order.refundStatus) |
| | | && getRefundableAmount(order) > 0 |
| | | const statusLabel = (order) => refundStatusMap[order.refundStatus] || orderStatusMap[order.status] || order.status |
| | | const statusType = (order) => refundStatusTypeMap[order.refundStatus] || orderStatusTypeMap[order.status] || 'info' |
| | | const refundableAmount = computed(() => Number( |
| | | refundOrderRecord.value?.refundableAmount |
| | | ?? (Number(refundOrderRecord.value?.paymentAmount ?? refundOrderRecord.value?.amount ?? 0) |
| | | - Number(refundOrderRecord.value?.refundedAmount ?? 0)), |
| | | )) |
| | | const paymentAmount = computed(() => Number( |
| | | refundOrderRecord.value?.paymentAmount ?? refundOrderRecord.value?.amount ?? 0, |
| | | )) |
| | | const loadOrders = async () => { |
| | | loading.value = true |
| | | try { |
| | | const [startDate, endDate] = dateRange.value || [] |
| | | const result = await getOrders({ |
| | | page: query.page, size: query.size, keyword: query.keyword.trim() || undefined, visitorId, |
| | | page: query.page, size: query.size, keyword: query.keyword.trim() || undefined, visitorId, guideId, |
| | | scenicName: query.scenicName || undefined, status: query.status || undefined, startDate, endDate, |
| | | }) |
| | | records.value = result.records |
| | |
| | | loadOrders() |
| | | } |
| | | const openDetail = (order) => router.push(`/orders/${order.id}`) |
| | | const refund = async (order) => { |
| | | const openRefundDialog = (order) => { |
| | | refundOrderRecord.value = order |
| | | refundForm.amount = '' |
| | | refundDialogVisible.value = true |
| | | } |
| | | const resetRefundDialog = () => { |
| | | refundOrderRecord.value = null |
| | | refundForm.amount = '' |
| | | } |
| | | const submitRefund = async () => { |
| | | const amountText = String(refundForm.amount || '').trim() |
| | | if (!/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(amountText)) { |
| | | ElMessage.warning('请输入正确的退款金额,最多保留两位小数') |
| | | return |
| | | } |
| | | const amount = Number(amountText) |
| | | if (amount <= 0) { |
| | | ElMessage.warning('退款金额必须大于0') |
| | | return |
| | | } |
| | | if (amount > refundableAmount.value) { |
| | | ElMessage.warning('退款金额不能超过当前可退金额') |
| | | return |
| | | } |
| | | refundSubmitting.value = true |
| | | try { |
| | | await ElMessageBox.confirm(`确认对订单“${order.orderNo}”执行退款吗?`, '订单退款', { |
| | | type: 'warning', confirmButtonText: '确认退款', cancelButtonText: '取消', |
| | | }) |
| | | await refundOrder(order.id) |
| | | ElMessage.success('退款成功') |
| | | loadOrders() |
| | | } catch (error) { |
| | | if (error !== 'cancel' && error !== 'close') throw error |
| | | const result = await refundOrder(refundOrderRecord.value.id, amount.toFixed(2)) |
| | | if (result?.refundStatus === 'REFUNDED') { |
| | | ElMessage.success('退款成功') |
| | | } else { |
| | | ElMessage.info('退款申请已提交,等待支付渠道确认') |
| | | } |
| | | refundDialogVisible.value = false |
| | | await loadOrders() |
| | | } finally { |
| | | refundSubmitting.value = false |
| | | } |
| | | } |
| | | const changePage = (page) => { query.page = page; loadOrders() } |
| | |
| | | <el-table-column label="下单时间" min-width="160" align="center"> |
| | | <template #default="{ row }">{{ formatDateTime(row.placedAt) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="meetingPointCount" label="讲解路线汇合点" min-width="115" align="center" /> |
| | | <el-table-column label="讲解路线汇合点" min-width="115" align="center"> |
| | | <template #default="{ row }">{{ row.meetingPointName || '-' }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="guideName" label="导游姓名" min-width="100" align="center"> |
| | | <template #default="{ row }">{{ row.guideName || '-' }}</template> |
| | | </el-table-column> |
| | |
| | | </el-table-column> |
| | | <el-table-column label="状态" min-width="125" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="orderStatusTypeMap[row.status] || 'info'" effect="plain">{{ orderStatusMap[row.status] || row.status }}</el-tag> |
| | | <el-tag :type="statusType(row)" effect="plain">{{ statusLabel(row) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="125" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button v-if="canRefund(row)" link type="primary" @click="refund(row)">退款</el-button> |
| | | <el-button v-if="canRefund(row)" link type="primary" @click="openRefundDialog(row)">退款</el-button> |
| | | <el-button link type="primary" @click="openDetail(row)">查看</el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-dialog |
| | | v-model="refundDialogVisible" |
| | | class="refund-dialog" |
| | | title="退款" |
| | | width="520px" |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="!refundSubmitting" |
| | | :show-close="!refundSubmitting" |
| | | @closed="resetRefundDialog" |
| | | > |
| | | <div v-if="refundOrderRecord" class="refund-content"> |
| | | <div class="refund-info-grid"> |
| | | <span class="refund-label">订单编号</span> |
| | | <span>{{ refundOrderRecord.orderNo || '-' }}</span> |
| | | <span class="refund-label">游客手机号</span> |
| | | <span>{{ refundOrderRecord.visitorMobile || '-' }}</span> |
| | | |
| | | <span class="refund-label">订单金额</span> |
| | | <strong>¥{{ formatMoney(refundOrderRecord.amount) }}</strong> |
| | | <span class="refund-label">实际支付</span> |
| | | <strong>¥{{ formatMoney(paymentAmount) }}</strong> |
| | | |
| | | <span class="refund-label">支付流水号</span> |
| | | <span class="refund-flow-no">{{ refundOrderRecord.paymentOrderNo || '-' }}</span> |
| | | |
| | | <span class="refund-label">当前可退金额</span> |
| | | <strong>¥{{ formatMoney(refundableAmount) }}</strong> |
| | | </div> |
| | | |
| | | <div class="refund-amount-row"> |
| | | <span class="refund-label">退款金额</span> |
| | | <el-input |
| | | v-model="refundForm.amount" |
| | | inputmode="decimal" |
| | | maxlength="14" |
| | | placeholder="请输入" |
| | | @keyup.enter="submitRefund" |
| | | > |
| | | <template #append>元</template> |
| | | </el-input> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-button type="primary" :loading="refundSubmitting" @click="submitRefund">确认退款</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | </section> |
| | | </template> |
| | | |
| | |
| | | .table-card :deep(.el-card__body) { overflow: hidden; } |
| | | .pagination-row { display: flex; justify-content: flex-end; align-items: center; gap: 18px; margin-top: 24px; } |
| | | .pagination-row > span { color: #64748b; font-size: 13px; } |
| | | .refund-content { padding: 4px 20px 10px; } |
| | | .refund-info-grid { display: grid; grid-template-columns: 92px minmax(120px, 1fr) 92px minmax(120px, 1fr); gap: 22px 12px; align-items: center; } |
| | | .refund-label { color: #303133; font-weight: 600; } |
| | | .refund-flow-no { grid-column: span 3; overflow-wrap: anywhere; } |
| | | .refund-amount-row { display: grid; grid-template-columns: 92px 1fr; gap: 12px; align-items: center; margin-top: 28px; } |
| | | :global(.refund-dialog .el-dialog__header) { margin-right: 0; padding-bottom: 18px; text-align: center; border-bottom: 1px solid #ebeef5; } |
| | | :global(.refund-dialog .el-dialog__title) { color: #1f2937; font-size: 18px; font-weight: 700; } |
| | | :global(.refund-dialog .el-dialog__footer) { padding-top: 10px; text-align: center; } |
| | | :global(.refund-dialog .el-dialog__footer .el-button) { min-width: 160px; } |
| | | @media (max-width: 680px) { |
| | | .refund-info-grid { grid-template-columns: 92px 1fr; } |
| | | .refund-flow-no { grid-column: auto; } |
| | | } |
| | | </style> |