378 lines
11 KiB
Vue
378 lines
11 KiB
Vue
<template>
|
|
<div v-can="permissionCodes.customerFollowUpReminder.read" class="follow-up-reminder-detail">
|
|
<t-empty v-if="!detail" :description="isLoading ? '正在加载跟进提醒' : '跟进提醒不存在'" />
|
|
<template v-else>
|
|
<customer-profile-card
|
|
class="customer"
|
|
:customer="detail.customer"
|
|
v-entrance-transition
|
|
@click="toCustomer"
|
|
/>
|
|
<t-cell-group title="提醒信息" theme="card" v-entrance-transition="70">
|
|
<t-cell title="业务状态">
|
|
<template #note>
|
|
<t-tag size="small" :theme="getStatusTheme(detail.status)" :content="getStatusText(detail.status)" />
|
|
</template>
|
|
</t-cell>
|
|
<t-cell title="负责人" :note="getResponsibleName(detail)" />
|
|
<t-cell title="提醒时间" :note="detail.remindAt ? Time.toDateTime(detail.remindAt) : '-'" />
|
|
<t-cell title="创建时间">
|
|
<template #note>
|
|
<pass-time-label :timestamp="detail.createdAt" underline />
|
|
</template>
|
|
</t-cell>
|
|
</t-cell-group>
|
|
<t-cell-group title="跟进内容" theme="card" v-entrance-transition="140">
|
|
<div class="content bg-white" v-text="detail.content || '未填写跟进内容'" />
|
|
<div class="media-wrap bg-white">
|
|
<media-selector
|
|
:model-value="attachmentUrlList"
|
|
:photo-items="attachmentItemList"
|
|
:show-add="false"
|
|
:show-remove="false"
|
|
:show-viewer-actions="false"
|
|
empty-text="暂无附件"
|
|
title="附件"
|
|
/>
|
|
</div>
|
|
</t-cell-group>
|
|
<t-cell-group title="通知方式" theme="card" v-entrance-transition="210">
|
|
<t-cell v-if="hasChannel('INTERNAL')" title="站内通知">
|
|
<template #note>
|
|
<t-tag
|
|
size="small"
|
|
variant="outline"
|
|
:theme="internalNotifyStatus ? getNotifyStatusTheme(internalNotifyStatus.status) : 'primary'"
|
|
:content="internalNotifyStatus ? getNotifyStatusText(internalNotifyStatus.status) : '已配置'"
|
|
/>
|
|
</template>
|
|
</t-cell>
|
|
<t-cell v-if="hasChannel('SMS')" title="短信通知">
|
|
<template #note>
|
|
<t-tag
|
|
size="small"
|
|
theme="primary"
|
|
variant="outline"
|
|
:content="detail.smsNotifyDetailId ? '已进入当天批次' : '等待当天 08:00 批次'"
|
|
/>
|
|
</template>
|
|
</t-cell>
|
|
<t-cell v-if="!detail.notifyChannelList?.length" title="通知方式" note="不发送通知" />
|
|
<div class="tips gray bg-white" v-text="'站内通知在提醒时间后生效;短信按提醒日期在 08:00 聚合发送。通知状态仅作辅助展示,不代表跟进提醒业务状态。'" />
|
|
</t-cell-group>
|
|
<div class="actions" v-entrance-transition="350">
|
|
<t-button
|
|
v-if="canComplete && detail.status === 'PENDING'"
|
|
theme="primary"
|
|
:disabled="isOperating"
|
|
:loading="isCompleting"
|
|
@click="confirmComplete"
|
|
>完成跟进</t-button>
|
|
<t-button
|
|
v-if="canDelete"
|
|
theme="danger"
|
|
:loading="isDeleting"
|
|
:disabled="isOperating"
|
|
@click="confirmDelete"
|
|
>删除提醒</t-button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { DialogPlugin } from "tdesign-mobile-vue";
|
|
import { NotifyAPI, type NotifyDetail, useUserStore } from "timi-web-api";
|
|
import { PassTimeLabel, Time } from "timi-web";
|
|
import { Message } from "@/utils/message";
|
|
import { getErrorMessage } from "@/utils/error";
|
|
import FollowUpReminderAPI from "@/api/FollowUpReminderAPI";
|
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
|
import MediaSelector from "@/components/mediaSelector/MediaSelector.vue";
|
|
import { useNavBarStore } from "@/store/navBarStore";
|
|
import type { FollowUpReminder, FollowUpReminderNotifyChannel, FollowUpReminderStatus } from "@/types/FollowUpReminder";
|
|
import type { PhotoItem } from "@/types/Photo";
|
|
import { getAttachmentReadURL } from "@/utils/attachment";
|
|
import { corePermissionCodes, permissionCodes } from "@/utils/permission";
|
|
|
|
type NotifyTagTheme = "default" | "success" | "warning" | "primary" | "danger";
|
|
|
|
defineOptions({
|
|
name: "FollowUpReminderDetail"
|
|
});
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const navBarStore = useNavBarStore();
|
|
const userStore = useUserStore();
|
|
|
|
const detail = ref<FollowUpReminder>();
|
|
const isLoading = ref(false);
|
|
const isCompleting = ref(false);
|
|
const isDeleting = ref(false);
|
|
const internalNotifyStatus = ref<NotifyDetail>();
|
|
let loadToken = 0;
|
|
|
|
const canUpdate = computed(() => detail.value?.status === "PENDING"
|
|
&& userStore.hasPermissions(permissionCodes.customerFollowUpReminder.update));
|
|
const canComplete = computed(() => userStore.hasPermissions(permissionCodes.customerFollowUpReminder.complete));
|
|
const canDelete = computed(() => userStore.hasPermissions(permissionCodes.customerFollowUpReminder.delete));
|
|
const isOperating = computed(() => isCompleting.value || isDeleting.value);
|
|
const attachmentItemList = computed<PhotoItem[]>(() => (detail.value?.attachmentList || [])
|
|
.filter(item => !!item.id)
|
|
.map(item => ({
|
|
attachment: item,
|
|
attachType: item.attachType || "PHOTO",
|
|
mimeType: item.mimeType,
|
|
previewUrl: getAttachmentReadURL(item.id || ""),
|
|
url: getAttachmentReadURL(item.id || "", 90)
|
|
})));
|
|
const attachmentUrlList = computed(() => attachmentItemList.value.map(item => item.url));
|
|
|
|
function hasChannel(channel: FollowUpReminderNotifyChannel): boolean {
|
|
const item = detail.value;
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
return item.notifyChannelList?.includes(channel)
|
|
|| (channel === "INTERNAL" && item.internalNotifyEnabled === true)
|
|
|| (channel === "SMS" && item.smsNotifyEnabled === true)
|
|
|| false;
|
|
}
|
|
|
|
function getResponsibleName(item: FollowUpReminder): string {
|
|
return item.responsibleUser?.nick || item.responsibleUser?.name || item.responsibleUserId || "未命名负责人";
|
|
}
|
|
|
|
function getStatusText(status?: FollowUpReminderStatus): string {
|
|
return status === "COMPLETED" ? "已完成" : "待完成";
|
|
}
|
|
|
|
function getStatusTheme(status?: FollowUpReminderStatus): "success" | "warning" {
|
|
return status === "COMPLETED" ? "success" : "warning";
|
|
}
|
|
|
|
function getNotifyStatusText(status?: NotifyDetail["status"]): string {
|
|
const map: Record<NonNullable<NotifyDetail["status"]>, string> = {
|
|
WAITING: "等待生效",
|
|
REMINDED: "已生效",
|
|
TIMEOUT: "已超时",
|
|
CANCEL: "已取消",
|
|
FAIL: "发送失败"
|
|
};
|
|
return status ? map[status] : "状态未知";
|
|
}
|
|
|
|
function getNotifyStatusTheme(status?: NotifyDetail["status"]): NotifyTagTheme {
|
|
if (status === "REMINDED") {
|
|
return "success";
|
|
}
|
|
if (status === "FAIL" || status === "TIMEOUT") {
|
|
return "danger";
|
|
}
|
|
if (status === "WAITING") {
|
|
return "warning";
|
|
}
|
|
return "primary";
|
|
}
|
|
|
|
async function loadDetail(): Promise<void> {
|
|
const token = loadToken + 1;
|
|
loadToken = token;
|
|
detail.value = undefined;
|
|
internalNotifyStatus.value = undefined;
|
|
navBarStore.clearRight();
|
|
const id = String(route.params.id || "");
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
isLoading.value = true;
|
|
try {
|
|
const result = await FollowUpReminderAPI.detail(id);
|
|
if (token !== loadToken) {
|
|
return;
|
|
}
|
|
detail.value = result;
|
|
setEditAction();
|
|
await loadInternalNotifyStatus(result, token);
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "跟进提醒详情加载失败"));
|
|
} finally {
|
|
if (token === loadToken) {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setEditAction(): void {
|
|
if (canUpdate.value) {
|
|
navBarStore.setRightText("编辑", "var(--td-brand-color)", toEdit);
|
|
}
|
|
}
|
|
|
|
async function loadInternalNotifyStatus(item: FollowUpReminder, token: number): Promise<void> {
|
|
if (!item.internalNotifyDetailId || !userStore.hasPermissions(corePermissionCodes.notify.read)) {
|
|
return;
|
|
}
|
|
try {
|
|
const result = await NotifyAPI.internalDetail(item.internalNotifyDetailId);
|
|
if (token === loadToken) {
|
|
internalNotifyStatus.value = result;
|
|
}
|
|
} catch {
|
|
// 站内通知详情只对当前接收用户开放,无法读取时不影响提醒详情。
|
|
}
|
|
}
|
|
|
|
async function toCustomer(): Promise<void> {
|
|
if (detail.value?.customerId) {
|
|
await router.push({
|
|
name: "CustomerDetail",
|
|
params: { id: detail.value.customerId },
|
|
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
|
|
});
|
|
}
|
|
}
|
|
|
|
function getCurrentRouteDepth(): number {
|
|
const transitionDepth = Array.isArray(route.query.transitionDepth)
|
|
? route.query.transitionDepth[0]
|
|
: route.query.transitionDepth;
|
|
const depth = Number(transitionDepth);
|
|
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
|
|
return depth;
|
|
}
|
|
return Number(route.meta.depth ?? 0);
|
|
}
|
|
|
|
async function toEdit(): Promise<void> {
|
|
if (detail.value?.id && canUpdate.value) {
|
|
await router.push({ name: "CustomerFollowUpReminderEdit", params: { id: detail.value.id } });
|
|
}
|
|
}
|
|
|
|
function confirmComplete(): void {
|
|
if (!detail.value?.id || !canComplete.value || detail.value.status !== "PENDING" || isOperating.value) {
|
|
return;
|
|
}
|
|
const dialog = DialogPlugin.confirm({
|
|
title: "完成跟进提醒",
|
|
content: "完成后不能重新打开,确定完成此跟进提醒吗?",
|
|
cancelBtn: "取消",
|
|
confirmBtn: "完成",
|
|
onCancel() {
|
|
dialog.destroy();
|
|
},
|
|
onConfirm: async () => {
|
|
dialog.destroy();
|
|
await complete(detail.value?.id || "");
|
|
}
|
|
});
|
|
}
|
|
|
|
async function complete(id: string): Promise<void> {
|
|
isCompleting.value = true;
|
|
try {
|
|
await FollowUpReminderAPI.complete(id);
|
|
Message.success("跟进提醒已完成");
|
|
await loadDetail();
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "跟进提醒完成失败"));
|
|
} finally {
|
|
isCompleting.value = false;
|
|
}
|
|
}
|
|
|
|
function confirmDelete(): void {
|
|
if (!detail.value?.id || !canDelete.value || isOperating.value) {
|
|
return;
|
|
}
|
|
const dialog = DialogPlugin.confirm({
|
|
title: "删除跟进提醒",
|
|
content: "删除后列表中将不再显示,确定删除这条跟进提醒吗?",
|
|
cancelBtn: "取消",
|
|
confirmBtn: "删除",
|
|
onCancel() {
|
|
dialog.destroy();
|
|
},
|
|
onConfirm: async () => {
|
|
dialog.destroy();
|
|
await remove(detail.value?.id || "");
|
|
}
|
|
});
|
|
}
|
|
|
|
async function remove(id: string): Promise<void> {
|
|
isDeleting.value = true;
|
|
navBarStore.clearRight();
|
|
try {
|
|
await FollowUpReminderAPI.remove(id);
|
|
Message.success("跟进提醒已删除");
|
|
await router.replace({ name: "CustomerFollowUpReminderList" });
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "跟进提醒删除失败"));
|
|
setEditAction();
|
|
} finally {
|
|
isDeleting.value = false;
|
|
}
|
|
}
|
|
|
|
watch(() => route.params.id, async () => {
|
|
await loadDetail();
|
|
}, { immediate: true });
|
|
|
|
onUnmounted(() => navBarStore.clearRight());
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.follow-up-reminder-detail {
|
|
gap: 1rem;
|
|
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.customer {
|
|
margin: 0 1rem;
|
|
}
|
|
|
|
.content {
|
|
padding: 1rem;
|
|
line-height: 1.7;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.media-wrap {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.tips {
|
|
padding: 1rem;
|
|
font-size: .875rem;
|
|
}
|
|
|
|
.actions {
|
|
--td-button-border-radius: 1rem;
|
|
|
|
gap: .5rem;
|
|
padding: 0 1rem;
|
|
display: flex;
|
|
|
|
.t-button {
|
|
flex: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
:global(html.theme-dark) {
|
|
.follow-up-reminder-detail {
|
|
.content,
|
|
.attachment-wrapper {
|
|
background: rgba(30, 41, 59, .9);
|
|
border-color: rgba(148, 163, 184, .16);
|
|
}
|
|
}
|
|
}
|
|
</style>
|