<script lang="ts" setup>
|
import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
|
|
import { computed, onMounted, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { Page } from '@vben/common-ui';
|
import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants';
|
|
import { Button, Card, Descriptions, Input, message, Popconfirm, Select, Tag } from 'ant-design-vue';
|
|
import {
|
assignTicket,
|
cancelTicket,
|
classifyTicket,
|
closeTicket,
|
getTicket,
|
resolveTicket,
|
} from '#/api/aftersales/ticket';
|
|
defineOptions({ name: 'AftersalesTicketDetail' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const loading = ref(false);
|
const ticket = ref<AftersalesTicketApi.Ticket>();
|
|
const ticketId = computed(() => Number(route.params.id));
|
|
const isDraft = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.DRAFT);
|
const isProcessing = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PROCESSING);
|
const isPendingReturn = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PENDING_RETURN);
|
const isPendingRefund = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PENDING_REFUND);
|
const isResolved = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.RESOLVED);
|
|
// 分配处理人
|
const showAssignInput = ref(false);
|
const assignUserId = ref<number>();
|
|
// 问题判定
|
const showClassifyInput = ref(false);
|
const classifyIssueType = ref<number>();
|
const classifyIssueCategory = ref('');
|
const classifySeverityLevel = ref<number>();
|
|
const issueTypeOptions = [
|
{ label: '一般问题', value: 1 },
|
{ label: '维修问题', value: 2 },
|
{ label: '退货问题', value: 3 },
|
];
|
|
const severityOptions = [
|
{ label: '轻微', value: 0 },
|
{ label: '一般', value: 1 },
|
{ label: '严重', value: 2 },
|
{ label: '致命', value: 3 },
|
];
|
|
const statusMap: Record<number, { color: string; label: string }> = {
|
[AftersaleTicketStatusEnum.DRAFT]: { color: 'default', label: '草稿' },
|
[AftersaleTicketStatusEnum.PROCESSING]: { color: 'processing', label: '处理中' },
|
[AftersaleTicketStatusEnum.PENDING_RETURN]: { color: 'warning', label: '待退货' },
|
[AftersaleTicketStatusEnum.PENDING_REFUND]: { color: 'warning', label: '待退款/换货' },
|
[AftersaleTicketStatusEnum.RESOLVED]: { color: 'success', label: '已完结' },
|
[AftersaleTicketStatusEnum.CLOSED]: { color: 'default', label: '已关闭' },
|
[AftersaleTicketStatusEnum.CANCELLED]: { color: 'error', label: '已取消' },
|
};
|
|
const issueTypeMap: Record<number, string> = {
|
1: '一般问题',
|
2: '维修问题',
|
3: '退货问题',
|
};
|
|
const severityMap: Record<number, { color: string; label: string }> = {
|
0: { color: 'green', label: '轻微' },
|
1: { color: 'blue', label: '一般' },
|
2: { color: 'orange', label: '严重' },
|
3: { color: 'red', label: '致命' },
|
};
|
|
async function loadTicket() {
|
loading.value = true;
|
try {
|
ticket.value = await getTicket(ticketId.value);
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function toggleAssignInput() {
|
showAssignInput.value = !showAssignInput.value;
|
assignUserId.value = undefined;
|
}
|
|
async function handleAssign() {
|
if (!assignUserId.value) {
|
message.warning('请输入处理人ID');
|
return;
|
}
|
await assignTicket(ticketId.value, assignUserId.value);
|
message.success('分配成功');
|
showAssignInput.value = false;
|
await loadTicket();
|
}
|
|
function toggleClassifyInput() {
|
showClassifyInput.value = !showClassifyInput.value;
|
if (showClassifyInput.value) {
|
classifyIssueType.value = ticket.value?.issueType;
|
classifyIssueCategory.value = ticket.value?.issueCategory || '';
|
classifySeverityLevel.value = ticket.value?.severityLevel;
|
}
|
}
|
|
async function handleClassify() {
|
if (!classifyIssueType.value) {
|
message.warning('请选择问题类型');
|
return;
|
}
|
await classifyTicket({
|
id: ticketId.value,
|
issueType: classifyIssueType.value,
|
issueCategory: classifyIssueCategory.value || undefined,
|
severityLevel: classifySeverityLevel.value,
|
});
|
message.success('问题判定成功');
|
showClassifyInput.value = false;
|
await loadTicket();
|
}
|
|
async function handleResolve() {
|
await resolveTicket(ticketId.value);
|
message.success('完结成功');
|
await loadTicket();
|
}
|
|
async function handleClose() {
|
await closeTicket(ticketId.value);
|
message.success('关闭成功');
|
await loadTicket();
|
}
|
|
async function handleCancel() {
|
await cancelTicket(ticketId.value);
|
message.success('取消成功');
|
await loadTicket();
|
}
|
|
function handleCreateReturn() {
|
router.push({
|
name: 'AftersalesReturnDetail',
|
params: { id: 'new' },
|
query: { ticketId: ticketId.value },
|
});
|
}
|
|
function goBack() {
|
router.back();
|
}
|
|
onMounted(() => {
|
loadTicket();
|
});
|
</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">售后工单详情</span>
|
</div>
|
</template>
|
|
<Card v-if="ticket" title="基本信息" class="mb-4">
|
<Descriptions :column="3" bordered size="small">
|
<Descriptions.Item label="工单单号">{{ ticket.no }}</Descriptions.Item>
|
<Descriptions.Item label="工单主题">{{ ticket.name }}</Descriptions.Item>
|
<Descriptions.Item label="状态">
|
<Tag :color="statusMap[ticket.status!]?.color">
|
{{ statusMap[ticket.status!]?.label }}
|
</Tag>
|
</Descriptions.Item>
|
<Descriptions.Item label="客户">{{ ticket.customerName }}</Descriptions.Item>
|
<Descriptions.Item label="销售订单号">{{ ticket.saleOrderNo }}</Descriptions.Item>
|
<Descriptions.Item label="问题类型">
|
{{ ticket.issueType ? issueTypeMap[ticket.issueType] : '-' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="严重程度">
|
<Tag v-if="ticket.severityLevel !== undefined" :color="severityMap[ticket.severityLevel]?.color">
|
{{ severityMap[ticket.severityLevel]?.label }}
|
</Tag>
|
<span v-else>-</span>
|
</Descriptions.Item>
|
<Descriptions.Item label="负责人">{{ ticket.ownerUserName || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="处理人">{{ ticket.handlerUserName || '-' }}</Descriptions.Item>
|
<Descriptions.Item label="问题描述" :span="3">
|
{{ ticket.issueDescription || '-' }}
|
</Descriptions.Item>
|
<Descriptions.Item label="备注" :span="3">
|
{{ ticket.remark || '-' }}
|
</Descriptions.Item>
|
</Descriptions>
|
</Card>
|
|
<Card v-if="ticket?.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-center">需要维修</th>
|
<th class="border p-2 text-center">需要退货</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr v-for="item in ticket.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.saleQuantity }}</td>
|
<td class="border p-2 text-right">{{ item.returnQuantity || '-' }}</td>
|
<td class="border p-2 text-center">
|
<Tag :color="item.needRepair ? 'green' : 'default'">
|
{{ item.needRepair ? '是' : '否' }}
|
</Tag>
|
</td>
|
<td class="border p-2 text-center">
|
<Tag :color="item.needReturn ? 'blue' : 'default'">
|
{{ item.needReturn ? '是' : '否' }}
|
</Tag>
|
</td>
|
</tr>
|
</tbody>
|
</table>
|
</div>
|
</Card>
|
|
<!-- 附件 -->
|
<Card v-if="ticket?.attachmentList?.length" title="附件" class="mb-4">
|
<div class="flex flex-wrap gap-3">
|
<a
|
v-for="file in ticket.attachmentList"
|
:key="file.id"
|
:href="file.url"
|
target="_blank"
|
class="text-blue-500 hover:underline"
|
>
|
{{ file.name || '附件-' + file.id }}
|
</a>
|
</div>
|
</Card>
|
|
<!-- 问题判定表单 -->
|
<Card v-if="showClassifyInput" title="问题判定" class="mb-4">
|
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex items-center gap-1">
|
<span class="text-sm text-gray-500">问题类型:</span>
|
<Select
|
v-model:value="classifyIssueType"
|
:options="issueTypeOptions"
|
placeholder="请选择"
|
style="width: 140px"
|
/>
|
</div>
|
<div class="flex items-center gap-1">
|
<span class="text-sm text-gray-500">问题分类:</span>
|
<Input v-model:value="classifyIssueCategory" placeholder="如外观缺陷" style="width: 160px" />
|
</div>
|
<div class="flex items-center gap-1">
|
<span class="text-sm text-gray-500">严重程度:</span>
|
<Select
|
v-model:value="classifySeverityLevel"
|
:options="severityOptions"
|
placeholder="请选择"
|
style="width: 120px"
|
/>
|
</div>
|
<Button type="primary" size="small" @click="handleClassify">确认判定</Button>
|
<Button size="small" @click="toggleClassifyInput">取消</Button>
|
</div>
|
</Card>
|
|
<template #extra>
|
<div class="flex gap-2">
|
<!-- 创建退货申请 -->
|
<Button v-if="isProcessing && ticket?.issueType === AftersaleIssueTypeEnum.RETURN" type="primary" @click="handleCreateReturn">
|
创建退货申请
|
</Button>
|
|
<!-- 分配处理人 -->
|
<template v-if="isDraft || isProcessing">
|
<template v-if="!showAssignInput">
|
<Button @click="toggleAssignInput">分配处理人</Button>
|
</template>
|
<template v-else>
|
<Input
|
v-model:value="assignUserId"
|
placeholder="处理人ID"
|
type="number"
|
style="width: 120px"
|
@pressEnter="handleAssign"
|
/>
|
<Button size="small" type="primary" @click="handleAssign">确认</Button>
|
<Button size="small" @click="toggleAssignInput">取消</Button>
|
</template>
|
</template>
|
|
<!-- 问题判定 -->
|
<Button v-if="isProcessing && !showClassifyInput" @click="toggleClassifyInput">问题判定</Button>
|
|
<!-- 完结 -->
|
<Popconfirm
|
v-if="isProcessing || isPendingReturn || isPendingRefund"
|
title="确认完结该工单?"
|
@confirm="handleResolve"
|
>
|
<Button type="primary">完结</Button>
|
</Popconfirm>
|
|
<!-- 关闭 -->
|
<Popconfirm
|
v-if="isResolved"
|
title="确认关闭该工单?关闭后不可恢复。"
|
@confirm="handleClose"
|
>
|
<Button type="primary">关闭</Button>
|
</Popconfirm>
|
|
<!-- 取消 -->
|
<Popconfirm
|
v-if="isDraft || isProcessing"
|
title="确认取消该工单?取消后不可恢复。"
|
@confirm="handleCancel"
|
>
|
<Button danger>取消</Button>
|
</Popconfirm>
|
</div>
|
</template>
|
</Page>
|
</template>
|