| | |
| | | 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 { |
| | | Button, |
| | | Card, |
| | | Descriptions, |
| | | Input, |
| | | message, |
| | | Popconfirm, |
| | | Select, |
| | | Switch, |
| | | Tag, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | assignTicket, |
| | |
| | | getTicket, |
| | | resolveTicket, |
| | | } from '#/api/aftersales/ticket'; |
| | | import type { AftersalesVisitApi } from '#/api/aftersales/visit'; |
| | | import { |
| | | createVisit, |
| | | getVisitListByTicketId, |
| | | } from '#/api/aftersales/visit'; |
| | | |
| | | defineOptions({ name: 'AftersalesTicketDetail' }); |
| | | |
| | |
| | | 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 isVisitPassed = computed(() => ticket.value?.visitStatus === 1); |
| | | |
| | | // 回访记录 |
| | | const visits = ref<AftersalesVisitApi.Visit[]>([]); |
| | | const showVisitForm = ref(false); |
| | | const visitForm = ref<AftersalesVisitApi.CreateVisit>({ |
| | | ticketId: 0, |
| | | visitType: 0, |
| | | content: '', |
| | | satisfied: true, |
| | | recurred: false, |
| | | }); |
| | | |
| | | const visitTypeOptions = [ |
| | | { label: '电话回访', value: 0 }, |
| | | { label: '上门回访', value: 1 }, |
| | | { label: '远程回访', value: 2 }, |
| | | { label: '其他', value: 3 }, |
| | | ]; |
| | | const visitTypeMap: Record<number, string> = { |
| | | 0: '电话', |
| | | 1: '上门', |
| | | 2: '远程', |
| | | 3: '其他', |
| | | }; |
| | | |
| | | // 分配处理人 |
| | | const showAssignInput = ref(false); |
| | |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function loadVisits() { |
| | | visits.value = await getVisitListByTicketId(ticketId.value); |
| | | } |
| | | |
| | | function toggleVisitForm() { |
| | | showVisitForm.value = !showVisitForm.value; |
| | | if (showVisitForm.value) { |
| | | visitForm.value = { |
| | | ticketId: ticketId.value, |
| | | visitType: 0, |
| | | content: '', |
| | | satisfied: true, |
| | | recurred: false, |
| | | }; |
| | | } |
| | | } |
| | | |
| | | async function handleCreateVisit() { |
| | | await createVisit({ ...visitForm.value, ticketId: ticketId.value }); |
| | | message.success('回访登记成功'); |
| | | showVisitForm.value = false; |
| | | await Promise.all([loadVisits(), loadTicket()]); |
| | | } |
| | | |
| | | function toggleAssignInput() { |
| | |
| | | |
| | | onMounted(() => { |
| | | loadTicket(); |
| | | loadVisits(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="负责人">{{ ticket.ownerUserName || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="处理人">{{ ticket.handlerUserName || '-' }}</Descriptions.Item> |
| | | <Descriptions.Item label="回访状态"> |
| | | <Tag v-if="ticket.visitStatus === 1" color="green">已回访通过</Tag> |
| | | <Tag v-else color="orange">待回访</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="问题沉淀" :span="2"> |
| | | <template v-if="ticket.problemNo">{{ ticket.problemNo }}</template> |
| | | <template v-else>-</template> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="问题描述" :span="3"> |
| | | {{ ticket.issueDescription || '-' }} |
| | | </Descriptions.Item> |
| | |
| | | > |
| | | {{ file.name || '附件-' + file.id }} |
| | | </a> |
| | | </div> |
| | | </Card> |
| | | |
| | | <!-- 回访记录 --> |
| | | <Card v-if="ticket" title="回访记录" class="mb-4"> |
| | | <template #extra> |
| | | <Button |
| | | v-if="isResolved && !showVisitForm" |
| | | type="primary" |
| | | size="small" |
| | | @click="toggleVisitForm" |
| | | > |
| | | 登记回访 |
| | | </Button> |
| | | </template> |
| | | |
| | | <div v-if="isResolved" class="mb-3 text-sm text-gray-500"> |
| | | 登记「满意 且 未复发」的回访后,工单才可归档;登记「不满意或复发」会自动退回处理中。 |
| | | </div> |
| | | |
| | | <!-- 登记回访表单 --> |
| | | <div |
| | | v-if="showVisitForm" |
| | | class="mb-4 flex flex-wrap items-center gap-3 rounded bg-gray-50 p-3" |
| | | > |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">回访方式:</span> |
| | | <Select |
| | | v-model:value="visitForm.visitType" |
| | | :options="visitTypeOptions" |
| | | style="width: 120px" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">回访内容:</span> |
| | | <Input |
| | | v-model:value="visitForm.content" |
| | | placeholder="客户反馈内容" |
| | | style="width: 260px" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">是否满意:</span> |
| | | <Switch |
| | | v-model:checked="visitForm.satisfied" |
| | | checked-children="满意" |
| | | un-checked-children="不满意" |
| | | /> |
| | | </div> |
| | | <div class="flex items-center gap-1"> |
| | | <span class="text-sm text-gray-500">是否复发:</span> |
| | | <Switch |
| | | v-model:checked="visitForm.recurred" |
| | | checked-children="复发" |
| | | un-checked-children="未复发" |
| | | /> |
| | | </div> |
| | | <Button type="primary" size="small" @click="handleCreateVisit">提交</Button> |
| | | <Button size="small" @click="toggleVisitForm">取消</Button> |
| | | </div> |
| | | |
| | | <!-- 回访记录列表 --> |
| | | <div v-if="visits.length === 0" class="text-sm text-gray-400">暂无回访记录</div> |
| | | <div v-else 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-center">满意</th> |
| | | <th class="border p-2 text-center">复发</th> |
| | | <th class="border p-2 text-left">内容</th> |
| | | <th class="border p-2 text-left">备注</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="visit in visits" :key="visit.id"> |
| | | <td class="border p-2">{{ visit.visitTime }}</td> |
| | | <td class="border p-2">{{ visitTypeMap[visit.visitType!] ?? '-' }}</td> |
| | | <td class="border p-2">{{ visit.visitorUserName || '-' }}</td> |
| | | <td class="border p-2 text-center"> |
| | | <Tag :color="visit.satisfied ? 'green' : 'red'"> |
| | | {{ visit.satisfied ? '满意' : '不满意' }} |
| | | </Tag> |
| | | </td> |
| | | <td class="border p-2 text-center"> |
| | | <Tag v-if="visit.recurred" color="orange">复发</Tag> |
| | | <Tag v-else color="default">未复发</Tag> |
| | | </td> |
| | | <td class="border p-2">{{ visit.content || '-' }}</td> |
| | | <td class="border p-2">{{ visit.remark || '-' }}</td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </Card> |
| | | |
| | |
| | | <Button type="primary">完结</Button> |
| | | </Popconfirm> |
| | | |
| | | <!-- 关闭 --> |
| | | <!-- 归档(仅回访通过后可归档;未回访请在“回访记录”里登记) --> |
| | | <Popconfirm |
| | | v-if="isResolved" |
| | | title="确认关闭该工单?关闭后不可恢复。" |
| | | v-if="isResolved && isVisitPassed" |
| | | title="确认归档该工单?归档后自动沉淀一条问题,工单只读不可恢复。" |
| | | @confirm="handleClose" |
| | | > |
| | | <Button type="primary">关闭</Button> |
| | | <Button type="primary">归档</Button> |
| | | </Popconfirm> |
| | | |
| | | <!-- 取消 --> |