修改 通知详情样式 和 即时通讯

This commit is contained in:
浅念
2025-08-26 09:55:42 +08:00
parent 1bd9d163ec
commit aefd43f79d
7 changed files with 222 additions and 104 deletions
+23 -35
View File
@@ -16,7 +16,7 @@
import appLogo from '@/assets/img/common/logox.png' import appLogo from '@/assets/img/common/logox.png'
const userStore = useUserStore() const userStore = useUserStore()
const { language } = storeToRefs(userStore) const { language, accessToken } = storeToRefs(userStore)
function setCompanyFromUrl() { function setCompanyFromUrl() {
const userStore = useUserStore() const userStore = useUserStore()
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
@@ -99,37 +99,6 @@
} }
} catch (error) {} } catch (error) {}
} }
const showEventNotification = () => {
if (Notification.permission !== 'granted') {
ElMessage({
message: '请先授予通知权限',
type: 'warning'
})
return
}
try {
const notification = new Notification('带事件监听的通知', {
body: '这个通知演示各种事件监听',
icon: 'https://element-plus.org/images/element-plus-logo.svg'
})
notification.onshow = () => {
console.log('通知显示事件触发')
}
notification.onclick = () => {
console.log('通知点击事件触发')
notification.close()
}
notification.onclose = () => {
console.log('通知关闭事件触发')
}
notification.onerror = (error) => {
// console.log(`通知错误事件: ${error.message}`)
}
console.log('已发送带事件监听的通知')
} catch (error) {
// console.log(`发送通知失败: ${error.message}`)
}
}
onBeforeMount(() => { onBeforeMount(() => {
setThemeTransitionClass(true) setThemeTransitionClass(true)
setCompanyFromUrl() setCompanyFromUrl()
@@ -165,14 +134,13 @@
// 获取用户信息 // 获取用户信息
getUserInfo() getUserInfo()
// 初始化 WebSocket // 初始化 WebSocket
const { accessToken } = useUserStore()
// showEventNotification() // showEventNotification()
if (accessToken) { if (accessToken.value) {
import('@/utils/sys/wsClient').then( import('@/utils/sys/wsClient').then(
({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => { ({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => {
// 页面刷新时重置重连次数 // 页面刷新时重置重连次数
resetWebSocketReconnect() resetWebSocketReconnect()
initWebSocket(accessToken) initWebSocket(accessToken.value)
// 添加调试功能,可以在控制台查看WebSocket状态 // 添加调试功能,可以在控制台查看WebSocket状态
// @ts-ignore // @ts-ignore
window.getWebSocketStatus = getWebSocketStatus window.getWebSocketStatus = getWebSocketStatus
@@ -185,6 +153,26 @@
mittBus.on('ws:message', handleWsMessageNotification) mittBus.on('ws:message', handleWsMessageNotification)
}) })
// 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新)
watch(accessToken, async (token) => {
if (token) {
const { initWebSocket, getWebSocketStatus } = await import('@/utils/sys/wsClient')
const status = getWebSocketStatus?.()
// 避免重复初始化:已连接或正在连接时不再初始化
if (
!status?.readyState ||
(status.readyState !== WebSocket.OPEN && status.readyState !== WebSocket.CONNECTING)
) {
initWebSocket(token)
}
} else {
// 退出登录时,关闭并重置
const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient')
closeWebSocket()
resetWebSocketReconnect()
}
})
// 获取用户信息 // 获取用户信息
const getUserInfo = async () => { const getUserInfo = async () => {
if (userStore.isLogin) { if (userStore.isLogin) {
+1 -1
View File
@@ -23,7 +23,7 @@ export const menuService = {
ElMessage.error('暂无系统菜单权限') ElMessage.error('暂无系统菜单权限')
return { menuList: [] } return { menuList: [] }
} }
console.log('menuData', menuData) // console.log('menuData', menuData)
// 处理菜单数据 // 处理菜单数据
const menuList = menuData.map((route: any) => menuDataToRouter(route)) const menuList = menuData.map((route: any) => menuDataToRouter(route))
// 验证处理后的菜单列表 // 验证处理后的菜单列表
@@ -91,17 +91,21 @@
</div> </div>
<!-- 全局通知详情弹窗 --> <!-- 全局通知详情弹窗 -->
<Teleport to="body"> <Teleport to="body">
<ElDialog v-model="dialogVisible" title="通知详情" align-center :append-to-body="true"> <ElDialog v-model="dialogVisible" title="通知详情" align-center :append-to-body="true" width="640px">
<div class="detail-info-table"> <div class="detail-dialog">
<div class="row"> <div class="section">
<div>发送人:{{ noticeDetail.sender_name }}</div> <div class="info-grid">
<div class="label">发送人</div>
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
</div> </div>
<div class="row"> <div class="info-grid">
<div>通知标题:{{ noticeDetail.title }}</div> <div class="label">通知标题</div>
<div class="value">{{ noticeDetail.title || '-' }}</div>
</div> </div>
<div class="row"> </div>
<div> <div class="section">
通知内容: <div class="label" style="margin-bottom: 8px">通知内容</div>
<div class="content-box">
<div class="message-text" v-html="noticeDetail.content"></div> <div class="message-text" v-html="noticeDetail.content"></div>
</div> </div>
</div> </div>
@@ -282,7 +286,42 @@
} }
.message-text { .message-text {
padding: 10px 14px; padding: 10px 14px;
line-height: 1.4; line-height: 1.7;
white-space: pre-line; white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px;
}
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
} }
</style> </style>
+43 -13
View File
@@ -1,5 +1,6 @@
import mittBus from './mittBus' import mittBus from './mittBus'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { ElNotification } from 'element-plus'
type JsonValue = Record<string, any> | any[] type JsonValue = Record<string, any> | any[]
@@ -13,6 +14,10 @@ let reconnectTimer: any = null
let heartbeatTimer: any = null let heartbeatTimer: any = null
let reconnectAttempts = 0 let reconnectAttempts = 0
let isInitialConnection = true let isInitialConnection = true
let isConnecting = false
let lastInitToken: string | null = null
let lastInitAt = 0
const INIT_DEBOUNCE_MS = 1200
// 生成唯一ID // 生成唯一ID
const generateTraceId = (): string => { const generateTraceId = (): string => {
@@ -108,6 +113,7 @@ const handleMessage = (event: MessageEvent) => {
const setupWebSocketHandlers = (ws: WebSocket) => { const setupWebSocketHandlers = (ws: WebSocket) => {
ws.onopen = () => { ws.onopen = () => {
isInitialConnection = false isInitialConnection = false
isConnecting = false
startHeartbeat() startHeartbeat()
mittBus.emit('ws:open') mittBus.emit('ws:open')
sendPing() sendPing()
@@ -117,10 +123,19 @@ const setupWebSocketHandlers = (ws: WebSocket) => {
ws.onerror = (err) => { ws.onerror = (err) => {
mittBus.emit('ws:error', err) mittBus.emit('ws:error', err)
isConnecting = false
try {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close()
}
} catch {
// 忽略关闭错误
}
} }
ws.onclose = () => { ws.onclose = () => {
mittBus.emit('ws:close') mittBus.emit('ws:close')
isConnecting = false
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
scheduleReconnect() scheduleReconnect()
} }
@@ -129,11 +144,10 @@ const setupWebSocketHandlers = (ws: WebSocket) => {
// 安排重连 // 安排重连
const scheduleReconnect = () => { const scheduleReconnect = () => {
if (!isInitialConnection) { // 每次安排重连都递增(即使从未成功连接过)
reconnectAttempts += 1 reconnectAttempts += 1
}
if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) { if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`) console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`)
mittBus.emit('ws:max-reconnect-reached') mittBus.emit('ws:max-reconnect-reached')
return return
@@ -144,7 +158,7 @@ const scheduleReconnect = () => {
console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`) console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`)
reconnectTimer = setTimeout(() => { reconnectTimer = setTimeout(() => {
const { accessToken } = useUserStore() const { accessToken } = useUserStore()
if (accessToken) { if (accessToken && !isConnecting) {
try { try {
if (socket) { if (socket) {
try { try {
@@ -156,11 +170,13 @@ const scheduleReconnect = () => {
} }
const url = `${WS_BASE}${accessToken}` const url = `${WS_BASE}${accessToken}`
isConnecting = true
socket = new WebSocket(url) socket = new WebSocket(url)
setupWebSocketHandlers(socket) setupWebSocketHandlers(socket)
} catch (e) { } catch (e) {
mittBus.emit('ws:error', e) mittBus.emit('ws:error', e)
if (reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) { isConnecting = false
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
scheduleReconnect() scheduleReconnect()
} }
} }
@@ -172,23 +188,33 @@ const scheduleReconnect = () => {
export const initWebSocket = (token: string) => { export const initWebSocket = (token: string) => {
try { try {
if (!token) return if (!token) return
if (socket) { const now = Date.now()
try { // 简单防抖:在短时间内重复触发则跳过
socket.close() if (now - lastInitAt < INIT_DEBOUNCE_MS) {
} catch { return
// 忽略关闭错误
} }
// 避免并发初始化:已在连接中或已连接且 token 未变化则跳过
if (isConnecting) return
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
if (lastInitToken === token) {
return
}
// token 变更:需要切换连接
try { socket.close() } catch {}
socket = null socket = null
} }
cleanup() cleanup()
reconnectAttempts = 0
isInitialConnection = true
const url = `${WS_BASE}${token}` const url = `${WS_BASE}${token}`
isConnecting = true
lastInitToken = token
lastInitAt = now
socket = new WebSocket(url) socket = new WebSocket(url)
setupWebSocketHandlers(socket) setupWebSocketHandlers(socket)
} catch (e) { } catch (e) {
mittBus.emit('ws:error', e) mittBus.emit('ws:error', e)
isConnecting = false
scheduleReconnect() scheduleReconnect()
} }
} }
@@ -204,6 +230,8 @@ export const closeWebSocket = () => {
socket = null socket = null
reconnectAttempts = 0 reconnectAttempts = 0
isInitialConnection = true isInitialConnection = true
isConnecting = false
lastInitToken = null
} }
// 发送WebSocket消息 // 发送WebSocket消息
@@ -239,5 +267,7 @@ export const getWebSocketStatus = () => ({
maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS, maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS,
isReconnecting: reconnectTimer !== null, isReconnecting: reconnectTimer !== null,
isHeartbeating: heartbeatTimer !== null, isHeartbeating: heartbeatTimer !== null,
isInitialConnection isInitialConnection,
isConnecting,
lastInitToken
}) })
+49 -10
View File
@@ -89,17 +89,21 @@
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
<ElDialog v-model="dialogVisiblexq" title="通知详情" align-center> <ElDialog v-model="dialogVisiblexq" title="通知详情" align-center width="640px">
<div class="detail-info-table"> <div class="detail-dialog">
<div class="row"> <div class="section">
<div>发送人:{{ noticeDetail.sender_name }}</div> <div class="info-grid">
<div class="label">发送人</div>
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
</div> </div>
<div class="row"> <div class="info-grid">
<div>通知标题:{{ noticeDetail.title }}</div> <div class="label">通知标题</div>
<div class="value">{{ noticeDetail.title || '-' }}</div>
</div> </div>
<div class="row"> </div>
<div> <div class="section">
通知内容: <div class="label" style="margin-bottom: 8px">通知内容</div>
<div class="content-box">
<div class="message-text" v-html="noticeDetail.content"></div> <div class="message-text" v-html="noticeDetail.content"></div>
</div> </div>
</div> </div>
@@ -264,7 +268,42 @@
} }
.message-text { .message-text {
padding: 10px 14px; padding: 10px 14px;
line-height: 1.4; line-height: 1.7;
white-space: pre-line; white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px;
}
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
} }
</style> </style>
+49 -28
View File
@@ -77,17 +77,21 @@
</template> </template>
</ArtTable> </ArtTable>
</div> </div>
<ElDialog v-model="dialogVisible" title="通知详情" align-center> <ElDialog v-model="dialogVisible" title="通知详情" align-center width="640px">
<div class="detail-info-table"> <div class="detail-dialog">
<div class="row"> <div class="section">
<div>发送人:{{ noticeDetail.sender_name }}</div> <div class="info-grid">
<div class="label">发送人</div>
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
</div> </div>
<div class="row"> <div class="info-grid">
<div>通知标题:{{ noticeDetail.title }}</div> <div class="label">通知标题</div>
<div class="value">{{ noticeDetail.title || '-' }}</div>
</div> </div>
<div class="row"> </div>
<div> <div class="section">
通知内容: <div class="label" style="margin-bottom: 8px">通知内容</div>
<div class="content-box">
<div class="message-text" v-html="noticeDetail.content"></div> <div class="message-text" v-html="noticeDetail.content"></div>
</div> </div>
</div> </div>
@@ -203,23 +207,45 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.detail-info-table { .message-text {
padding-left: 15px; padding: 10px 14px;
.row { line-height: 1.7;
white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex; display: flex;
justify-content: flex-start; flex-direction: column;
margin-bottom: 6px; gap: 16px;
}
> div { .section {
flex: 1; background: var(--el-fill-color-blank);
min-width: 0; border: 1px solid var(--el-border-color-light);
margin-right: 16px; border-radius: 8px;
overflow: hidden; padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px; font-size: 14px;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
} }
.customer-page { .customer-page {
background: var(--art-main-bg-color); background: var(--art-main-bg-color);
@@ -247,9 +273,4 @@
display: flex; display: flex;
gap: 12px; gap: 12px;
} }
.message-text {
padding: 10px 14px;
line-height: 1.4;
white-space: pre-line;
}
</style> </style>
+1
View File
@@ -173,6 +173,7 @@
if (valid) { if (valid) {
try { try {
const res = await CrmService.customerAdd(form.value) const res = await CrmService.customerAdd(form.value)
console.log(res)
ElMessage.success(res?.message || '提交成功!') ElMessage.success(res?.message || '提交成功!')
router.push('/crmpc/customer') router.push('/crmpc/customer')
} catch (e: any) { } catch (e: any) {