286 lines
7.6 KiB
Vue
286 lines
7.6 KiB
Vue
<script lang="ts" setup>
|
||
import type { NotificationItem } from '@vben/layouts';
|
||
|
||
import {computed, onUnmounted, ref, watch} from 'vue';
|
||
|
||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||
|
||
// import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
||
import { useWatermark } from '@vben/hooks';
|
||
// import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
||
// import { CircleHelp } from '@vben/icons';
|
||
import {
|
||
BasicLayout,
|
||
LockScreen,
|
||
Notification,
|
||
UserDropdown,
|
||
} from '@vben/layouts';
|
||
import { preferences } from '@vben/preferences';
|
||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||
// import { openWindow } from '@vben/utils';
|
||
|
||
import {
|
||
Bell,
|
||
CalendarClock,
|
||
Megaphone,
|
||
Users,
|
||
} from 'lucide-vue-next';
|
||
|
||
// import { $t } from '#/locales';
|
||
import DoctorTransferFloat from '#/components/doctor-transfer-float/DoctorTransferFloat.vue';
|
||
import HeaderVipBadge from '#/components/vip/HeaderVipBadge.vue';
|
||
import { useAuthStore } from '#/store';
|
||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||
import SwitchAccountModal from '#/layouts/components/SwitchAccountModal.vue';
|
||
import { useRouter } from 'vue-router';
|
||
import {getNoticeListApi, readAllApi} from "#/views/notice/api";
|
||
import {formatTimeToRelative} from "#/util/tool";
|
||
import {notification} from "ant-design-vue";
|
||
|
||
const router = useRouter();
|
||
const switchAccountModalRef = ref<InstanceType<typeof SwitchAccountModal>>();
|
||
|
||
// 样式相关
|
||
const typeIcons = {
|
||
0: Megaphone,
|
||
1: Bell,
|
||
2: CalendarClock,
|
||
};
|
||
|
||
const typeColors = {
|
||
0: 'text-sky-500',
|
||
1: 'text-amber-500',
|
||
2: 'text-emerald-500',
|
||
};
|
||
|
||
const typeBgColors = {
|
||
0: 'bg-sky-50',
|
||
1: 'bg-amber-50',
|
||
2: 'bg-emerald-50',
|
||
};
|
||
|
||
const typeBorderColors = {
|
||
0: 'border-sky-200',
|
||
1: 'border-amber-200',
|
||
2: 'border-emerald-200',
|
||
};
|
||
|
||
const notifications = ref<NotificationItem[]>([]);
|
||
|
||
// 获取消息列表数据
|
||
const getNoticeList = async () => {
|
||
try {
|
||
const res = await getNoticeListApi({
|
||
status: 0,
|
||
});
|
||
let message = '未读消息';
|
||
if (res.items.length > 0) {
|
||
if (res.items.length > notifications.value.length) {
|
||
if (notifications.value.length === 0) {
|
||
message = '新消息';
|
||
}
|
||
notification.info({
|
||
message: `您有${message}`,
|
||
description: `您有 ${res.items.length} 条${message}请注意查收`,
|
||
duration: 5,
|
||
});
|
||
}
|
||
|
||
notifications.value = res.items.map(item => ({
|
||
id: item.id,
|
||
icon: typeIcons[item.type],
|
||
// 将API返回的数据映射到组件需要的格式
|
||
message: item.title,
|
||
title: getMessageTypeName(item.type),
|
||
date: formatTimeToRelative(item.created_at),
|
||
color: typeColors[item.type],
|
||
isRead: item.status
|
||
}));
|
||
} else {
|
||
notifications.value = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取消息列表失败:', error);
|
||
}
|
||
};
|
||
getNoticeList();
|
||
// 每隔二十秒刷新一次
|
||
let intervalId = setInterval(getNoticeList, 20_000);
|
||
|
||
// 封装定时器重启逻辑
|
||
const restartInterval = () => {
|
||
clearInterval(intervalId);
|
||
getNoticeList(); // 立即获取最新数据
|
||
intervalId = setInterval(getNoticeList, 20_000);
|
||
};
|
||
|
||
// 处理跨标签页修改
|
||
const handleStorageChange = (event) => {
|
||
if (event.key === 'readStatus') {
|
||
restartInterval();
|
||
}
|
||
};
|
||
|
||
// 处理当前标签页修改(核心新增部分)
|
||
const handleLocalChange = () => {
|
||
restartInterval();
|
||
};
|
||
|
||
// 添加双重监听
|
||
window.addEventListener('storage', handleStorageChange); // 跨标签页监听
|
||
const originalSetItem = localStorage.setItem.bind(localStorage);
|
||
|
||
// 拦截当前页的 localStorage.setItem 操作
|
||
localStorage.setItem = (key, value) => {
|
||
originalSetItem(key, value);
|
||
if (key === 'readStatus') {
|
||
handleLocalChange(); // 手动触发当前页回调
|
||
}
|
||
};
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
clearInterval(intervalId);
|
||
window.removeEventListener('storage', handleStorageChange);
|
||
localStorage.setItem = originalSetItem; // 恢复原始方法
|
||
});
|
||
|
||
// 消息类型定义
|
||
const messageTypes = [
|
||
{ id: -1, name: '全部消息' },
|
||
{ id: 0, name: '系统公告', icon: Megaphone },
|
||
{ id: 1, name: '系统通知', icon: Bell },
|
||
{ id: 2, name: '周报提醒', icon: CalendarClock },
|
||
];
|
||
// 根据类型ID获取类型名称
|
||
const getMessageTypeName = (typeId) => {
|
||
const type = messageTypes.find(t => t.id === typeId);
|
||
return type ? type.name : '未知类型';
|
||
};
|
||
const goNoticeList = () => {
|
||
// TODO 跳转通知列表
|
||
router.push('/notice/list');
|
||
};
|
||
|
||
// 处理消息点击
|
||
const handleItemClick = (item) => {
|
||
notifications.value.forEach((value) => {
|
||
if (value.id === item.id) {
|
||
value.status = 1;
|
||
value.isRead = 1;
|
||
}
|
||
});
|
||
router.push(`/notice/detail/${item.id}`);
|
||
};
|
||
|
||
const userStore = useUserStore();
|
||
const authStore = useAuthStore();
|
||
const accessStore = useAccessStore();
|
||
const { destroyWatermark, updateWatermark } = useWatermark();
|
||
const showDot = computed(() =>
|
||
notifications.value.some((item) => !item.isRead),
|
||
);
|
||
|
||
/** 与原先悬浮窗一致:有 doctor_id 且绑定门店才显示传方入口 */
|
||
const showDoctorTransfer = computed(() => {
|
||
const info = userStore.userInfo as Record<string, any> | null | undefined;
|
||
if (!info) return false;
|
||
return !!(info.doctor_id && info.store_id);
|
||
});
|
||
|
||
const menus = computed(() => [
|
||
{
|
||
handler: () => {
|
||
switchAccountModalRef.value?.openModal();
|
||
},
|
||
icon: Users,
|
||
text: '切换账号',
|
||
},
|
||
]);
|
||
|
||
const avatar = computed(() => {
|
||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||
});
|
||
|
||
async function handleLogout() {
|
||
await authStore.logout(false);
|
||
destroyWatermark();
|
||
}
|
||
|
||
function handleNoticeClear() {
|
||
readAllApi().then(() => {
|
||
notifications.value = [];
|
||
// 成功后可以重新获取数据或者直接更新本地状态
|
||
notification.success({
|
||
message: '标记成功',
|
||
duration: 2,
|
||
});
|
||
});
|
||
}
|
||
|
||
function handleMakeAll() {
|
||
notifications.value.forEach((item) => (item.isRead = true));
|
||
}
|
||
watch(
|
||
() => preferences.app.watermark,
|
||
async (enable) => {
|
||
if (enable) {
|
||
await updateWatermark({
|
||
// 这里更改水印内容
|
||
// content: `${userStore.userInfo?.nick_name || import.meta.env.VITE_APP_TITLE}`,
|
||
content: `${userStore.userInfo?.nick_name}\r\n${userStore.userInfo?.phone || import.meta.env.VITE_APP_TITLE}`,
|
||
});
|
||
} else {
|
||
destroyWatermark();
|
||
}
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
||
<!-- index=55:紧挨全局搜索(50)右侧 -->
|
||
<template #header-right-55>
|
||
<DoctorTransferFloat v-if="showDoctorTransfer" />
|
||
</template>
|
||
<template #user-dropdown>
|
||
<div class="flex items-center">
|
||
<HeaderVipBadge />
|
||
<UserDropdown
|
||
:avatar
|
||
:description="userStore.userInfo?.email"
|
||
:menus
|
||
:tag-text="userStore.userInfo?.roles?.name"
|
||
:text="userStore.userInfo?.nick_name"
|
||
@logout="handleLogout"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template #notification>
|
||
<Notification
|
||
:dot="showDot"
|
||
:notifications="notifications"
|
||
@clear="handleNoticeClear"
|
||
@make-all="handleMakeAll"
|
||
@read="handleItemClick"
|
||
@view-all="goNoticeList"
|
||
/>
|
||
</template>
|
||
<template #extra>
|
||
<AuthenticationLoginExpiredModal
|
||
v-model:open="accessStore.loginExpired"
|
||
:avatar
|
||
>
|
||
<LoginForm />
|
||
</AuthenticationLoginExpiredModal>
|
||
<SwitchAccountModal ref="switchAccountModalRef" />
|
||
</template>
|
||
<template #lock-screen>
|
||
<LockScreen :avatar @to-login="handleLogout" />
|
||
</template>
|
||
</BasicLayout>
|
||
</template>
|