refactor: ♻️ 通知公告、字典重构问题修复和优化
This commit is contained in:
@@ -113,7 +113,7 @@ class NoticeAPI {
|
||||
/** 获取我的通知分页列表 */
|
||||
static getMyNoticePage(queryParams?: NoticePageQuery) {
|
||||
return request<any, PageResult<NoticePageVO[]>>({
|
||||
url: `${NOTICE_BASE_URL}/my/page`,
|
||||
url: `${NOTICE_BASE_URL}/my-page`,
|
||||
method: "get",
|
||||
params: queryParams,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<template v-if="tag">
|
||||
<el-tag :type="tag">{{ label }}</el-tag>
|
||||
<template v-if="tagType">
|
||||
<el-tag :type="tagType" :size="tagSize">{{ label }}</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ label }}</span>
|
||||
@@ -10,26 +10,55 @@
|
||||
<script setup lang="ts">
|
||||
import DictDataAPI from "@/api/dict-data";
|
||||
import Cache from "@/utils/cache";
|
||||
import requestCache from "@/utils/requestCache"; // 导入共享的缓存
|
||||
|
||||
const props = defineProps({
|
||||
dictCode: String,
|
||||
value: [String, Number],
|
||||
code: String,
|
||||
modelValue: [String, Number],
|
||||
size: {
|
||||
type: String,
|
||||
default: "default",
|
||||
},
|
||||
});
|
||||
|
||||
const label = ref("");
|
||||
const tag = ref<
|
||||
const tagType = ref<
|
||||
"success" | "warning" | "info" | "primary" | "danger" | undefined
|
||||
>();
|
||||
|
||||
const tagSize = ref(props.size as "default" | "large" | "small");
|
||||
|
||||
const dictCache = new Cache("dict_");
|
||||
|
||||
const getLabelAndTagByValue = async (dictCode: string, value: any) => {
|
||||
// 先从本地缓存中获取字典数据
|
||||
let dictData = dictCache.getCache(dictCode);
|
||||
|
||||
// 如果本地缓存没有数据,则检查是否已经发起请求
|
||||
if (!dictData) {
|
||||
dictData = await DictDataAPI.getOptions(dictCode);
|
||||
dictCache.setCache(dictCode, dictData, 3 * 60 * 1000); // 缓存 3 分钟
|
||||
if (!requestCache.has(dictCode)) {
|
||||
// 发起请求并存入请求缓存,确保后续请求能复用此 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);
|
||||
return {
|
||||
label: dictEntry ? dictEntry.label : "",
|
||||
@@ -40,13 +69,16 @@ const getLabelAndTagByValue = async (dictCode: string, value: any) => {
|
||||
// 监听 props 的变化,获取并更新 label 和 tag
|
||||
const fetchLabelAndTag = async () => {
|
||||
const result = await getLabelAndTagByValue(
|
||||
props.dictCode as string,
|
||||
props.value
|
||||
props.code as string,
|
||||
props.modelValue
|
||||
);
|
||||
label.value = result.label;
|
||||
tag.value = result.tag;
|
||||
tagType.value = result.tag;
|
||||
};
|
||||
|
||||
// 首次挂载时获取字典数据
|
||||
onMounted(fetchLabelAndTag);
|
||||
watch(() => props.value, fetchLabelAndTag);
|
||||
|
||||
// 当 modelValue 发生变化时重新获取
|
||||
watch(() => props.modelValue, fetchLabelAndTag);
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,7 @@ const props = defineProps({
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
|
||||
@@ -1,58 +1,176 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown class="flex-center h-full align-middle">
|
||||
<el-badge v-if="messages.length > 0" :value="messages.length" :max="99">
|
||||
<div><i-ep-bell /></div>
|
||||
</el-badge>
|
||||
<el-badge v-else>
|
||||
<i-ep-bell />
|
||||
</el-badge>
|
||||
<el-dropdown class="flex-center wh-full align-middle">
|
||||
<div class="wh-full">
|
||||
<el-badge
|
||||
:offset="[-10, 15]"
|
||||
v-if="notices.length > 0"
|
||||
:value="notices.length"
|
||||
: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>
|
||||
<div class="p-5">
|
||||
<template v-if="messages.length > 0">
|
||||
<div
|
||||
class="w400px flex-x-between py-2"
|
||||
v-for="(item, index) in messages"
|
||||
:key="index"
|
||||
>
|
||||
<div>
|
||||
<el-tag type="success" size="small">系统通知</el-tag>
|
||||
<el-link
|
||||
type="primary"
|
||||
@click="readNotice(item.id)"
|
||||
class="ml-1"
|
||||
<div class="p-2">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="通知" name="notice">
|
||||
<template v-if="notices.length > 0">
|
||||
<div
|
||||
class="w400px flex-x-between p-1"
|
||||
v-for="(item, index) in notices"
|
||||
:key="index"
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
<div>
|
||||
{{ item.publishTime }}
|
||||
</el-tab-pane>
|
||||
<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>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex-center h150px w350px">
|
||||
<el-empty :image-size="30" 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>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="待办" name="task">
|
||||
<template v-if="tasks.length > 0">
|
||||
<div
|
||||
class="w400px flex-x-between p-1"
|
||||
v-for="(item, index) in tasks"
|
||||
: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="tasks.length > 0"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="markAllAsRead"
|
||||
>
|
||||
<span class="text-xs">全部已读</span>
|
||||
</el-link>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -62,18 +180,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NoticeAPI, { NoticePageQuery, NoticePageVO } from "@/api/notice";
|
||||
import WebSocketManager from "@/utils/socket";
|
||||
import NoticeAPI, { NoticePageVO } from "@/api/notice";
|
||||
import WebSocketManager from "@/utils/websocket";
|
||||
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();
|
||||
|
||||
// 获取未读消息列表并连接 WebSocket
|
||||
onMounted(() => {
|
||||
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 5, isRead: 0 }).then(
|
||||
(data) => {
|
||||
messages.value = data.list;
|
||||
notices.value = data.list;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -81,10 +202,12 @@ onMounted(() => {
|
||||
console.log("收到消息:", message);
|
||||
const data = JSON.parse(message);
|
||||
const id = data.id;
|
||||
if (!messages.value.some((msg) => msg.id === id)) {
|
||||
messages.value.unshift({
|
||||
if (!notices.value.some((notice) => notice.id == id)) {
|
||||
notices.value.unshift({
|
||||
id,
|
||||
title: data.title,
|
||||
type: data.type,
|
||||
publishTime: data.publishTime,
|
||||
});
|
||||
|
||||
ElNotification({
|
||||
@@ -100,9 +223,9 @@ onMounted(() => {
|
||||
// 阅读通知公告
|
||||
function readNotice(id: string) {
|
||||
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) {
|
||||
messages.value.splice(index, 1); // 从消息列表中移除已读消息
|
||||
notices.value.splice(index, 1); // 从消息列表中移除已读消息
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +237,14 @@ function viewMore() {
|
||||
// 全部已读
|
||||
function markAllAsRead() {
|
||||
NoticeAPI.readAll().then(() => {
|
||||
messages.value = [];
|
||||
notices.value = [];
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.layout-top .notification-icon,
|
||||
.layout-mix .notification-icon {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<section class="app-main" :style="{ minHeight: minHeight }">
|
||||
<section class="app-main" :style="{ height: height }">
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition
|
||||
@@ -19,8 +19,9 @@
|
||||
import { useSettingsStore, useTagsViewStore } from "@/store";
|
||||
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) {
|
||||
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
||||
} else {
|
||||
@@ -32,6 +33,7 @@ const minHeight = computed(() => {
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,10 +10,8 @@ import "element-plus/theme-chalk/dark/css-vars.css";
|
||||
import "@/styles/index.scss";
|
||||
import "uno.css";
|
||||
import "animate.css";
|
||||
import { InstallCodeMirror } from "codemirror-editor-vue3";
|
||||
|
||||
const app = createApp(App);
|
||||
// 注册插件
|
||||
app.use(setupPlugins);
|
||||
app.use(InstallCodeMirror);
|
||||
app.mount("#app");
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { App } from "vue";
|
||||
|
||||
import { setupDirective } from "@/directive";
|
||||
import { setupI18n } from "@/lang";
|
||||
import { setupRouter } from "@/router";
|
||||
import { setupStore } from "@/store";
|
||||
import type { App } from "vue";
|
||||
import { setupElIcons } from "./icons";
|
||||
import { setupPermission } from "./permission";
|
||||
import webSocketManager from "@/utils/websocket";
|
||||
import { InstallCodeMirror } from "codemirror-editor-vue3";
|
||||
|
||||
export default {
|
||||
install(app: App<Element>) {
|
||||
@@ -20,5 +23,9 @@ export default {
|
||||
setupElIcons(app);
|
||||
// 路由守卫
|
||||
setupPermission();
|
||||
// 初始化 WebSocket
|
||||
webSocketManager.setupWebSocket();
|
||||
// 注册 CodeMirror
|
||||
app.use(InstallCodeMirror);
|
||||
},
|
||||
};
|
||||
|
||||
4
src/utils/requestCache.ts
Normal file
4
src/utils/requestCache.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// 创建一个共享的 requestCache
|
||||
const requestCache = new Map<string, Promise<any>>();
|
||||
|
||||
export default requestCache;
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import { getToken } from "@/utils/auth";
|
||||
|
||||
const MAX_RECONNECT_ATTEMPTS = 3;
|
||||
const RECONNECT_DELAY_MS = 5000;
|
||||
const HEARTBEAT_INTERVAL_MS = 30000;
|
||||
|
||||
class WebSocketManager {
|
||||
private client: Client | null = null;
|
||||
private reconnectAttempts: number = 0;
|
||||
private messageHandlers: Map<string, ((message: string) => void)[]> =
|
||||
new Map();
|
||||
|
||||
constructor() {}
|
||||
|
||||
private getOrCreateClient(onError?: (error: any) => void): Client {
|
||||
// 初始化 WebSocket 客户端
|
||||
setupWebSocket() {
|
||||
const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
|
||||
|
||||
if (this.client) {
|
||||
if (this.client && this.client.connected) {
|
||||
console.log("客户端已存在并且连接正常");
|
||||
return this.client;
|
||||
}
|
||||
|
||||
@@ -25,10 +22,10 @@ class WebSocketManager {
|
||||
connectHeaders: {
|
||||
Authorization: getToken(),
|
||||
},
|
||||
heartbeatIncoming: HEARTBEAT_INTERVAL_MS,
|
||||
heartbeatOutgoing: HEARTBEAT_INTERVAL_MS,
|
||||
heartbeatIncoming: 30000,
|
||||
heartbeatOutgoing: 30000,
|
||||
onConnect: () => {
|
||||
console.log(`已连接到 WebSocket 服务器: ${endpoint}`);
|
||||
console.log(`连接到 WebSocket 服务器: ${endpoint}`);
|
||||
this.messageHandlers.forEach((handlers, topic) => {
|
||||
handlers.forEach((handler) => {
|
||||
this.subscribeToTopic(topic, handler);
|
||||
@@ -36,33 +33,22 @@ class WebSocketManager {
|
||||
});
|
||||
},
|
||||
onStompError: (frame) => {
|
||||
console.error(
|
||||
`连接错误: ${endpoint}, 错误消息: ${frame.headers["message"]}`
|
||||
);
|
||||
console.error(`连接错误: ${frame.headers["message"]}`);
|
||||
console.error(`错误详情: ${frame.body}`);
|
||||
if (onError) {
|
||||
onError(frame);
|
||||
}
|
||||
this.handleReconnect();
|
||||
},
|
||||
onDisconnect: () => {
|
||||
console.log(`已断开连接: ${endpoint}`);
|
||||
this.handleReconnect();
|
||||
console.log(`WebSocket 连接已断开: ${endpoint}`);
|
||||
},
|
||||
});
|
||||
|
||||
this.client.activate();
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public subscribeToTopic(
|
||||
topic: string,
|
||||
onMessage: (message: string) => void,
|
||||
onError?: (error: any) => void
|
||||
) {
|
||||
// 订阅主题
|
||||
public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
|
||||
console.log(`正在订阅主题: ${topic}`);
|
||||
if (!this.client || !this.client.connected) {
|
||||
console.log("WebSocket 尚未连接,正在连接...");
|
||||
this.getOrCreateClient(onError);
|
||||
this.setupWebSocket();
|
||||
}
|
||||
|
||||
if (this.messageHandlers.has(topic)) {
|
||||
@@ -72,7 +58,6 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
if (this.client?.connected) {
|
||||
console.log(`正在订阅主题: ${topic}`);
|
||||
this.client.subscribe(topic, (message) => {
|
||||
const handlers = this.messageHandlers.get(topic);
|
||||
handlers?.forEach((handler) => handler(message.body));
|
||||
@@ -80,23 +65,7 @@ class WebSocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
private handleReconnect() {
|
||||
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("达到最大重连次数,停止重连");
|
||||
}
|
||||
}
|
||||
|
||||
// 断开 WebSocket 连接
|
||||
public disconnect() {
|
||||
if (this.client) {
|
||||
console.log("断开 WebSocket 连接");
|
||||
@@ -155,7 +155,7 @@
|
||||
<el-icon class="ml-1"><Notification /></el-icon>
|
||||
</div>
|
||||
<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-link>
|
||||
</div>
|
||||
@@ -167,16 +167,14 @@
|
||||
:key="index"
|
||||
class="flex-y-center py-3"
|
||||
>
|
||||
<el-tag :type="getNoticeLevelTag(item.level)" size="small">
|
||||
{{ getNoticeLabel(item.type) }}
|
||||
</el-tag>
|
||||
<DictLabel code="notice_type" v-model="item.type" size="small" />
|
||||
<el-text
|
||||
truncated
|
||||
class="!mx-2 flex-1 !text-xs !text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
{{ item.title }}
|
||||
</el-text>
|
||||
<el-link>
|
||||
<el-link @click="viewNoticeDetail(item.id)">
|
||||
<el-icon class="text-sm"><View /></el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
@@ -184,20 +182,27 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<NoticeDetail ref="noticeDetailRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WebSocketManager from "@/utils/socket";
|
||||
|
||||
defineOptions({
|
||||
name: "Dashboard",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import WebSocketManager from "@/utils/websocket";
|
||||
import router from "@/router";
|
||||
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { NoticeTypeEnum, getNoticeLabel } from "@/enums/NoticeTypeEnum";
|
||||
import StatsAPI, { VisitStatsVO } from "@/api/log";
|
||||
import NoticeAPI, { NoticePageVO } from "@/api/notice";
|
||||
|
||||
const noticeDetailRef = ref();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const date: Date = new Date();
|
||||
const greetings = computed(() => {
|
||||
@@ -247,13 +252,13 @@ interface VisitStats {
|
||||
icon: string;
|
||||
tagType: "primary" | "success" | "warning";
|
||||
growthRate: number;
|
||||
/** 粒度 */
|
||||
// 粒度
|
||||
granularity: string;
|
||||
/** 今日数量输出文档 */
|
||||
// 今日数量
|
||||
todayCount: number;
|
||||
totalCount: number;
|
||||
}
|
||||
/** 加载访问统计数据 */
|
||||
// 加载访问统计数据
|
||||
const loadVisitStatsData = async () => {
|
||||
const list: VisitStatsVO[] = await StatsAPI.getVisitStats();
|
||||
|
||||
@@ -314,88 +319,30 @@ const getVisitStatsIcon = (type: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const notices = ref([
|
||||
{
|
||||
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 天。",
|
||||
},
|
||||
const notices = ref<NoticePageVO[]>([]);
|
||||
|
||||
{
|
||||
level: 2,
|
||||
type: NoticeTypeEnum.SECURITY_ALERT,
|
||||
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: "公司年度体检通知已发布,请各位员工按时参加。",
|
||||
},
|
||||
]);
|
||||
// 查看更多
|
||||
function viewMoreNotice() {
|
||||
router.push({ path: "/myNotice" });
|
||||
}
|
||||
|
||||
const getNoticeLevelTag = (type: number) => {
|
||||
switch (type) {
|
||||
case 0:
|
||||
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);
|
||||
});
|
||||
// 阅读通知公告
|
||||
function viewNoticeDetail(id: string) {
|
||||
noticeDetailRef.value.openNotice(id);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
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>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const contentConfig: IContentConfig = {
|
||||
id: 1,
|
||||
username: "tom",
|
||||
avatar:
|
||||
"https://oss.youlai.tech/youlai-boot/2023/05/16/811270ef31f548af9cffc026dfc3777b.gif",
|
||||
"https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||
percent: 99,
|
||||
price: 10,
|
||||
url: "https://www.baidu.com",
|
||||
@@ -31,7 +31,7 @@ const contentConfig: IContentConfig = {
|
||||
id: 2,
|
||||
username: "jerry",
|
||||
avatar:
|
||||
"https://oss.youlai.tech/youlai-boot/2023/05/16/811270ef31f548af9cffc026dfc3777b.gif",
|
||||
"https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
|
||||
percent: 88,
|
||||
price: 999,
|
||||
url: "https://www.google.com",
|
||||
|
||||
@@ -143,7 +143,7 @@ function connectWebSocket() {
|
||||
connectHeaders: {
|
||||
Authorization: getToken(),
|
||||
},
|
||||
debug: (str) => {
|
||||
debug: (str: any) => {
|
||||
console.log(str);
|
||||
},
|
||||
onConnect: () => {
|
||||
@@ -155,14 +155,14 @@ function connectWebSocket() {
|
||||
type: "tip",
|
||||
});
|
||||
|
||||
stompClient.subscribe("/topic/notice", (res) => {
|
||||
stompClient.subscribe("/topic/notice", (res: any) => {
|
||||
messages.value.push({
|
||||
sender: "Server",
|
||||
content: res.body,
|
||||
});
|
||||
});
|
||||
|
||||
stompClient.subscribe("/user/queue/greeting", (res) => {
|
||||
stompClient.subscribe("/user/queue/greeting", (res: any) => {
|
||||
const messageData = JSON.parse(res.body) as MessageType;
|
||||
messages.value.push({
|
||||
sender: messageData.sender,
|
||||
@@ -170,7 +170,7 @@ function connectWebSocket() {
|
||||
});
|
||||
});
|
||||
},
|
||||
onStompError: (frame) => {
|
||||
onStompError: (frame: any) => {
|
||||
console.error("Broker reported error: " + frame.headers["message"]);
|
||||
console.error("Additional details: " + frame.body);
|
||||
},
|
||||
|
||||
@@ -174,8 +174,8 @@ import DictDataAPI, {
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const dictCode = route.query.dictCode as string;
|
||||
const dictName = route.query.dictName as string;
|
||||
const dictCode = ref(route.query.dictCode as string);
|
||||
const dictName = ref(route.query.dictName as string);
|
||||
|
||||
const queryFormRef = ref(ElForm);
|
||||
const dataFormRef = ref(ElForm);
|
||||
@@ -201,15 +201,15 @@ const formData = reactive<DictDataForm>({});
|
||||
|
||||
// 监听路由参数变化,更新字典数据
|
||||
watch(
|
||||
() => route.query.dictCode,
|
||||
(newDictCode) => {
|
||||
if (newDictCode !== queryParams.dictCode) {
|
||||
queryParams.dictCode = newDictCode as string;
|
||||
handleQuery();
|
||||
}
|
||||
() => [route.query.dictCode, route.query.dictName],
|
||||
([newDictCode, newDictName]) => {
|
||||
queryParams.dictCode = newDictCode as string;
|
||||
dictCode.value = newDictCode as string;
|
||||
dictName.value = newDictName as string;
|
||||
|
||||
handleQuery();
|
||||
}
|
||||
);
|
||||
|
||||
const computedRules = computed(() => {
|
||||
const rules: Partial<Record<string, any>> = {
|
||||
value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
|
||||
@@ -291,6 +291,8 @@ function handleCloseDialog() {
|
||||
dataFormRef.value.clearValidate();
|
||||
|
||||
formData.id = undefined;
|
||||
formData.sort = 1;
|
||||
formData.status = 1;
|
||||
}
|
||||
/**
|
||||
* 删除字典
|
||||
|
||||
@@ -43,13 +43,8 @@
|
||||
<el-descriptions-item label="发布时间:">
|
||||
{{ notice.publishTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="内容:">
|
||||
<el-input
|
||||
v-model="notice.content"
|
||||
type="textarea"
|
||||
style="max-height: 400px"
|
||||
:readonly="true"
|
||||
/>
|
||||
<el-descriptions-item label="公告内容:">
|
||||
<div v-html="notice.content"></div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
@@ -70,27 +70,21 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column
|
||||
align="center"
|
||||
key="title"
|
||||
label="通知标题"
|
||||
prop="title"
|
||||
min-width="150"
|
||||
/>
|
||||
<el-table-column align="center" label="通知类型" min-width="150">
|
||||
<el-table-column label="通知标题" prop="title" min-width="200" />
|
||||
<el-table-column align="center" label="通知类型" width="150">
|
||||
<template #default="scope">
|
||||
<DictLabel :dictCode="'notice_type'" :value="scope.row.type" />
|
||||
<DictLabel :code="'notice_type'" v-model="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="发布人"
|
||||
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">
|
||||
<DictLabel :dictCode="'notice_level'" :value="scope.row.level" />
|
||||
<DictLabel code="notice_level" v-model="scope.row.level" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -121,7 +115,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="操作时间" min-width="220">
|
||||
<el-table-column label="操作时间" width="250">
|
||||
<template #default="scope">
|
||||
<div class="flex-x-start">
|
||||
<span>创建时间:</span>
|
||||
@@ -141,7 +135,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -230,16 +224,16 @@
|
||||
<el-form-item label="通知类型" prop="type">
|
||||
<dictionary
|
||||
type="button"
|
||||
v-model="formData.type"
|
||||
code="notice_type"
|
||||
v-model="formData.type"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级" prop="level">
|
||||
<el-radio-group v-model="formData.level">
|
||||
<el-radio value="L">低</el-radio>
|
||||
<el-radio value="M">中</el-radio>
|
||||
<el-radio value="H">高</el-radio>
|
||||
</el-radio-group>
|
||||
<el-form-item label="通知等级" prop="level">
|
||||
<dictionary
|
||||
type="button"
|
||||
code="notice_level"
|
||||
v-model="formData.level"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标类型" prop="targetType">
|
||||
<el-radio-group v-model="formData.targetType">
|
||||
|
||||
@@ -31,27 +31,21 @@
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column
|
||||
align="center"
|
||||
key="title"
|
||||
label="通知标题"
|
||||
prop="title"
|
||||
min-width="150"
|
||||
/>
|
||||
<el-table-column align="center" label="通知类型" min-width="150">
|
||||
<el-table-column label="通知标题" prop="title" min-width="200" />
|
||||
<el-table-column align="center" label="通知类型" width="150">
|
||||
<template #default="scope">
|
||||
<DictLabel :dictCode="'notice_type'" :value="scope.row.type" />
|
||||
<DictLabel code="notice_type" v-model="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="发布人"
|
||||
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">
|
||||
<DictLabel :dictCode="'notice_level'" :value="scope.row.type" />
|
||||
<DictLabel code="notice_level" v-model="scope.row.level" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -59,19 +53,19 @@
|
||||
key="releaseTime"
|
||||
label="发布时间"
|
||||
prop="publishTime"
|
||||
min-width="100"
|
||||
width="150"
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="发布人"
|
||||
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">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" fixed="right" label="操作" width="80">
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<el-input
|
||||
v-model="permKeywords"
|
||||
clearable
|
||||
class="w-[200px]"
|
||||
class="w-[150px]"
|
||||
placeholder="菜单权限名称"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -176,7 +176,7 @@
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div class="flex-center">
|
||||
<div class="flex-center ml-5">
|
||||
<el-button type="primary" size="small" plain @click="togglePermTree">
|
||||
<i-ep-switch />
|
||||
{{ isExpanded ? "收缩" : "展开" }}
|
||||
|
||||
Reference in New Issue
Block a user