refactor: ♻️ 通知公告、字典重构问题修复和优化

This commit is contained in:
ray
2024-10-08 01:02:48 +08:00
parent 42f7782d56
commit ff53ed6060
18 changed files with 341 additions and 269 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "vue3-element-admin", "name": "vue3-element-admin",
"version": "2.16.0", "version": "2.16.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -113,7 +113,7 @@ class NoticeAPI {
/** 获取我的通知分页列表 */ /** 获取我的通知分页列表 */
static getMyNoticePage(queryParams?: NoticePageQuery) { static getMyNoticePage(queryParams?: NoticePageQuery) {
return request<any, PageResult<NoticePageVO[]>>({ return request<any, PageResult<NoticePageVO[]>>({
url: `${NOTICE_BASE_URL}/my/page`, url: `${NOTICE_BASE_URL}/my-page`,
method: "get", method: "get",
params: queryParams, params: queryParams,
}); });

View File

@@ -1,6 +1,6 @@
<template> <template>
<template v-if="tag"> <template v-if="tagType">
<el-tag :type="tag">{{ label }}</el-tag> <el-tag :type="tagType" :size="tagSize">{{ label }}</el-tag>
</template> </template>
<template v-else> <template v-else>
<span>{{ label }}</span> <span>{{ label }}</span>
@@ -10,26 +10,55 @@
<script setup lang="ts"> <script setup lang="ts">
import DictDataAPI from "@/api/dict-data"; import DictDataAPI from "@/api/dict-data";
import Cache from "@/utils/cache"; import Cache from "@/utils/cache";
import requestCache from "@/utils/requestCache"; // 导入共享的缓存
const props = defineProps({ const props = defineProps({
dictCode: String, code: String,
value: [String, Number], modelValue: [String, Number],
size: {
type: String,
default: "default",
},
}); });
const label = ref(""); const label = ref("");
const tag = ref< const tagType = ref<
"success" | "warning" | "info" | "primary" | "danger" | undefined "success" | "warning" | "info" | "primary" | "danger" | undefined
>(); >();
const tagSize = ref(props.size as "default" | "large" | "small");
const dictCache = new Cache("dict_"); const dictCache = new Cache("dict_");
const getLabelAndTagByValue = async (dictCode: string, value: any) => { const getLabelAndTagByValue = async (dictCode: string, value: any) => {
// 先从本地缓存中获取字典数据
let dictData = dictCache.getCache(dictCode); let dictData = dictCache.getCache(dictCode);
// 如果本地缓存没有数据,则检查是否已经发起请求
if (!dictData) { if (!dictData) {
dictData = await DictDataAPI.getOptions(dictCode); if (!requestCache.has(dictCode)) {
dictCache.setCache(dictCode, dictData, 3 * 60 * 1000); // 缓存 3 分钟 // 发起请求并存入请求缓存,确保后续请求能复用此 Promise
const requestPromise = DictDataAPI.getOptions(dictCode)
.then((data) => {
dictCache.setCache(dictCode, data, 3 * 60 * 1000); // 缓存 3 分钟
requestCache.delete(dictCode); // 请求完成后删除请求缓存
return data;
})
.catch((error) => {
requestCache.delete(dictCode); // 出错时也要删除缓存
throw error;
});
// 将当前请求存入请求缓存
requestCache.set(dictCode, requestPromise);
}
// 等待请求完成并获取数据
dictData = await requestCache.get(dictCode);
console.log(`Received data for ${dictCode}:`, dictData);
} }
// 查找对应的字典项
const dictEntry = dictData.find((item: any) => item.value == value); const dictEntry = dictData.find((item: any) => item.value == value);
return { return {
label: dictEntry ? dictEntry.label : "", label: dictEntry ? dictEntry.label : "",
@@ -40,13 +69,16 @@ const getLabelAndTagByValue = async (dictCode: string, value: any) => {
// 监听 props 的变化,获取并更新 label 和 tag // 监听 props 的变化,获取并更新 label 和 tag
const fetchLabelAndTag = async () => { const fetchLabelAndTag = async () => {
const result = await getLabelAndTagByValue( const result = await getLabelAndTagByValue(
props.dictCode as string, props.code as string,
props.value props.modelValue
); );
label.value = result.label; label.value = result.label;
tag.value = result.tag; tagType.value = result.tag;
}; };
// 首次挂载时获取字典数据
onMounted(fetchLabelAndTag); onMounted(fetchLabelAndTag);
watch(() => props.value, fetchLabelAndTag);
// 当 modelValue 发生变化时重新获取
watch(() => props.modelValue, fetchLabelAndTag);
</script> </script>

View File

@@ -25,7 +25,7 @@ const props = defineProps({
}, },
modelValue: { modelValue: {
type: [String, Number], type: [String, Number],
required: true, required: false,
}, },
placeholder: { placeholder: {
type: String, type: String,

View File

@@ -1,58 +1,176 @@
<template> <template>
<div> <div>
<el-dropdown class="flex-center h-full align-middle"> <el-dropdown class="flex-center wh-full align-middle">
<el-badge v-if="messages.length > 0" :value="messages.length" :max="99"> <div class="wh-full">
<div><i-ep-bell /></div> <el-badge
</el-badge> :offset="[-10, 15]"
<el-badge v-else> v-if="notices.length > 0"
<i-ep-bell /> :value="notices.length"
</el-badge> :max="99"
class="wh-full"
>
<i-ep-bell class="notification-icon h-full" />
</el-badge>
<el-badge :offset="[-10, 15]" v-else>
<i-ep-bell class="notification-icon h-full" />
</el-badge>
</div>
<template #dropdown> <template #dropdown>
<div class="p-5"> <div class="p-2">
<template v-if="messages.length > 0"> <el-tabs v-model="activeTab">
<div <el-tab-pane label="通知" name="notice">
class="w400px flex-x-between py-2" <template v-if="notices.length > 0">
v-for="(item, index) in messages" <div
:key="index" class="w400px flex-x-between p-1"
> v-for="(item, index) in notices"
<div> :key="index"
<el-tag type="success" size="small">系统通知</el-tag>
<el-link
type="primary"
@click="readNotice(item.id)"
class="ml-1"
> >
{{ item.title }} <div class="flex-center">
<DictLabel
code="notice_type"
v-model="item.type"
size="small"
class="mr-1"
/>
<el-text
type="primary"
@click="readNotice(item.id)"
size="small"
class="w200px cursor-pointer"
truncated
>
{{ item.title }}
</el-text>
</div>
<div>
{{ item.publishTime }}
</div>
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无通知" />
</div>
</template>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" :underline="false" @click="viewMore">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="notices.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link> </el-link>
</div> </div>
<div> </el-tab-pane>
{{ item.publishTime }} <el-tab-pane label="消息" name="message">
<template v-if="messages.length > 0">
<div
class="w400px flex-x-between p-1"
v-for="(item, index) in messages"
:key="index"
>
<div>
<DictLabel
code="notice_type"
v-model="item.type"
size="small"
/>
<el-link
type="primary"
@click="readNotice(item.id)"
class="ml-1"
>
{{ item.title }}
</el-link>
</div>
<div>
{{ item.publishTime }}
</div>
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无消息" />
</div>
</template>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" :underline="false" @click="viewMore">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="messages.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div> </div>
</div> </el-tab-pane>
</template>
<template v-else> <el-tab-pane label="待办" name="task">
<div class="flex-center h150px w350px"> <template v-if="tasks.length > 0">
<el-empty :image-size="30" description="暂无消息" /> <div
</div> class="w400px flex-x-between p-1"
</template> v-for="(item, index) in tasks"
<el-divider /> :key="index"
<div class="flex-x-between"> >
<el-link type="primary" :underline="false" @click="viewMore"> <div>
<span class="text-xs">查看更多</span> <DictLabel
<el-icon class="text-xs"> code="notice_type"
<ArrowRight /> v-model="item.type"
</el-icon> size="small"
</el-link> />
<el-link <el-link
v-if="messages.length > 0" type="primary"
type="primary" @click="readNotice(item.id)"
:underline="false" class="ml-1"
@click="markAllAsRead" >
> {{ item.title }}
<span class="text-xs">全部已读</span> </el-link>
</el-link> </div>
</div> <div>
{{ item.publishTime }}
</div>
</div>
</template>
<template v-else>
<div class="flex-center h150px w350px">
<el-empty :image-size="50" description="暂无待办" />
</div>
</template>
<el-divider />
<div class="flex-x-between">
<el-link type="primary" :underline="false" @click="viewMore">
<span class="text-xs">查看更多</span>
<el-icon class="text-xs">
<ArrowRight />
</el-icon>
</el-link>
<el-link
v-if="tasks.length > 0"
type="primary"
:underline="false"
@click="markAllAsRead"
>
<span class="text-xs">全部已读</span>
</el-link>
</div>
</el-tab-pane>
</el-tabs>
</div> </div>
</template> </template>
</el-dropdown> </el-dropdown>
@@ -62,18 +180,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import NoticeAPI, { NoticePageQuery, NoticePageVO } from "@/api/notice"; import NoticeAPI, { NoticePageVO } from "@/api/notice";
import WebSocketManager from "@/utils/socket"; import WebSocketManager from "@/utils/websocket";
import router from "@/router"; import router from "@/router";
const messages = ref<NoticePageVO[]>([]); const activeTab = ref("notice");
const notices = ref<NoticePageVO[]>([]);
const messages = ref<any[]>([]);
const tasks = ref<any[]>([]);
const noticeDetailRef = ref(); const noticeDetailRef = ref();
// 获取未读消息列表并连接 WebSocket // 获取未读消息列表并连接 WebSocket
onMounted(() => { onMounted(() => {
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then( NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then(
(data) => { (data) => {
messages.value = data.list; notices.value = data.list;
} }
); );
@@ -81,10 +202,12 @@ onMounted(() => {
console.log("收到消息:", message); console.log("收到消息:", message);
const data = JSON.parse(message); const data = JSON.parse(message);
const id = data.id; const id = data.id;
if (!messages.value.some((msg) => msg.id === id)) { if (!notices.value.some((notice) => notice.id == id)) {
messages.value.unshift({ notices.value.unshift({
id, id,
title: data.title, title: data.title,
type: data.type,
publishTime: data.publishTime,
}); });
ElNotification({ ElNotification({
@@ -100,9 +223,9 @@ onMounted(() => {
// 阅读通知公告 // 阅读通知公告
function readNotice(id: string) { function readNotice(id: string) {
noticeDetailRef.value.openNotice(id); noticeDetailRef.value.openNotice(id);
const index = messages.value.findIndex((msg) => msg.id === id); const index = notices.value.findIndex((notice) => notice.id === id);
if (index >= 0) { if (index >= 0) {
messages.value.splice(index, 1); // 从消息列表中移除已读消息 notices.value.splice(index, 1); // 从消息列表中移除已读消息
} }
} }
@@ -114,9 +237,14 @@ function viewMore() {
// 全部已读 // 全部已读
function markAllAsRead() { function markAllAsRead() {
NoticeAPI.readAll().then(() => { NoticeAPI.readAll().then(() => {
messages.value = []; notices.value = [];
}); });
} }
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.layout-top .notification-icon,
.layout-mix .notification-icon {
color: #fff;
}
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<section class="app-main" :style="{ minHeight: minHeight }"> <section class="app-main" :style="{ height: height }">
<router-view> <router-view>
<template #default="{ Component, route }"> <template #default="{ Component, route }">
<transition <transition
@@ -19,8 +19,9 @@
import { useSettingsStore, useTagsViewStore } from "@/store"; import { useSettingsStore, useTagsViewStore } from "@/store";
import variables from "@/styles/variables.module.scss"; import variables from "@/styles/variables.module.scss";
const cachedViews = computed(() => useTagsViewStore().cachedViews); // 缓存页面集合 // 缓存页面集合
const minHeight = computed(() => { const cachedViews = computed(() => useTagsViewStore().cachedViews);
const height = computed(() => {
if (useSettingsStore().tagsView) { if (useSettingsStore().tagsView) {
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`; return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
} else { } else {
@@ -32,6 +33,7 @@ const minHeight = computed(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.app-main { .app-main {
position: relative; position: relative;
overflow-y: auto;
background-color: var(--el-bg-color-page); background-color: var(--el-bg-color-page);
} }
</style> </style>

View File

@@ -10,10 +10,8 @@ import "element-plus/theme-chalk/dark/css-vars.css";
import "@/styles/index.scss"; import "@/styles/index.scss";
import "uno.css"; import "uno.css";
import "animate.css"; import "animate.css";
import { InstallCodeMirror } from "codemirror-editor-vue3";
const app = createApp(App); const app = createApp(App);
// 注册插件 // 注册插件
app.use(setupPlugins); app.use(setupPlugins);
app.use(InstallCodeMirror);
app.mount("#app"); app.mount("#app");

View File

@@ -1,10 +1,13 @@
import type { App } from "vue";
import { setupDirective } from "@/directive"; import { setupDirective } from "@/directive";
import { setupI18n } from "@/lang"; import { setupI18n } from "@/lang";
import { setupRouter } from "@/router"; import { setupRouter } from "@/router";
import { setupStore } from "@/store"; import { setupStore } from "@/store";
import type { App } from "vue";
import { setupElIcons } from "./icons"; import { setupElIcons } from "./icons";
import { setupPermission } from "./permission"; import { setupPermission } from "./permission";
import webSocketManager from "@/utils/websocket";
import { InstallCodeMirror } from "codemirror-editor-vue3";
export default { export default {
install(app: App<Element>) { install(app: App<Element>) {
@@ -20,5 +23,9 @@ export default {
setupElIcons(app); setupElIcons(app);
// 路由守卫 // 路由守卫
setupPermission(); setupPermission();
// 初始化 WebSocket
webSocketManager.setupWebSocket();
// 注册 CodeMirror
app.use(InstallCodeMirror);
}, },
}; };

View File

@@ -0,0 +1,4 @@
// 创建一个共享的 requestCache
const requestCache = new Map<string, Promise<any>>();
export default requestCache;

View File

@@ -1,22 +1,19 @@
import { Client } from "@stomp/stompjs"; import { Client } from "@stomp/stompjs";
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
const MAX_RECONNECT_ATTEMPTS = 3;
const RECONNECT_DELAY_MS = 5000;
const HEARTBEAT_INTERVAL_MS = 30000;
class WebSocketManager { class WebSocketManager {
private client: Client | null = null; private client: Client | null = null;
private reconnectAttempts: number = 0;
private messageHandlers: Map<string, ((message: string) => void)[]> = private messageHandlers: Map<string, ((message: string) => void)[]> =
new Map(); new Map();
constructor() {} constructor() {}
private getOrCreateClient(onError?: (error: any) => void): Client { // 初始化 WebSocket 客户端
setupWebSocket() {
const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT; const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
if (this.client) { if (this.client && this.client.connected) {
console.log("客户端已存在并且连接正常");
return this.client; return this.client;
} }
@@ -25,10 +22,10 @@ class WebSocketManager {
connectHeaders: { connectHeaders: {
Authorization: getToken(), Authorization: getToken(),
}, },
heartbeatIncoming: HEARTBEAT_INTERVAL_MS, heartbeatIncoming: 30000,
heartbeatOutgoing: HEARTBEAT_INTERVAL_MS, heartbeatOutgoing: 30000,
onConnect: () => { onConnect: () => {
console.log(`连接到 WebSocket 服务器: ${endpoint}`); console.log(`连接到 WebSocket 服务器: ${endpoint}`);
this.messageHandlers.forEach((handlers, topic) => { this.messageHandlers.forEach((handlers, topic) => {
handlers.forEach((handler) => { handlers.forEach((handler) => {
this.subscribeToTopic(topic, handler); this.subscribeToTopic(topic, handler);
@@ -36,33 +33,22 @@ class WebSocketManager {
}); });
}, },
onStompError: (frame) => { onStompError: (frame) => {
console.error( console.error(`连接错误: ${frame.headers["message"]}`);
`连接错误: ${endpoint}, 错误消息: ${frame.headers["message"]}`
);
console.error(`错误详情: ${frame.body}`); console.error(`错误详情: ${frame.body}`);
if (onError) {
onError(frame);
}
this.handleReconnect();
}, },
onDisconnect: () => { onDisconnect: () => {
console.log(`已断开连接: ${endpoint}`); console.log(`WebSocket 连接已断开: ${endpoint}`);
this.handleReconnect();
}, },
}); });
this.client.activate(); this.client.activate();
return this.client;
} }
public subscribeToTopic( // 订阅主题
topic: string, public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
onMessage: (message: string) => void, console.log(`正在订阅主题: ${topic}`);
onError?: (error: any) => void
) {
if (!this.client || !this.client.connected) { if (!this.client || !this.client.connected) {
console.log("WebSocket 尚未连接,正在连接..."); this.setupWebSocket();
this.getOrCreateClient(onError);
} }
if (this.messageHandlers.has(topic)) { if (this.messageHandlers.has(topic)) {
@@ -72,7 +58,6 @@ class WebSocketManager {
} }
if (this.client?.connected) { if (this.client?.connected) {
console.log(`正在订阅主题: ${topic}`);
this.client.subscribe(topic, (message) => { this.client.subscribe(topic, (message) => {
const handlers = this.messageHandlers.get(topic); const handlers = this.messageHandlers.get(topic);
handlers?.forEach((handler) => handler(message.body)); handlers?.forEach((handler) => handler(message.body));
@@ -80,23 +65,7 @@ class WebSocketManager {
} }
} }
private handleReconnect() { // 断开 WebSocket 连接
if (this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
this.reconnectAttempts++;
console.log(
`重连尝试 (${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`
);
setTimeout(() => {
this.client?.deactivate();
this.client = null;
this.getOrCreateClient();
}, RECONNECT_DELAY_MS);
} else {
console.error("达到最大重连次数,停止重连");
}
}
public disconnect() { public disconnect() {
if (this.client) { if (this.client) {
console.log("断开 WebSocket 连接"); console.log("断开 WebSocket 连接");

View File

@@ -155,7 +155,7 @@
<el-icon class="ml-1"><Notification /></el-icon> <el-icon class="ml-1"><Notification /></el-icon>
</div> </div>
<el-link type="primary"> <el-link type="primary">
<span class="text-xs">查看更多</span> <span class="text-xs" @click="viewMoreNotice">查看更多</span>
<el-icon class="text-xs"><ArrowRight /></el-icon> <el-icon class="text-xs"><ArrowRight /></el-icon>
</el-link> </el-link>
</div> </div>
@@ -167,16 +167,14 @@
:key="index" :key="index"
class="flex-y-center py-3" class="flex-y-center py-3"
> >
<el-tag :type="getNoticeLevelTag(item.level)" size="small"> <DictLabel code="notice_type" v-model="item.type" size="small" />
{{ getNoticeLabel(item.type) }}
</el-tag>
<el-text <el-text
truncated truncated
class="!mx-2 flex-1 !text-xs !text-[var(--el-text-color-secondary)]" class="!mx-2 flex-1 !text-xs !text-[var(--el-text-color-secondary)]"
> >
{{ item.title }} {{ item.title }}
</el-text> </el-text>
<el-link> <el-link @click="viewNoticeDetail(item.id)">
<el-icon class="text-sm"><View /></el-icon> <el-icon class="text-sm"><View /></el-icon>
</el-link> </el-link>
</div> </div>
@@ -184,20 +182,27 @@
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<NoticeDetail ref="noticeDetailRef" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import WebSocketManager from "@/utils/socket";
defineOptions({ defineOptions({
name: "Dashboard", name: "Dashboard",
inheritAttrs: false, inheritAttrs: false,
}); });
import WebSocketManager from "@/utils/websocket";
import router from "@/router";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { NoticeTypeEnum, getNoticeLabel } from "@/enums/NoticeTypeEnum"; import { NoticeTypeEnum, getNoticeLabel } from "@/enums/NoticeTypeEnum";
import StatsAPI, { VisitStatsVO } from "@/api/log"; import StatsAPI, { VisitStatsVO } from "@/api/log";
import NoticeAPI, { NoticePageVO } from "@/api/notice";
const noticeDetailRef = ref();
const userStore = useUserStore(); const userStore = useUserStore();
const date: Date = new Date(); const date: Date = new Date();
const greetings = computed(() => { const greetings = computed(() => {
@@ -247,13 +252,13 @@ interface VisitStats {
icon: string; icon: string;
tagType: "primary" | "success" | "warning"; tagType: "primary" | "success" | "warning";
growthRate: number; growthRate: number;
/** 粒度 */ // 粒度
granularity: string; granularity: string;
/** 今日数量输出文档 */ // 今日数量
todayCount: number; todayCount: number;
totalCount: number; totalCount: number;
} }
/** 加载访问统计数据 */ // 加载访问统计数据
const loadVisitStatsData = async () => { const loadVisitStatsData = async () => {
const list: VisitStatsVO[] = await StatsAPI.getVisitStats(); const list: VisitStatsVO[] = await StatsAPI.getVisitStats();
@@ -314,88 +319,30 @@ const getVisitStatsIcon = (type: string) => {
} }
}; };
const notices = ref([ const notices = ref<NoticePageVO[]>([]);
{
level: 2,
type: NoticeTypeEnum.SYSTEM_UPGRADE,
title: "v2.12.0 新增系统日志,访问趋势统计功能。",
},
{
level: 0,
type: NoticeTypeEnum.COMPANY_NEWS,
title: "公司将在 7 月 1 日举办年中总结大会,请各部门做好准备。",
},
{
level: 3,
type: NoticeTypeEnum.HOLIDAY_NOTICE,
title: "端午节假期从 6 月 12 日至 6 月 14 日放假,共 3 天。",
},
{ // 查看更多
level: 2, function viewMoreNotice() {
type: NoticeTypeEnum.SECURITY_ALERT, router.push({ path: "/myNotice" });
title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", }
},
{
level: 2,
type: NoticeTypeEnum.SYSTEM_MAINTENANCE,
title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。",
},
{
level: 0,
type: NoticeTypeEnum.OTHER,
title: "公司新规章制度发布,请大家及时查阅。",
},
{
level: 3,
type: NoticeTypeEnum.HOLIDAY_NOTICE,
title: "中秋节假期从 9 月 22 日至 9 月 24 日放假,共 3 天。",
},
{
level: 1,
type: NoticeTypeEnum.COMPANY_NEWS,
title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。",
},
{
level: 2,
type: NoticeTypeEnum.SECURITY_ALERT,
title:
"请注意,近期有恶意软件通过即时通讯工具传播,请勿下载不明来源的文件。",
},
{
level: 2,
type: NoticeTypeEnum.SYSTEM_MAINTENANCE,
title: "系统将于下周日凌晨 3 点进行升级,预计维护时间为 1 小时。",
},
{
level: 3,
type: NoticeTypeEnum.OTHER,
title: "公司年度体检通知已发布,请各位员工按时参加。",
},
]);
const getNoticeLevelTag = (type: number) => { // 阅读通知公告
switch (type) { function viewNoticeDetail(id: string) {
case 0: noticeDetailRef.value.openNotice(id);
return "danger";
case 1:
return "warning";
case 2:
return "primary";
default:
return "success";
}
};
function connectWebSocket() {
WebSocketManager.getOrCreateClient("/topic/onlineUserCount", (message) => {
onlineUserCount.value = JSON.parse(message);
});
} }
onMounted(() => { onMounted(() => {
loadVisitStatsData(); loadVisitStatsData();
connectWebSocket();
// 获取我的通知公告
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 10 }).then((data) => {
notices.value = data.list;
});
WebSocketManager.subscribeToTopic("/topic/onlineUserCount", (data) => {
console.log("收到在线用户数量:", data);
onlineUserCount.value = JSON.parse(data);
});
}); });
</script> </script>

View File

@@ -16,7 +16,7 @@ const contentConfig: IContentConfig = {
id: 1, id: 1,
username: "tom", username: "tom",
avatar: avatar:
"https://oss.youlai.tech/youlai-boot/2023/05/16/811270ef31f548af9cffc026dfc3777b.gif", "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
percent: 99, percent: 99,
price: 10, price: 10,
url: "https://www.baidu.com", url: "https://www.baidu.com",
@@ -31,7 +31,7 @@ const contentConfig: IContentConfig = {
id: 2, id: 2,
username: "jerry", username: "jerry",
avatar: avatar:
"https://oss.youlai.tech/youlai-boot/2023/05/16/811270ef31f548af9cffc026dfc3777b.gif", "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
percent: 88, percent: 88,
price: 999, price: 999,
url: "https://www.google.com", url: "https://www.google.com",

View File

@@ -143,7 +143,7 @@ function connectWebSocket() {
connectHeaders: { connectHeaders: {
Authorization: getToken(), Authorization: getToken(),
}, },
debug: (str) => { debug: (str: any) => {
console.log(str); console.log(str);
}, },
onConnect: () => { onConnect: () => {
@@ -155,14 +155,14 @@ function connectWebSocket() {
type: "tip", type: "tip",
}); });
stompClient.subscribe("/topic/notice", (res) => { stompClient.subscribe("/topic/notice", (res: any) => {
messages.value.push({ messages.value.push({
sender: "Server", sender: "Server",
content: res.body, content: res.body,
}); });
}); });
stompClient.subscribe("/user/queue/greeting", (res) => { stompClient.subscribe("/user/queue/greeting", (res: any) => {
const messageData = JSON.parse(res.body) as MessageType; const messageData = JSON.parse(res.body) as MessageType;
messages.value.push({ messages.value.push({
sender: messageData.sender, sender: messageData.sender,
@@ -170,7 +170,7 @@ function connectWebSocket() {
}); });
}); });
}, },
onStompError: (frame) => { onStompError: (frame: any) => {
console.error("Broker reported error: " + frame.headers["message"]); console.error("Broker reported error: " + frame.headers["message"]);
console.error("Additional details: " + frame.body); console.error("Additional details: " + frame.body);
}, },

View File

@@ -174,8 +174,8 @@ import DictDataAPI, {
const route = useRoute(); const route = useRoute();
const dictCode = route.query.dictCode as string; const dictCode = ref(route.query.dictCode as string);
const dictName = route.query.dictName as string; const dictName = ref(route.query.dictName as string);
const queryFormRef = ref(ElForm); const queryFormRef = ref(ElForm);
const dataFormRef = ref(ElForm); const dataFormRef = ref(ElForm);
@@ -201,15 +201,15 @@ const formData = reactive<DictDataForm>({});
// 监听路由参数变化,更新字典数据 // 监听路由参数变化,更新字典数据
watch( watch(
() => route.query.dictCode, () => [route.query.dictCode, route.query.dictName],
(newDictCode) => { ([newDictCode, newDictName]) => {
if (newDictCode !== queryParams.dictCode) { queryParams.dictCode = newDictCode as string;
queryParams.dictCode = newDictCode as string; dictCode.value = newDictCode as string;
handleQuery(); dictName.value = newDictName as string;
}
handleQuery();
} }
); );
const computedRules = computed(() => { const computedRules = computed(() => {
const rules: Partial<Record<string, any>> = { const rules: Partial<Record<string, any>> = {
value: [{ required: true, message: "请输入字典值", trigger: "blur" }], value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
@@ -291,6 +291,8 @@ function handleCloseDialog() {
dataFormRef.value.clearValidate(); dataFormRef.value.clearValidate();
formData.id = undefined; formData.id = undefined;
formData.sort = 1;
formData.status = 1;
} }
/** /**
* 删除字典 * 删除字典

View File

@@ -43,13 +43,8 @@
<el-descriptions-item label="发布时间:"> <el-descriptions-item label="发布时间:">
{{ notice.publishTime }} {{ notice.publishTime }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="内容:"> <el-descriptions-item label="公告内容:">
<el-input <div v-html="notice.content"></div>
v-model="notice.content"
type="textarea"
style="max-height: 400px"
:readonly="true"
/>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>

View File

@@ -70,27 +70,21 @@
> >
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
<el-table-column <el-table-column label="通知标题" prop="title" min-width="200" />
align="center" <el-table-column align="center" label="通知类型" width="150">
key="title"
label="通知标题"
prop="title"
min-width="150"
/>
<el-table-column align="center" label="通知类型" min-width="150">
<template #default="scope"> <template #default="scope">
<DictLabel :dictCode="'notice_type'" :value="scope.row.type" /> <DictLabel :code="'notice_type'" v-model="scope.row.type" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
align="center" align="center"
label="发布人" label="发布人"
prop="publisherName" prop="publisherName"
min-width="100" width="150"
/> />
<el-table-column align="center" label="通知等级" min-width="100"> <el-table-column align="center" label="通知等级" width="100">
<template #default="scope"> <template #default="scope">
<DictLabel :dictCode="'notice_level'" :value="scope.row.level" /> <DictLabel code="notice_level" v-model="scope.row.level" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -121,7 +115,7 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作时间" min-width="220"> <el-table-column label="操作时间" width="250">
<template #default="scope"> <template #default="scope">
<div class="flex-x-start"> <div class="flex-x-start">
<span>创建时间</span> <span>创建时间</span>
@@ -141,7 +135,7 @@
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="220"> <el-table-column align="center" fixed="right" label="操作" width="150">
<template #default="scope"> <template #default="scope">
<el-button <el-button
type="primary" type="primary"
@@ -230,16 +224,16 @@
<el-form-item label="通知类型" prop="type"> <el-form-item label="通知类型" prop="type">
<dictionary <dictionary
type="button" type="button"
v-model="formData.type"
code="notice_type" code="notice_type"
v-model="formData.type"
/> />
</el-form-item> </el-form-item>
<el-form-item label="优先级" prop="level"> <el-form-item label="通知等级" prop="level">
<el-radio-group v-model="formData.level"> <dictionary
<el-radio value="L"></el-radio> type="button"
<el-radio value="M"></el-radio> code="notice_level"
<el-radio value="H"></el-radio> v-model="formData.level"
</el-radio-group> />
</el-form-item> </el-form-item>
<el-form-item label="目标类型" prop="targetType"> <el-form-item label="目标类型" prop="targetType">
<el-radio-group v-model="formData.targetType"> <el-radio-group v-model="formData.targetType">

View File

@@ -31,27 +31,21 @@
highlight-current-row highlight-current-row
> >
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
<el-table-column <el-table-column label="通知标题" prop="title" min-width="200" />
align="center" <el-table-column align="center" label="通知类型" width="150">
key="title"
label="通知标题"
prop="title"
min-width="150"
/>
<el-table-column align="center" label="通知类型" min-width="150">
<template #default="scope"> <template #default="scope">
<DictLabel :dictCode="'notice_type'" :value="scope.row.type" /> <DictLabel code="notice_type" v-model="scope.row.type" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
align="center" align="center"
label="发布人" label="发布人"
prop="publisherName" prop="publisherName"
min-width="100" width="100"
/> />
<el-table-column align="center" label="通知等级" min-width="100"> <el-table-column align="center" label="通知等级" width="100">
<template #default="scope"> <template #default="scope">
<DictLabel :dictCode="'notice_level'" :value="scope.row.type" /> <DictLabel code="notice_level" v-model="scope.row.level" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -59,19 +53,19 @@
key="releaseTime" key="releaseTime"
label="发布时间" label="发布时间"
prop="publishTime" prop="publishTime"
min-width="100" width="150"
/> />
<el-table-column <el-table-column
align="center" align="center"
label="发布人" label="发布人"
prop="publisherName" prop="publisherName"
min-width="100" width="150"
/> />
<el-table-column align="center" label="状态" min-width="100"> <el-table-column align="center" label="状态" width="100">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.isRead == 1" type="success">已读</el-tag> <el-tag v-if="scope.row.isRead == 1" type="success">已读</el-tag>
<el-tag v-if="scope.row.isRead == 0" type="warning">未读</el-tag> <el-tag v-else type="info">未读</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="80"> <el-table-column align="center" fixed="right" label="操作" width="80">

View File

@@ -168,7 +168,7 @@
<el-input <el-input
v-model="permKeywords" v-model="permKeywords"
clearable clearable
class="w-[200px]" class="w-[150px]"
placeholder="菜单权限名称" placeholder="菜单权限名称"
> >
<template #prefix> <template #prefix>
@@ -176,7 +176,7 @@
</template> </template>
</el-input> </el-input>
<div class="flex-center"> <div class="flex-center ml-5">
<el-button type="primary" size="small" plain @click="togglePermTree"> <el-button type="primary" size="small" plain @click="togglePermTree">
<i-ep-switch /> <i-ep-switch />
{{ isExpanded ? "收缩" : "展开" }} {{ isExpanded ? "收缩" : "展开" }}