更新websocket功能
This commit is contained in:
@@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { useI18nStore } from './stores/modules/i18n'
|
||||
import { useLayoutStore } from './stores/modules/layout'
|
||||
import { useUserStore } from './stores/modules/user'
|
||||
import { useMessageStore } from './stores/modules/message'
|
||||
import { useWebSocket } from './composables/useWebSocket'
|
||||
import { theme } from 'ant-design-vue'
|
||||
import i18n from './i18n'
|
||||
@@ -27,6 +28,9 @@ const layoutStore = useLayoutStore()
|
||||
// user store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// message store
|
||||
const messageStore = useMessageStore()
|
||||
|
||||
// WebSocket
|
||||
const { initWebSocket, closeWebSocket } = useWebSocket()
|
||||
|
||||
@@ -77,9 +81,26 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听用户信息变化,当用户信息完整时初始化 WebSocket
|
||||
watch(
|
||||
() => [userStore.token, userStore.userInfo],
|
||||
() => {
|
||||
if (userStore.isUserInfoComplete()) {
|
||||
initWebSocket()
|
||||
} else if (!userStore.isLoggedIn()) {
|
||||
// 用户未登录,关闭 WebSocket
|
||||
closeWebSocket()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
// 恢复消息数据
|
||||
messageStore.restoreMessages()
|
||||
|
||||
// 从持久化的 store 中读取语言设置并同步到 i18n
|
||||
i18n.global.locale.value = i18nStore.currentLocale
|
||||
|
||||
@@ -91,8 +112,8 @@ onMounted(async () => {
|
||||
document.documentElement.style.setProperty('--primary-color', layoutStore.themeColor)
|
||||
}
|
||||
|
||||
// 初始化 WebSocket 连接
|
||||
if (userStore.isLoggedIn()) {
|
||||
// 尝试初始化 WebSocket 连接
|
||||
if (userStore.isUserInfoComplete()) {
|
||||
initWebSocket()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ref } from 'vue'
|
||||
import { getWebSocket } from '@/utils/websocket'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { useMessageStore } from '@/stores/modules/message'
|
||||
import { useDictionaryStore } from '@/stores/modules/dictionary'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { message, notification } from 'ant-design-vue'
|
||||
import config from '@/config'
|
||||
|
||||
/**
|
||||
@@ -13,23 +14,40 @@ import config from '@/config'
|
||||
export function useWebSocket() {
|
||||
const ws = ref(null)
|
||||
const userStore = useUserStore()
|
||||
const messageStore = useMessageStore()
|
||||
const dictionaryStore = useDictionaryStore()
|
||||
const reconnectTimer = ref(null)
|
||||
|
||||
/**
|
||||
* 初始化 WebSocket 连接
|
||||
*/
|
||||
function initWebSocket() {
|
||||
if (!userStore.token) {
|
||||
console.warn('未登录,无法初始化 WebSocket')
|
||||
// 检查用户信息是否完整
|
||||
if (!userStore.isUserInfoComplete()) {
|
||||
console.warn('用户信息不完整,等待用户信息加载...')
|
||||
|
||||
// 延迟重试,等待用户信息加载
|
||||
if (reconnectTimer.value) {
|
||||
clearTimeout(reconnectTimer.value)
|
||||
}
|
||||
reconnectTimer.value = setTimeout(() => {
|
||||
initWebSocket()
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
|
||||
if (!userStore.userInfo || !userStore.userInfo.id) {
|
||||
console.warn('用户信息不完整,无法初始化 WebSocket')
|
||||
// 如果已经连接,不再重复连接
|
||||
if (ws.value && ws.value.isConnected) {
|
||||
console.log('WebSocket 已连接')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始初始化 WebSocket...', {
|
||||
userId: userStore.userInfo.id,
|
||||
username: userStore.userInfo.username
|
||||
})
|
||||
|
||||
// 使用配置文件中的 WS_URL
|
||||
ws.value = getWebSocket(userStore.userInfo.id, userStore.token, {
|
||||
wsUrl: config.WS_URL,
|
||||
@@ -40,8 +58,7 @@ export function useWebSocket() {
|
||||
})
|
||||
|
||||
// 注册消息处理器
|
||||
ws.value.on('dictionary_update', handleDictionaryUpdate)
|
||||
ws.value.on('dictionary_item_update', handleDictionaryItemUpdate)
|
||||
registerMessageHandlers()
|
||||
|
||||
// 连接
|
||||
ws.value.connect()
|
||||
@@ -50,11 +67,64 @@ export function useWebSocket() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册所有消息处理器
|
||||
*/
|
||||
function registerMessageHandlers() {
|
||||
if (!ws.value) return
|
||||
|
||||
// 字典更新
|
||||
ws.value.on('dictionary_update', handleDictionaryUpdate)
|
||||
ws.value.on('dictionary_item_update', handleDictionaryItemUpdate)
|
||||
|
||||
// 系统通知
|
||||
ws.value.on('notification', handleNotification)
|
||||
|
||||
// 数据更新
|
||||
ws.value.on('data_update', handleDataUpdate)
|
||||
|
||||
// 心跳响应
|
||||
ws.value.on('heartbeat_response', handleHeartbeatResponse)
|
||||
|
||||
// 连接确认
|
||||
ws.value.on('connected', handleConnected)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册所有消息处理器
|
||||
*/
|
||||
function unregisterMessageHandlers() {
|
||||
if (!ws.value) return
|
||||
|
||||
ws.value.off('dictionary_update')
|
||||
ws.value.off('dictionary_item_update')
|
||||
ws.value.off('notification')
|
||||
ws.value.off('data_update')
|
||||
ws.value.off('heartbeat_response')
|
||||
ws.value.off('connected')
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接打开
|
||||
*/
|
||||
function handleOpen(event) {
|
||||
console.log('WebSocket 连接已建立', event)
|
||||
|
||||
// 发送连接确认
|
||||
if (ws.value) {
|
||||
ws.value.send('auth', {
|
||||
token: userStore.token,
|
||||
user_id: userStore.userInfo.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理连接确认
|
||||
*/
|
||||
function handleConnected(data) {
|
||||
console.log('WebSocket 连接已确认', data)
|
||||
message.success('实时连接已建立')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +139,7 @@ export function useWebSocket() {
|
||||
*/
|
||||
function handleError(error) {
|
||||
console.error('WebSocket 错误:', error)
|
||||
message.error('实时连接出现错误,正在重连...')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,6 +147,84 @@ export function useWebSocket() {
|
||||
*/
|
||||
function handleClose(event) {
|
||||
console.log('WebSocket 连接已关闭', event)
|
||||
|
||||
// 如果不是手动关闭,显示提示
|
||||
if (event.code !== 1000) {
|
||||
message.warning('实时连接已断开')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳响应
|
||||
*/
|
||||
function handleHeartbeatResponse(data) {
|
||||
console.log('收到心跳响应:', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统通知
|
||||
*/
|
||||
function handleNotification(data) {
|
||||
console.log('收到系统通知:', data)
|
||||
|
||||
const { title, message: content, type, timestamp } = data
|
||||
|
||||
// 添加到消息 store
|
||||
messageStore.addMessage({
|
||||
type: type || 'notification',
|
||||
title: title || '系统通知',
|
||||
content: content || '',
|
||||
timestamp: timestamp || Date.now()
|
||||
})
|
||||
|
||||
// 显示通知(根据类型)
|
||||
const notificationConfig = {
|
||||
message: title || '系统通知',
|
||||
description: content,
|
||||
duration: 4.5,
|
||||
placement: 'topRight'
|
||||
}
|
||||
|
||||
// 根据类型设置不同样式
|
||||
switch (type) {
|
||||
case 'success':
|
||||
notification.success(notificationConfig)
|
||||
break
|
||||
case 'error':
|
||||
notification.error(notificationConfig)
|
||||
break
|
||||
case 'warning':
|
||||
notification.warning(notificationConfig)
|
||||
break
|
||||
default:
|
||||
notification.info(notificationConfig)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数据更新
|
||||
*/
|
||||
async function handleDataUpdate(data) {
|
||||
console.log('收到数据更新:', data)
|
||||
|
||||
const { resource_type, action, timestamp } = data
|
||||
|
||||
// 添加到消息 store
|
||||
messageStore.handleDataUpdate(data)
|
||||
|
||||
// 根据资源类型执行相应操作
|
||||
switch (resource_type) {
|
||||
case 'dictionary':
|
||||
case 'dictionary_item':
|
||||
// 刷新字典缓存
|
||||
try {
|
||||
await dictionaryStore.refresh(true)
|
||||
} catch (error) {
|
||||
console.error('刷新字典缓存失败:', error)
|
||||
}
|
||||
break
|
||||
// 可以添加其他资源类型的处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,12 +233,6 @@ export function useWebSocket() {
|
||||
async function handleDictionaryUpdate(data) {
|
||||
console.log('字典分类已更新:', data)
|
||||
|
||||
const { action, resource_type, timestamp } = data
|
||||
|
||||
if (resource_type !== 'dictionary') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 刷新字典缓存
|
||||
await dictionaryStore.refresh(true)
|
||||
@@ -107,12 +250,6 @@ export function useWebSocket() {
|
||||
async function handleDictionaryItemUpdate(data) {
|
||||
console.log('字典项已更新:', data)
|
||||
|
||||
const { action, resource_type, timestamp } = data
|
||||
|
||||
if (resource_type !== 'dictionary_item') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 刷新字典缓存
|
||||
await dictionaryStore.refresh(true)
|
||||
@@ -124,23 +261,59 @@ export function useWebSocket() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到服务器
|
||||
*/
|
||||
function send(type, data) {
|
||||
if (ws.value && ws.value.isConnected) {
|
||||
ws.value.send(type, data)
|
||||
} else {
|
||||
console.warn('WebSocket 未连接,无法发送消息')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 WebSocket 连接
|
||||
*/
|
||||
function closeWebSocket() {
|
||||
// 清除重试定时器
|
||||
if (reconnectTimer.value) {
|
||||
clearTimeout(reconnectTimer.value)
|
||||
reconnectTimer.value = null
|
||||
}
|
||||
|
||||
if (ws.value) {
|
||||
// 取消注册消息处理器
|
||||
ws.value.off('dictionary_update')
|
||||
ws.value.off('dictionary_item_update')
|
||||
unregisterMessageHandlers()
|
||||
|
||||
ws.value.disconnect()
|
||||
ws.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新连接 WebSocket
|
||||
*/
|
||||
function reconnect() {
|
||||
closeWebSocket()
|
||||
setTimeout(() => {
|
||||
initWebSocket()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
function isConnected() {
|
||||
return ws.value && ws.value.isConnected
|
||||
}
|
||||
|
||||
return {
|
||||
ws,
|
||||
initWebSocket,
|
||||
closeWebSocket
|
||||
closeWebSocket,
|
||||
reconnect,
|
||||
isConnected,
|
||||
send
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,22 @@ export default {
|
||||
clearCacheFailed: 'Failed to clear cache',
|
||||
messages: 'Messages',
|
||||
tasks: 'Tasks',
|
||||
notification: 'Notification',
|
||||
task: 'Task',
|
||||
warning: 'Warning',
|
||||
markAllAsRead: 'Mark All as Read',
|
||||
clearAll: 'Clear All',
|
||||
noMessages: 'No Messages',
|
||||
noTasks: 'No Tasks',
|
||||
confirmClear: 'Confirm Clear',
|
||||
confirmClearMessages: 'Are you sure you want to clear all messages?',
|
||||
markedAsRead: 'Marked as Read',
|
||||
realtimeConnected: 'Real-time connection established',
|
||||
realtimeDisconnected: 'Real-time connection disconnected',
|
||||
realtimeError: 'Real-time connection error, reconnecting...',
|
||||
dataUpdated: 'Data Updated',
|
||||
dataCreated: 'Data Created',
|
||||
dataDeleted: 'Data Deleted',
|
||||
fullscreen: 'Fullscreen',
|
||||
personalCenter: 'Personal Center',
|
||||
systemSettings: 'System Settings',
|
||||
|
||||
@@ -38,9 +38,22 @@ export default {
|
||||
clearCacheFailed: '清除缓存失败',
|
||||
messages: '消息',
|
||||
tasks: '任务',
|
||||
notification: '通知',
|
||||
task: '任务',
|
||||
warning: '警告',
|
||||
markAllAsRead: '全部标为已读',
|
||||
clearAll: '清空全部',
|
||||
noMessages: '暂无消息',
|
||||
noTasks: '暂无任务',
|
||||
confirmClear: '确认清空',
|
||||
confirmClearMessages: '确定要清空所有消息吗?',
|
||||
markedAsRead: '已标记为已读',
|
||||
realtimeConnected: '实时连接已建立',
|
||||
realtimeDisconnected: '实时连接已断开',
|
||||
realtimeError: '实时连接出现错误,正在重连...',
|
||||
dataUpdated: '数据已更新',
|
||||
dataCreated: '数据已创建',
|
||||
dataDeleted: '数据已删除',
|
||||
fullscreen: '全屏',
|
||||
personalCenter: '个人中心',
|
||||
systemSettings: '系统设置',
|
||||
|
||||
@@ -8,27 +8,76 @@
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<a-dropdown :trigger="['click']" placement="bottomRight">
|
||||
<a-dropdown v-model:open="messageVisible" :trigger="['click']" placement="bottomRight">
|
||||
<a-badge :count="messageCount" :offset="[-5, 5]">
|
||||
<a-button type="text" class="action-btn">
|
||||
<BellOutlined />
|
||||
</a-button>
|
||||
</a-badge>
|
||||
<template #overlay>
|
||||
<a-card class="dropdown-card" :title="$t('common.messages')" :bordered="false">
|
||||
<template #extra>
|
||||
<a @click="clearMessages">{{ $t('common.clearAll') }}</a>
|
||||
<a-card class="dropdown-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="message-header">
|
||||
<span>{{ $t('common.messages') }}</span>
|
||||
<a-space size="small">
|
||||
<a-button v-if="messageCount > 0" type="link" size="small" @click="markAllAsRead">
|
||||
{{ $t('common.markAllAsRead') }}
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="clearMessages">
|
||||
{{ $t('common.clearAll') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 消息类型筛选 -->
|
||||
<div class="message-tabs">
|
||||
<a-tabs v-model:activeKey="currentMessageType" size="small" @change="changeMessageType">
|
||||
<a-tab-pane key="all" :tab="$t('common.all')" />
|
||||
<a-tab-pane key="notification" :tab="$t('common.notification')" />
|
||||
<a-tab-pane key="task" :tab="$t('common.task')" />
|
||||
<a-tab-pane key="warning" :tab="$t('common.warning')" />
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<div class="message-list">
|
||||
<div v-for="msg in messages" :key="msg.id" class="message-item" :class="{ unread: !msg.read }">
|
||||
<div
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
class="message-item"
|
||||
:class="{ unread: !msg.read }"
|
||||
@click="handleMessageRead(msg)"
|
||||
>
|
||||
<div class="message-content">
|
||||
<div class="message-title">{{ msg.title }}</div>
|
||||
<div class="message-time">{{ msg.time }}</div>
|
||||
<div class="message-content-text">{{ msg.content }}</div>
|
||||
<div class="message-time">{{ messageStore.formatMessageTime(msg.timestamp) }}</div>
|
||||
</div>
|
||||
<a-badge v-if="!msg.read" dot />
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
class="delete-btn"
|
||||
@click.stop="handleDeleteMessage(msg.id)"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
</a-button>
|
||||
</div>
|
||||
<a-empty v-if="messages.length === 0" :description="$t('common.noMessages')" />
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="messagesTotal > messagesPageSize" class="message-pagination">
|
||||
<a-pagination
|
||||
v-model:current="messagesPage"
|
||||
v-model:pageSize="messagesPageSize"
|
||||
:total="messagesTotal"
|
||||
size="small"
|
||||
:show-size-changer="false"
|
||||
@change="handleMessagePageChange"
|
||||
/>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
@@ -111,6 +160,7 @@ import { useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { useI18nStore } from '@/stores/modules/i18n'
|
||||
import { useMessageStore, MessageType } from '@/stores/modules/message'
|
||||
import { DownOutlined, UserOutlined, LogoutOutlined, FullscreenOutlined, FullscreenExitOutlined, BellOutlined, CheckSquareOutlined, GlobalOutlined, SearchOutlined, SettingOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import search from './search.vue'
|
||||
@@ -125,20 +175,37 @@ const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const i18nStore = useI18nStore()
|
||||
const messageStore = useMessageStore()
|
||||
|
||||
const isFullscreen = ref(false)
|
||||
const searchVisible = ref(false)
|
||||
const taskVisible = ref(false)
|
||||
const messageVisible = ref(false)
|
||||
const currentMessageType = ref('all')
|
||||
const messagesPage = ref(1)
|
||||
const messagesPageSize = ref(10)
|
||||
|
||||
// 消息数据
|
||||
const messages = ref([
|
||||
{ id: 1, title: '系统通知:新版本已发布', time: '10分钟前', read: false },
|
||||
{ id: 2, title: '任务提醒:请完成待审核的用户', time: '30分钟前', read: false },
|
||||
{ id: 3, title: '安全警告:检测到异常登录', time: '1小时前', read: true },
|
||||
{ id: 4, title: '数据备份已完成', time: '2小时前', read: true },
|
||||
])
|
||||
// 从 store 获取消息数据
|
||||
const messages = computed(() => {
|
||||
const result = messageStore.getMessages({
|
||||
page: messagesPage.value,
|
||||
pageSize: messagesPageSize.value,
|
||||
type: currentMessageType.value === 'all' ? null : currentMessageType.value
|
||||
})
|
||||
return result.list
|
||||
})
|
||||
|
||||
const messageCount = computed(() => messages.value.filter((m) => !m.read).length)
|
||||
// 消息总数(用于分页)
|
||||
const messagesTotal = computed(() => {
|
||||
return messageStore.getMessages({
|
||||
page: messagesPage.value,
|
||||
pageSize: messagesPageSize.value,
|
||||
type: currentMessageType.value === 'all' ? null : currentMessageType.value
|
||||
}).total
|
||||
})
|
||||
|
||||
// 未读消息数量
|
||||
const messageCount = computed(() => messageStore.unreadCount)
|
||||
|
||||
// 任务数据
|
||||
const tasks = ref([
|
||||
@@ -180,8 +247,45 @@ const showSearch = () => {
|
||||
|
||||
// 清除消息
|
||||
const clearMessages = () => {
|
||||
messages.value = []
|
||||
message.success(t('common.cleared'))
|
||||
Modal.confirm({
|
||||
title: t('common.confirmClear'),
|
||||
content: t('common.confirmClearMessages'),
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: () => {
|
||||
messageStore.clearAll()
|
||||
message.success(t('common.cleared'))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 标记消息为已读
|
||||
const handleMessageRead = (msg) => {
|
||||
if (!msg.read) {
|
||||
messageStore.markAsRead(msg.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记所有消息为已读
|
||||
const markAllAsRead = () => {
|
||||
messageStore.markAllAsRead()
|
||||
message.success(t('common.markedAsRead'))
|
||||
}
|
||||
|
||||
// 删除消息
|
||||
const handleDeleteMessage = (msgId) => {
|
||||
messageStore.removeMessage(msgId)
|
||||
}
|
||||
|
||||
// 切换消息类型
|
||||
const changeMessageType = (type) => {
|
||||
currentMessageType.value = type
|
||||
messagesPage.value = 1
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleMessagePageChange = (page) => {
|
||||
messagesPage.value = page
|
||||
}
|
||||
|
||||
// 显示任务抽屉
|
||||
@@ -318,26 +422,145 @@ const handleLogout = () => {
|
||||
}
|
||||
|
||||
.dropdown-card {
|
||||
width: 320px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
width: 380px;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-card-head) {
|
||||
padding: 12px 16px;
|
||||
padding: 8px 16px;
|
||||
min-height: auto;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.ant-card-head-title {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
padding: 12px 16px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.message-list,
|
||||
.task-list {
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-item,
|
||||
.message-tabs {
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 4px 8px;
|
||||
margin: 0 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
|
||||
.message-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background-color: rgba(24, 144, 255, 0.04);
|
||||
padding-left: 12px;
|
||||
margin-left: 4px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #1890ff;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(24, 144, 255, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
margin-right: 24px;
|
||||
|
||||
.message-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-content-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.message-item:hover .delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-pagination {
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
.task-item {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
@@ -347,30 +570,7 @@ const handleLogout = () => {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background-color: rgba(24, 144, 255, 0.04);
|
||||
padding: 10px;
|
||||
margin: 0 -10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
.message-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.task-item {
|
||||
.completed {
|
||||
&.completed {
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
275
resources/admin/src/stores/modules/message.js
Normal file
275
resources/admin/src/stores/modules/message.js
Normal file
@@ -0,0 +1,275 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { customStorage } from '../persist'
|
||||
|
||||
/**
|
||||
* 消息类型枚举
|
||||
*/
|
||||
export const MessageType = {
|
||||
NOTIFICATION: 'notification', // 系统通知
|
||||
TASK: 'task', // 任务提醒
|
||||
WARNING: 'warning', // 警告
|
||||
ERROR: 'error', // 错误
|
||||
SUCCESS: 'success', // 成功
|
||||
INFO: 'info' // 信息
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息优先级枚举
|
||||
*/
|
||||
export const MessagePriority = {
|
||||
LOW: 'low',
|
||||
MEDIUM: 'medium',
|
||||
HIGH: 'high',
|
||||
URGENT: 'urgent'
|
||||
}
|
||||
|
||||
export const useMessageStore = defineStore(
|
||||
'message',
|
||||
() => {
|
||||
// 消息列表
|
||||
const messages = ref([])
|
||||
|
||||
// 最大消息数量
|
||||
const maxMessages = 100
|
||||
|
||||
// 获取未读消息数量
|
||||
const unreadCount = computed(() => {
|
||||
return messages.value.filter((m) => !m.read).length
|
||||
})
|
||||
|
||||
// 获取所有消息数量
|
||||
const totalCount = computed(() => {
|
||||
return messages.value.length
|
||||
})
|
||||
|
||||
// 根据类型获取消息数量
|
||||
const getCountByType = (type) => {
|
||||
return messages.value.filter((m) => m.type === type).length
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
function addMessage(message) {
|
||||
const newMessage = {
|
||||
id: Date.now() + Math.random(),
|
||||
type: MessageType.NOTIFICATION,
|
||||
priority: MessagePriority.MEDIUM,
|
||||
title: '',
|
||||
content: '',
|
||||
read: false,
|
||||
timestamp: Date.now(),
|
||||
...message
|
||||
}
|
||||
|
||||
// 添加到列表开头
|
||||
messages.value.unshift(newMessage)
|
||||
|
||||
// 限制消息数量
|
||||
if (messages.value.length > maxMessages) {
|
||||
messages.value = messages.value.slice(0, maxMessages)
|
||||
}
|
||||
|
||||
// 持久化到 localStorage
|
||||
persistMessages()
|
||||
|
||||
return newMessage
|
||||
}
|
||||
|
||||
// 批量添加消息
|
||||
function addMessages(newMessages) {
|
||||
newMessages.forEach((msg) => addMessage(msg))
|
||||
}
|
||||
|
||||
// 标记消息为已读
|
||||
function markAsRead(messageId) {
|
||||
const message = messages.value.find((m) => m.id === messageId)
|
||||
if (message) {
|
||||
message.read = true
|
||||
persistMessages()
|
||||
}
|
||||
}
|
||||
|
||||
// 标记所有消息为已读
|
||||
function markAllAsRead() {
|
||||
messages.value.forEach((m) => {
|
||||
m.read = true
|
||||
})
|
||||
persistMessages()
|
||||
}
|
||||
|
||||
// 删除消息
|
||||
function removeMessage(messageId) {
|
||||
const index = messages.value.findIndex((m) => m.id === messageId)
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1)
|
||||
persistMessages()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有消息
|
||||
function clearAll() {
|
||||
messages.value = []
|
||||
persistMessages()
|
||||
}
|
||||
|
||||
// 清空已读消息
|
||||
function clearRead() {
|
||||
messages.value = messages.value.filter((m) => !m.read)
|
||||
persistMessages()
|
||||
}
|
||||
|
||||
// 根据类型清空消息
|
||||
function clearByType(type) {
|
||||
messages.value = messages.value.filter((m) => m.type !== type)
|
||||
persistMessages()
|
||||
}
|
||||
|
||||
// 格式化消息时间
|
||||
function formatMessageTime(timestamp) {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`
|
||||
} else if (diff < 7 * day) {
|
||||
return `${Math.floor(diff / day)}天前`
|
||||
} else {
|
||||
const date = new Date(timestamp)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化消息到 localStorage
|
||||
function persistMessages() {
|
||||
try {
|
||||
localStorage.setItem('message-store', JSON.stringify(messages.value))
|
||||
} catch (error) {
|
||||
console.error('持久化消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 从 localStorage 恢复消息
|
||||
function restoreMessages() {
|
||||
try {
|
||||
const stored = localStorage.getItem('message-store')
|
||||
if (stored) {
|
||||
messages.value = JSON.parse(stored)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('恢复消息失败:', error)
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 WebSocket 消息
|
||||
function handleWebSocketMessage(data) {
|
||||
const { type, title, message, content, ...extra } = data
|
||||
|
||||
addMessage({
|
||||
type: type || MessageType.NOTIFICATION,
|
||||
title: title || '系统通知',
|
||||
content: message || content || '',
|
||||
...extra
|
||||
})
|
||||
}
|
||||
|
||||
// 处理数据更新消息
|
||||
function handleDataUpdate(data) {
|
||||
const { resource_type, action } = data
|
||||
|
||||
let title = '数据更新'
|
||||
let content = ''
|
||||
|
||||
switch (action) {
|
||||
case 'create':
|
||||
title = '新建成功'
|
||||
content = `新的${resource_type}已创建`
|
||||
break
|
||||
case 'update':
|
||||
title = '更新成功'
|
||||
content = `${resource_type}数据已更新`
|
||||
break
|
||||
case 'delete':
|
||||
title = '删除成功'
|
||||
content = `${resource_type}数据已删除`
|
||||
break
|
||||
default:
|
||||
content = `${resource_type}数据已${action}`
|
||||
}
|
||||
|
||||
addMessage({
|
||||
type: MessageType.SUCCESS,
|
||||
title,
|
||||
content
|
||||
})
|
||||
}
|
||||
|
||||
// 获取消息列表(带分页)
|
||||
function getMessages(options = {}) {
|
||||
const { page = 1, pageSize = 20, type = null, read = null } = options
|
||||
|
||||
let filtered = [...messages.value]
|
||||
|
||||
// 按类型过滤
|
||||
if (type) {
|
||||
filtered = filtered.filter((m) => m.type === type)
|
||||
}
|
||||
|
||||
// 按已读状态过滤
|
||||
if (read !== null) {
|
||||
filtered = filtered.filter((m) => m.read === read)
|
||||
}
|
||||
|
||||
// 分页
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const list = filtered.slice(start, end)
|
||||
const total = filtered.length
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
unreadCount,
|
||||
totalCount,
|
||||
MessageType,
|
||||
MessagePriority,
|
||||
addMessage,
|
||||
addMessages,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
removeMessage,
|
||||
clearAll,
|
||||
clearRead,
|
||||
clearByType,
|
||||
formatMessageTime,
|
||||
handleWebSocketMessage,
|
||||
handleDataUpdate,
|
||||
getMessages,
|
||||
getCountByType,
|
||||
restoreMessages
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'message-store',
|
||||
storage: customStorage,
|
||||
pick: [] // 不自动持久化,使用自定义 persistMessages 方法
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -85,6 +85,11 @@ import userRoutes from '@/config/routes'
|
||||
return !!token.value
|
||||
}
|
||||
|
||||
// 检查用户信息是否完整(用于 WebSocket 初始化)
|
||||
function isUserInfoComplete() {
|
||||
return !!(token.value && userInfo.value && userInfo.value.id)
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userInfo,
|
||||
@@ -97,6 +102,7 @@ import userRoutes from '@/config/routes'
|
||||
setPermissions,
|
||||
logout,
|
||||
isLoggedIn,
|
||||
isUserInfoComplete,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user