diff --git a/HISTORY.md b/HISTORY.md index 554b73b..d21b76c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,7 @@ 迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开 2025.05.15 0.1.6 +增加admin, mobile 消息io连接 增加socketio 路由 支持 增加socketio server 支持 修正文件分类后端api路由查询表名为file_categories diff --git a/client/admin/pages_messages.tsx b/client/admin/pages_messages.tsx index 0e8cdf3..577febd 100644 --- a/client/admin/pages_messages.tsx +++ b/client/admin/pages_messages.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd'; +import { io, Socket } from 'socket.io-client'; import type { TableProps } from 'antd'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; @@ -8,8 +9,12 @@ import 'dayjs/locale/zh-cn'; import { MessageAPI , UserAPI } from './api/index.ts'; import type { UserMessage } from '../share/types.ts'; import { MessageStatusNameMap , MessageStatus} from '../share/types.ts'; +import { useAuth } from "./hooks_sys.tsx"; -export const MessagesPage = () => { +export const MessagesPage = () => { + const { token } = useAuth(); + const [socket, setSocket] = useState(null); + const [isSocketConnected, setIsSocketConnected] = useState(false); const queryClient = useQueryClient(); const [form] = Form.useForm(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -59,8 +64,58 @@ export const MessagesPage = () => { }); // 发送消息 + // 初始化Socket.IO连接 + useEffect(() => { + if (!token) return; + + const newSocket = io('/', { + path: '/socket.io', + transports: ['websocket'], + autoConnect: false, + query: { + socket_token: token + } + }); + + newSocket.on('connect', () => { + setIsSocketConnected(true); + message.success('实时消息连接已建立'); + }); + + newSocket.on('disconnect', () => { + setIsSocketConnected(false); + message.warning('实时消息连接已断开'); + }); + + newSocket.on('error', (err) => { + message.error(`实时消息错误: ${err}`); + }); + + newSocket.connect(); + setSocket(newSocket); + + return () => { + newSocket.disconnect(); + }; + }, [token]); + const sendMessageMutation = useMutation({ - mutationFn: (data: any) => MessageAPI.sendMessage(data), + mutationFn: async (data: any) => { + // 优先使用Socket.IO发送 + if (isSocketConnected && socket) { + return new Promise((resolve, reject) => { + socket.emit('message:send', data, (response: any) => { + if (response.error) { + reject(new Error(response.error)); + } else { + resolve(response.data); + } + }); + }); + } + // 回退到HTTP API + return MessageAPI.sendMessage(data); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['messages'] }); queryClient.invalidateQueries({ queryKey: ['unreadCount'] }); @@ -266,10 +321,11 @@ export const MessagesPage = () => { - diff --git a/client/mobile/pages_messages.tsx b/client/mobile/pages_messages.tsx index a476c41..724d9fa 100644 --- a/client/mobile/pages_messages.tsx +++ b/client/mobile/pages_messages.tsx @@ -1,16 +1,20 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { BellIcon } from '@heroicons/react/24/outline'; import { MessageStatus } from '../share/types.ts'; - +import { io, Socket } from 'socket.io-client'; // 添加通知页面组件 import { MessageAPI } from './api/index.ts'; +import { useAuth } from "./hooks.tsx"; export const NotificationsPage = () => { + const { token , user} = useAuth(); const queryClient = useQueryClient(); + const [socket, setSocket] = React.useState(null); + const [isSubscribed, setIsSubscribed] = React.useState(false); // 获取消息列表 const { data: messages, isLoading } = useQuery({ @@ -18,6 +22,60 @@ export const NotificationsPage = () => { queryFn: () => MessageAPI.getMessages(), }); + // 初始化Socket.IO连接 + useEffect(() => { + if (!token || !user) return; + + const newSocket = io('/', { + path: '/socket.io', + transports: ['websocket'], + withCredentials: true, + query: { + socket_token: token + } + }); + + setSocket(newSocket); + + // 订阅消息频道 + newSocket.on('connect', () => { + newSocket.emit('message:subscribe', `user_${user.id}`); + setIsSubscribed(true); + }); + + // 处理实时消息 + newSocket.on('message:broadcasted', (newMessage) => { + queryClient.setQueryData(['messages'], (oldData: any) => { + if (!oldData) return oldData; + return { + ...oldData, + data: [newMessage, ...oldData.data] + }; + }); + + // 更新未读计数 + queryClient.setQueryData(['unreadCount'], (oldData: any) => { + if (!oldData) return oldData; + return { + ...oldData, + count: oldData.count + 1 + }; + }); + }); + + // 错误处理 + newSocket.on('error', (error) => { + console.error('Socket error:', error); + }); + + return () => { + if (newSocket) { + newSocket.emit('message:unsubscribe', `user_${user.id}`); + newSocket.disconnect(); + } + }; + }, [queryClient, token]); + // 获取未读消息数量 const { data: unreadCount } = useQuery({ queryKey: ['unreadCount'], diff --git a/deno.json b/deno.json index 1bed6f7..e95dfac 100644 --- a/deno.json +++ b/deno.json @@ -29,7 +29,8 @@ "react-hook-form": "https://esm.d8d.fun/react-hook-form@7.55.0?dev&deps=react@18.3.1,react-dom@18.3.1", "@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?dev&deps=react@18.3.1,react-dom@18.3.1", "@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?dev&deps=react@18.3.1,react-dom@18.3.1", - "socket.io": "https://deno.land/x/socket_io@0.2.1/mod.ts" + "socket.io": "https://deno.land/x/socket_io@0.2.1/mod.ts", + "socket.io-client": "https://esm.d8d.fun/socket.io-client@4.8.1" }, "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext", "deno.ns"] diff --git a/server/routes_io_messages.ts b/server/routes_io_messages.ts index 9812707..f40b5f7 100644 --- a/server/routes_io_messages.ts +++ b/server/routes_io_messages.ts @@ -17,6 +17,83 @@ interface MessageListData { } export function setupMessageEvents({ socket , apiClient }:Variables) { + // 订阅频道 + socket.on('message:subscribe', (channel: string) => { + try { + socket.join(channel); + socket.emit('message:subscribed', { + message: `成功订阅频道: ${channel}`, + channel + }); + } catch (error) { + console.error('订阅频道失败:', error); + socket.emit('error', '订阅频道失败'); + } + }); + + // 取消订阅 + socket.on('message:unsubscribe', (channel: string) => { + try { + socket.leave(channel); + socket.emit('message:unsubscribed', { + message: `已取消订阅频道: ${channel}`, + channel + }); + } catch (error) { + console.error('取消订阅失败:', error); + socket.emit('error', '取消订阅失败'); + } + }); + + // 广播消息 + socket.on('message:broadcast', async (data: { + channel?: string; + title: string; + content: string; + type: MessageType; + }) => { + try { + const { channel, title, content, type } = data; + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + // 创建广播消息 + const [messageId] = await apiClient.database.table('messages').insert({ + title, + content, + type, + sender_id: user.id, + sender_name: user.username, + is_broadcast: 1, + created_at: apiClient.database.fn.now(), + updated_at: apiClient.database.fn.now() + }); + + // 广播到所有客户端或特定频道 + const broadcastTarget = channel ? socket.to(channel) : socket.broadcast; + broadcastTarget.emit('message:broadcasted', { + id: messageId, + title, + content, + type, + sender_id: user.id, + sender_name: user.username, + created_at: new Date().toISOString() + }); + + socket.emit('message:broadcasted', { + message: '广播消息发送成功', + data: { id: messageId } + }); + } catch (error) { + console.error('广播消息失败:', error); + socket.emit('error', '广播消息失败'); + } + }); + // 发送消息 socket.on('message:send', async (data: MessageSendData) => { try {