<script lang="ts" setup>
|
import type { TicketFormType } from '../data';
|
|
import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
|
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|
import { computed, nextTick, ref } from 'vue';
|
import { useRouter } from 'vue-router';
|
|
import { useVbenModal } from '@vben/common-ui';
|
import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants';
|
|
import { Button, Divider, Input, message, Popconfirm, Select, Tag } from 'ant-design-vue';
|
|
import { useVbenForm } from '#/adapter/form';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import {
|
cancelTicket,
|
classifyTicket,
|
closeTicket,
|
createTicket,
|
getTicket,
|
resolveTicket,
|
updateTicket,
|
} from '#/api/aftersales/ticket';
|
import { getSaleOrder } from '#/api/erp/sale/order';
|
import { $t } from '#/locales';
|
|
import { useTicketFormSchema, useTicketItemFormSchema, useTicketItemGridColumns } from '../data';
|
|
defineOptions({ name: 'TicketForm' });
|
|
const emit = defineEmits(['success']);
|
const router = useRouter();
|
|
const TS = AftersaleTicketStatusEnum;
|
const IT = AftersaleIssueTypeEnum;
|
|
const formType = ref<TicketFormType>('create');
|
const formData = ref<AftersalesTicketApi.Ticket>();
|
const items = ref<AftersalesTicketApi.TicketItem[]>([]);
|
const editingItemIndex = ref<number | null>(null);
|
const submitting = ref(false);
|
|
// 问题判定
|
const showClassify = ref(false);
|
const classifyIssueType = ref<number>();
|
const classifyCategory = ref('');
|
const classifySeverity = ref<number>();
|
|
const issueTypeOpts = [
|
{ label: '一般问题', value: IT.GENERAL },
|
{ label: '维修问题', value: IT.REPAIR },
|
{ label: '退货问题', value: IT.RETURN },
|
];
|
const severityOpts = [
|
{ label: '轻微', value: 0 },
|
{ label: '一般', value: 1 },
|
{ label: '严重', value: 2 },
|
{ label: '致命', value: 3 },
|
];
|
|
const isEdit = computed(() => ['create', 'update'].includes(formType.value));
|
const isDraft = computed(() => formData.value?.status === TS.DRAFT);
|
const isProcessing = computed(() => formData.value?.status === TS.PROCESSING);
|
const isPendingReturn = computed(() => formData.value?.status === TS.PENDING_RETURN);
|
const isPendingRefund = computed(() => formData.value?.status === TS.PENDING_REFUND);
|
const isResolved = computed(() => formData.value?.status === TS.RESOLVED);
|
const isReturnIssue = computed(() => formData.value?.issueType === IT.RETURN);
|
|
const title = computed(() => {
|
if (formType.value === 'detail') return $t('ui.actionTitle.view', ['售后工单']);
|
if (formData.value?.id) return $t('ui.actionTitle.edit', ['售后工单']);
|
return $t('ui.actionTitle.create', ['售后工单']);
|
});
|
|
// 销售订单物料过滤
|
const orderItems = ref<ErpSaleOrderApi.SaleOrderItem[]>([]);
|
|
function handleSaleOrderChange(item: Record<string, unknown> | undefined) {
|
if (!item?.id) {
|
orderItems.value = [];
|
return;
|
}
|
orderItems.value = (item.items as ErpSaleOrderApi.SaleOrderItem[]) || [];
|
}
|
|
// 主表单
|
const [Form, formApi] = useVbenForm({
|
commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 110 },
|
layout: 'horizontal', schema: [], showDefaultActions: false, wrapperClass: 'grid-cols-3',
|
});
|
|
// 明细行表单
|
const [ItemForm, itemFormApi] = useVbenForm({
|
commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 100 },
|
layout: 'horizontal', schema: [], showDefaultActions: false, wrapperClass: 'grid-cols-3',
|
});
|
|
// 明细行表格
|
const [ItemsGrid, itemsGridApi] = useVbenVxeGrid({
|
gridOptions: {
|
columns: useTicketItemGridColumns(true),
|
height: 300,
|
keepSource: true,
|
proxyConfig: {
|
ajax: {
|
query: async () => {
|
return {
|
list: items.value,
|
total: items.value.length,
|
};
|
},
|
},
|
},
|
rowConfig: {
|
keyField: 'id',
|
isHover: true,
|
},
|
pagerConfig: { enabled: false },
|
toolbarConfig: { enabled: false },
|
} as VxeTableGridOptions<AftersalesTicketApi.TicketItem>,
|
});
|
|
async function refreshItemsGrid() {
|
await nextTick();
|
itemsGridApi.query();
|
}
|
|
// Modal(工单表单弹框)必须在 ItemModal 之前,确保 connectedComponent 注入到正确的弹框
|
const [Modal, modalApi] = useVbenModal({
|
async onOpenChange(isOpen: boolean) {
|
if (!isOpen) { formData.value = undefined; items.value = []; orderItems.value = []; showClassify.value = false; return; }
|
const data = modalApi.getData<{ formType: TicketFormType; id?: number }>();
|
formType.value = data.formType;
|
itemsGridApi.setGridOptions({ columns: useTicketItemGridColumns(isEdit.value) });
|
formApi.setState({ schema: useTicketFormSchema(formType.value, formApi, handleSaleOrderChange) });
|
formApi.setDisabled(!isEdit.value);
|
modalApi.setState({ showConfirmButton: false, showCancelButton: false });
|
if (data?.id) {
|
modalApi.lock();
|
try {
|
formData.value = await getTicket(data.id);
|
items.value = formData.value?.items || [];
|
await refreshItemsGrid();
|
await formApi.setValues(formData.value as Record<string, unknown>);
|
// 回显附件
|
if (formData.value?.attachmentList?.length) {
|
const blobIds = formData.value.attachmentList.map((item) => ({
|
uid: String(item.id), name: item.name || '', url: item.url || '',
|
status: 'done', id: item.id,
|
}));
|
await formApi.setFieldValue('blobIds', blobIds);
|
}
|
if (formData.value?.saleOrderId) {
|
const order = await getSaleOrder(formData.value.saleOrderId);
|
orderItems.value = (order as Record<string, unknown>).items as ErpSaleOrderApi.SaleOrderItem[] || [];
|
}
|
} finally { modalApi.unlock(); }
|
} else {
|
formData.value = undefined;
|
items.value = [];
|
await refreshItemsGrid();
|
await formApi.resetForm();
|
}
|
},
|
});
|
|
const [ItemModal, itemModalApi] = useVbenModal({
|
onConfirm: async () => {
|
const { valid } = await itemFormApi.validate();
|
if (!valid) return;
|
const d = (await itemFormApi.getValues()) as unknown as AftersalesTicketApi.TicketItem;
|
if (editingItemIndex.value !== null) {
|
items.value[editingItemIndex.value] = { ...items.value[editingItemIndex.value], ...d };
|
} else {
|
items.value.push({ ...d });
|
}
|
await refreshItemsGrid();
|
itemModalApi.close();
|
},
|
onOpenChange(isOpen: boolean) {
|
if (!isOpen) { editingItemIndex.value = null; return; }
|
const d = itemModalApi.getData<AftersalesTicketApi.TicketItem | null>();
|
itemFormApi.setState({ schema: useTicketItemFormSchema(formData.value?.customerId, formData.value?.saleOrderNo, orderItems.value, itemFormApi) });
|
if (d) { itemFormApi.setValues(d as Record<string, unknown>); } else { itemFormApi.resetForm(); }
|
},
|
});
|
|
function addItem() { editingItemIndex.value = null; itemModalApi.setData(null).open(); }
|
function editItem(i: number) { editingItemIndex.value = i; itemModalApi.setData(items.value[i]).open(); }
|
async function delItem(i: number) { items.value.splice(i, 1); await refreshItemsGrid(); }
|
|
// === 提交 ===
|
async function handleSubmit() {
|
const { valid } = await formApi.validate();
|
if (!valid) return;
|
submitting.value = true;
|
try {
|
const data = (await formApi.getValues()) as Record<string, unknown>;
|
if (formData.value?.id) {
|
await updateTicket({ ...formData.value, ...data, items: items.value } as AftersalesTicketApi.Ticket);
|
} else {
|
await createTicket({ ...data, items: items.value } as AftersalesTicketApi.Ticket);
|
}
|
message.success($t('ui.actionMessage.operationSuccess'));
|
await modalApi.close();
|
emit('success');
|
} catch (e: unknown) {
|
message.error((e as Error).message || '提交失败');
|
} finally { submitting.value = false; }
|
}
|
|
function toggleClassify() {
|
showClassify.value = !showClassify.value;
|
if (showClassify.value) {
|
classifyIssueType.value = formData.value?.issueType;
|
classifyCategory.value = formData.value?.issueCategory || '';
|
classifySeverity.value = formData.value?.severityLevel;
|
}
|
}
|
async function doClassify() {
|
if (!classifyIssueType.value || !formData.value?.id) return message.warning('请选择问题类型');
|
submitting.value = true;
|
try {
|
await classifyTicket({ id: formData.value.id, issueType: classifyIssueType.value, issueCategory: classifyCategory.value || undefined, severityLevel: classifySeverity.value });
|
message.success('判定成功'); showClassify.value = false;
|
formData.value = await getTicket(formData.value.id);
|
await formApi.setValues(formData.value as Record<string, unknown>);
|
} finally { submitting.value = false; }
|
}
|
async function doResolve() {
|
if (!formData.value?.id) return;
|
submitting.value = true;
|
try { await resolveTicket(formData.value.id); message.success('完结成功'); await modalApi.close(); emit('success'); }
|
finally { submitting.value = false; }
|
}
|
async function doClose() {
|
if (!formData.value?.id) return;
|
submitting.value = true;
|
try { await closeTicket(formData.value.id); message.success('关闭成功'); await modalApi.close(); emit('success'); }
|
finally { submitting.value = false; }
|
}
|
async function doCancel() {
|
if (!formData.value?.id) return;
|
submitting.value = true;
|
try { await cancelTicket(formData.value.id); message.success('取消成功'); await modalApi.close(); emit('success'); }
|
finally { submitting.value = false; }
|
}
|
function toCreateReturn() {
|
if (!formData.value?.id) return;
|
router.push({ name: 'AftersalesReturnDetail', params: { id: 'new' }, query: { ticketId: formData.value.id } });
|
modalApi.close();
|
}
|
</script>
|
|
<template>
|
<Modal :title="title" class="w-4/5">
|
<Form class="mx-4" />
|
|
<!-- 明细行 -->
|
<template v-if="isEdit || formData?.id">
|
<Divider>工单明细</Divider>
|
<div class="mx-4">
|
<div v-if="isEdit" class="mb-2"><Button type="primary" @click="addItem">添加物料</Button></div>
|
<ItemsGrid>
|
<template #needRepair="{ row }"><Tag :color="row.needRepair ? 'blue' : 'default'">{{ row.needRepair ? '是' : '否' }}</Tag></template>
|
<template #needReturn="{ row }"><Tag :color="row.needReturn ? 'orange' : 'default'">{{ row.needReturn ? '是' : '否' }}</Tag></template>
|
<template v-if="isEdit" #itemActions="{ row, rowIndex }">
|
<TableAction :actions="[
|
{ label: $t('common.edit'), type: 'link', icon: ACTION_ICON.EDIT, onClick: () => editItem(rowIndex) },
|
{ label: $t('common.delete'), type: 'link', danger: true, icon: ACTION_ICON.DELETE, popConfirm: { title: $t('ui.actionMessage.deleteConfirm', [row.itemName||'']), confirm: () => delItem(rowIndex) } },
|
]" />
|
</template>
|
</ItemsGrid>
|
</div>
|
</template>
|
|
<!-- 问题判定 -->
|
<template v-if="showClassify">
|
<Divider>问题判定</Divider>
|
<div class="mx-4 flex flex-wrap items-center gap-3">
|
<span>问题类型:</span><Select v-model:value="classifyIssueType" :options="issueTypeOpts" style="width:140px" />
|
<span>问题分类:</span><Input v-model:value="classifyCategory" style="width:160px" />
|
<span>严重程度:</span><Select v-model:value="classifySeverity" :options="severityOpts" style="width:120px" />
|
<Button type="primary" size="small" :loading="submitting" @click="doClassify">确认判定</Button>
|
<Button size="small" @click="toggleClassify">取消</Button>
|
</div>
|
</template>
|
|
<!-- 底部按钮 -->
|
<template #prepend-footer>
|
<div v-if="isEdit" class="flex flex-auto items-center gap-2">
|
<!-- 问题判定: 处理中 -->
|
<Button v-if="isProcessing && !showClassify" @click="toggleClassify">问题判定</Button>
|
<!-- 创建退货: 处理中 + 退货问题 -->
|
<Button v-if="isProcessing && isReturnIssue" type="primary" @click="toCreateReturn">创建退货申请</Button>
|
<!-- 完结: 处理中/待退货/待退款 -->
|
<template v-if="isProcessing || isPendingReturn || isPendingRefund">
|
<Popconfirm title="确认完结该工单?" @confirm="doResolve"><Button type="primary" :loading="submitting">完结</Button></Popconfirm>
|
</template>
|
<!-- 关闭: 已完结 -->
|
<template v-if="isResolved">
|
<Popconfirm title="确认关闭该工单?关闭后不可恢复。" @confirm="doClose"><Button type="primary" :loading="submitting">关闭</Button></Popconfirm>
|
</template>
|
<!-- 取消: 草稿/处理中 -->
|
<template v-if="isDraft || isProcessing">
|
<Popconfirm title="确认取消该工单?取消后不可恢复。" @confirm="doCancel"><Button danger :loading="submitting">取消工单</Button></Popconfirm>
|
</template>
|
</div>
|
</template>
|
|
<!-- 提交/取消按钮(编辑模式) -->
|
<template #footer>
|
<div v-if="isEdit" class="flex justify-end gap-2">
|
<Button @click="modalApi.close()">{{ $t('common.cancel') }}</Button>
|
<Button type="primary" :loading="submitting" @click="handleSubmit">{{ $t('common.confirm') }}</Button>
|
</div>
|
</template>
|
</Modal>
|
|
<ItemModal :title="editingItemIndex !== null ? '编辑物料' : '添加物料'" class="w-1/2">
|
<ItemForm class="mx-4" />
|
</ItemModal>
|
</template>
|