<script lang="ts" setup>
|
import type { AftersalesReturnApi } from '#/api/aftersales/return';
|
import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
|
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
|
|
import { computed, onMounted, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { Page } from '@vben/common-ui';
|
import { AftersaleReturnStatusEnum } from '@vben/constants';
|
|
import { Button, Card, Descriptions, message, Popconfirm, Select, Tag } from 'ant-design-vue';
|
|
import { useVbenForm } from '#/adapter/form';
|
import { approveReturn, createReturn, getReturn } from '#/api/aftersales/return';
|
import { getTicket } from '#/api/aftersales/ticket';
|
import { AftersalesTicketSelect } from '#/views/aftersales/ticket/components';
|
import { getSaleOrder } from '#/api/erp/sale/order';
|
|
defineOptions({ name: 'AftersalesReturnDetail' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const loading = ref(false);
|
const submitting = ref(false);
|
const returnData = ref<AftersalesReturnApi.Return>();
|
const ticketData = ref<AftersalesTicketApi.Ticket>();
|
const saleOrderItems = ref<ErpSaleOrderApi.SaleOrderItem[]>([]);
|
|
const returnId = computed(() => route.params.id as string);
|
const ticketIdFromQuery = computed(() => Number(route.query.ticketId || 0));
|
const isCreate = computed(() => returnId.value === 'new');
|
|
const isDraft = computed(() => returnData.value?.status === AftersaleReturnStatusEnum.DRAFT);
|
|
const statusMap: Record<number, { color: string; label: string }> = {
|
[AftersaleReturnStatusEnum.DRAFT]: { color: 'default', label: '草稿' },
|
[AftersaleReturnStatusEnum.AUDITING]: { color: 'processing', label: '销售审核中' },
|
[AftersaleReturnStatusEnum.SYNCED_MES]: { color: 'processing', label: '已同步MES' },
|
[AftersaleReturnStatusEnum.QC_CHECKING]: { color: 'warning', label: '检验中' },
|
[AftersaleReturnStatusEnum.QC_DONE]: { color: 'warning', label: '检验完成' },
|
[AftersaleReturnStatusEnum.INBOUND]: { color: 'success', label: '已入库' },
|
[AftersaleReturnStatusEnum.REFUND_DONE]: { color: 'success', label: '退款/换货完成' },
|
[AftersaleReturnStatusEnum.CLOSED]: { color: 'default', label: '已关闭' },
|
[AftersaleReturnStatusEnum.CANCELLED]: { color: 'error', label: '已取消' },
|
};
|
|
const returnTypeMap: Record<number, string> = {
|
1: '退款退货',
|
2: '换货',
|
3: '仅退款',
|
};
|
|
const returnTypeOptions = [
|
{ label: '退款退货', value: 1 },
|
{ label: '换货', value: 2 },
|
{ label: '仅退款', value: 3 },
|
];
|
|
const [CreateForm, createFormApi] = useVbenForm({
|
commonConfig: {
|
componentProps: { class: 'w-full' },
|
formItemClass: 'col-span-1',
|
labelWidth: 100,
|
},
|
layout: 'horizontal',
|
schema: [
|
{
|
fieldName: 'returnType',
|
label: '退货类型',
|
component: 'Select',
|
componentProps: {
|
options: returnTypeOptions,
|
placeholder: '请选择退货类型',
|
},
|
rules: 'selectRequired',
|
},
|
{
|
fieldName: 'returnReason',
|
label: '退货原因',
|
component: 'Textarea',
|
formItemClass: 'col-span-3',
|
componentProps: {
|
placeholder: '请输入退货原因',
|
rows: 3,
|
},
|
rules: 'required',
|
},
|
{
|
fieldName: 'defectCategory',
|
label: '缺陷类别',
|
component: 'Input',
|
componentProps: { placeholder: '请输入缺陷类别' },
|
},
|
{
|
fieldName: 'remark',
|
label: '备注',
|
component: 'Textarea',
|
formItemClass: 'col-span-3',
|
componentProps: {
|
placeholder: '请输入备注',
|
rows: 3,
|
},
|
},
|
],
|
showDefaultActions: false,
|
wrapperClass: 'grid-cols-3',
|
});
|
|
async function loadReturn() {
|
loading.value = true;
|
try {
|
returnData.value = await getReturn(Number(returnId.value));
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function loadTicketForCreate() {
|
if (!ticketIdFromQuery.value) return;
|
loading.value = true;
|
try {
|
ticketData.value = await getTicket(ticketIdFromQuery.value);
|
if (ticketData.value?.saleOrderId) {
|
const order = await getSaleOrder(ticketData.value.saleOrderId);
|
saleOrderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
|
}
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function handleTicketChange(item: Record<string, unknown> | undefined) {
|
if (!item?.id) { ticketData.value = undefined; return; }
|
loading.value = true;
|
try {
|
ticketData.value = await getTicket(item.id as number);
|
if (ticketData.value?.saleOrderId) {
|
const order = await getSaleOrder(ticketData.value.saleOrderId);
|
saleOrderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
|
}
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
const needReturnItems = computed(() =>
|
(ticketData.value?.items || []).filter((item) => item.needReturn),
|
);
|
|
async function handleCreateReturn() {
|
const { valid } = await createFormApi.validate();
|
if (!valid) return;
|
|
submitting.value = true;
|
try {
|
const formValues = await createFormApi.getValues();
|
const newId = await createReturn({
|
ticketId: ticketData.value!.id!,
|
customerId: ticketData.value!.customerId!,
|
saleOrderId: ticketData.value!.saleOrderId,
|
returnType: formValues.returnType as number,
|
returnReason: formValues.returnReason as string,
|
defectCategory: formValues.defectCategory as string | undefined,
|
remark: formValues.remark as string | undefined,
|
items: needReturnItems.value.map((item) => {
|
const orderItem = saleOrderItems.value.find((oi) => oi.productId === item.itemId);
|
const unitPrice = orderItem?.productPrice || 0;
|
const returnQuantity = item.returnQuantity || 0;
|
return {
|
ticketItemId: item.id,
|
itemId: item.itemId,
|
itemCode: item.itemCode,
|
itemName: item.itemName,
|
batchId: item.batchId,
|
batchCode: item.batchCode,
|
returnQuantity,
|
unitPrice,
|
returnPrice: Number((unitPrice * returnQuantity).toFixed(2)),
|
};
|
}),
|
});
|
message.success('退货申请创建成功');
|
router.replace({ name: 'AftersalesReturnDetail', params: { id: newId } });
|
} catch {
|
// 创建失败,留在当前页
|
} finally {
|
submitting.value = false;
|
}
|
}
|
|
async function handleApprove() {
|
await approveReturn(returnData.value!.id!);
|
message.success('审核通过,已同步MES创建退货单');
|
await loadReturn();
|
}
|
|
function goBack() {
|
router.back();
|
}
|
|
onMounted(async () => {
|
if (isCreate.value) {
|
await loadTicketForCreate();
|
} else {
|
await loadReturn();
|
}
|
});
|
</script>
|
|
<template>
|
<Page auto-content-height :loading="loading">
|
<template #title>
|
<div class="flex items-center gap-3">
|
<Button @click="goBack">返回</Button>
|
<span class="text-lg font-bold">
|
{{ isCreate ? '创建退货申请' : '售后退货申请详情' }}
|
</span>
|
</div>
|
</template>
|
|
<!-- 创建模式 -->
|
<template v-if="isCreate && ticketData">
|
<Card title="工单信息" class="mb-4">
|
<Descriptions :column="3" bordered size="small">
|
<Descriptions.Item label="工单单号">{{ ticketData.no }}</Descriptions.Item>
|
<Descriptions.Item label="工单主题">{{ ticketData.name }}</Descriptions.Item>
|
<Descriptions.Item label="客户">{{ ticketData.customerName }}</Descriptions.Item>
|
<Descriptions.Item label="销售订单号">{{ ticketData.saleOrderNo || '-' }}</Descriptions.Item>
|
</Descriptions>
|
</Card>
|
|
<Card v-if="needReturnItems.length" title="退货明细" class="mb-4">
|
<div class="overflow-x-auto">
|
<table class="w-full border text-sm">
|
<thead class="bg-gray-50">
|
<tr>
|
<th class="border p-2 text-left">物料编码</th>
|
<th class="border p-2 text-left">物料名称</th>
|
<th class="border p-2 text-left">批次号</th>
|
<th class="border p-2 text-right">退货数量</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr v-for="item in needReturnItems" :key="item.id">
|
<td class="border p-2">{{ item.itemCode }}</td>
|
<td class="border p-2">{{ item.itemName }}</td>
|
<td class="border p-2">{{ item.batchCode || '-' }}</td>
|
<td class="border p-2 text-right">{{ item.returnQuantity || '-' }}</td>
|
</tr>
|
</tbody>
|
</table>
|
</div>
|
</Card>
|
|
<Card title="退货信息" class="mb-4">
|
<CreateForm />
|
<div class="mt-4">
|
<Button type="primary" :loading="submitting" @click="handleCreateReturn">
|
提交退货申请
|
</Button>
|
</div>
|
</Card>
|
</template>
|
|
<!-- 选择工单 -->
|
<template v-else-if="isCreate && !ticketData && !loading">
|
<Card title="选择售后工单" class="mb-4">
|
<div class="flex items-center gap-3">
|
<span class="text-sm text-gray-500">售后工单:</span>
|
<AftersalesTicketSelect class="!w-64" @change="handleTicketChange" />
|
</div>
|
</Card>
|
</template>
|
|
<!-- 查看模式 -->
|
<template v-else-if="!isCreate && returnData">
|
<Card title="基本信息" class="mb-4">
|
<Descriptions :column="3" bordered size="small">
|
<Descriptions.Item label="退货单号">{{ returnData.no }}</Descriptions.Item>
|
<Descriptions.Item label="状态">
|
<Tag :color="statusMap[returnData.status!]?.color">
|
{{ statusMap[returnData.status!]?.label }}
|
</Tag>
|
</Descriptions.Item>
|
<Descriptions.Item label="客户">{{ returnData.customerName || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="关联工单">
|
<Button type="link" class="!p-0" @click="router.push({ name: 'AftersalesTicketDetail', params: { id: returnData.ticketId } })">
|
{{ returnData.ticketId }}
|
</Button>
|
</Descriptions.Item>
|
<Descriptions.Item label="退货类型">
|
<Tag>{{ returnData.returnType ? returnTypeMap[returnData.returnType] : '-' }}</Tag>
|
</Descriptions.Item>
|
<Descriptions.Item label="MES退货单号">
|
{{ returnData.mesReturnCode || '-' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="退货原因" :span="3">
|
{{ returnData.returnReason || '-' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="缺陷分类">{{ returnData.defectCategory || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="应退总金额">
|
{{ returnData.totalReturnPrice ? `¥${returnData.totalReturnPrice}` : '-' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="实退金额">
|
{{ returnData.actualRefundPrice ? `¥${returnData.actualRefundPrice}` : '-' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="质检结论">{{ returnData.qualityResult || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="入库时间">{{ returnData.returnInboundTime || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="退款时间">{{ returnData.refundTime || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="备注" :span="3">
|
{{ returnData.remark || '-' }}
|
</Descriptions.Item>
|
</Descriptions>
|
</Card>
|
|
<Card v-if="returnData?.items?.length" title="退货明细" class="mb-4">
|
<div class="overflow-x-auto">
|
<table class="w-full border text-sm">
|
<thead class="bg-gray-50">
|
<tr>
|
<th class="border p-2 text-left">物料编码</th>
|
<th class="border p-2 text-left">物料名称</th>
|
<th class="border p-2 text-left">批次号</th>
|
<th class="border p-2 text-right">退货数量</th>
|
<th class="border p-2 text-right">单价</th>
|
<th class="border p-2 text-right">退货金额</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr v-for="item in returnData.items" :key="item.id">
|
<td class="border p-2">{{ item.itemCode }}</td>
|
<td class="border p-2">{{ item.itemName }}</td>
|
<td class="border p-2">{{ item.batchCode || '-' }}</td>
|
<td class="border p-2 text-right">{{ item.returnQuantity }}</td>
|
<td class="border p-2 text-right">{{ item.unitPrice ? `¥${item.unitPrice}` : '-' }}</td>
|
<td class="border p-2 text-right">{{ item.returnPrice ? `¥${item.returnPrice}` : '-' }}</td>
|
</tr>
|
</tbody>
|
</table>
|
</div>
|
</Card>
|
</template>
|
|
<template #extra>
|
<div v-if="!isCreate && isDraft" class="flex gap-2">
|
<Popconfirm
|
title="确认审核通过该退货申请?审核后将自动同步MES创建退货单。"
|
@confirm="handleApprove"
|
>
|
<Button type="primary">审核通过</Button>
|
</Popconfirm>
|
</div>
|
</template>
|
</Page>
|
</template>
|