Files
gao-customer-h5/src/pages/followUpReminder/FollowUpReminderDetail.vue
T

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>