287 lines
9.3 KiB
Vue
Raw Normal View History

2026-02-12 21:38:29 +08:00
<template>
<div class="system-menu-container layout-padding">
2026-03-11 00:09:41 +08:00
<ProTable ref="tableRef" :request-api="orderPageGet" :columns="columns" :pagination="true"
:tool-button="true">
2026-04-05 21:01:02 +08:00
<template #tools>
<el-button v-auth="'order:export'" type="primary" :loading="exportLoading" @click="handleExport">
<template #icon>
<el-icon><Download /></el-icon>
</template>
导出
</el-button>
</template>
2026-02-12 21:38:29 +08:00
<template #index="scope">
{{ scope.$index + 1 }}
</template>
2026-04-01 15:16:01 +08:00
<template #paymentMethod="scope">
2026-04-09 23:12:14 +08:00
{{ scope.row.paymentMethod || '-' }}
2026-04-01 15:16:01 +08:00
</template>
<template #orderType="scope">
2026-04-21 20:39:02 +08:00
<el-tag :type="scope.row.orderType == '收入'? 'success' : 'error'">
{{ (scope.row.orderType == '收入' ? '收款' : scope.row.orderType) || '-' }}
</el-tag>
2026-04-01 15:16:01 +08:00
</template>
<template #registrationMode="scope">
2026-04-09 23:12:14 +08:00
{{ scope.row.registrationMode || '-' }}
</template>
<template #orderStatus="scope">
2026-04-21 09:39:55 +08:00
{{ scope.row.orderStatusText || '-' }}
2026-04-09 23:12:14 +08:00
</template>
<template #paymentProofUrl="scope">
<el-image v-if="scope.row.paymentProofUrl"
style="width: 40px; height: 40px"
:src="scope.row.paymentProofUrl"
:preview-src-list="[scope.row.paymentProofUrl]"
fit="cover" />
<span v-else>-</span>
</template>
<template #originalAmount="scope">
{{ formatAmount(scope.row.originalAmount) }}
</template>
<template #discountAmount="scope">
{{ formatAmount(scope.row.discountAmount) }}
</template>
2026-04-21 08:39:02 +08:00
<template #refundAmount="scope">
{{ formatAmount(scope.row.refundAmount) }}
</template>
2026-04-09 23:12:14 +08:00
<template #actualAmount="scope">
2026-04-21 08:39:02 +08:00
{{ formatActualAmount(scope.row) }}
2026-04-01 15:16:01 +08:00
</template>
2026-02-12 21:38:29 +08:00
<template #action="scope">
2026-03-11 00:09:41 +08:00
<el-button size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
2026-02-12 21:38:29 +08:00
查看详情 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
2026-03-23 18:01:01 +08:00
<el-dialog title="订单详情" v-model="isShowDialog" width="1100px">
<el-skeleton :rows="6" animated v-if="detailLoading" />
<el-empty v-else-if="!detailTableData.length" description="暂无订单明细数据" />
<el-table v-else :data="detailTableData" border>
<el-table-column prop="projectName" label="项目" align="center" />
<el-table-column prop="groupName" label="组别" align="center" />
<el-table-column prop="participantNames" label="参赛队员" align="center" show-overflow-tooltip />
2026-05-07 08:32:55 +08:00
<el-table-column v-if="shouldShowFee" prop="fee" label="费用(元)" align="center" width="120">
2026-04-09 23:12:14 +08:00
<template #default="scope">
{{ formatAmount(scope.row.fee) }}
</template>
</el-table-column>
2026-03-23 18:01:01 +08:00
</el-table>
2026-02-12 21:38:29 +08:00
<template #footer>
<span class="dialog-footer">
2026-03-23 18:01:01 +08:00
<el-button @click="isShowDialog = false">取消</el-button>
<el-button type="primary" @click="isShowDialog = false">确定</el-button>
2026-02-12 21:38:29 +08:00
</span>
</template>
</el-dialog>
</div>
</template>
2026-03-11 00:09:41 +08:00
<script setup >
2026-03-23 18:01:01 +08:00
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
2026-04-05 21:01:02 +08:00
import { Download } from '@element-plus/icons-vue';
2026-02-12 21:38:29 +08:00
import ProTable from '/@/components/ProTable/index.vue';
2026-04-05 21:01:02 +08:00
import { orderPageGet , orderDetailGet, orderExportGet } from '/@/api/match/order';
2026-02-12 21:38:29 +08:00
2026-04-05 21:01:02 +08:00
const tableRef = ref();
const exportLoading = ref(false);
2026-02-12 21:38:29 +08:00
const formData = ref({
2026-03-23 18:01:01 +08:00
orderNo: '',
eventId: 0,
eventName: '',
registrationMode: 0,
teamName: '',
contactName: '',
contactPhone: '',
totalAmount: 0,
discountAmount: 0,
payableAmount: 0,
status: 0,
createdTime: '',
paidTime: '',
items:[],
persons:[]
2026-02-12 21:38:29 +08:00
})
2026-03-23 18:01:01 +08:00
2026-02-12 21:38:29 +08:00
2026-03-11 00:09:41 +08:00
const columns = ref([
{ prop: 'orderNo', label: '订单编号', align: 'center', search: { el: 'input', }, fixed: 'left', width: 150 },
2026-04-01 15:16:01 +08:00
{ width: 180, prop: 'unitName', label: '单位名称', align: 'center', },
{ width: 120, prop: 'eventName', label: '所属赛事', search: { el: 'input', }, },
2026-04-21 20:39:02 +08:00
{ width: 120, prop: 'orderType', label: '订单类型', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '收款', value: 1 }, { label: '退款', value: 2 },], },
2026-04-01 15:16:01 +08:00
{ width: 120, prop: 'relatedOrderNo', label: '关联订单' },
2026-04-09 23:12:14 +08:00
{ width: 100, prop: 'paymentMethod', label: '支付方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '线上', value: 2 }, { label: '线下', value: 1 },], },
{ width: 100, prop: 'paymentProofUrl', label: '付款凭证' },
{ width: 100, prop: 'registrationMode', label: '报名方式', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '个人', value: 1 }, { label: '团队', value: 2 },], },
2026-04-21 10:02:55 +08:00
{ width: 120, prop: 'contactName', label: '报名用户' },
2026-04-01 15:16:01 +08:00
{ width: 120, prop: 'contactPhone', label: '报名账号', search: { el: 'input' } },
2026-04-21 10:02:55 +08:00
{ width: 100, prop: 'originalAmount', label: '原金额' },
2026-04-21 20:39:02 +08:00
{ width: 120, prop: 'actualAmount', label: '实付金额' },
2026-04-21 10:02:55 +08:00
{ width: 100, prop: 'discountAmount', label: '优惠金额' },
{ width: 100, prop: 'refundAmount', label: '退款金额', align: 'center' },
2026-04-21 20:39:02 +08:00
{ width: 90, prop: 'orderStatus', label: '订单状态', search: { el: 'select', }, enum: [{ label: '全部', value: '' }, { label: '待支付', value: 0 }, { label: '已支付', value: 1 }, { label: '已取消', value: 2 }, { label: '已退款', value: 3 }, { label: '已发货', value: 4 }, { label: '已完成', value: 5 },], },
2026-04-09 23:12:14 +08:00
{ width: 160, prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
2026-03-11 00:09:41 +08:00
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 120, },
2026-02-12 21:38:29 +08:00
]);
const isShowDialog = ref(false);
2026-03-23 18:01:01 +08:00
const detailLoading = ref(false);
2026-04-09 23:12:14 +08:00
const formatAmount = (value) => {
if (value === null || value === undefined || value === '') return '-';
const num = Number(value);
2026-04-21 08:39:02 +08:00
if (isNaN(num) || num === 0) return '-';
return num.toFixed(2);
};
const formatActualAmount = (row) => {
if (row.isRefundOrder) {
if (row.relatedOrderActualAmount !== null && row.relatedOrderActualAmount !== undefined) {
const num = Number(row.relatedOrderActualAmount);
const amountStr = isNaN(num) || num === 0 ? '-' : num.toFixed(2);
return amountStr;
}
return '-';
} else {
return formatAmount(row.actualAmount);
}
2026-04-09 23:12:14 +08:00
};
2026-03-23 18:01:01 +08:00
const normalizeNames = (value) => {
if (Array.isArray(value)) return value.filter(Boolean).join('、');
if (typeof value === 'string') return value;
return '';
};
2026-02-12 21:38:29 +08:00
2026-05-07 08:32:55 +08:00
// ChargeMode 枚举值:0=免费,1=定价(赛事),2=单价(赛事),3=单价(项目)
// 只有单价(项目)时才显示费用列
const shouldShowFee = computed(() => {
return formData.value?.chargeMode === 3;
});
2026-03-23 18:01:01 +08:00
const detailTableData = computed(() => {
const items = formData.value?.items ?? [];
const persons = formData.value?.persons ?? [];
2026-05-07 08:32:55 +08:00
2026-03-23 18:01:01 +08:00
return items.map((item) => {
2026-05-07 08:32:55 +08:00
// 方案1: 使用 registrationItemId 进行精确匹配(强一致)
let personNamesByItem = persons
.filter((p) => String(p?.registrationItemId) === String(item?.id))
2026-03-23 18:01:01 +08:00
.map((p) => p?.name)
.filter(Boolean);
2026-05-07 08:32:55 +08:00
// 方案2: 使用项目ID匹配
if (personNamesByItem.length === 0) {
personNamesByItem = persons
.filter((p) => String(p?.projectId) === String(item?.projectId))
.map((p) => p?.name)
.filter(Boolean);
}
// 方案3: 最后保底,用项目名和组名去重匹配
if (personNamesByItem.length === 0) {
personNamesByItem = [...new Set(
persons
.filter((p) => p?.groupName === item?.groupName)
.map((p) => p?.name)
.filter(Boolean)
)];
}
const participantNames = personNamesByItem.length > 0
? personNamesByItem.join('、')
: '-';
2026-04-09 23:12:14 +08:00
const fee = item?.fee ?? item?.subtotal ?? ((Number(item?.unitPrice) || 0) * (Number(item?.personCount) || 0));
2026-03-23 18:01:01 +08:00
return {
projectName: item?.projectName ?? '-',
groupName: item?.groupName ?? item?.projectTypeName ?? '-',
participantNames,
fee,
};
});
});
const onOpenMenu = async(id) => {
if (!id) return;
isShowDialog.value = true;
detailLoading.value = true;
const { succeeded, data } = await orderDetailGet({ id });
if (!succeeded) {
ElMessage.error('订单详情加载失败');
formData.value.items = [];
formData.value.persons = [];
detailLoading.value = false;
return;
}
2026-02-12 21:38:29 +08:00
formData.value = {
2026-03-23 18:01:01 +08:00
...formData.value,
...(data ?? {}),
items: data?.items ?? [],
persons: data?.persons ?? [],
2026-02-12 21:38:29 +08:00
};
2026-03-23 18:01:01 +08:00
detailLoading.value = false;
2026-02-12 21:38:29 +08:00
isShowDialog.value = true;
};
2026-04-05 21:01:02 +08:00
const handleExport = async () => {
try {
exportLoading.value = true;
const searchParams = tableRef.value?.searchParam || {};
const response = await orderExportGet(searchParams);
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `订单列表_${new Date().getTime()}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
} catch (error) {
console.error('导出失败:', error);
ElMessage.error('导出失败,请稍后重试');
} finally {
exportLoading.value = false;
}
};
2026-02-12 21:38:29 +08:00
2026-03-23 18:01:01 +08:00
2026-02-12 21:38:29 +08:00
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>