src/views/aftersales/ticket/detail.vue
@@ -7,7 +7,17 @@
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,
@@ -17,6 +27,11 @@
  getTicket,
  resolveTicket,
} from '#/api/aftersales/ticket';
import type { AftersalesVisitApi } from '#/api/aftersales/visit';
import {
  createVisit,
  getVisitListByTicketId,
} from '#/api/aftersales/visit';
defineOptions({ name: 'AftersalesTicketDetail' });
@@ -32,6 +47,32 @@
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);
@@ -86,6 +127,30 @@
  } 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() {
@@ -161,6 +226,7 @@
onMounted(() => {
  loadTicket();
  loadVisits();
});
</script>
@@ -195,6 +261,14 @@
        </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>
@@ -253,6 +327,101 @@
        >
          {{ 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>
@@ -323,13 +492,13 @@
          <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>
        <!-- 取消 -->