285 lines
7.7 KiB
Vue
285 lines
7.7 KiB
Vue
<template>
|
|
<div v-can="corePermissionCodes.notify.read" class="notify-detail">
|
|
<t-empty
|
|
v-if="!detail"
|
|
:description="isLoading ? '正在加载通知' : '通知不存在'"
|
|
/>
|
|
<template v-else>
|
|
<t-cell-group title="通知状态" theme="card" v-entrance-transition>
|
|
<t-cell title="发送用户" :note="`${detail.detailList?.length || 0} 人`" arrow @click="openRecipientList"/>
|
|
<t-cell title="已读用户" :note="`${readCount} 人`" arrow @click="openReadRecipientList"/>
|
|
<t-cell title="未读用户" :note="`${unreadCount} 人`" arrow @click="openUnreadRecipientList"/>
|
|
<t-cell title="当前状态">
|
|
<template #note>
|
|
<t-tag size="small" :theme="getSummaryTheme" :content="getSummaryStatus" />
|
|
</template>
|
|
</t-cell>
|
|
</t-cell-group>
|
|
<t-button
|
|
v-if="canCancelNotify && canCancel"
|
|
class="bg-white red cell-button"
|
|
variant="text"
|
|
block
|
|
:loading="isCanceling"
|
|
v-entrance-transition="70"
|
|
@click="confirmCancel"
|
|
content="取消发送"
|
|
/>
|
|
<notify-content
|
|
class="preview"
|
|
:detail="mainDetail"
|
|
:subject="getSubject(detail)"
|
|
:time="getSendTime(mainDetail)"
|
|
:resolve-attachment-url="resolveAttachmentUrl"
|
|
v-entrance-transition="140"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Time } from "timi-web";
|
|
import { NotifyAPI, type Notify, type NotifyDetail, useUserStore } from "timi-web-api";
|
|
import { DialogPlugin } from "tdesign-mobile-vue";
|
|
import { Message } from "@/utils/message";
|
|
import { getErrorMessage } from "@/utils/error";
|
|
import { getAttachmentReadURL } from "@/utils/attachment";
|
|
import { useNavBarStore } from "@/store/navBarStore";
|
|
import { corePermissionCodes } from "@/utils/permission";
|
|
import NotifyContent from "@/pages/notify/component/NotifyContent.vue";
|
|
|
|
type NotifyTagTheme = "default" | "success" | "warning" | "primary" | "danger";
|
|
type RecipientFilter = "all" | "unread" | "read";
|
|
|
|
defineOptions({
|
|
name: "NotifyDetail"
|
|
});
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const navBarStore = useNavBarStore();
|
|
const userStore = useUserStore();
|
|
const canDeleteNotify = computed(() => userStore.hasPermissions(corePermissionCodes.notify.delete));
|
|
const canCancelNotify = computed(() => userStore.hasPermissions(corePermissionCodes.notify.cancel));
|
|
|
|
// ---------- 通知数据 ----------
|
|
|
|
const detail = ref<Notify>();
|
|
const isLoading = ref(false);
|
|
const isCanceling = ref(false);
|
|
const isDeleting = ref(false);
|
|
let loadToken = 0;
|
|
|
|
const mainDetail = computed(() => detail.value?.detailList?.[0]);
|
|
const readCount = computed(() => detail.value?.detailList?.filter(item => Boolean(item.readAt)).length || 0);
|
|
const unreadCount = computed(() => detail.value?.detailList?.filter(item => !item.readAt).length || 0);
|
|
const canCancel = computed(() => detail.value?.detailList?.some(item => isCancelable(item)) || false);
|
|
const getSummaryStatus = computed(() => {
|
|
const detailList = detail.value?.detailList || [];
|
|
if (canCancel.value) {
|
|
return "待发送";
|
|
}
|
|
if (detailList.length && detailList.every(item => item.status === "CANCEL")) {
|
|
return "已取消";
|
|
}
|
|
if (detailList.some(item => item.status === "FAIL" || item.status === "TIMEOUT")) {
|
|
return "部分失败";
|
|
}
|
|
return "已发送";
|
|
});
|
|
const getSummaryTheme = computed<NotifyTagTheme>(() => {
|
|
if (getSummaryStatus.value === "待发送") {
|
|
return "warning";
|
|
}
|
|
if (getSummaryStatus.value === "已取消") {
|
|
return "default";
|
|
}
|
|
if (getSummaryStatus.value === "部分失败") {
|
|
return "danger";
|
|
}
|
|
return "success";
|
|
});
|
|
|
|
function resolveAttachmentUrl(attachmentId: string): string {
|
|
return getAttachmentReadURL(attachmentId);
|
|
}
|
|
|
|
async function loadDetail(): Promise<void> {
|
|
const requestToken = loadToken + 1;
|
|
loadToken = requestToken;
|
|
detail.value = undefined;
|
|
navBarStore.clearRight();
|
|
const notifyId = String(route.params.id || "");
|
|
if (!notifyId) {
|
|
return;
|
|
}
|
|
|
|
isLoading.value = true;
|
|
try {
|
|
const result = await NotifyAPI.managementDetail(notifyId);
|
|
if (requestToken !== loadToken) {
|
|
return;
|
|
}
|
|
detail.value = result;
|
|
setRightAction();
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "通知详情加载失败"));
|
|
} finally {
|
|
if (requestToken === loadToken) {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setRightAction(): void {
|
|
if (detail.value?.id && canDeleteNotify.value) {
|
|
navBarStore.setRightText("删除", "var(--td-error-color)", confirmDelete);
|
|
}
|
|
}
|
|
|
|
// ---------- 通知操作 ----------
|
|
|
|
function isCancelable(item: NotifyDetail): boolean {
|
|
return item.status === "WAITING" && Boolean(item.sendAt && Time.now() < item.sendAt);
|
|
}
|
|
|
|
async function confirmCancel(): Promise<void> {
|
|
const notifyId = detail.value?.id;
|
|
if (!notifyId || !canCancelNotify.value || !canCancel.value || isCanceling.value) {
|
|
return;
|
|
}
|
|
const dialog = DialogPlugin.confirm({
|
|
title: "取消定时通知",
|
|
content: "确定取消这条尚未发送的通知吗?",
|
|
cancelBtn: "返回",
|
|
confirmBtn: "取消发送",
|
|
onCancel() {
|
|
dialog.destroy();
|
|
},
|
|
async onConfirm() {
|
|
dialog.destroy();
|
|
isCanceling.value = true;
|
|
try {
|
|
await NotifyAPI.managementCancel(notifyId);
|
|
await loadDetail();
|
|
Message.success("通知已取消");
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "通知取消失败"));
|
|
} finally {
|
|
isCanceling.value = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function confirmDelete(): Promise<void> {
|
|
const notifyId = detail.value?.id;
|
|
if (!notifyId || !canDeleteNotify.value || isDeleting.value) {
|
|
return;
|
|
}
|
|
const dialog = DialogPlugin.confirm({
|
|
title: "删除通知",
|
|
content: "确定删除这条通知吗?删除后收件人也将无法查看。",
|
|
cancelBtn: "返回",
|
|
confirmBtn: "删除",
|
|
onCancel() {
|
|
dialog.destroy();
|
|
},
|
|
async onConfirm() {
|
|
dialog.destroy();
|
|
isDeleting.value = true;
|
|
navBarStore.clearRight();
|
|
try {
|
|
await NotifyAPI.managementDelete(notifyId);
|
|
Message.success("通知已删除");
|
|
await router.replace({ name: "NotifyList" });
|
|
} catch (error) {
|
|
Message.error(getErrorMessage(error, "通知删除失败"));
|
|
setRightAction();
|
|
} finally {
|
|
isDeleting.value = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------- 展示 ----------
|
|
|
|
function getSubject(item: Notify): string {
|
|
return item.detailList?.[0]?.subject?.trim() || "站内通知";
|
|
}
|
|
|
|
function getSendTime(item?: NotifyDetail): string {
|
|
if (!item?.sendAt) {
|
|
return "-";
|
|
}
|
|
if (isCancelable(item)) {
|
|
return `定时 ${Time.toDateTime(item.sendAt)}`;
|
|
}
|
|
return Time.toDateTime(item.sendAt);
|
|
}
|
|
|
|
async function openRecipientList(): Promise<void> {
|
|
const notifyId = detail.value?.id;
|
|
if (!notifyId) {
|
|
return;
|
|
}
|
|
await router.push({
|
|
name: "NotifyRecipientList",
|
|
params: { id: notifyId },
|
|
query: { filter: "all" satisfies RecipientFilter }
|
|
});
|
|
}
|
|
|
|
async function openReadRecipientList(): Promise<void> {
|
|
await openRecipientListByFilter("read");
|
|
}
|
|
|
|
async function openUnreadRecipientList(): Promise<void> {
|
|
await openRecipientListByFilter("unread");
|
|
}
|
|
|
|
async function openRecipientListByFilter(filter: RecipientFilter): Promise<void> {
|
|
const notifyId = detail.value?.id;
|
|
if (!notifyId) {
|
|
return;
|
|
}
|
|
await router.push({
|
|
name: "NotifyRecipientList",
|
|
params: { id: notifyId },
|
|
query: { filter }
|
|
});
|
|
}
|
|
|
|
// ---------- 生命周期 ----------
|
|
|
|
watch(() => route.params.id, async () => {
|
|
await loadDetail();
|
|
}, { immediate: true });
|
|
|
|
onMounted(() => navBarStore.clearRight());
|
|
onUnmounted(() => navBarStore.clearRight());
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.notify-detail {
|
|
gap: 1rem;
|
|
min-height: 100%;
|
|
padding: var(--app-nav-height) 0 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.article {
|
|
padding: 1.25rem 1rem;
|
|
margin: 0 1rem;
|
|
background: var(--td-bg-color-container);
|
|
box-shadow: var(--td-shadow-1);
|
|
border-radius: 1.4rem;
|
|
}
|
|
|
|
.preview {
|
|
background: #FFF;
|
|
}
|
|
}
|
|
</style>
|