refactor(system): 重构消息中心功能

- 移除 WebSocket相关代码
- 新增 SSE (Server-Sent Events) 实现消息推送
- 优化消息中心未读数量展示和更新逻辑- 调整消息中心相关 API 和前端展示
This commit is contained in:
李小涛
2025-06-22 13:09:49 +08:00
parent fa063a8611
commit e8212501e2
18 changed files with 200 additions and 447 deletions

View File

@@ -2,7 +2,7 @@
ENV = 'development'
# 本地环境接口地址
VITE_API_URL = 'http://127.0.0.1:8001'
VITE_API_URL = 'http://127.0.0.1:8000'
# 是否启用按钮权限
VITE_PM_ENABLED = true

View File

@@ -35,7 +35,6 @@ const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket';
const core = useCore();
const router = useRouter();
// 获取版本号
@@ -92,63 +91,5 @@ onMounted(() => {
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
other.useFavicon();
if (!websocket.websocket) {
//websockt 模块
try {
websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
}
},
{
deep: true,
}
);
// websocket相关代码
import { messageCenterStore } from '/@/stores/messageCenter';
const wsReceive = (message: any) => {
const data = JSON.parse(message.data);
const { unread } = data;
const messageCenter = messageCenterStore();
messageCenter.setUnread(unread);
if (data.contentType === 'SYSTEM') {
ElNotification({
title: '系统消息',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 5000,
});
} else if (data.contentType === 'Content') {
ElMessageBox.confirm(data.content, data.notificationTitle, {
confirmButtonText: data.notificationButton,
dangerouslyUseHTMLString: true,
cancelButtonText: '关闭',
type: 'info',
closeOnClickModal: false,
}).then(() => {
ElMessageBox.close();
const path = data.path;
if (route.path === path) {
core.bus.emit('onNewTask', { name: 'onNewTask' });
} else {
router.push({ path});
}
})
.catch(() => {});
}
};
onBeforeUnmount(() => {
// 关闭连接
websocket.close();
});
</script>

View File

@@ -4,7 +4,6 @@
class="tableSelector"
multiple
:collapseTags="props.tableConfig.collapseTags"
@remove-tag="removeTag"
v-model="data"
placeholder="请选择"
@visible-change="visibleChange"
@@ -29,9 +28,9 @@
max-height="200"
height="200"
:highlight-current-row="!props.tableConfig.isMultiple"
@selection-change="handleSelectionChange"
@selection-change="handleSelectionChange"
@select="handleSelectionChange"
@selectAll="handleSelectionChange"
@selectAll="handleSelectionChange"
@current-change="handleCurrentChange"
>
<el-table-column v-if="props.tableConfig.isMultiple" fixed type="selection" reserve-selection width="55" />
@@ -59,34 +58,36 @@
</template>
<script setup lang="ts">
import {computed, defineProps, onMounted, reactive, ref, watch} from 'vue';
import { computed, defineProps, onMounted, reactive, ref, watch } from 'vue';
import XEUtils from 'xe-utils';
import { request } from '/@/utils/service';
const props = defineProps({
modelValue: {
type: Array || String || Number,
default: () => []
},
type: Array || String || Number,
default: () => [],
},
tableConfig: {
type: Object,
default:{
url: null,
label: null, //显示值
value: null, //数据值
isTree: false,
lazy: true,
size:'default',
load: () => {},
data: [], //默认数据
isMultiple: false, //是否多选
collapseTags:false,
treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项
},
},
type: Object,
default: {
url: null,
label: null, //显示值
value: null, //数据值
isTree: false,
lazy: true,
size: 'default',
load: () => {},
data: [], //默认数据
isMultiple: false, //是否多选
collapseTags: false,
treeProps: { children: 'children', hasChildren: 'hasChildren' },
columns: [], //每一项对应的列表项
},
},
displayLabel: {},
} as any);
console.log(props.tableConfig);
const emit = defineEmits(['update:modelValue']);
// tableRef
const tableRef = ref();
@@ -137,6 +138,8 @@ const handleCurrentChange = (val: any) => {
*/
const getDict = async () => {
const url = props.tableConfig.url;
console.log(url);
const params = {
page: pageConfig.page,
limit: pageConfig.limit,
@@ -162,29 +165,25 @@ const getDict = async () => {
// 获取节点值
const getNodeValues = () => {
console.log(props.tableConfig.url);
request({
url:props.tableConfig.valueUrl,
method:'post',
data:{ids:props.modelValue}
}).then(res=>{
if(res.data.length>0){
data.value = res.data.map((item:any)=>{
return item[props.tableConfig.label]
})
tableRef.value!.clearSelection()
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(
row,
true,
false
)
})
}
})
}
url: props.tableConfig.url,
method: 'post',
data: { ids: props.modelValue },
}).then((res) => {
if (res.data.length > 0) {
data.value = res.data.map((item: any) => {
return item[props.tableConfig.label];
});
tableRef.value!.clearSelection();
res.data.forEach((row) => {
tableRef.value!.toggleRowSelection(row, true, false);
});
}
});
};
/**
* 下拉框展开/关闭
@@ -205,12 +204,11 @@ const handlePageChange = (page: any) => {
getDict();
};
onMounted(()=>{
setTimeout(()=>{
getNodeValues()
},1000)
})
onMounted(() => {
// setTimeout(() => {
// getNodeValues();
// }, 1000);
});
</script>
<style scoped>

View File

@@ -57,33 +57,14 @@
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
></i>
</div>
<div>
<span v-if="!isSocketOpen" class="online-status-span">
<el-popconfirm
width="250"
ref="onlinePopoverRef"
:confirm-button-text="$t('message.user.retry')"
:icon="InfoFilled"
trigger="hover"
icon-color="#626AEF"
:title="$t('message.user.onlinePrompt')"
@confirm="onlineConfirmEvent"
>
<template #reference>
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="getBaseURL(userInfos.avatar) || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge>
</template>
</el-popconfirm>
</span>
</div>
<div></div>
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link">
<span v-if="isSocketOpen">
<el-badge is-dot class="item" :class="{'online-status': isSocketOpen,'online-down':!isSocketOpen}">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge>
</span>
<span v-if="isSocketOpen">
<el-badge is-dot class="item" :class="{ 'online-status': isSocketOpen, 'online-down': !isSocketOpen }">
<img :src="userInfos.avatar || headerImage" class="layout-navbars-breadcrumb-user-link-photo mr5" />
</el-badge>
</span>
{{ userInfos.username === '' ? 'common' : userInfos.username }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
@@ -115,8 +96,7 @@ import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage';
import headerImage from '/@/assets/img/headerImage.png';
import websocket from '/@/utils/websocket';
import { InfoFilled } from '@element-plus/icons-vue'
import { InfoFilled } from '@element-plus/icons-vue';
// 引入组件
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
@@ -148,17 +128,6 @@ const layoutUserFlexNum = computed(() => {
// 定义变量内容
const { isSocketOpen } = storeToRefs(useUserInfo());
// websocket状态
const onlinePopoverRef = ref()
const onlineConfirmEvent = () => {
if (!isSocketOpen.value) {
websocket.is_reonnect = true
websocket.reconnect_current = 1
websocket.reconnect()
}
// 手动隐藏弹出
unref(onlinePopoverRef).popperRef?.delayHide?.()
}
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
@@ -237,8 +206,10 @@ const onLanguageChange = (lang: string) => {
initI18nOrSize('globalI18n', 'disabledI18n');
};
// 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => {
state[attr] = Local.get('themeConfig')[value];
const initI18nOrSize = (value: string, attr: keyof typeof state) => {
const themeConfig = Local.get('themeConfig') as { [key: string]: any } | null;
const configValue = ((themeConfig && themeConfig[value]) as string) || '';
state[attr] = configValue as unknown as never;
};
// 页面加载时
onMounted(() => {
@@ -246,12 +217,32 @@ onMounted(() => {
initI18nOrSize('globalComponentSize', 'disabledSize');
initI18nOrSize('globalI18n', 'disabledI18n');
}
getMessageCenterCount();
});
//消息中心的未读数量
import { messageCenterStore } from '/@/stores/messageCenter';
import {getBaseURL} from "/@/utils/baseUrl";
import { getBaseURL } from '/@/utils/baseUrl';
const messageCenter = messageCenterStore();
let eventSource: EventSource | null = null; // 存储 EventSource 实例
const token = Session.get('token');
const getMessageCenterCount = () => {
// 创建 EventSource 实例并连接到后端 SSE 端点
eventSource = new EventSource(`${getBaseURL()}/sse/?token=${token}`); // 替换为你的后端地址
// 监听消息事件
eventSource.onmessage = function (event) {
messageCenter.setUnread(+event.data); // 更新总记录数
};
// 错误处理
eventSource.onerror = function (err) {
console.error('SSE 错误:', err);
if (eventSource !== null && eventSource.readyState === EventSource.CLOSED) {
console.log('连接已关闭');
}
};
};
</script>
<style scoped lang="scss">
@@ -298,29 +289,29 @@ const messageCenter = messageCenterStore();
:deep(.el-badge__content.is-fixed) {
top: 12px;
}
.online-status{
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #18bc9c;
}
}
.online-down{
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #979b9c;
}
}
.online-status {
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #18bc9c;
}
}
.online-down {
cursor: pointer;
:deep(.el-badge__content.is-fixed) {
top: 30px;
font-size: 14px;
left: 5px;
height: 12px;
width: 12px;
padding: 0;
background-color: #979b9c;
}
}
}
</style>

View File

@@ -2,7 +2,8 @@
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">{{ $t('message.user.newTitle') }}</div>
<!-- <div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div>-->
<!-- <div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div> -->
</div>
<div class="content-box">
<template v-if="state.newsList.length > 0">
@@ -21,7 +22,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { reactive,onBeforeMount,ref,onMounted } from 'vue';
import { reactive, onBeforeMount, ref, onMounted } from 'vue';
// 定义变量内容
const state = reactive({
@@ -33,27 +34,28 @@ const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
import {useRouter } from "vue-router";
const route = useRouter()
import { useRouter } from 'vue-router';
const route = useRouter();
const onGoToGiteeClick = () => {
route.push('/messageCenter')
route.push('/messageCenter');
};
//获取最新消息
import { request } from "/@/utils/service";
const getLastMsg= ()=>{
request({
url: '/api/system/message_center/get_newest_msg/',
method: 'get',
params: {}
}).then((res:any) => {
const { data } = res
state.newsList= [data]
})
}
onMounted(()=>{
getLastMsg()
})
import { request } from '/@/utils/service';
const getLastMsg = () => {
request({
url: '/api/system/message_center/get_newest_msg/',
method: 'get',
params: {},
}).then((res: any) => {
const { data } = res;
console.log(data);
state.newsList = [data];
});
};
onMounted(() => {
getLastMsg();
});
</script>
<style scoped lang="scss">

View File

@@ -72,9 +72,6 @@ export const useUserInfo = defineStore('userInfo', {
Session.set('userInfo', this.userInfos);
}
},
async setWebSocketState(socketState: boolean) {
this.isSocketOpen = socketState;
},
async getApiUserInfo() {
return request({
url: '/api/system/user/user_info/',

View File

@@ -131,10 +131,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
data: [
{ value: 0, label: '按用户' },
{ value: 1, label: '按角色' },
{
value: 2,
label: '按部门',
},
{ value: 2, label: '按部门' },
{ value: 3, label: '通知公告' },
],
}),
@@ -142,14 +139,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
component: {
optionName: 'el-radio-button',
},
rules: [
{
required: true,
message: '必选项',
// @ts-ignore
trigger: ['blur', 'change'],
},
],
rules: [{ required: true, message: '必选项', trigger: ['blur', 'change'] }],
},
},
target_user: {
@@ -191,10 +181,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
{ required: true, message: '必填项' },
],
},
column: {

View File

@@ -11,6 +11,7 @@
highlight-current
show-checkbox
default-expand-all
:check-on-click-leaf="false"
>
</el-tree>
</template>