update AIChart, customer followup, media selector
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user