2024-05-19 21:20:42 +08:00
|
|
|
<script lang="ts" setup>
|
2024-08-05 21:12:22 +08:00
|
|
|
import type { NotificationItem } from '@vben/layouts';
|
|
|
|
|
|
2025-04-10 17:08:23 +08:00
|
|
|
import {computed, onUnmounted, ref, watch} from 'vue';
|
2024-06-01 23:15:29 +08:00
|
|
|
|
2024-07-13 17:25:15 +08:00
|
|
|
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
2025-03-19 17:06:36 +08:00
|
|
|
|
2025-03-07 08:18:45 +08:00
|
|
|
// import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
|
2024-10-06 17:27:32 +08:00
|
|
|
import { useWatermark } from '@vben/hooks';
|
2025-03-07 08:18:45 +08:00
|
|
|
// import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
|
2025-03-19 17:06:36 +08:00
|
|
|
// import { CircleHelp } from '@vben/icons';
|
2024-07-11 11:05:01 +08:00
|
|
|
import {
|
|
|
|
|
BasicLayout,
|
2024-07-12 12:14:09 +08:00
|
|
|
LockScreen,
|
2024-07-11 11:05:01 +08:00
|
|
|
Notification,
|
|
|
|
|
UserDropdown,
|
|
|
|
|
} from '@vben/layouts';
|
2024-07-23 00:03:59 +08:00
|
|
|
import { preferences } from '@vben/preferences';
|
2024-09-10 21:48:51 +08:00
|
|
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
2025-03-07 08:18:45 +08:00
|
|
|
// import { openWindow } from '@vben/utils';
|
2024-05-19 21:20:42 +08:00
|
|
|
|
2025-04-10 17:08:23 +08:00
|
|
|
import {
|
|
|
|
|
Bell,
|
|
|
|
|
CalendarClock,
|
|
|
|
|
Megaphone,
|
|
|
|
|
} from 'lucide-vue-next';
|
|
|
|
|
|
2025-03-07 08:18:45 +08:00
|
|
|
// import { $t } from '#/locales';
|
2024-07-30 21:10:28 +08:00
|
|
|
import { useAuthStore } from '#/store';
|
2024-09-10 21:48:51 +08:00
|
|
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
2025-04-10 17:08:23 +08:00
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
import {getNoticeListApi, readAllApi} from "#/views/notice/api";
|
|
|
|
|
import {formatTimeToRelative} from "#/util/tool";
|
|
|
|
|
import {notification} from "ant-design-vue";
|
2024-06-30 15:03:37 +08:00
|
|
|
|
2025-04-10 17:08:23 +08:00
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
// 样式相关
|
|
|
|
|
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[]>([
|
|
|
|
|
// // TODO 通知列表
|
|
|
|
|
// {
|
|
|
|
|
// avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
|
|
|
|
// date: '3小时前',
|
|
|
|
|
// isRead: true,
|
|
|
|
|
// message: '描述信息描述信息描述信息',
|
|
|
|
|
// title: '收到了 14 份新周报',
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// avatar: 'https://avatar.vercel.sh/1',
|
|
|
|
|
// date: '刚刚',
|
|
|
|
|
// isRead: false,
|
|
|
|
|
// message: '描述信息描述信息描述信息',
|
|
|
|
|
// title: '朱偏右 回复了你',
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// avatar: 'https://avatar.vercel.sh/1',
|
|
|
|
|
// date: '2024-01-01',
|
|
|
|
|
// isRead: false,
|
|
|
|
|
// message: '描述信息描述信息描述信息',
|
|
|
|
|
// title: '曲丽丽 评论了你',
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// avatar: 'https://avatar.vercel.sh/satori',
|
|
|
|
|
// date: '1天前',
|
|
|
|
|
// isRead: false,
|
|
|
|
|
// message: '描述信息描述信息描述信息',
|
|
|
|
|
// title: '代办提醒',
|
|
|
|
|
// },
|
|
|
|
|
// ]);
|
|
|
|
|
const notifications = ref<NotificationItem[]>([]);
|
|
|
|
|
|
|
|
|
|
// 获取消息列表数据
|
|
|
|
|
const getNoticeList = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getNoticeListApi({
|
|
|
|
|
status: 0,
|
|
|
|
|
});
|
|
|
|
|
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
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取消息列表失败:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
getNoticeList();
|
|
|
|
|
// 每隔十秒刷新一次
|
|
|
|
|
let intervalId = setInterval(getNoticeList, 10_000);
|
|
|
|
|
|
|
|
|
|
// 封装定时器重启逻辑
|
|
|
|
|
const restartInterval = () => {
|
|
|
|
|
clearInterval(intervalId);
|
|
|
|
|
getNoticeList(); // 立即获取最新数据
|
|
|
|
|
intervalId = setInterval(getNoticeList, 10_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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
router.push(`/notice/detail/${item.id}`);
|
|
|
|
|
};
|
2024-05-19 21:20:42 +08:00
|
|
|
|
2024-07-30 21:10:28 +08:00
|
|
|
const userStore = useUserStore();
|
|
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
const accessStore = useAccessStore();
|
2024-10-06 17:27:32 +08:00
|
|
|
const { destroyWatermark, updateWatermark } = useWatermark();
|
2024-07-07 16:19:17 +08:00
|
|
|
const showDot = computed(() =>
|
|
|
|
|
notifications.value.some((item) => !item.isRead),
|
|
|
|
|
);
|
|
|
|
|
|
2024-05-19 21:20:42 +08:00
|
|
|
const menus = computed(() => [
|
2025-03-07 08:18:45 +08:00
|
|
|
// TODO 后续放个人中心
|
|
|
|
|
// {
|
|
|
|
|
// handler: () => {
|
2025-03-19 17:06:36 +08:00
|
|
|
// UpdatePasswordModalApi.open();
|
|
|
|
|
// },
|
|
|
|
|
// icon: 'carbon:password',
|
|
|
|
|
// text: '修改密码',
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// handler: () => {
|
2025-03-07 08:18:45 +08:00
|
|
|
// openWindow(VBEN_DOC_URL, {
|
|
|
|
|
// target: '_blank',
|
|
|
|
|
// });
|
|
|
|
|
// },
|
|
|
|
|
// icon: BookOpenText,
|
|
|
|
|
// text: $t('ui.widgets.document'),
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// handler: () => {
|
|
|
|
|
// openWindow(VBEN_GITHUB_URL, {
|
|
|
|
|
// target: '_blank',
|
|
|
|
|
// });
|
|
|
|
|
// },
|
|
|
|
|
// icon: MdiGithub,
|
|
|
|
|
// text: 'GitHub',
|
|
|
|
|
// },
|
|
|
|
|
// {
|
|
|
|
|
// handler: () => {
|
|
|
|
|
// openWindow(`${VBEN_GITHUB_URL}/issues`, {
|
|
|
|
|
// target: '_blank',
|
|
|
|
|
// });
|
|
|
|
|
// },
|
|
|
|
|
// icon: CircleHelp,
|
|
|
|
|
// text: $t('ui.widgets.qa'),
|
|
|
|
|
// },
|
2024-05-19 21:20:42 +08:00
|
|
|
]);
|
|
|
|
|
|
2024-07-12 12:14:09 +08:00
|
|
|
const avatar = computed(() => {
|
2024-07-30 21:10:28 +08:00
|
|
|
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
2024-07-12 12:14:09 +08:00
|
|
|
});
|
|
|
|
|
|
2024-07-05 23:15:46 +08:00
|
|
|
async function handleLogout() {
|
2024-08-19 22:59:42 +08:00
|
|
|
await authStore.logout(false);
|
2025-03-07 08:18:45 +08:00
|
|
|
destroyWatermark();
|
2024-05-19 21:20:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleNoticeClear() {
|
2025-04-10 17:08:23 +08:00
|
|
|
readAllApi().then(() => {
|
|
|
|
|
notifications.value = [];
|
|
|
|
|
// 成功后可以重新获取数据或者直接更新本地状态
|
|
|
|
|
notification.success({
|
|
|
|
|
message: '标记成功',
|
|
|
|
|
duration: 2,
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-05-19 21:20:42 +08:00
|
|
|
}
|
2024-07-07 16:19:17 +08:00
|
|
|
|
|
|
|
|
function handleMakeAll() {
|
|
|
|
|
notifications.value.forEach((item) => (item.isRead = true));
|
|
|
|
|
}
|
2024-10-06 17:27:32 +08:00
|
|
|
watch(
|
|
|
|
|
() => preferences.app.watermark,
|
|
|
|
|
async (enable) => {
|
|
|
|
|
if (enable) {
|
|
|
|
|
await updateWatermark({
|
2025-03-07 08:18:45 +08:00
|
|
|
// 这里更改水印内容
|
|
|
|
|
// 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}`,
|
2024-10-06 17:27:32 +08:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
destroyWatermark();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
immediate: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-05-19 21:20:42 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2024-07-06 14:41:18 +08:00
|
|
|
<BasicLayout @clear-preferences-and-logout="handleLogout">
|
2024-05-19 21:20:42 +08:00
|
|
|
<template #user-dropdown>
|
|
|
|
|
<UserDropdown
|
2024-07-12 12:14:09 +08:00
|
|
|
:avatar
|
2025-03-07 08:18:45 +08:00
|
|
|
:description="userStore.userInfo?.email"
|
2024-07-12 12:14:09 +08:00
|
|
|
:menus
|
2025-03-07 08:18:45 +08:00
|
|
|
:tag-text="userStore.userInfo?.roles?.name"
|
|
|
|
|
:text="userStore.userInfo?.nick_name"
|
2024-05-19 21:20:42 +08:00
|
|
|
@logout="handleLogout"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
<template #notification>
|
|
|
|
|
<Notification
|
2024-07-07 16:19:17 +08:00
|
|
|
:dot="showDot"
|
2024-05-19 21:20:42 +08:00
|
|
|
:notifications="notifications"
|
|
|
|
|
@clear="handleNoticeClear"
|
2024-07-07 16:19:17 +08:00
|
|
|
@make-all="handleMakeAll"
|
2025-04-10 17:08:23 +08:00
|
|
|
@read="handleItemClick"
|
|
|
|
|
@view-all="goNoticeList"
|
2024-05-19 21:20:42 +08:00
|
|
|
/>
|
|
|
|
|
</template>
|
2024-07-12 12:14:09 +08:00
|
|
|
<template #extra>
|
2024-07-11 20:11:11 +08:00
|
|
|
<AuthenticationLoginExpiredModal
|
2024-07-30 21:10:28 +08:00
|
|
|
v-model:open="accessStore.loginExpired"
|
2024-07-18 21:31:34 +08:00
|
|
|
:avatar
|
2024-09-10 21:48:51 +08:00
|
|
|
>
|
|
|
|
|
<LoginForm />
|
|
|
|
|
</AuthenticationLoginExpiredModal>
|
2024-07-11 11:05:01 +08:00
|
|
|
</template>
|
2024-07-12 12:14:09 +08:00
|
|
|
<template #lock-screen>
|
2024-07-23 00:03:59 +08:00
|
|
|
<LockScreen :avatar @to-login="handleLogout" />
|
2024-07-12 12:14:09 +08:00
|
|
|
</template>
|
2024-05-19 21:20:42 +08:00
|
|
|
</BasicLayout>
|
|
|
|
|
</template>
|