refactor(system): 重构消息中心功能
- 移除 WebSocket相关代码 - 新增 SSE (Server-Sent Events) 实现消息推送 - 优化消息中心未读数量展示和更新逻辑- 调整消息中心相关 API 和前端展示
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
highlight-current
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
:check-on-click-leaf="false"
|
||||
>
|
||||
</el-tree>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user