2026-05-15 11:07:22 +08:00

287 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="orderPageGet" :columns="columns" :pagination="true"
:tool-button="true">
<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>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #paymentMethod="scope">
{{ scope.row.paymentMethod || '-' }}
</template>
<template #orderType="scope">
<el-tag :type="scope.row.orderType == '收入'? 'success' : 'error'">
{{ (scope.row.orderType == '收入' ? '收款' : scope.row.orderType) || '-' }}
</el-tag>
</template>
<template #registrationMode="scope">
{{ scope.row.registrationMode || '-' }}
</template>
<template #orderStatus="scope">
{{ scope.row.orderStatusText || '-' }}
</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>
<template #refundAmount="scope">
{{ formatAmount(scope.row.refundAmount) }}
</template>
<template #actualAmount="scope">
{{ formatActualAmount(scope.row) }}
</template>
<template #action="scope">
<el-button size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
查看详情 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
<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 />
<el-table-column v-if="shouldShowFee" prop="fee" label="费用(元)" align="center" width="120">
<template #default="scope">
{{ formatAmount(scope.row.fee) }}
</template>
</el-table-column>
</el-table>
<template #footer>
<span class="dialog-footer">
<el-button @click="isShowDialog = false">取消</el-button>
<el-button type="primary" @click="isShowDialog = false">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup >
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { Download } from '@element-plus/icons-vue';
import ProTable from '/@/components/ProTable/index.vue';
import { orderPageGet , orderDetailGet, orderExportGet } from '/@/api/match/order';
const tableRef = ref();
const exportLoading = ref(false);
const formData = ref({
orderNo: '',
eventId: 0,
eventName: '',
registrationMode: 0,
teamName: '',
contactName: '',
contactPhone: '',
totalAmount: 0,
discountAmount: 0,
payableAmount: 0,
status: 0,
createdTime: '',
paidTime: '',
items:[],
persons:[]
})
const columns = ref([
{ prop: 'orderNo', label: '订单编号', align: 'center', search: { el: 'input', }, fixed: 'left', width: 150 },
{ width: 180, prop: 'unitName', label: '单位名称', align: 'center', },
{ width: 120, prop: 'eventName', label: '所属赛事', search: { el: 'input', }, },
{ width: 120, prop: 'orderType', label: '订单类型', search: { el: 'select', }, enum: [{ label: '全部', value: 0 }, { label: '收款', value: 1 }, { label: '退款', value: 2 },], },
{ width: 120, prop: 'relatedOrderNo', label: '关联订单' },
{ 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 },], },
{ width: 120, prop: 'contactName', label: '报名用户' },
{ width: 120, prop: 'contactPhone', label: '报名账号', search: { el: 'input' } },
{ width: 100, prop: 'originalAmount', label: '原金额' },
{ width: 120, prop: 'actualAmount', label: '实付金额' },
{ width: 100, prop: 'discountAmount', label: '优惠金额' },
{ width: 100, prop: 'refundAmount', label: '退款金额', align: 'center' },
{ width: 90, prop: 'orderStatus', label: '订单状态', search: { el: 'select', }, enum: [{ label: '全部', value: '' }, { label: '待支付', value: 0 }, { label: '已支付', value: 1 }, { label: '已退款', value: 3 }, { label: '报名失败', value: 6 },], },
{ width: 160, prop: 'createdTime', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 120, },
]);
const isShowDialog = ref(false);
const detailLoading = ref(false);
const formatAmount = (value) => {
if (value === null || value === undefined || value === '') return '-';
const num = Number(value);
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);
}
};
const normalizeNames = (value) => {
if (Array.isArray(value)) return value.filter(Boolean).join('、');
if (typeof value === 'string') return value;
return '';
};
// ChargeMode 枚举值:0=免费,1=定价(赛事),2=单价(赛事),3=单价(项目)
// 只有单价(项目)时才显示费用列
const shouldShowFee = computed(() => {
return formData.value?.chargeMode === 3;
});
const detailTableData = computed(() => {
const items = formData.value?.items ?? [];
const persons = formData.value?.persons ?? [];
return items.map((item) => {
// 方案1: 使用 registrationItemId 进行精确匹配(强一致)
let personNamesByItem = persons
.filter((p) => String(p?.registrationItemId) === String(item?.id))
.map((p) => p?.name)
.filter(Boolean);
// 方案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('、')
: '-';
const fee = item?.fee ?? item?.subtotal ?? ((Number(item?.unitPrice) || 0) * (Number(item?.personCount) || 0));
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;
}
formData.value = {
...formData.value,
...(data ?? {}),
items: data?.items ?? [],
persons: data?.persons ?? [],
};
detailLoading.value = false;
isShowDialog.value = true;
};
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;
}
};
</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>