diff --git a/README.md b/README.md index 8823ef1..7eecb28 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,15 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 +ces [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**本仓库近期因为一些原因重建,丢失了原有所有的commit记录,仅有Gitee仓库留有完整备份。现在已经重建成功,欢迎提交。** +## 新项目友链 +**我们正在开发以Dear Imgui + C++的HalloChat客户端,使用GNU-LGPL许可证!** +新仓库链接:[HalloChat-DearImgui](https://github.com/Ink-dark/HalloChat-cpp-on-imgui) -**注意:** -1. 本项目为测试版本,存在已知问题和功能缺失,且不定期更新,如果有发现问题请及时提交issues联系我,感谢支持。 +## 特别须知 +1. 本项目尚处于测试版本,存在已知问题和功能缺失,且不定期更新,如果有发现问题请及时提交issues联系我,感谢支持。 2. 本项目使用GitHub Actions 进行企业微信机器人通知(仅Push事件),用于通知项目维护者有新的代码提交。 - 企业微信机器人Webhook URL:`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${{ secrets.WECHAT_QIYE_WEBHOOK_URL }}` - 请将上述URL添加到您的项目的GitHub Actions Secrets 中,名称为 `WECHAT_QIYE_WEBHOOK_URL`。 @@ -21,12 +24,21 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 // 版权所有 © 2025 Ink-dark(墨染柒DarkSeven) // 遵循 MIT 开源许可证 ``` +## 项目开发者 +### 墨染柒DarkSeven +- 邮箱:moranqidarkseven@hallochat.cn +- 个人博客:[墨染柒的个人博客-GitHub Pages(可能停止更新)](Ink-dark.github.io) + +### SANYOU (LUCA.NEX) +- 邮箱:you.san1@icloud.com +- 个人网站:[lucanex.top](https://www.lucanex.top/) + ## 联系我们 - 项目仓库:[HalloChat](https://github.com/Ink-dark/HalloChat) - 问题反馈:[Issues](https://github.com/Ink-dark/HalloChat/issues) - 邮件联系: - 1. hallochatdev@hanjiang88luntan.eu.org(项目开发团队) - 2. d16671480856@163.com(项目开发者Ink-dark/墨染柒DarkSeven) + 1. dev@hallochat.cn(项目开发团队) + 2. moranqidarkseven@hallochat.cn(项目开发者Ink-dark/墨染柒DarkSeven) 3. 企业微信:(正在配置中) ## 项目概述 @@ -277,6 +289,9 @@ JWT_REFRESH_SECRET=your_refresh_secret > **注意**:上述所有命令需在管理员模式下的PowerShell中执行。 ## 加入我们 + +注意:因企业微信企业账号封禁,我们已改用飞书进行协作。飞书的企业名称为“比特火炬”。 + 如果你对HalloChat项目感兴趣,欢迎加入我们的开发团队。你可以通过以下方式联系我们,我们会在一定时间内回复,请在邮件中包含以下信息: - 你的姓名(可以不要求真实姓名,昵称即可) - 你的邮箱(用于回复和项目合作) @@ -287,11 +302,11 @@ JWT_REFRESH_SECRET=your_refresh_secret - 参与项目的技术讨论和决策 - 项目进度的及时更新通知 - 项目相关的技术支持和帮助 -- 项目团队企业微信(要求提供手机号,用于加入项目团队) +- 项目团队飞书(要求提供手机号,用于加入项目团队) 当你做好了以上准备,你可以通过以下方式联系我们: - 1. hallochatdev@hanjiang88luntan.eu.org(项目开发团队) - 2. d16671480856@163.com(项目开发者Ink-dark/墨染柒DarkSeven) - 3. 企业微信:(因企业微信限制,目前暂无法直接提供添加方式) + 1. dev@hallochat.cn(项目开发团队) + 2. moranqidarkseven@hallochoat.cn(项目开发者Ink-dark/墨染柒DarkSeven) +再次感谢每一个为项目做出贡献的开发者! diff --git a/client/.npmrc b/client/.npmrc index cb44a39..16d8c69 100644 --- a/client/.npmrc +++ b/client/.npmrc @@ -1,3 +1,3 @@ registry=https://registry.npmmirror.com/ -electron_mirror=https://cdn.npmmirror.com/binaries/electron/ -electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ \ No newline at end of file +# Electron mirrors should be set via environment variables to avoid npm warnings +# e.g., ELECTRON_MIRROR="https://cdn.npmmirror.com/binaries/electron/" \ No newline at end of file diff --git a/client/electron-main.js b/client/electron-main.js index 89d8e64..429c969 100644 --- a/client/electron-main.js +++ b/client/electron-main.js @@ -37,6 +37,9 @@ function saveWindowConfig(windowName, bounds) { // 全局引用,防止窗口被垃圾回收 let loginWindow = null; let mainWindow = null; +let serverSelectionWindow = null; +let settingsWindow = null; +let chatWindows = {}; // 存储多个聊天窗口 // 创建登录窗口 function createLoginWindow() { @@ -261,6 +264,278 @@ ipcMain.on('show-error-box', (event, title, content) => { dialog.showErrorBox(title, content); }); +// 监听打开设置窗口事件 +ipcMain.on('open-settings-window', () => { + log.info('打开设置窗口'); + createSettingsWindow(); +}); + +// 监听打开服务器选择窗口事件 +ipcMain.on('open-server-selection-window', () => { + log.info('打开服务器选择窗口'); + createServerSelectionWindow(); +}); + +// 监听打开聊天窗口事件 +ipcMain.on('open-chat-window', (event, { chatId, chatType, chatName }) => { + log.info('打开聊天窗口:', { chatId, chatType, chatName }); + createChatWindow(chatId, chatType, chatName); +}); + +// 监听关闭聊天窗口事件 +ipcMain.on('close-chat-window', (event, chatId) => { + log.info('关闭聊天窗口:', chatId); + if (chatWindows[chatId] && !chatWindows[chatId].isDestroyed()) { + chatWindows[chatId].close(); + } +}); + +// 创建设置窗口 +function createSettingsWindow() { + const config = loadWindowConfig(); + const settingsConfig = config.settings || {}; + + // 如果窗口已存在,先关闭 + if (settingsWindow && !settingsWindow.isDestroyed()) { + settingsWindow.focus(); + return; + } + + settingsWindow = new BrowserWindow({ + ...settingsConfig, + width: settingsConfig.width || 600, + height: settingsConfig.height || 600, + minWidth: 400, + minHeight: 500, + title: 'HalloChat - 设置', + parent: mainWindow, + modal: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true, + webSecurity: false, + allowRunningInsecureContent: true + }, + icon: path.join(__dirname, 'public/favicon.ico') + }); + + // 加载URL + try { + const forceLocalFile = app.isPackaged; + + if (forceLocalFile) { + const possiblePaths = [ + path.join(__dirname, 'build', 'index.html'), + path.join(process.resourcesPath, 'build', 'index.html'), + path.join(__dirname, '..', 'build', 'index.html') + ]; + + let foundPath = null; + for (const p of possiblePaths) { + log.info('尝试加载文件:', p); + try { + settingsWindow.loadFile(p, { query: { view: 'settings' } }); + foundPath = p; + break; + } catch (err) { + log.warn('文件加载失败:', p, err.message); + } + } + + if (!foundPath) { + throw new Error('无法找到有效的index.html文件'); + } + } else { + settingsWindow.loadURL('http://localhost:3000/?view=settings'); + } + } catch (error) { + log.error('加载窗口内容失败:', error); + dialog.showErrorBox('加载失败', '无法加载应用内容: ' + error.message); + } + + // 保存窗口大小和位置 + settingsWindow.on('resize', () => { + if (!settingsWindow.isMaximized()) { + saveWindowConfig('settings', settingsWindow.getBounds()); + } + }); + + settingsWindow.on('move', () => { + if (!settingsWindow.isMaximized()) { + saveWindowConfig('settings', settingsWindow.getBounds()); + } + }); + + settingsWindow.on('closed', () => { + settingsWindow = null; + }); +} + +// 创建设服务器选择窗口 +function createServerSelectionWindow() { + const config = loadWindowConfig(); + const serverConfig = config.serverSelection || {}; + + // 如果窗口已存在,先关闭 + if (serverSelectionWindow && !serverSelectionWindow.isDestroyed()) { + serverSelectionWindow.focus(); + return; + } + + serverSelectionWindow = new BrowserWindow({ + ...serverConfig, + width: serverConfig.width || 500, + height: serverConfig.height || 400, + minWidth: 400, + minHeight: 300, + title: 'HalloChat - 选择服务器', + parent: mainWindow || loginWindow, + modal: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true, + webSecurity: false, + allowRunningInsecureContent: true + }, + icon: path.join(__dirname, 'public/favicon.ico') + }); + + // 加载URL + try { + const forceLocalFile = app.isPackaged; + + if (forceLocalFile) { + const possiblePaths = [ + path.join(__dirname, 'build', 'index.html'), + path.join(process.resourcesPath, 'build', 'index.html'), + path.join(__dirname, '..', 'build', 'index.html') + ]; + + let foundPath = null; + for (const p of possiblePaths) { + log.info('尝试加载文件:', p); + try { + serverSelectionWindow.loadFile(p, { query: { view: 'serverSelection' } }); + foundPath = p; + break; + } catch (err) { + log.warn('文件加载失败:', p, err.message); + } + } + + if (!foundPath) { + throw new Error('无法找到有效的index.html文件'); + } + } else { + serverSelectionWindow.loadURL('http://localhost:3000/?view=serverSelection'); + } + } catch (error) { + log.error('加载窗口内容失败:', error); + dialog.showErrorBox('加载失败', '无法加载应用内容: ' + error.message); + } + + // 保存窗口大小和位置 + serverSelectionWindow.on('resize', () => { + if (!serverSelectionWindow.isMaximized()) { + saveWindowConfig('serverSelection', serverSelectionWindow.getBounds()); + } + }); + + serverSelectionWindow.on('move', () => { + if (!serverSelectionWindow.isMaximized()) { + saveWindowConfig('serverSelection', serverSelectionWindow.getBounds()); + } + }); + + serverSelectionWindow.on('closed', () => { + serverSelectionWindow = null; + }); +} + +// 创建聊天窗口 +function createChatWindow(chatId, chatType, chatName) { + const config = loadWindowConfig(); + const chatConfig = config[`chat_${chatId}`] || {}; + + // 如果窗口已存在,先关闭 + if (chatWindows[chatId] && !chatWindows[chatId].isDestroyed()) { + chatWindows[chatId].focus(); + return; + } + + chatWindows[chatId] = new BrowserWindow({ + ...chatConfig, + width: chatConfig.width || 800, + height: chatConfig.height || 600, + minWidth: 600, + minHeight: 400, + title: `HalloChat - ${chatName}`, + parent: mainWindow, + modal: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true, + webSecurity: false, + allowRunningInsecureContent: true + }, + icon: path.join(__dirname, 'public/favicon.ico') + }); + + // 加载URL + try { + const forceLocalFile = app.isPackaged; + + if (forceLocalFile) { + const possiblePaths = [ + path.join(__dirname, 'build', 'index.html'), + path.join(process.resourcesPath, 'build', 'index.html'), + path.join(__dirname, '..', 'build', 'index.html') + ]; + + let foundPath = null; + for (const p of possiblePaths) { + log.info('尝试加载文件:', p); + try { + chatWindows[chatId].loadFile(p, { query: { view: 'chat', chatId, chatType, chatName } }); + foundPath = p; + break; + } catch (err) { + log.warn('文件加载失败:', p, err.message); + } + } + + if (!foundPath) { + throw new Error('无法找到有效的index.html文件'); + } + } else { + chatWindows[chatId].loadURL(`http://localhost:3000/?view=chat&chatId=${chatId}&chatType=${chatType}&chatName=${encodeURIComponent(chatName)}`); + } + } catch (error) { + log.error('加载窗口内容失败:', error); + dialog.showErrorBox('加载失败', '无法加载应用内容: ' + error.message); + } + + // 保存窗口大小和位置 + chatWindows[chatId].on('resize', () => { + if (!chatWindows[chatId].isMaximized()) { + saveWindowConfig(`chat_${chatId}`, chatWindows[chatId].getBounds()); + } + }); + + chatWindows[chatId].on('move', () => { + if (!chatWindows[chatId].isMaximized()) { + saveWindowConfig(`chat_${chatId}`, chatWindows[chatId].getBounds()); + } + }); + + chatWindows[chatId].on('closed', () => { + delete chatWindows[chatId]; + }); +} + // 应用就绪后创建登录窗口 app.whenReady().then(() => { try { diff --git a/client/package.json b/client/package.json index 677eee8..c212062 100644 --- a/client/package.json +++ b/client/package.json @@ -37,9 +37,11 @@ "browserify-fs": "^1.0.0", "crypto-js": "^4.2.0", "electron-log": "^5.4.3", + "i18next": "^25.7.4", "path-browserify": "^1.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^16.5.3", "react-router-dom": "^7.9.3", "react-scripts": "5.0.1", "socket.io-client": "^4.7.2", @@ -66,6 +68,9 @@ "react-app/jest" ] }, + "overrides": { + "typescript": "^4.9.5" + }, "browserslist": { "production": [ ">0.2%", diff --git a/client/public/HalloChat.ico b/client/public/HalloChat.ico new file mode 100644 index 0000000..62fedc7 Binary files /dev/null and b/client/public/HalloChat.ico differ diff --git a/client/src/App.js b/client/src/App.js index 25b314f..1a9e736 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,12 +1,15 @@ import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; -import { Layout } from 'antd'; + import MainWindow from './components/MainWindow'; import Login from './components/Login'; +import Settings from './components/Settings'; +import ServerSelectionWindow from './components/ServerSelectionWindow'; +import ChatWindow from './components/ChatWindow'; import './App.css'; import './index.css'; import authService from './services/authService'; -import encryptedChatService from './services/encryptedChatService'; +// import encryptedChatService from './services/encryptedChatService'; // 暂时未使用,保留以备后续加密聊天功能使用 import chatService from './services/chatService'; @@ -99,18 +102,51 @@ function App() { return ( - +
{activeView === 'main' && isAuthenticated && currentUser ? ( + ) : activeView === 'settings' && isAuthenticated && currentUser ? ( + + ) : activeView === 'serverSelection' ? ( + + ) : activeView === 'chat' && isAuthenticated && currentUser ? ( + ) : ( )} - +
); } export default App; +// 请将此文件放到你的 electron 主进程入口(例如 electron/main.js 或 src/electron/main.js) +const { app, BrowserWindow } = require('electron'); +const path = require('path'); + +function createWindow() { + const win = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, // 强制启用 + nodeIntegration: false, // 禁止 node 集成 + enableRemoteModule: false, // 禁用远程模块 + webSecurity: true // 启用 web 安全(CSP, 同源策略) + } + }); + + win.loadURL('http://localhost:3000'); // 或者 file://... +} + +app.whenReady().then(createWindow); diff --git a/client/src/components/ChatWindow.css b/client/src/components/ChatWindow.css index 434ba88..8c39be5 100644 --- a/client/src/components/ChatWindow.css +++ b/client/src/components/ChatWindow.css @@ -1,295 +1,189 @@ -/* ChatWindow 主容器 */ .chat-window { display: flex; flex-direction: column; height: 100%; - border: 1px solid #ddd; - border-radius: 8px; - overflow: hidden; background-color: #fff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -/* 聊天头部 */ .chat-header { - padding: 12px 16px; - background-color: #f5f5f5; - border-bottom: 1px solid #ddd; + padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + border-bottom: 1px solid #f0f0f0; } -.chat-header h3 { - margin: 0; +.contact-info { + display: flex; + align-items: center; + gap: 12px; +} + +.chat-avatar { + width: 40px; + height: 40px; + background-color: #e8f0fe; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + color: #4a90e2; font-size: 16px; +} + +.chat-name-wrapper h3 { + margin: 0; + font-size: 18px; font-weight: 600; - color: #333; + color: #1a1a1a; } -.status-indicator { +.chat-status { + font-size: 13px; + color: #8e8e93; +} + +.chat-header-actions { display: flex; - align-items: center; - font-size: 14px; + gap: 8px; +} + +.icon-btn { + background: none; + border: none; + padding: 8px; + cursor: pointer; color: #666; + border-radius: 8px; + transition: background-color 0.2s; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + +.icon-btn:hover { + background-color: #f5f5f5; + color: #1a1a1a; } -/* 消息容器 */ .messages-container { flex: 1; - padding: 16px; + padding: 24px; overflow-y: auto; - background-color: #fafafa; - scrollbar-width: thin; - scrollbar-color: #ccc transparent; + display: flex; + flex-direction: column; + gap: 16px; + background-color: #f9f9f9; } .messages-container::-webkit-scrollbar { - width: 6px; + width: 4px; } -.messages-container::-webkit-scrollbar-track { - background: transparent; +.messages-container::-webkit-scrollbar-thumb { + background: #e0e0e0; + border-radius: 4px; } -.messages-container::-webkit-scrollbar-thumb { - background-color: #ccc; - border-radius: 3px; +.message-wrapper { + display: flex; + flex-direction: column; + max-width: 70%; } -/* 无消息提示 */ -.no-messages { - text-align: center; - padding: 40px 20px; - color: #999; - font-size: 14px; +.message-wrapper.sent { + align-self: flex-end; + align-items: flex-end; } -/* 错误消息 */ -.error-message { - background-color: #ffebee; - color: #c62828; - padding: 8px 16px; - border-bottom: 1px solid #ffcdd2; - font-size: 14px; +.message-wrapper.received { + align-self: flex-start; + align-items: flex-start; } -/* 消息气泡 */ -.message { - max-width: 70%; - margin-bottom: 16px; - padding: 10px 14px; +.message-bubble { + padding: 12px 16px; border-radius: 18px; position: relative; word-wrap: break-word; - line-height: 1.4; - animation: fadeIn 0.3s ease-in-out; } -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } +.message-wrapper.received .message-bubble { + background-color: #ffffff; + color: #1a1a1a; + border-bottom-left-radius: 4px; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); } -.message.sent { - align-self: flex-end; +.message-wrapper.sent .message-bubble { background-color: #0084ff; - color: white; - margin-left: auto; + color: #fff; + border-bottom-right-radius: 4px; } -.message.received { - align-self: flex-start; - background-color: #e4e6eb; - color: #050505; +.message-bubble p { + margin: 0; + font-size: 15px; + line-height: 1.4; } .message-time { - font-size: 0.8em; - opacity: 0.7; - display: block; + font-size: 11px; margin-top: 4px; - text-align: right; -} - -.message.received .message-time { - text-align: left; + color: #8e8e93; } -/* 消息右键菜单 */ -.message-menu { - position: fixed; - background-color: white; - border: 1px solid #ddd; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - display: none; - min-width: 120px; - overflow: hidden; +.chat-footer { + padding: 16px 24px 24px; + display: flex; + align-items: center; + gap: 12px; + border-top: 1px solid #f0f0f0; } -.message-menu button { - display: block; - width: 100%; - padding: 8px 16px; - background: none; - border: none; - text-align: left; - font-size: 14px; - color: #333; - cursor: pointer; - transition: background-color 0.2s; +.input-actions { + display: flex; + gap: 4px; } -.message-menu button:hover { - background-color: #f5f5f5; +.message-input-wrapper { + flex: 1; + background-color: #f5f5f7; + border-radius: 20px; + padding: 0 16px; } -/* 编辑消息输入框 */ -.message input[type="text"] { +.message-input-wrapper input { width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 18px; - outline: none; - font-size: 14px; -} - -.message button { - margin-left: 8px; - padding: 6px 12px; - background-color: #0084ff; - color: white; + background: none; border: none; - border-radius: 14px; - cursor: pointer; - font-size: 14px; - transition: background-color 0.2s; -} - -.message button:hover { - background-color: #0066cc; -} - -.message button:last-child { - background-color: #666; -} - -.message button:last-child:hover { - background-color: #444; + padding: 10px 0; + font-size: 15px; + outline: none; + color: #1a1a1a; } -/* 撤回消息重新编辑按钮 */ -.edit-recalled-btn { - background: transparent; - color: #0084ff; +.send-btn { + width: 40px; + height: 40px; + background-color: #1a1a1a; + color: #fff; border: none; - padding: 2px 8px; - margin-left: 8px; - font-size: 12px; - cursor: pointer; - text-decoration: underline; -} - -.edit-recalled-btn:hover { - opacity: 0.8; -} - -/* 输入框区域 */ -.message-input { + border-radius: 50%; display: flex; - padding: 12px 16px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; align-items: center; -} - -.message-input input { - flex: 1; - padding: 10px 16px; - border: 1px solid #ddd; - border-radius: 22px; - outline: none; - font-size: 14px; - transition: border-color 0.2s; -} - -.message-input input:focus { - border-color: #0084ff; -} - -.message-input button { - margin-left: 12px; - padding: 10px 20px; - background-color: #0084ff; - color: white; - border: none; - border-radius: 22px; + justify-content: center; cursor: pointer; - font-size: 14px; - font-weight: 500; - transition: background-color 0.2s, transform 0.1s; -} - -.message-input button:hover { - background-color: #0066cc; -} - -.message-input button:active { - transform: scale(0.98); + transition: transform 0.2s; + font-size: 18px; } -/* 加密聊天特殊样式 */ -.chat-window.encrypted .chat-header { - background-color: #e3f2fd; -} - -/* 群组聊天特殊样式 */ -.chat-window.group .chat-header { - background-color: #f1f8e9; -} - -/* 频道聊天特殊样式 */ -.chat-window.channel .chat-header { - background-color: #fce4ec; -} - -/* 正在输入指示器 */ -.typing-indicator { - font-size: 0.8em; - color: #666; - margin-left: 8px; - font-style: italic; - animation: typingDots 1.4s infinite; -} - -@keyframes typingDots { - 0%, 60%, 100% { - opacity: 0.3; - } - 30% { - opacity: 1; - } -} - -/* 安全指示器 */ -.security-indicator { - font-size: 0.8em; - color: #4caf50; - display: flex; - align-items: center; - margin-left: 8px; +.send-btn:hover { + transform: scale(1.05); } -.security-indicator::before { - content: '🔒'; - margin-right: 4px; +.send-btn:active { + transform: scale(0.95); } \ No newline at end of file diff --git a/client/src/components/ChatWindow.js b/client/src/components/ChatWindow.js index 356773a..6d9ba08 100644 --- a/client/src/components/ChatWindow.js +++ b/client/src/components/ChatWindow.js @@ -1,48 +1,67 @@ import React, { useState, useEffect, useRef } from 'react'; -import Message from '../models/message'; +import { + PhoneOutlined, + VideoCameraOutlined, + AppstoreOutlined, + PaperClipOutlined, + SmileOutlined, + SendOutlined +} from '@ant-design/icons'; +// import Message from '../models/message'; // 暂时未使用,保留以备后续消息模型扩展使用 import chatService from '../services/chatService'; import './ChatWindow.css'; function ChatWindow({ currentUser, contact }) { - const [editContent, setEditContent] = useState(''); - const [editingMessageId, setEditingMessageId] = useState(null); + // const [editContent, setEditContent] = useState(''); // 暂时未使用,保留以备后续消息编辑功能使用 + // const [editingMessageId, setEditingMessageId] = useState(null); // 暂时未使用,保留以备后续消息编辑功能使用 const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); - const [isTyping, setIsTyping] = useState(false); - const [error, setError] = useState(null); + // const [isTyping, setIsTyping] = useState(false); // 暂时未使用,保留以备后续 typing 状态显示使用 + // const [error, setError] = useState(null); // 暂时未使用,保留以备后续错误处理使用 + // 暂时添加 setError 函数以避免编译错误 + const setError = (err) => console.error('Error:', err); const [isGroupChat, setIsGroupChat] = useState(false); - const messageMenuRef = useRef(null); + // const messageMenuRef = useRef(null); // 暂时未使用,保留以备后续消息菜单功能使用 + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); // 初始化聊天类型(单聊或群聊) useEffect(() => { setIsGroupChat(contact && contact.type === 'group'); }, [contact]); - const handleEditMessage = (messageId, content) => { - setEditingMessageId(messageId); - setEditContent(content); - }; - - const showMessageMenu = (e, message) => { - const menu = messageMenuRef.current; - if (menu) { - menu.style.display = 'block'; - menu.style.left = `${e.clientX}px`; - menu.style.top = `${e.clientY}px`; - - // 设置当前选中的消息 - menu.setAttribute('data-message-id', message.id); - menu.setAttribute('data-message-content', message.content); - - // 点击其他地方关闭菜单 - const closeMenu = () => { - menu.style.display = 'none'; - document.removeEventListener('click', closeMenu); - }; - document.addEventListener('click', closeMenu); - } - }; + // const handleEditMessage = (messageId, content) => { + // setEditingMessageId(messageId); + // setEditContent(content); + // }; // 暂时未使用,保留以备后续消息编辑功能使用 + + // const showMessageMenu = (e, message) => { + // const menu = messageMenuRef.current; + // if (menu) { + // menu.style.display = 'block'; + // menu.style.left = `${e.clientX}px`; + // menu.style.top = `${e.clientY}px`; + + // // 设置当前选中的消息 + // menu.setAttribute('data-message-id', message.id); + // menu.setAttribute('data-message-content', message.content); + + // // 点击其他地方关闭菜单 + // const closeMenu = () => { + // menu.style.display = 'none'; + // document.removeEventListener('click', closeMenu); + // }; + // document.addEventListener('click', closeMenu); + // } + // }; // 暂时未使用,保留以备后续消息菜单功能使用 useEffect(() => { if (!currentUser || !contact) return; @@ -52,88 +71,36 @@ function ChatWindow({ currentUser, contact }) { // 添加消息处理器 chatService.addMessageHandler((message) => { - // 确保消息属于当前聊天 const isMessageForCurrentChat = (!isGroupChat && (message.senderId === contact.id || message.receiverId === contact.id)) || (isGroupChat && message.groupId === contact.id); if (isMessageForCurrentChat) { setMessages(prev => [...prev, message]); - - // 自动标记接收到的消息为已读 if (message.senderId !== currentUser.id) { chatService.markAsRead(message.id); } } }); - // 添加已读状态处理器 - chatService.addReadHandler((messageId, receiverId) => { - setMessages(prev => prev.map(msg => msg.id === messageId ? { - ...msg, - isRead: true, - receiverId, - syncStatus: 'synced' - } : msg - )); - }); - - // 添加同步状态处理器 - chatService.addSyncHandler((messageId, status) => { - setMessages(prev => prev.map(msg => msg.id === messageId ? { ...msg, syncStatus: status } : msg - )); - }); - - // 添加输入状态处理器 - chatService.addTypingHandler((userId, typingStatus) => { - if (!isGroupChat && userId === contact.id) { - setIsTyping(typingStatus); - } - }); - - // 添加撤回消息处理器 - chatService.addRecallHandler((messageId) => { - setMessages(prev => prev.map(msg => { - if (msg.id === messageId) { - const recalledMessage = new Message({ - ...msg, - isRecalled: true, - content: '[消息已撤回]', - originalContent: msg.content, - canBeEdited: (new Date().getTime() - msg.timestamp) < 120000 - }); - recalledMessage.recall(); - return recalledMessage; - } - return msg; - })); - }); - - // 添加编辑消息处理器 - chatService.addEditHandler((messageId, newContent) => { - setMessages(prev => prev.map(msg => - msg.id === messageId ? { - ...msg, - content: newContent, - isEdited: true, - timestamp: new Date().getTime() - } : msg - )); - }); - - // 监听消息状态更新 - chatService.addStatusHandler((messageId, status) => { - setMessages(prev => prev.map(msg => - msg.id === messageId ? { ...msg, status } : msg - )); - }); - - // 加载历史消息 - 将函数移到内部以避免依赖问题 + // 加载历史消息 const loadHistoryMessages = async () => { try { - // 这里应该调用API获取历史消息 - // 暂时使用模拟数据 - console.log('加载历史消息:', contact.id); + const mockHistoryMessages = [ + { + id: 'msg1', + senderId: contact.id, + receiverId: currentUser.id, + content: 'Can you send me the files?', + type: 'text', + timestamp: Date.now() - 3600000, + isRead: true, + isDelivered: true, + status: 'delivered', + syncStatus: 'synced' + } + ]; + setMessages(mockHistoryMessages); } catch (err) { setError('加载历史消息失败: ' + err.message); } @@ -147,27 +114,21 @@ function ChatWindow({ currentUser, contact }) { }, [currentUser, contact, isGroupChat]); const handleSendMessage = () => { - if (!newMessage.trim()) { - setError('发送内容不能为空'); - return; - } + if (!newMessage.trim()) return; try { setError(null); let message; if (isGroupChat) { - // 发送群组消息 message = chatService.sendGroupMessage(contact.id, newMessage); } else { - // 发送单聊消息 message = chatService.sendMessage(contact.id, newMessage); } setMessages(prev => [...prev, message]); setNewMessage(''); - // 发送后停止输入状态 if (isGroupChat) { chatService.setGroupTypingStatus(contact.id, false); } else { @@ -189,121 +150,61 @@ function ChatWindow({ currentUser, contact }) { } }; - const handleRecallMessage = async (messageId) => { - try { - setError(null); - await chatService.recallMessage(messageId); - } catch (err) { - setError('撤回消息失败: ' + err.message); - } - }; - - const handleEditSubmit = async (messageId) => { - try { - setError(null); - if (!editContent.trim()) { - setError('编辑内容不能为空'); - return; - } - - await chatService.editMessage(messageId, editContent); - setEditingMessageId(null); - } catch (err) { - setError('编辑消息失败: ' + err.message); - } - }; - return (
- {error &&
{error}
} -
- - -
-
-

{contact.username}

-
- {contact.onlineStatus ? '在线' : '离线'} - {isTyping && 正在输入...} +
+
+ {contact.username?.charAt(0)} +
+
+

{contact.username}

+ {contact.onlineStatus ? 'Online' : 'Offline'} +
+
+
+ + +
- {messages.length === 0 ? ( -
- 暂无消息,开始聊天吧! -
- ) : ( - messages.map((message, index) => ( -
{ - e.preventDefault(); - if (message.senderId === currentUser.id) { - showMessageMenu(e, message); - } - }} - > - {editingMessageId === message.id ? ( - <> - setEditContent(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleEditSubmit(message.id)} /> - - - - ) : ( -

- {message.isRecalled ? ( - <> - [消息已撤回] - {message.canBeEdited && ( - - )} - - ) : message.content} -

- )} + {messages.map((message, index) => ( +
+
+

{message.content}

- {new Date(message.timestamp).toLocaleTimeString()} - {message.isEdited && ' (已编辑)'} - {message.isRead && ' ✓✓'} - {message.isDelivered && !message.isRead && ' ✓'} - {message.status === 'sending' && ' ↻'} - {message.status === 'error' && ' ⚠'} + {new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
- )) - )} +
+ ))} +
-
- e.key === 'Enter' && handleSendMessage()} /> - +
+
+ + +
+
+ e.key === 'Enter' && handleSendMessage()} /> +
+
); -}; +} export default ChatWindow; \ No newline at end of file diff --git a/client/src/components/ContactList.css b/client/src/components/ContactList.css index fe95e60..62163af 100644 --- a/client/src/components/ContactList.css +++ b/client/src/components/ContactList.css @@ -1,35 +1,275 @@ .contact-list { - width: 300px; + display: flex; + flex-direction: column; height: 100%; + background-color: #fff; +} + +.contact-list-header { + padding: 24px 20px 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.contact-list-header h2 { + margin: 0; + font-size: 24px; + font-weight: 700; + color: #1a1a1a; +} + +.header-actions { + display: flex; + gap: 8px; + position: relative; +} + +.dropdown-wrapper { + position: relative; +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 180px; + padding: 8px 0; + z-index: 1000; + animation: dropdownFadeIn 0.2s ease; +} + +@keyframes dropdownFadeIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dropdown-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + cursor: pointer; + transition: background-color 0.2s; + font-size: 14px; + color: #1a1a1a; +} + +.dropdown-item:hover { background-color: #f5f5f5; - border-right: 1px solid #ddd; +} + +.dropdown-item.danger { + color: #ff3b30; +} + +.dropdown-item.danger:hover { + background-color: #fff0ef; +} + +.dropdown-divider { + height: 1px; + background-color: #f0f0f0; + margin: 4px 0; +} + +.action-btn { + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: #666; + border-radius: 8px; + transition: background-color 0.2s; display: flex; - flex-direction: column; + align-items: center; + justify-content: center; } -.search-bar { - padding: 10px; - background-color: #fff; - border-bottom: 1px solid #ddd; +.action-btn:hover { + background-color: #f5f5f5; + color: #1a1a1a; } -.search-bar input { - width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 20px; +.search-container { + padding: 0 20px 20px; +} + +.search-box { + position: relative; + display: flex; + align-items: center; + background-color: #f5f5f7; + border-radius: 12px; + padding: 0 12px; + transition: background-color 0.2s; +} + +.search-box:focus-within { + background-color: #eeeeef; +} + +.search-icon { + color: #8e8e93; + font-size: 16px; +} + +.search-box input { + flex: 1; + background: none; + border: none; + padding: 10px 8px; + font-size: 15px; outline: none; - font-size: 14px; - transition: all 0.3s ease; + color: #1a1a1a; } -.search-bar input:focus { - border-color: #4a90e2; - box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); +.search-box input::placeholder { + color: #8e8e93; } .contacts-scroll { flex: 1; overflow-y: auto; - padding: 0 10px; + padding: 0 8px; +} + +.contacts-scroll::-webkit-scrollbar { + width: 4px; +} + +.contacts-scroll::-webkit-scrollbar-thumb { + background: #e0e0e0; + border-radius: 4px; +} + +.contact-item { + display: flex; + padding: 12px; + margin: 4px 8px; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + align-items: center; + gap: 12px; +} + +.contact-item:hover { + background-color: #f8f8f8; +} + +.contact-item.active { + background-color: #f0f0f0; +} + +.avatar-wrapper { + position: relative; +} + +.avatar-placeholder { + width: 48px; + height: 48px; + background-color: #e8f0fe; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + color: #4a90e2; + font-size: 18px; +} + +.status-dot { + position: absolute; + bottom: 2px; + right: 2px; + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid #fff; +} + +.status-dot.online { + background-color: #4caf50; +} + +.status-dot.offline { + background-color: #bdbdbd; +} + +.contact-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.contact-top { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.contact-name { + font-weight: 600; + font-size: 16px; + color: #1a1a1a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.contact-time { + font-size: 12px; + color: #8e8e93; +} + +.contact-bottom { + display: flex; + justify-content: space-between; + align-items: center; +} + +.last-message { + font-size: 14px; + color: #666; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + padding-right: 8px; +} + +.unread-badge { + background-color: #0084ff; + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 2px 6px; + border-radius: 10px; + min-width: 18px; + text-align: center; +} + +.loading-indicator, +.empty-contacts { + text-align: center; + padding: 40px 20px; + color: #8e8e93; +} + +.demo-mode-tip { + font-size: 12px; + color: #ff9500; + margin-top: 8px; } \ No newline at end of file diff --git a/client/src/components/ContactList.js b/client/src/components/ContactList.js index 31034a3..79904ac 100644 --- a/client/src/components/ContactList.js +++ b/client/src/components/ContactList.js @@ -1,4 +1,12 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import { + PlusSquareOutlined, + MoreOutlined, + SearchOutlined, + SettingOutlined, + UserOutlined, + LogoutOutlined +} from '@ant-design/icons'; import './ContactList.css'; const ContactList = ({ @@ -8,50 +16,45 @@ const ContactList = ({ onStartEncryptedChat, onCreateGroup, onCreateChannel, + onShowSettings, + onLogout, + isLoading = false, + serverConnected = false, settings = {} }) => { - const handleSettingChange = (contactId, settings) => { - // 处理联系人设置变更的逻辑 - console.log('Contact settings changed:', contactId, settings); - }; + const [activeContact, setActiveContact] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [filteredContacts, setFilteredContacts] = useState([]); + const [showDropdown, setShowDropdown] = useState(false); + const dropdownRef = useRef(null); - const [localContacts, setLocalContacts] = useState([ - { id: 'user2', username: '好友1', onlineStatus: true, isStarred: false, isPinned: false }, - { id: 'user3', username: '好友2', onlineStatus: false, isStarred: true, isPinned: false }, - { id: 'user4', username: '好友3', onlineStatus: true, isStarred: false, isPinned: true }, - ]); - - const toggleStar = (contactId) => { - const starredCount = localContacts.filter(c => c.isStarred).length; - const contact = localContacts.find(c => c.id === contactId); - - if (!contact.isStarred && starredCount >= 15) { - alert('星标用户已达上限(15人)'); - return; + // 打开服务器选择窗口 + const openServerSelectionWindow = () => { + if (window.Electron) { + window.Electron.ipcRenderer.send('open-server-selection-window'); } - - setLocalContacts(localContacts.map(contact => - contact.id === contactId - ? { ...contact, isStarred: !contact.isStarred } - : contact - )); }; - const togglePin = (contactId) => { - setLocalContacts(localContacts.map(contact => - contact.id === contactId - ? { ...contact, isPinned: !contact.isPinned } - : contact - )); - }; - const [activeContact, setActiveContact] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); - const [filteredContacts, setFilteredContacts] = useState([]); + // 点击外部关闭下拉菜单 + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setShowDropdown(false); + } + }; + + if (showDropdown) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showDropdown]); useEffect(() => { const filtered = (contacts || []).filter(contact => - (contact?.username?.toLowerCase() || '').includes(searchTerm.toLowerCase()) || - (contact?.uniqueId?.toLowerCase() || '').includes(searchTerm.toLowerCase()) + (contact?.username?.toLowerCase() || '').includes(searchTerm.toLowerCase()) ); setFilteredContacts(filtered); }, [contacts, searchTerm]); @@ -61,130 +64,101 @@ const ContactList = ({ onSelectContact(contact); }; - const handleEncryptedChat = (e, contact) => { - e.stopPropagation(); - setActiveContact(contact.id); - onStartEncryptedChat(contact); + const handleMenuItemClick = (action) => { + setShowDropdown(false); + action(); }; - + return (
-
-
{currentUser?.username?.charAt(0) || '?'}
-
-

{currentUser?.username || '未登录'}

-

- {currentUser?.onlineStatus ? '在线' : '离线'} -

+
+

Messages

+
+ +
+ + {showDropdown && ( +
+
handleMenuItemClick(onShowSettings)}> + 设置 +
+
handleMenuItemClick(() => alert('个人资料'))}> + 个人资料 +
+
handleMenuItemClick(openServerSelectionWindow)}> + 服务器设置 +
+
+
handleMenuItemClick(onLogout)}> + 退出登录 +
+
+ )} +
-
- - -
- -
-
+
+
+ setSearchTerm(e.target.value)} />
-
-

联系人

-
- {settings.chatListStarred && } - {settings.chatListPinned && 📌} -
-
- {(searchTerm ? filteredContacts : contacts || []).filter(contact => { - if (settings.chatListStarred && !contact.isStarred) return false; - if (settings.chatListPinned && !contact.isPinned) return false; - return true; - }).map(contact => ( -
handleSelectContact(contact)} - > -
{contact?.username?.charAt(0) || '?'}
-
-

{contact?.username || '未知用户'}

-

- {contact?.onlineStatus ? '在线' : `最后在线: ${contact?.lastOnlineTime ? new Date(contact.lastOnlineTime).toLocaleString() : '未知'}`} -

+ +
+ {isLoading ? ( +
+

加载中...

+
+ ) : contacts.length === 0 ? ( +
+

暂无联系人

+ {!serverConnected && ( +

当前为演示模式

+ )} +
+ ) : ( + (searchTerm ? filteredContacts : contacts || []).map(contact => ( +
handleSelectContact(contact)} + > +
+
+ {contact?.username?.charAt(0) || '?'} +
-
- - - - +
+ +
+
+ {contact?.username || 'Unknown'} + {contact?.time || ''} +
+
+ {contact?.lastMessage || ''} + {contact?.unread > 0 && ( + {contact.unread} + )}
- ))} -
+
+ )) + )}
); diff --git a/client/src/components/Login.css b/client/src/components/Login.css index 715e891..be7ac13 100644 --- a/client/src/components/Login.css +++ b/client/src/components/Login.css @@ -1,82 +1,420 @@ +/* 登录页面外层容器 - 全屏背景 */ +.login-wrapper { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #ffffff; + padding: 20px; + position: relative; + overflow: hidden; + transition: background 0.5s ease; +} + +/* 背景动画装饰 */ +.login-wrapper::before { + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(102, 126, 234, 0.03) 0%, transparent 70%); + animation: float 20s ease-in-out infinite; +} + +.login-wrapper::after { + content: ''; + position: absolute; + bottom: -50%; + left: -50%; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(102, 126, 234, 0.02) 0%, transparent 70%); + animation: float 25s ease-in-out infinite reverse; +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0); + } + 50% { + transform: translate(50px, 50px); + } +} + +/* 登录卡片容器 - 毛玻璃效果 */ .login-container { max-width: 480px; - min-width: 450px; - margin: 2rem auto; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - background-color: #fff; + width: 100%; + padding: 2.5rem; + border-radius: 20px; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), + 0 0 0 1px rgba(230, 230, 230, 0.5); + position: relative; + z-index: 1; + animation: slideIn 0.5s ease-out; +} + +/* 语言选择器 */ +.language-selector { + position: absolute; + top: 20px; + right: 20px; + z-index: 10; +} + +.language-select { + min-width: 140px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; +} + +.language-select .ant-select-selector { + background: rgba(255, 255, 255, 0.8) !important; + border: 1px solid rgba(102, 126, 234, 0.2) !important; + border-radius: 10px !important; + padding: 4px 12px !important; + height: auto !important; + transition: all 0.3s ease; +} + +.language-select:hover .ant-select-selector { + background: rgba(255, 255, 255, 1) !important; + border-color: rgba(102, 126, 234, 0.5) !important; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15); +} + +.language-select .ant-select-selection-item { + padding-right: 20px !important; + color: #4a5568; + font-weight: 500; +} + +.language-select .ant-select-arrow { + color: #667eea; +} + +/* 语言下拉菜单样式 */ +.ant-select-dropdown { + border-radius: 12px !important; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important; + overflow: hidden; +} + +.ant-select-item { + padding: 10px 16px !important; + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.ant-select-item-option-selected { + background-color: rgba(102, 126, 234, 0.1) !important; + color: #667eea !important; + font-weight: 600; +} + +.ant-select-item-option-active { + background-color: rgba(102, 126, 234, 0.05) !important; +} + +/* 移动端响应式 */ +@media (max-width: 640px) { + .language-selector { + top: 15px; + right: 15px; + } + + .language-select { + min-width: 120px; + font-size: 0.85rem; + } + + .language-select .ant-select-selector { + padding: 2px 8px !important; + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Logo 和标题区域 */ +.login-header { + text-align: center; + margin-bottom: 2.5rem; +} + +.login-logo { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4); + animation: logoFloat 3s ease-in-out infinite; + overflow: hidden; +} + +.login-logo-img { + width: 70px; + height: 70px; + object-fit: contain; +} + +@keyframes logoFloat { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } } .login-container h2 { text-align: center; - margin-bottom: 1.5rem; - color: #333; + margin-bottom: 0.5rem; + color: #2d3748; + font-size: 1.8rem; + font-weight: 700; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } -.form-group { - margin-bottom: 1.5rem; +.login-subtitle { + text-align: center; + color: #718096; + font-size: 0.95rem; + margin-bottom: 1rem; } -.form-group label { - display: block; +/* 服务器选择区域 */ +.server-section { + margin-bottom: 1.8rem; +} + +.server-section-title { + font-size: 0.9rem; + font-weight: 600; + color: #4a5568; + margin-bottom: 0.8rem; + display: flex; + align-items: center; + gap: 8px; +} + +.server-info-card { + padding: 1rem; + border: 2px solid #e2e8f0; + border-radius: 12px; + margin-bottom: 1rem; + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + transition: all 0.3s ease; +} + +.server-info-card:hover { + border-color: #667eea; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); +} + +.server-info-item { margin-bottom: 0.5rem; - font-weight: 500; + display: flex; + align-items: center; + gap: 8px; } -.form-group input { - width: 100%; - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; +.server-info-item:last-child { + margin-bottom: 0; +} + +.server-info-label { + font-weight: 600; + color: #4a5568; + min-width: 85px; } -.error-message { - color: #e74c3c; +.server-info-value { + font-family: 'Monaco', 'Menlo', monospace; + color: #667eea; + font-weight: 500; + flex: 1; +} + +.server-placeholder { + text-align: center; + padding: 1.2rem; + color: #a0aec0; + border: 2px dashed #e2e8f0; + border-radius: 12px; margin-bottom: 1rem; - padding: 0.5rem; - background-color: #fdecea; - border-radius: 4px; + background: #f7fafc; } -button[type="submit"] { +.server-select-btn { width: 100%; - padding: 0.75rem; - background-color: #4285f4; - color: white; - border: none; - border-radius: 4px; + height: 42px; + border-radius: 10px; + font-weight: 500; + transition: all 0.3s ease; +} + +.server-select-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +/* 表单样式优化 */ +.login-form .ant-form-item-label > label { + font-weight: 600; + color: #4a5568; + font-size: 0.9rem; +} + +.login-form .ant-input, +.login-form .ant-input-password { + padding: 0.75rem 1rem; + border-radius: 10px; + border: 2px solid #e2e8f0; + font-size: 0.95rem; + transition: all 0.3s ease; +} + +.login-form .ant-input:hover, +.login-form .ant-input-password:hover { + border-color: #cbd5e0; +} + +.login-form .ant-input:focus, +.login-form .ant-input-password:focus, +.login-form .ant-input-focused, +.login-form .ant-input-password-focused { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +/* 记住我复选框 */ +.login-form .ant-checkbox-wrapper { + font-size: 0.9rem; + color: #4a5568; +} + +.login-form .ant-checkbox-checked .ant-checkbox-inner { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; +} + +/* 提交按钮 */ +.login-submit-btn { + height: 48px; + border-radius: 12px; font-size: 1rem; - cursor: pointer; - transition: background-color 0.3s; + font-weight: 600; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + transition: all 0.3s ease; } -button[type="submit"]:hover { - background-color: #3367d6; +.login-submit-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); + background: linear-gradient(135deg, #5a67d8 0%, #6b46a0 100%); } -button[type="submit"]:disabled { - background-color: #cccccc; - cursor: not-allowed; +.login-submit-btn:active { + transform: translateY(0); } -.toggle-form { - margin-top: 1rem; +/* 切换表单链接 */ +.toggle-form-link { text-align: center; + margin-top: 1.5rem; + color: #718096; + font-size: 0.95rem; +} + +.toggle-form-link .ant-btn-link { + color: #667eea; + font-weight: 600; + padding: 0 4px; + transition: all 0.3s ease; } -.toggle-form button { - background: none; +.toggle-form-link .ant-btn-link:hover { + color: #764ba2; + transform: scale(1.05); +} + +/* 错误提示优化 */ +.login-form .ant-alert { + border-radius: 10px; + margin-bottom: 1.5rem; border: none; - color: #4285f4; - cursor: pointer; - padding: 0; - font-size: inherit; - text-decoration: underline; } -.toggle-form button:hover { - color: #3367d6; - text-decoration: none; +.login-form .ant-alert-error { + background: linear-gradient(135deg, #fff5f5 0%, #fed7d7 100%); +} + +/* 响应式设计 */ +@media (max-width: 576px) { + .login-container { + padding: 2rem 1.5rem; + border-radius: 16px; + } + + .login-logo { + width: 70px; + height: 70px; + font-size: 2rem; + } + + .login-container h2 { + font-size: 1.5rem; + } +} + +/* 加载状态 */ +.login-form .ant-btn-loading { + opacity: 0.8; +} + +/* 表单动画 */ +.login-form .ant-form-item { + animation: fadeInUp 0.5s ease-out; + animation-fill-mode: both; +} + +.login-form .ant-form-item:nth-child(1) { animation-delay: 0.1s; } +.login-form .ant-form-item:nth-child(2) { animation-delay: 0.2s; } +.login-form .ant-form-item:nth-child(3) { animation-delay: 0.3s; } +.login-form .ant-form-item:nth-child(4) { animation-delay: 0.4s; } +.login-form .ant-form-item:nth-child(5) { animation-delay: 0.5s; } + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/client/src/components/Login.js b/client/src/components/Login.js index 6a0d81b..ed36378 100644 --- a/client/src/components/Login.js +++ b/client/src/components/Login.js @@ -1,13 +1,18 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import CryptoJS from 'crypto-js'; import authService from '../services/authService'; import chatService from '../services/chatService'; import encryptedChatService from '../services/encryptedChatService'; import ServerSelectionWindow from './ServerSelectionWindow'; import './Login.css'; -import { Form, Input, Button, Checkbox, Alert, message } from 'antd'; +import { Form, Input, Button, Checkbox, Alert, message, Select, Popover, Divider } from 'antd'; +import { UserOutlined, LockOutlined, MailOutlined, CloudServerOutlined, CheckCircleOutlined, SettingOutlined } from '@ant-design/icons'; + +const { Option } = Select; const Login = ({ onLoginSuccess }) => { + const { t, i18n } = useTranslation(); const [username, setUsername] = useState(localStorage.getItem('halloChat_username') || ''); const [password, setPassword] = useState(() => { const savedPassword = localStorage.getItem('halloChat_password'); @@ -29,8 +34,27 @@ const Login = ({ onLoginSuccess }) => { const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isRegistering, setIsRegistering] = useState(false); - const [confirmPassword, setConfirmPassword] = useState(''); const [email, setEmail] = useState(''); + + // 管理员模式状态 + const [adminMode, setAdminMode] = useState(false); + + // 背景颜色设置 + const [bgColor, setBgColor] = useState(localStorage.getItem('halloChat_login_bg') || '#ffffff'); + + // 预设颜色 + const presetColors = [ + { color: '#ffffff', label: '白色' }, + { color: '#f0f4ff', label: '浅蓝色' }, + { color: '#fff0f6', label: '浅粉色' }, + { color: '#f6ffed', label: '浅绿色' }, + { color: '#fffbe6', label: '浅黄色' }, + { color: '#f5f5f5', label: '浅灰色' }, + { color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', label: '紫色渐变' }, + { color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', label: '粉色渐变' }, + { color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', label: '蓝色渐变' }, + { color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', label: '绿色渐变' }, + ]; useEffect(() => { // 从localStorage加载上次使用的服务器信息 @@ -55,28 +79,63 @@ const Login = ({ onLoginSuccess }) => { loadLastUsedServer(); }, [selectedServer]); + + // 应用背景颜色 + useEffect(() => { + const wrapper = document.querySelector('.login-wrapper'); + if (wrapper) { + wrapper.style.background = bgColor; + } + }, [bgColor]); + + // 切换背景颜色 + const handleBgColorChange = (color) => { + setBgColor(color); + localStorage.setItem('halloChat_login_bg', color); + message.success(t('login.bgColorChanged')); + }; - const handleLogin = async (e) => { + const handleLogin = async (values) => { setIsLoading(true); setError(''); try { - const usernameRegex = /^[\u4e00-\u9fa5a-zA-Z0-9_]{3,20}$/; - if (!username || !password) { - throw new Error('请输入用户名和密码'); - } - if (!usernameRegex.test(username)) { - throw new Error('用户名只能包含中文、字母、数字、下划线,长度3-20位'); + const { username, password } = values; + + // 管理员模式登录 + if (adminMode) { + if (password === 'hallochat123') { + // 创建一个模拟的管理员用户 + const adminUser = { + id: 'admin_' + Date.now(), + username: username || 'Admin', + email: 'admin@hallochat.local', + token: 'admin_dev_token_' + Date.now(), + isAdmin: true + }; + + // 存储管理员token和用户信息 + localStorage.setItem('halloChat_token', adminUser.token); + localStorage.setItem('halloChat_user', JSON.stringify(adminUser)); + + message.success('🔧 ' + t('login.adminModeSuccess')); + + // 直接进入聊天页面 + onLoginSuccess(adminUser); + return; + } else { + throw new Error(t('login.adminCodeError')); + } } // 检查是否已选择服务器 if (!selectedServer) { - throw new Error('请先选择服务器'); + throw new Error(t('login.pleaseSelectServer')); } const fullAddress = `${selectedServer.address}:${selectedServer.port}`; const serverName = selectedServer.name || selectedServer.address; - + if (rememberMe) { localStorage.setItem('halloChat_username', username); // 使用环境变量或安全存储的密钥 @@ -122,22 +181,16 @@ const Login = ({ onLoginSuccess }) => { } }; - const handleRegister = async () => { + const handleRegister = async (values) => { setIsLoading(true); setError(''); try { - if (!username || !password || !email) { - throw new Error('请填写所有必填字段'); - } - - if (password !== confirmPassword) { - throw new Error('两次输入的密码不一致'); - } + const { username, email, password } = values; // 检查是否已选择服务器 if (!selectedServer) { - throw new Error('请先选择服务器'); + throw new Error(t('login.pleaseSelectServer')); } const fullAddress = `${selectedServer.address}:${selectedServer.port}`; @@ -156,11 +209,10 @@ const Login = ({ onLoginSuccess }) => { onLoginSuccess(user); } catch (err) { - const errorCode = err.response?.data?.code; - const errorMsg = errorCode - ? `错误码 ${errorCode}:${err.response.data.message}` - : (err.message || '注册失败'); - setError(errorMsg); + const errorMsg = err.response?.data?.message + || err.message + || '注册失败'; + setError(errorMsg); } finally { setIsLoading(false); } @@ -170,7 +222,7 @@ const Login = ({ onLoginSuccess }) => { const handleServerSelected = (server) => { setSelectedServer(server); const fullAddress = `${server.address}:${server.port}`; - message.success(`已选择服务器: ${server.name} (${fullAddress})`); + message.success(t('login.selectServer') + `: ${server.name} (${fullAddress})`); }; // 打开服务器选择窗口 @@ -178,134 +230,305 @@ const Login = ({ onLoginSuccess }) => { setShowServerSelection(true); }; + // 处理语言切换 + const handleLanguageChange = (language) => { + i18n.changeLanguage(language); + }; + return ( -
-

欢迎使用HalloChat

- {error && } - - {/* 服务器选择区域 */} -
-

服务器

+
+
+ {/* 设置按钮 */} +
+ + {/* 语言设置 */} +
+
+ 🌐 {t('login.languageSetting')} +
+ +
+ + + + {/* 背景颜色设置 */} +
+
+ 🎨 {t('login.selectBgColor')} +
+
+ {presetColors.map((preset, index) => ( +
handleBgColorChange(preset.color)} + style={{ + height: '60px', + borderRadius: '8px', + background: preset.color, + cursor: 'pointer', + border: bgColor === preset.color ? '3px solid #667eea' : '2px solid #e2e8f0', + display: 'flex', + alignItems: 'flex-end', + justifyContent: 'center', + padding: '8px', + transition: 'all 0.3s ease', + boxShadow: bgColor === preset.color ? '0 4px 12px rgba(102, 126, 234, 0.3)' : 'none' + }} + > + + {preset.label} + +
+ ))} +
+
+
+ } + trigger="click" + placement="bottomRight" + > +
- {/* 当前选择的服务器 */} - {selectedServer ? ( -
-
- 服务器名称: - {selectedServer.name} + {/* Logo 和标题区域 */} +
+
+ HalloChat Logo +
+

{t('login.title')}

+

{t('login.subtitle')}

+
+ + {error && } + + {/* 服务器选择区域 */} +
+
+ + {t('login.serverConfig')} +
+ + {/* 当前选择的服务器 */} + {selectedServer ? ( +
+
+ + {t('login.serverName')}: + {selectedServer.name} +
+
+ + {t('login.serverAddress')}: + {selectedServer.address}:{selectedServer.port} +
-
- 服务器地址: - {selectedServer.address}:{selectedServer.port} + ) : ( +
+ {t('login.noServerSelected')}
-
+ )} + + {/* 选择服务器按钮 */} + +
+ + {/* 登录/注册表单 */} + {isRegistering ? ( +
+ + } + placeholder={t('login.email')} + size="large" + value={email} + onChange={(e) => setEmail(e.target.value)} + /> + + + } + placeholder={t('login.username')} + size="large" + value={username} + onChange={(e) => setUsername(e.target.value)} + /> + + + } + placeholder={t('login.password')} + size="large" + value={password} + onChange={(e) => setPassword(e.target.value)} + /> + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error(t('login.passwordNotMatch'))); + }, + }), + ]} + > + } + placeholder={t('login.confirmPassword')} + size="large" + /> + + + + +
+ {t('login.hasAccount')} +
+
) : ( -
- 未选择服务器 -
+
+ + } + placeholder={t('login.username')} + size="large" + value={username} + onChange={(e) => setUsername(e.target.value)} + /> + + + } + placeholder={t('login.password')} + size="large" + value={password} + onChange={(e) => setPassword(e.target.value)} + /> + + +
+ setRememberMe(e.target.checked)}> + {t('login.rememberMe')} + + setAdminMode(e.target.checked)} + style={{ color: '#667eea' }} + > + 🔧 {t('login.adminMode')} + +
+
+ + + +
+ {t('login.noAccount')} +
+
)} - {/* 选择服务器按钮 */} -
- -
+ {/* 服务器选择窗口 */} + setShowServerSelection(false)} + onServerSelected={handleServerSelected} + />
- - {/* 登录/注册表单 */} - {isRegistering ? ( -
- - setEmail(e.target.value)} /> - - - setUsername(e.target.value)} /> - - - setPassword(e.target.value)} /> - - - setConfirmPassword(e.target.value)} /> - - - - -
- 已有账号? -
-
- ) : ( -
- - setUsername(e.target.value)} /> - - - setPassword(e.target.value)} /> - - - setRememberMe(e.target.checked)}> - 记住我 - - - - - -
- 没有账号? -
-
- )} - - {/* 服务器选择窗口 */} - setShowServerSelection(false)} - onServerSelected={handleServerSelected} - />
); }; diff --git a/client/src/components/MainWindow.css b/client/src/components/MainWindow.css index f5366dd..d87d4d2 100644 --- a/client/src/components/MainWindow.css +++ b/client/src/components/MainWindow.css @@ -3,12 +3,14 @@ height: 100vh; width: 100vw; background-color: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } .sidebar { - width: 300px; + width: 350px; height: 100%; - border-right: 1px solid #ddd; + background-color: #fff; + border-right: 1px solid #f0f0f0; } .content-area { @@ -16,6 +18,7 @@ display: flex; flex-direction: column; height: 100%; + background-color: #fff; } .welcome-view { diff --git a/client/src/components/MainWindow.js b/client/src/components/MainWindow.js index 5f6c303..cc506b8 100644 --- a/client/src/components/MainWindow.js +++ b/client/src/components/MainWindow.js @@ -1,18 +1,27 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import ContactList from './ContactList'; import ChatWindow from './ChatWindow'; import GroupChatWindow from './GroupChatWindow'; import Settings from './Settings'; import Logout from './Logout'; -import Notification from './Notification'; +import authService from '../services/authService'; +import contactService from '../services/contactService'; import './MainWindow.css'; const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { - const [activeView, setActiveView] = useState('contacts'); - const [selectedContact, setSelectedContact] = useState(null); - const [selectedGroup, setSelectedGroup] = useState(null); + const { t } = useTranslation(); + const [activeView] = useState('contacts'); // 保留用于视图切换功能 + // const setActiveView = useState('contacts')[1]; // 暂时未使用,保留以备后续视图切换功能使用 + const [selectedContact] = useState(null); // 保留用于联系人选择功能 + // const setSelectedContact = useState(null)[1]; // 暂时未使用,保留以备后续联系人选择功能使用 + const [selectedGroup] = useState(null); // 保留用于群组选择功能 + // const setSelectedGroup = useState(null)[1]; // 暂时未使用,保留以备后续群组选择功能使用 const [showSettings, setShowSettings] = useState(false); const [showLogout, setShowLogout] = useState(false); + const [contacts, setContacts] = useState([]); + const [isLoadingContacts, setIsLoadingContacts] = useState(false); + const [serverConnected, setServerConnected] = useState(false); const [settings, setSettings] = useState({ sidebarStyle: 'default', chatListStarred: false, @@ -20,6 +29,15 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { theme: 'light' }); + // 模拟数据(用于演示模式)- 使用 useMemo 缓存以避免重复创建 + const mockContacts = useMemo(() => [ + { id: 'user2', username: 'Sarah Wilson', lastMessage: 'See you tomorrow!', time: '2:30 PM', unread: 2, onlineStatus: true, type: 'user' }, + { id: 'user3', username: 'Mike Johnson', lastMessage: 'Thanks for the update', time: '1:15 PM', onlineStatus: true, type: 'user' }, + { id: 'user4', username: 'Emily Chen', lastMessage: "Let's schedule a meeting", time: '12:45 PM', unread: 1, onlineStatus: true, type: 'user' }, + { id: 'user5', username: 'David Brown', lastMessage: 'Great job on the presentation!', time: '11:30 AM', onlineStatus: true, type: 'user' }, + { id: 'user6', username: 'Lisa Anderson', lastMessage: 'Can you send me the files?', time: 'Yesterday', onlineStatus: false, type: 'user' }, + ], []); + // 组件挂载时检查用户是否已登录 useEffect(() => { if (!currentUser) { @@ -30,6 +48,70 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { } }, [currentUser, onLogout]); + // 检测服务器连接状态并加载联系人 + useEffect(() => { + const checkConnectionAndLoadData = async () => { + if (!currentUser) return; + + try { + // 检查服务器连接 + const connectionResult = await authService.checkServerConnection(); + + if (connectionResult.success) { + console.log('[服务器连接] 成功,切换到生产模式'); + setServerConnected(true); + + // 加载真实联系人数据 + setIsLoadingContacts(true); + try { + const friends = await contactService.getFriends(); + const groups = await contactService.getGroups(); + + // 转换为统一格式 + const formattedContacts = [ + ...friends.map(friend => ({ + id: friend._id || friend.id, + username: friend.username || friend.name, + lastMessage: friend.lastMessage || '', + time: friend.lastMessageTime ? new Date(friend.lastMessageTime).toLocaleTimeString() : '', + unread: friend.unreadCount || 0, + onlineStatus: friend.onlineStatus || false, + type: 'user' + })), + ...groups.map(group => ({ + id: group._id || group.id, + username: group.name, + lastMessage: group.lastMessage || '', + time: group.lastMessageTime ? new Date(group.lastMessageTime).toLocaleTimeString() : '', + unread: group.unreadCount || 0, + onlineStatus: true, + type: 'group' + })) + ]; + + setContacts(formattedContacts); + console.log(`[联系人加载] 成功加载 ${formattedContacts.length} 个联系人`); + } catch (error) { + console.error('[联系人加载] 失败,使用模拟数据:', error); + setContacts(mockContacts); + } finally { + setIsLoadingContacts(false); + } + } else { + console.log('[服务器连接] 失败,使用演示模式'); + setServerConnected(false); + setContacts(mockContacts); + } + } catch (error) { + console.error('[模式检测] 错误,使用演示模式:', error); + setServerConnected(false); + setContacts(mockContacts); + } + }; + + checkConnectionAndLoadData(); + }, [currentUser, mockContacts]); // 添加 mockContacts 作为依赖 + const handleLogout = () => { setShowLogout(true); }; @@ -42,68 +124,91 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { setShowLogout(false); }; + // 打开设置窗口 + // const openSettingsWindow = () => { + // if (window.Electron) { + // window.Electron.ipcRenderer.send('open-settings-window'); + // } + // }; // 暂时未使用,保留以备后续设置窗口功能使用 + + // 打开服务器选择窗口 + // const openServerSelectionWindow = () => { + // if (window.Electron) { + // window.Electron.ipcRenderer.send('open-server-selection-window'); + // } + // }; // 暂时未使用,保留以备后续服务器选择窗口功能使用 + + // 打开聊天窗口 + const openChatWindow = (contact) => { + if (window.Electron) { + window.Electron.ipcRenderer.send('open-chat-window', { + chatId: contact.id, + chatType: contact.type, + chatName: contact.username + }); + } + }; + return (
- -
- - { - setSelectedContact(contact); - setSelectedGroup(null); - setActiveView('chat'); - }} - onGroupSelect={(group) => { - setSelectedGroup(group); - setSelectedContact(null); - setActiveView('group-chat'); - }} + onLogout={handleConfirmLogout} + onSettingsChange={setSettings} + onBack={() => setShowSettings(false)} /> -
- -
- {!currentUser && ( -
-

请先登录

-

正在重定向到登录界面...

+ ) : ( + <> +
+ { + // 打开新的聊天窗口 + openChatWindow(contact); + }} + onStartEncryptedChat={() => console.log('开始加密聊天')} + onCreateGroup={() => console.log('创建群组')} + onCreateChannel={() => console.log('创建频道')} + onShowSettings={() => setShowSettings(true)} + onLogout={handleLogout} + />
- )} - - {currentUser && activeView === 'contacts' && ( -
-

欢迎回来,{currentUser.username}

-

请从左侧选择联系人开始聊天

+ +
+ {!currentUser && ( +
+

{t('main.pleaseLogin')}

+

{t('main.redirectingToLogin')}

+
+ )} + + {currentUser && activeView === 'contacts' && ( +
+

{t('main.welcomeBack')},{currentUser.username}

+

{t('main.selectContactToChat')}

+
+ )} + + {currentUser && showLogout && ( + + )} + + {currentUser && activeView === 'chat' && selectedContact && ( + + )} + + {currentUser && activeView === 'group-chat' && selectedGroup && ( + + )}
- )} - - {currentUser && showLogout && ( - - )} - - {currentUser && activeView === 'chat' && selectedContact && ( - - )} - - {currentUser && activeView === 'group-chat' && selectedGroup && ( - - )} - - {currentUser && showSettings && ( - - )} -
+ + )}
); }; diff --git a/client/src/components/ServerSelectionWindow.js b/client/src/components/ServerSelectionWindow.js index f3c47ed..edfb1cd 100644 --- a/client/src/components/ServerSelectionWindow.js +++ b/client/src/components/ServerSelectionWindow.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Button, Form, Input, Modal, Table, Tag, message, Alert } from 'antd'; import { CheckOutlined, @@ -11,7 +11,8 @@ import authService from '../services/authService'; const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { const [servers, setServers] = useState([]); - const [foundServers, setFoundServers] = useState([]); + const [foundServers] = useState([]); // 暂时未使用,以备后续局域网服务器发现功能使用 + // const setFoundServers = useState([])[1]; // 暂时未使用,保留以备后续局域网服务器发现功能使用 const [selectedServer, setSelectedServer] = useState(null); const [isAddingServer, setIsAddingServer] = useState(false); const [isEditingServer, setIsEditingServer] = useState(false); @@ -29,7 +30,8 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { useEffect(() => { if (visible) { loadSavedServers(); - discoverLocalServers(); + // 移除局域网发现,因为它可能导致卡顿 + // discoverLocalServers(); } }, [visible]); @@ -47,15 +49,15 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { }; // 发现局域网服务器 - const discoverLocalServers = () => { - try { - // 这里可以添加mDNS发现逻辑 - // 暂时使用空数组 - setFoundServers([]); - } catch (err) { - console.log('发现局域网服务器失败:', err); - } - }; + // const discoverLocalServers = () => { + // try { + // // 这里可以添加mDNS发现逻辑 + // // 暂时使用空数组 + // setFoundServers([]); + // } catch (err) { + // console.log('发现局域网服务器失败:', err); + // } + // }; // 暂时未使用,保留以备后续局域网服务器发现功能使用 // 测试服务器连接 const testServerConnection = async (address, port) => { @@ -97,8 +99,8 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { setIsEditingServer(true); }; - // 处理删除服务器 - const handleDeleteServer = (server) => { + // 处理删除服务器 - 使用 useCallback 缓存以避免重复创建 + const handleDeleteServer = useCallback((server) => { Modal.confirm({ title: '确认删除', content: `确定要删除服务器 "${server.name}" 吗?`, @@ -117,7 +119,7 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { message.success('服务器删除成功'); }, }); - }; + }, [servers, selectedServer]); // 更新服务器信息 const updateServer = async () => { @@ -212,8 +214,8 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { }; // 处理服务器选择 - // 处理服务器测试 - const handleTestServer = async (server) => { + // 处理服务器测试 - 使用 useCallback 缓存以避免重复创建 + const handleTestServer = useCallback(async (server) => { setServerStatuses(prev => ({ ...prev, [server.id]: { status: 'testing' } @@ -239,14 +241,15 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { } })); } - }; + }, []); - const handleServerSelect = (server) => { + // 处理服务器选择 - 使用 useCallback 优化 + const handleServerSelect = useCallback((server) => { setSelectedServer(server); - }; + }, []); - // 确认选择服务器 - const handleConfirmSelection = () => { + // 确认选择服务器 - 使用 useCallback 优化 + const handleConfirmSelection = useCallback(() => { if (!selectedServer) { message.error('请先选择一个服务器'); return; @@ -257,10 +260,10 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { } onClose(); - }; + }, [selectedServer, onServerSelected, onClose]); - // 服务器表格列定义 - const serverColumns = [ + // 服务器表格列定义 - 使用 useMemo 优化 + const serverColumns = useMemo(() => [ { title: '服务器名称', dataIndex: 'name', @@ -399,13 +402,13 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => {
), }, - ]; + ], [selectedServer, serverStatuses, handleServerSelect, handleDeleteServer, handleTestServer]); - // 合并局域网服务器和已保存服务器 - const combinedServers = [ + // 合并局域网服务器和已保存服务器 - 使用 useMemo 优化 + const combinedServers = useMemo(() => [ ...foundServers.map(server => ({ ...server, isLocal: true, key: `local-${server.address}` })), ...servers.map(server => ({ ...server, isLocal: false, key: `saved-${server.id}` })) - ]; + ], [foundServers, servers]); return ( { ]} width="80%" style={{ maxWidth: '1200px', minWidth: '600px' }} - bodyStyle={{ - maxHeight: '70vh', - overflow: 'auto', - padding: '16px 24px' + styles={{ + body: { + maxHeight: '70vh', + overflow: 'auto', + padding: '16px 24px' + } }} + destroyOnHidden={false} + maskClosable={true} + keyboard={true} > {error && } @@ -449,7 +457,7 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { columns={serverColumns} dataSource={combinedServers} pagination={{ - pageSize: 5, + pageSize: 10, showSizeChanger: true, pageSizeOptions: ['5', '10', '20'], showTotal: (total) => `共 ${total} 个服务器` @@ -465,6 +473,8 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { } })} scroll={{ x: 'max-content', y: 'calc(60vh - 120px)' }} + size="middle" + loading={false} /> ) : (
@@ -525,4 +535,4 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { ); }; -export default ServerSelectionWindow; \ No newline at end of file +export default React.memo(ServerSelectionWindow); \ No newline at end of file diff --git a/client/src/components/Settings.css b/client/src/components/Settings.css index fd493dc..7d4124c 100644 --- a/client/src/components/Settings.css +++ b/client/src/components/Settings.css @@ -1,3 +1,213 @@ +.settings-page { + display: flex; + flex-direction: column; + height: 100vh; + width: 100%; + background-color: #f9f9f9; +} + +.settings-header { + display: flex; + align-items: center; + padding: 16px 24px; + background-color: #fff; + border-bottom: 1px solid #f0f0f0; +} + +.back-btn { + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: #1a1a1a; + border-radius: 8px; + transition: background-color 0.2s; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; +} + +.back-btn:hover { + background-color: #f5f5f5; +} + +.settings-header h2 { + flex: 1; + margin: 0; + font-size: 20px; + font-weight: 600; + text-align: center; + color: #1a1a1a; +} + +.header-spacer { + width: 32px; +} + +.settings-content { + flex: 1; + overflow-y: auto; + padding: 24px; + max-width: 800px; + margin: 0 auto; + width: 100%; +} + +.user-info-card { + background: #fff; + border-radius: 16px; + padding: 32px; + text-align: center; + margin-bottom: 24px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} + +.user-avatar-large { + width: 80px; + height: 80px; + background-color: #e8f0fe; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: #4a90e2; + font-size: 32px; + margin: 0 auto 16px; +} + +.user-info-card h3 { + margin: 0 0 8px; + font-size: 24px; + font-weight: 600; + color: #1a1a1a; +} + +.user-email { + margin: 0; + font-size: 14px; + color: #8e8e93; +} + +.user-uid { + margin-top: 12px; + padding: 8px 16px; + background-color: #f5f5f7; + border-radius: 8px; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.uid-label { + font-size: 12px; + font-weight: 600; + color: #8e8e93; + text-transform: uppercase; +} + +.uid-value { + font-size: 14px; + font-weight: 600; + color: #1a1a1a; + font-family: 'Monaco', 'Courier New', monospace; + letter-spacing: 1px; +} + +.settings-section { + background: #fff; + border-radius: 16px; + padding: 24px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} + +.settings-section h3 { + margin: 0 0 16px; + font-size: 18px; + font-weight: 600; + color: #1a1a1a; +} + +.settings-section select, +.settings-section input[type="range"], +.settings-section input[type="file"] { + width: 100%; + padding: 10px 12px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 15px; + outline: none; + transition: border-color 0.2s; +} + +.settings-section select:focus { + border-color: #4a90e2; +} + +.settings-section label { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0; + cursor: pointer; + font-size: 15px; + color: #1a1a1a; + border-bottom: 1px solid #f5f5f5; +} + +.settings-section label:last-of-type { + border-bottom: none; +} + +.settings-section input[type="checkbox"] { + width: 20px; + height: 20px; + cursor: pointer; +} + +.setting-item { + margin-bottom: 16px; +} + +.setting-item label { + display: block; + margin-bottom: 8px; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.settings-actions { + background: #fff; + border-radius: 16px; + padding: 24px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); +} + +.logout-btn { + width: 100%; + padding: 14px; + background-color: #ff3b30; + color: #fff; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; +} + +.logout-btn:hover { + background-color: #d32f2f; +} + +.logout-btn:active { + transform: scale(0.98); +} + +/* 旧样式兼容 */ .settings-container { padding: 20px; max-width: 600px; diff --git a/client/src/components/Settings.js b/client/src/components/Settings.js index 8447c9b..75a8c2a 100644 --- a/client/src/components/Settings.js +++ b/client/src/components/Settings.js @@ -1,11 +1,14 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ArrowLeftOutlined } from '@ant-design/icons'; import './Settings.css'; /** * 设置组件 - 管理应用程序的各种设置选项 * 在多窗口架构中,设置的更改会通过props传递到主应用 */ -const Settings = ({ currentUser, onLogout, onSettingsChange }) => { +const Settings = ({ currentUser, onLogout, onSettingsChange, onBack }) => { + const { t, i18n } = useTranslation(); const [settings, setSettings] = useState({ sidebarStyle: 'default', chatListStarred: false, @@ -15,6 +18,11 @@ const Settings = ({ currentUser, onLogout, onSettingsChange }) => { soundVolume: 50, customSound: null, customSounds: [], + // 通知设置 + notificationSound: true, + notificationVibration: false, + notificationPreview: true, + notificationDesktop: true, soundSchemes: { starred: 'default', normal: 'default', @@ -68,27 +76,106 @@ const Settings = ({ currentUser, onLogout, onSettingsChange }) => { } }; + /** + * 处理语言变更 + */ + const handleLanguageChange = (language) => { + i18n.changeLanguage(language); + }; + return ( -
-

设置

- {currentUser &&

当前用户: {currentUser.username}

} +
+
+ +

{t('settings.title')}

+
+
+ +
+ {currentUser && ( +
+
+ {currentUser.username?.charAt(0) || '?'} +
+

{currentUser.username}

+

{currentUser.email || 'user@example.com'}

+ {currentUser.uid && ( +
+ UID: + {currentUser.uid} +
+ )} +
+ )} +
+

{t('settings.languageSelection')}

+ +
+
-

消息通知

+

{t('settings.notification')}

+ + + + + + + + +
- +
- + {
{settings.messageSound === 'custom' && (
- + {
-

侧边栏样式

+

{t('settings.sidebarStyle')}

-

聊天列表

+

{t('settings.chatList')}

-

主题

+

{t('settings.theme')}

-

联系人铃声方案

+

{t('settings.soundScheme')}

- +
- +
- + +
); diff --git a/client/src/i18n/config.js b/client/src/i18n/config.js new file mode 100644 index 0000000..1d21225 --- /dev/null +++ b/client/src/i18n/config.js @@ -0,0 +1,71 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// 导入翻译资源 +import zhCN from './locales/zh-CN.json'; +import zhTW from './locales/zh-TW.json'; +import enUS from './locales/en-US.json'; +import ruRU from './locales/ru-RU.json'; + +// 获取浏览器语言 +const getBrowserLanguage = () => { + const browserLang = navigator.language || navigator.languages[0]; + + // 映射浏览器语言到我们支持的语言 + if (browserLang.startsWith('zh')) { + if (browserLang.includes('TW') || browserLang.includes('HK')) { + return 'zh-TW'; + } + return 'zh-CN'; + } + if (browserLang.startsWith('en')) { + return 'en-US'; + } + if (browserLang.startsWith('ru')) { + return 'ru-RU'; + } + + // 默认返回简体中文 + return 'zh-CN'; +}; + +// 从本地存储获取用户设置的语言,如果没有则使用浏览器语言 +const getInitialLanguage = () => { + const savedLanguage = localStorage.getItem('halloChat_language'); + return savedLanguage || getBrowserLanguage(); +}; + +// 配置 i18next +i18n + .use(initReactI18next) // 将 i18next 传递给 react-i18next + .init({ + resources: { + 'zh-CN': { + translation: zhCN + }, + 'zh-TW': { + translation: zhTW + }, + 'en-US': { + translation: enUS + }, + 'ru-RU': { + translation: ruRU + } + }, + lng: getInitialLanguage(), // 默认语言 + fallbackLng: 'zh-CN', // 后备语言 + interpolation: { + escapeValue: false // React 已经默认转义了 + }, + react: { + useSuspense: false // 禁用 Suspense,避免加载问题 + } + }); + +// 监听语言变化,保存到本地存储 +i18n.on('languageChanged', (lng) => { + localStorage.setItem('halloChat_language', lng); +}); + +export default i18n; diff --git a/client/src/i18n/locales/en-US.json b/client/src/i18n/locales/en-US.json new file mode 100644 index 0000000..15e5139 --- /dev/null +++ b/client/src/i18n/locales/en-US.json @@ -0,0 +1,101 @@ +{ + "login": { + "title": "Welcome to HalloChat", + "subtitle": "Secure, Fast, and Reliable Instant Messaging Platform", + "username": "Username", + "password": "Password", + "email": "Email", + "confirmPassword": "Confirm Password", + "rememberMe": "Remember Me", + "loginButton": "Login", + "registerButton": "Register", + "hasAccount": "Already have an account?", + "noAccount": "Don't have an account?", + "goLogin": "Login Now", + "goRegister": "Register Now", + "selectServer": "Select Server", + "changeServer": "Change Server", + "serverConfig": "Server Configuration", + "serverName": "Server Name", + "serverAddress": "Server Address", + "noServerSelected": "No server selected, please select a server first", + "pleaseSelectServer": "Please select a server first", + "pleaseEnterUsername": "Please enter username", + "pleaseEnterPassword": "Please enter password", + "pleaseEnterEmail": "Please enter a valid email address", + "pleaseConfirmPassword": "Please confirm password", + "passwordNotMatch": "Passwords do not match", + "usernameFormat": "Username can only contain Chinese characters, letters, numbers, and underscores, 3-20 characters long", + "passwordFormat": "Password must contain at least three of: uppercase letters, lowercase letters, numbers, special characters, 6-20 characters long", + "errorTitle": "Error", + "adminMode": "Admin Mode", + "adminModeSuccess": "Admin mode login successful", + "adminCodeError": "Admin code is incorrect", + "selectBgColor": "Select Background Color", + "bgColorChanged": "Background color changed", + "languageSetting": "Language Setting" + }, + "settings": { + "title": "Settings", + "currentUser": "Current User", + "language": "Language", + "languageSelection": "Language Selection", + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "sidebarStyle": "Sidebar Style", + "default": "Default", + "compact": "Compact", + "qq9Style": "QQ9 Style", + "chatList": "Chat List", + "showStarredContacts": "Show Starred Contacts", + "showPinnedChats": "Show Pinned Chats", + "notification": "Message Notification", + "messageSound": "Message Sound", + "soundVolume": "Volume", + "customSound": "Custom Ringtone", + "selectLocalSound": "Select Local Sound (.wav/.mp3)", + "soundScheme": "Contact Sound Scheme", + "starredContactSound": "Starred Contact Sound", + "normalContactSound": "Normal Contact Sound", + "soundDefault": "Default", + "soundDing": "Ding", + "soundBell": "Bell", + "soundChime": "Chime", + "soundCustom": "Custom", + "logout": "Logout" + }, + "main": { + "contacts": "Contacts", + "chats": "Chats", + "settings": "Settings", + "search": "Search", + "addFriend": "Add Friend", + "createGroup": "Create Group", + "welcomeBack": "Welcome Back", + "pleaseLogin": "Please Login First", + "redirectingToLogin": "Redirecting to login page...", + "selectContactToChat": "Please select a contact from the left to start chatting" + }, + "chat": { + "typeMessage": "Type a message...", + "send": "Send", + "sendFile": "Send File", + "sendImage": "Send Image", + "voiceCall": "Voice Call", + "videoCall": "Video Call", + "moreOptions": "More Options" + }, + "common": { + "confirm": "Confirm", + "cancel": "Cancel", + "save": "Save", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "loading": "Loading...", + "success": "Success", + "error": "Error", + "warning": "Warning" + } +} diff --git a/client/src/i18n/locales/ru-RU.json b/client/src/i18n/locales/ru-RU.json new file mode 100644 index 0000000..0930fcc --- /dev/null +++ b/client/src/i18n/locales/ru-RU.json @@ -0,0 +1,101 @@ +{ + "login": { + "title": "Добро пожаловать в HalloChat", + "subtitle": "Безопасная, быстрая и надежная платформа для обмена сообщениями", + "username": "Имя пользователя", + "password": "Пароль", + "email": "Электронная почта", + "confirmPassword": "Подтвердите пароль", + "rememberMe": "Запомнить меня", + "loginButton": "Войти", + "registerButton": "Зарегистрироваться", + "hasAccount": "Уже есть аккаунт?", + "noAccount": "Нет аккаунта?", + "goLogin": "Войти сейчас", + "goRegister": "Зарегистрироваться сейчас", + "selectServer": "Выбрать сервер", + "changeServer": "Сменить сервер", + "serverConfig": "Конфигурация сервера", + "serverName": "Имя сервера", + "serverAddress": "Адрес сервера", + "noServerSelected": "Сервер не выбран, пожалуйста, выберите сервер", + "pleaseSelectServer": "Пожалуйста, сначала выберите сервер", + "pleaseEnterUsername": "Пожалуйста, введите имя пользователя", + "pleaseEnterPassword": "Пожалуйста, введите пароль", + "pleaseEnterEmail": "Пожалуйста, введите действительный адрес электронной почты", + "pleaseConfirmPassword": "Пожалуйста, подтвердите пароль", + "passwordNotMatch": "Пароли не совпадают", + "usernameFormat": "Имя пользователя может содержать только китайские символы, буквы, цифры и подчеркивания, длиной 3-20 символов", + "passwordFormat": "Пароль должен содержать как минимум три типа символов: заглавные буквы, строчные буквы, цифры, специальные символы, длиной 6-20 символов", + "errorTitle": "Ошибка", + "adminMode": "Режим администратора", + "adminModeSuccess": "Вход в режиме администратора выполнен успешно", + "adminCodeError": "Неверный код администратора", + "selectBgColor": "Выберите цвет фона", + "bgColorChanged": "Цвет фона изменен", + "languageSetting": "Настройка языка" + }, + "settings": { + "title": "Настройки", + "currentUser": "Текущий пользователь", + "language": "Язык", + "languageSelection": "Выбор языка", + "theme": "Тема", + "light": "Светлая", + "dark": "Тёмная", + "sidebarStyle": "Стиль боковой панели", + "default": "По умолчанию", + "compact": "Компактный", + "qq9Style": "Стиль QQ9", + "chatList": "Список чатов", + "showStarredContacts": "Показать избранные контакты", + "showPinnedChats": "Показать закрепленные чаты", + "notification": "Уведомление о сообщении", + "messageSound": "Звук сообщения", + "soundVolume": "Громкость", + "customSound": "Пользовательский рингтон", + "selectLocalSound": "Выбрать локальный звук (.wav/.mp3)", + "soundScheme": "Звуковая схема контактов", + "starredContactSound": "Звук избранного контакта", + "normalContactSound": "Звук обычного контакта", + "soundDefault": "По умолчанию", + "soundDing": "Динь", + "soundBell": "Колокольчик", + "soundChime": "Колокол", + "soundCustom": "Пользовательский", + "logout": "Выйти" + }, + "main": { + "contacts": "Контакты", + "chats": "Чаты", + "settings": "Настройки", + "search": "Поиск", + "addFriend": "Добавить друга", + "createGroup": "Создать группу", + "welcomeBack": "Добро пожаловать обратно", + "pleaseLogin": "Пожалуйста, войдите в систему", + "redirectingToLogin": "Перенаправление на страницу входа...", + "selectContactToChat": "Пожалуйста, выберите контакт слева, чтобы начать общение" + }, + "chat": { + "typeMessage": "Введите сообщение...", + "send": "Отправить", + "sendFile": "Отправить файл", + "sendImage": "Отправить изображение", + "voiceCall": "Голосовой вызов", + "videoCall": "Видеозвонок", + "moreOptions": "Дополнительные опции" + }, + "common": { + "confirm": "Подтвердить", + "cancel": "Отмена", + "save": "Сохранить", + "delete": "Удалить", + "edit": "Редактировать", + "close": "Закрыть", + "loading": "Загрузка...", + "success": "Успешно", + "error": "Ошибка", + "warning": "Предупреждение" + } +} diff --git a/client/src/i18n/locales/zh-CN.json b/client/src/i18n/locales/zh-CN.json new file mode 100644 index 0000000..436e1ca --- /dev/null +++ b/client/src/i18n/locales/zh-CN.json @@ -0,0 +1,101 @@ +{ + "login": { + "title": "欢迎使用 HalloChat", + "subtitle": "安全、快速、可靠的即时通讯平台", + "username": "用户名", + "password": "密码", + "email": "邮箱", + "confirmPassword": "确认密码", + "rememberMe": "记住我", + "loginButton": "立即登录", + "registerButton": "立即注册", + "hasAccount": "已有账号?", + "noAccount": "没有账号?", + "goLogin": "立即登录", + "goRegister": "去注册", + "selectServer": "选择服务器", + "changeServer": "更换服务器", + "serverConfig": "服务器配置", + "serverName": "服务器名称", + "serverAddress": "服务器地址", + "noServerSelected": "未选择服务器,请先选择服务器", + "pleaseSelectServer": "请先选择服务器", + "pleaseEnterUsername": "请输入用户名", + "pleaseEnterPassword": "请输入密码", + "pleaseEnterEmail": "请输入有效的邮箱地址", + "pleaseConfirmPassword": "请确认密码", + "passwordNotMatch": "两次输入的密码不一致", + "usernameFormat": "用户名只能包含中文、字母、数字、下划线,长度3-20位", + "passwordFormat": "密码必须包含大写字母、小写字母、数字、特殊符号中的至少三种,长度为6-20个字符", + "errorTitle": "错误提示", + "adminMode": "管理员模式", + "adminModeSuccess": "管理员模式登录成功", + "adminCodeError": "管理员代码错误", + "selectBgColor": "选择背景颜色", + "bgColorChanged": "背景颜色已更改", + "languageSetting": "语言设置" + }, + "settings": { + "title": "设置", + "currentUser": "当前用户", + "language": "语言", + "languageSelection": "语言选择", + "theme": "主题", + "light": "浅色", + "dark": "深色", + "sidebarStyle": "侧边栏样式", + "default": "默认", + "compact": "紧凑", + "qq9Style": "QQ9风格", + "chatList": "聊天列表", + "showStarredContacts": "显示星标联系人", + "showPinnedChats": "显示置顶聊天", + "notification": "消息通知", + "messageSound": "消息提示音", + "soundVolume": "音量", + "customSound": "自定义铃声", + "selectLocalSound": "选择本地铃声(.wav/.mp3)", + "soundScheme": "联系人铃声方案", + "starredContactSound": "星标联系人铃声", + "normalContactSound": "普通联系人铃声", + "soundDefault": "默认", + "soundDing": "叮咚", + "soundBell": "铃声", + "soundChime": "钟声", + "soundCustom": "自定义铃声", + "logout": "登出" + }, + "main": { + "contacts": "联系人", + "chats": "聊天", + "settings": "设置", + "search": "搜索", + "addFriend": "添加好友", + "createGroup": "创建群组", + "welcomeBack": "欢迎回来", + "pleaseLogin": "请先登录", + "redirectingToLogin": "正在重定向到登录界面...", + "selectContactToChat": "请从左侧选择联系人开始聊天" + }, + "chat": { + "typeMessage": "输入消息...", + "send": "发送", + "sendFile": "发送文件", + "sendImage": "发送图片", + "voiceCall": "语音通话", + "videoCall": "视频通话", + "moreOptions": "更多选项" + }, + "common": { + "confirm": "确认", + "cancel": "取消", + "save": "保存", + "delete": "删除", + "edit": "编辑", + "close": "关闭", + "loading": "加载中...", + "success": "成功", + "error": "错误", + "warning": "警告" + } +} diff --git a/client/src/i18n/locales/zh-TW.json b/client/src/i18n/locales/zh-TW.json new file mode 100644 index 0000000..72a6719 --- /dev/null +++ b/client/src/i18n/locales/zh-TW.json @@ -0,0 +1,101 @@ +{ + "login": { + "title": "歡迎使用 HalloChat", + "subtitle": "安全、快速、可靠的即時通訊平台", + "username": "用戶名", + "password": "密碼", + "email": "郵箱", + "confirmPassword": "確認密碼", + "rememberMe": "記住我", + "loginButton": "立即登錄", + "registerButton": "立即註冊", + "hasAccount": "已有賬號?", + "noAccount": "沒有賬號?", + "goLogin": "立即登錄", + "goRegister": "去註冊", + "selectServer": "選擇服務器", + "changeServer": "更換服務器", + "serverConfig": "服務器配置", + "serverName": "服務器名稱", + "serverAddress": "服務器地址", + "noServerSelected": "未選擇服務器,請先選擇服務器", + "pleaseSelectServer": "請先選擇服務器", + "pleaseEnterUsername": "請輸入用戶名", + "pleaseEnterPassword": "請輸入密碼", + "pleaseEnterEmail": "請輸入有效的郵箱地址", + "pleaseConfirmPassword": "請確認密碼", + "passwordNotMatch": "兩次輸入的密碼不一致", + "usernameFormat": "用戶名只能包含中文、字母、數字、下劃線,長度3-20位", + "passwordFormat": "密碼必須包含大寫字母、小寫字母、數字、特殊符號中的至少三種,長度為6-20個字符", + "errorTitle": "錯誤提示", + "adminMode": "管理員模式", + "adminModeSuccess": "管理員模式登錄成功", + "adminCodeError": "管理員代碼錯誤", + "selectBgColor": "選擇背景顏色", + "bgColorChanged": "背景顏色已更改", + "languageSetting": "語言設置" + }, + "settings": { + "title": "設置", + "currentUser": "當前用戶", + "language": "語言", + "languageSelection": "語言選擇", + "theme": "主題", + "light": "淺色", + "dark": "深色", + "sidebarStyle": "側邊欄樣式", + "default": "默認", + "compact": "緊湊", + "qq9Style": "QQ9風格", + "chatList": "聊天列表", + "showStarredContacts": "顯示星標聯繫人", + "showPinnedChats": "顯示置頂聊天", + "notification": "消息通知", + "messageSound": "消息提示音", + "soundVolume": "音量", + "customSound": "自定義鈴聲", + "selectLocalSound": "選擇本地鈴聲(.wav/.mp3)", + "soundScheme": "聯繫人鈴聲方案", + "starredContactSound": "星標聯繫人鈴聲", + "normalContactSound": "普通聯繫人鈴聲", + "soundDefault": "默認", + "soundDing": "叮咚", + "soundBell": "鈴聲", + "soundChime": "鐘聲", + "soundCustom": "自定義鈴聲", + "logout": "登出" + }, + "main": { + "contacts": "聯繫人", + "chats": "聊天", + "settings": "設置", + "search": "搜索", + "addFriend": "添加好友", + "createGroup": "創建群組", + "welcomeBack": "歡迎回來", + "pleaseLogin": "請先登錄", + "redirectingToLogin": "正在重定向到登錄界面...", + "selectContactToChat": "請從左側選擇聯繫人開始聊天" + }, + "chat": { + "typeMessage": "輸入消息...", + "send": "發送", + "sendFile": "發送文件", + "sendImage": "發送圖片", + "voiceCall": "語音通話", + "videoCall": "視頻通話", + "moreOptions": "更多選項" + }, + "common": { + "confirm": "確認", + "cancel": "取消", + "save": "保存", + "delete": "刪除", + "edit": "編輯", + "close": "關閉", + "loading": "加載中...", + "success": "成功", + "error": "錯誤", + "warning": "警告" + } +} diff --git a/client/src/index.js b/client/src/index.js index e3c8900..28fc95c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import { AuthProvider } from './contexts/AuthContext'; import './index.css'; +import './i18n/config'; // 导入 i18n 配置 // 客户端应用入口文件 console.log('HalloChat 客户端启动中...'); diff --git a/client/src/services/chatService.js b/client/src/services/chatService.js index 06f5a2f..86264ed 100644 --- a/client/src/services/chatService.js +++ b/client/src/services/chatService.js @@ -16,6 +16,86 @@ class ChatService { this.syncHandlers = []; // 用于同步状态 } + // 保存消息到本地存储 + saveMessageToLocal(message) { + try { + const key = `messages_${this.currentUser.id}_${message.type === 'group' ? message.groupId : message.receiverId || message.senderId}`; + const messages = this.getMessagesFromLocal(key); + const existingMessageIndex = messages.findIndex(m => m.id === message.id); + + if (existingMessageIndex >= 0) { + // 更新现有消息 + messages[existingMessageIndex] = message; + } else { + // 添加新消息 + messages.push(message); + } + + localStorage.setItem(key, JSON.stringify(messages)); + } catch (error) { + console.error('保存消息到本地存储失败:', error); + } + } + + // 从本地存储获取消息 + getMessagesFromLocal(key) { + try { + const messages = localStorage.getItem(key); + return messages ? JSON.parse(messages) : []; + } catch (error) { + console.error('从本地存储获取消息失败:', error); + return []; + } + } + + // 获取与特定联系人/群组的聊天记录 + getChatHistory(contactId, isGroup = false) { + const key = `messages_${this.currentUser.id}_${isGroup ? contactId : contactId}`; + return this.getMessagesFromLocal(key); + } + + // 同步未发送成功的消息 + syncPendingMessages() { + try { + // 遍历所有本地存储的消息,查找状态为pending的消息 + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(`messages_${this.currentUser.id}_`)) { + const messages = this.getMessagesFromLocal(key); + const pendingMessages = messages.filter(m => m.syncStatus === 'pending' || m.status === 'sending'); + + pendingMessages.forEach(message => { + console.log(`重新发送消息: ${message.id}`); + // 更新消息状态为sending + message.status = 'sending'; + this.saveMessageToLocal(message); + + // 重新发送消息到服务器 + if (message.type === 'group' || message.groupId) { + this.socket.emit('message', { + content: message.content, + type: message.type, + receiverId: message.receiverId, + groupId: message.groupId, + channelId: message.channelId + }); + } else { + this.socket.emit('message', { + content: message.content, + type: message.type, + receiverId: message.receiverId, + groupId: message.groupId, + channelId: message.channelId + }); + } + }); + } + } + } catch (error) { + console.error('同步未发送消息失败:', error); + } + } + // 设置服务器地址 setServerAddress(address) { this.serverAddress = address; @@ -25,12 +105,20 @@ class ChatService { connect(user) { this.currentUser = user; this.socket = io(this.serverAddress, { - query: { userId: user.id } + query: { userId: user.id }, + reconnection: true, // 启用自动重连 + reconnectionAttempts: Infinity, // 无限次重连尝试 + reconnectionDelay: 1000, // 重连延迟(毫秒) + reconnectionDelayMax: 5000, // 最大重连延迟(毫秒) + timeout: 20000, // 连接超时时间(毫秒) + transports: ['websocket'] // 使用websocket传输 }); // 监听消息 this.socket.on('message', (messageData) => { const message = new Message(messageData); + // 保存接收的消息到本地存储 + this.saveMessageToLocal(message); this.messageHandlers.forEach(handler => handler(message)); }); @@ -64,15 +152,49 @@ class ChatService { this.syncHandlers.forEach(handler => handler(messageId, status)); }); + // 监听连接成功 + this.socket.on('connect', () => { + console.log('WebSocket连接成功'); + // 重新同步未发送成功的消息 + this.syncPendingMessages(); + }); + + // 监听连接断开 + this.socket.on('disconnect', (reason) => { + console.log('WebSocket连接断开:', reason); + }); + + // 监听重连尝试 + this.socket.on('reconnect_attempt', (attemptNumber) => { + console.log(`WebSocket重连尝试 #${attemptNumber}`); + }); + + // 监听重连成功 + this.socket.on('reconnect', (attemptNumber) => { + console.log(`WebSocket重连成功,尝试次数: ${attemptNumber}`); + // 重新同步未发送成功的消息 + this.syncPendingMessages(); + }); + + // 监听重连失败 + this.socket.on('reconnect_failed', () => { + console.error('WebSocket重连失败'); + }); + + // 监听连接超时 + this.socket.on('connect_timeout', (timeout) => { + console.error(`WebSocket连接超时: ${timeout}ms`); + }); + // 监听错误 this.socket.on('error', (error) => { - console.error('WebSocket error:', error); + console.error('WebSocket错误:', error); }); } // 发送消息 sendMessage(receiverId, content, type = 'text', groupId = null, channelId = null) { - const message = new Message({ + const message = new Message({ id: `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // 临时ID senderId: this.currentUser.id, receiverId, @@ -83,6 +205,9 @@ class ChatService { syncStatus: 'pending' }); + // 保存消息到本地存储 + this.saveMessageToLocal(message); + // 发送消息到服务器 this.socket.emit('message', { content, diff --git a/client/src/services/contactService.js b/client/src/services/contactService.js new file mode 100644 index 0000000..1168f52 --- /dev/null +++ b/client/src/services/contactService.js @@ -0,0 +1,93 @@ +import axios from 'axios'; +import authService from './authService'; + +class ContactService { + constructor() { + this.apiUrl = authService.apiUrl; + } + + getToken() { + return localStorage.getItem('halloChat_token'); + } + + getAuthHeaders() { + const token = this.getToken(); + return { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + } + + // 获取好友列表 + async getFriends() { + try { + const response = await axios.get(`${this.apiUrl}/friends`, { + headers: this.getAuthHeaders() + }); + return response.data?.data || []; + } catch (error) { + console.error('获取好友列表失败:', error); + throw error; + } + } + + // 添加好友 + async addFriend(friendUsername) { + try { + const response = await axios.post( + `${this.apiUrl}/friends/add`, + { friendUsername }, + { headers: this.getAuthHeaders() } + ); + return response.data?.data; + } catch (error) { + console.error('添加好友失败:', error); + throw error; + } + } + + // 删除好友 + async removeFriend(friendId) { + try { + const response = await axios.delete( + `${this.apiUrl}/friends/${friendId}`, + { headers: this.getAuthHeaders() } + ); + return response.data; + } catch (error) { + console.error('删除好友失败:', error); + throw error; + } + } + + // 获取群组列表 + async getGroups() { + try { + const response = await axios.get(`${this.apiUrl}/groups`, { + headers: this.getAuthHeaders() + }); + return response.data?.data || []; + } catch (error) { + console.error('获取群组列表失败:', error); + throw error; + } + } + + // 创建群组 + async createGroup(groupData) { + try { + const response = await axios.post( + `${this.apiUrl}/groups`, + groupData, + { headers: this.getAuthHeaders() } + ); + return response.data?.data; + } catch (error) { + console.error('创建群组失败:', error); + throw error; + } + } +} + +const contactService = new ContactService(); +export default contactService; diff --git a/client/src/services/messageService.js b/client/src/services/messageService.js new file mode 100644 index 0000000..88f23d1 --- /dev/null +++ b/client/src/services/messageService.js @@ -0,0 +1,102 @@ +import axios from 'axios'; +import authService from './authService'; + +class MessageService { + constructor() { + this.apiUrl = authService.apiUrl; + } + + getToken() { + return localStorage.getItem('halloChat_token'); + } + + getAuthHeaders() { + const token = this.getToken(); + return { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + } + + // 获取与某个用户的聊天历史 + async getChatHistory(contactId, limit = 50, offset = 0) { + try { + const response = await axios.get( + `${this.apiUrl}/messages/history/${contactId}`, + { + headers: this.getAuthHeaders(), + params: { limit, offset } + } + ); + return response.data?.data || []; + } catch (error) { + console.error('获取聊天历史失败:', error); + throw error; + } + } + + // 发送消息 + async sendMessage(receiverId, content, type = 'text') { + try { + const response = await axios.post( + `${this.apiUrl}/messages/send`, + { receiverId, content, type }, + { headers: this.getAuthHeaders() } + ); + return response.data?.data; + } catch (error) { + console.error('发送消息失败:', error); + throw error; + } + } + + // 标记消息为已读 + async markAsRead(messageId) { + try { + const response = await axios.put( + `${this.apiUrl}/messages/${messageId}/read`, + {}, + { headers: this.getAuthHeaders() } + ); + return response.data; + } catch (error) { + console.error('标记消息已读失败:', error); + throw error; + } + } + + // 获取群组聊天历史 + async getGroupChatHistory(groupId, limit = 50, offset = 0) { + try { + const response = await axios.get( + `${this.apiUrl}/messages/group/${groupId}`, + { + headers: this.getAuthHeaders(), + params: { limit, offset } + } + ); + return response.data?.data || []; + } catch (error) { + console.error('获取群组聊天历史失败:', error); + throw error; + } + } + + // 发送群组消息 + async sendGroupMessage(groupId, content, type = 'text') { + try { + const response = await axios.post( + `${this.apiUrl}/messages/group/${groupId}`, + { content, type }, + { headers: this.getAuthHeaders() } + ); + return response.data?.data; + } catch (error) { + console.error('发送群组消息失败:', error); + throw error; + } + } +} + +const messageService = new MessageService(); +export default messageService; diff --git a/docs/LOGIN_UI_UPGRADE.md b/docs/LOGIN_UI_UPGRADE.md new file mode 100644 index 0000000..3140583 --- /dev/null +++ b/docs/LOGIN_UI_UPGRADE.md @@ -0,0 +1,103 @@ +# 登录页面 UI 优化说明 + +## 🎨 设计升级概览 + +本次优化将 HalloChat 登录页面从传统设计升级为现代化、高端的用户界面,提升用户体验和视觉吸引力。 + +## ✨ 主要改进 + +### 1. **视觉设计** +- ✅ **渐变背景**: 采用紫色系渐变(#667eea → #764ba2),营造专业科技感 +- ✅ **毛玻璃效果**: 登录卡片使用半透明背景和模糊效果(backdrop-filter) +- ✅ **动态装饰**: 添加浮动的背景装饰元素,增加页面动感 +- ✅ **现代阴影**: 使用多层阴影营造立体感和深度 + +### 2. **品牌展示** +- ✅ **Logo 区域**: 添加渐变色 Logo 图标,带有浮动动画 +- ✅ **标题优化**: 使用渐变文字效果,更具视觉冲击力 +- ✅ **副标题**: 添加产品 Slogan,强化品牌认知 + +### 3. **交互体验** +- ✅ **输入框图标**: 为每个输入框添加语义化图标 +- ✅ **大尺寸输入**: 增大输入框尺寸,提升移动端体验 +- ✅ **圆角设计**: 统一使用圆角设计,更加柔和友好 +- ✅ **悬停效果**: 所有交互元素都有平滑的悬停动画 +- ✅ **渐进动画**: 表单元素依次淡入,提升视觉流畅度 + +### 4. **服务器选择优化** +- ✅ **卡片式展示**: 服务器信息以精美卡片形式展示 +- ✅ **图标标识**: 使用云服务器和确认图标,增强可识别性 +- ✅ **悬停动效**: 卡片悬停时上浮并显示阴影 +- ✅ **渐变背景**: 服务器卡片采用淡蓝色渐变 + +### 5. **按钮设计** +- ✅ **渐变按钮**: 主按钮使用紫色渐变,更加醒目 +- ✅ **立体阴影**: 按钮带有颜色匹配的阴影效果 +- ✅ **按压反馈**: 点击时有下沉动画,提供触感反馈 +- ✅ **加载状态**: 优化加载动画的视觉效果 + +### 6. **响应式设计** +- ✅ **移动端适配**: 针对小屏幕优化布局和尺寸 +- ✅ **弹性布局**: 使用 Flexbox 确保各种屏幕都能完美展示 + + +## 🎯 设计亮点 +### 配色方案 +```css +主色调: #667eea (紫蓝) +辅助色: #764ba2 (紫色) +强调色: #4a5568 (深灰) +背景色: #f7fafc (浅灰) +文字色: #2d3748 (炭黑) +``` + +### 动画效果 +1. **入场动画**: 卡片从下方滑入并淡入 +2. **Logo 浮动**: Logo 持续上下浮动 +3. **背景装饰**: 大型圆形光晕缓慢移动 +4. **表单渐入**: 表单项依次淡入,有延迟效果 +5. **按钮反馈**: 悬停上浮,点击下沉 + +### 用户体验优化 +- **清晰的视觉层次**: 通过颜色、大小、阴影建立信息层级 +- **即时反馈**: 所有交互都有视觉和动画反馈 +- **引导性设计**: 使用图标和颜色引导用户操作 +- **无障碍考虑**: 保持足够的对比度和可读性 + +## 📱 兼容性 + +- ✅ 现代浏览器(Chrome, Firefox, Safari, Edge) +- ✅ 移动端浏览器 +- ✅ Electron 桌面应用 +- ⚠️ backdrop-filter 在旧版浏览器可能不支持(会降级为纯色背景) + +## 🚀 使用的技术 + +- **CSS3 动画**: 使用 @keyframes 创建流畅动画 +- **渐变**: linear-gradient 和 radial-gradient +- **模糊效果**: backdrop-filter: blur() +- **Flexbox**: 响应式布局 +- **Ant Design 图标**: 现代化图标库 + +## 📸 视觉效果 + +新的登录页面具有以下特征: +- 🌈 紫色渐变背景,专业科技感 +- 💎 半透明毛玻璃卡片,时尚现代 +- ✨ 流畅的动画效果,提升体验 +- 🎨 统一的设计语言,品牌感强 +- 📱 完美的响应式适配 + +## 🔄 后续优化建议 + +1. 可以根据品牌需求调整配色方案 +2. 可以添加暗色模式支持 +3. 可以集成更多动画效果(如粒子背景) +4. 可以添加多语言切换功能 +5. 可以增加社交登录按钮(如微信、QQ等) + +--- + +**优化完成时间**: 2026-01-29 +**设计师**: SanYou(LUCA.NEX) +**技术栈**: React + Ant Design + CSS3 diff --git a/server/manager/HalloChat.ico b/server/manager/HalloChat.ico new file mode 100644 index 0000000..62fedc7 Binary files /dev/null and b/server/manager/HalloChat.ico differ diff --git a/server/manager/requirements.txt b/server/manager/requirements.txt index a94b3ae..ec3ed43 100644 --- a/server/manager/requirements.txt +++ b/server/manager/requirements.txt @@ -1,5 +1,6 @@ # HalloChat server management console required Python libraries -# Note: This project only uses Python standard libraries, no additional third-party libraries needed +# Third-party dependencies: +requests>=2.25.0 # Used standard libraries: # - os: Operating system interface diff --git a/server/manager/server_manager.log b/server/manager/server_manager.log new file mode 100644 index 0000000..e765188 --- /dev/null +++ b/server/manager/server_manager.log @@ -0,0 +1,178 @@ +[2025-11-12 00:01:55] HalloChat服务器管理器启动 +[2025-11-12 00:05:39] HalloChat服务器管理器启动 +[2025-11-12 00:05:42] 开始下载MongoDB... +[2025-11-12 00:05:42] 下载MongoDB时出错: No module named 'requests' +[2025-11-12 00:07:06] HalloChat服务器管理器启动 +[2025-11-12 00:07:08] 开始下载MongoDB... +[2025-11-12 00:07:09] 开始下载MongoDB安装程序: https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.0-signed.msi +[2025-11-12 00:08:07] MongoDB安装程序下载完成: C:\Users\haoya\AppData\Local\Temp\tmp9i3ufb3_\mongodb-installer.msi +[2025-11-12 00:08:07] 提示用户以管理员权限运行安装向导 +[2025-11-12 00:12:55] MongoDB下载安装成功 +[2025-11-12 00:13:00] 开始安装依赖... +[2025-11-12 00:13:54] [npm] npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +[2025-11-12 00:13:55] [npm] npm warn deprecated @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead +[2025-11-12 00:13:55] [npm] npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported +[2025-11-12 00:13:55] [npm] npm warn deprecated supertest@6.3.4: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net +[2025-11-12 00:13:55] [npm] npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported +[2025-11-12 00:13:55] [npm] npm warn deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead +[2025-11-12 00:13:55] [npm] npm warn deprecated multer@1.4.5-lts.2: Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version. +[2025-11-12 00:13:56] [npm] npm warn deprecated superagent@8.1.2: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net +[2025-11-12 00:13:57] [npm] npm warn deprecated eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options. +[2025-11-12 00:22:14] HalloChat服务器管理器启动 +[2025-11-12 00:22:22] 开始安装依赖... +[2025-11-12 00:22:25] [npm] up to date, audited 777 packages in 3s +[2025-11-12 00:22:25] [npm] 161 packages are looking for funding +[2025-11-12 00:22:25] [npm] run `npm fund` for details +[2025-11-12 00:22:25] [npm] 2 high severity vulnerabilities +[2025-11-12 00:22:25] [npm] To address all issues (including breaking changes), run: +[2025-11-12 00:22:25] [npm] npm audit fix --force +[2025-11-12 00:22:25] [npm] Run `npm audit` for details. +[2025-11-12 00:22:25] 依赖安装成功 +[2025-11-12 00:22:25] 依赖安装过程完成(无论成功或失败) +[2025-11-12 00:22:29] 检测到依赖未安装,正在尝试安装... +[2025-11-12 00:22:31] 正在启动服务器... +[2025-11-12 00:22:31] 服务器启动命令已发送 +[2025-11-12 00:22:31] +[2025-11-12 00:22:31] > hallo-chat-server@2.1.1-alpha start +[2025-11-12 00:22:31] > node src/index.js +[2025-11-12 00:22:31] +[2025-11-12 00:22:32] 读取日志时出错: 'gbk' codec can't decode byte 0xa8 in position 20: illegal multibyte sequence +[2025-11-12 00:22:39] 正在停止服务器... +[2025-11-12 00:22:40] 服务器已停止 +[2025-11-12 00:22:59] 正在停止MongoDB服务... +[2025-11-12 00:23:00] Windows服务停止MongoDB失败: 发生系统错误 5。 + +拒绝访问。 + + +[2025-11-12 00:23:00] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5: + +拒绝访问。 + + +[2025-11-12 00:23:00] 已终止所有MongoDB进程 +[2025-11-12 00:23:02] MongoDB服务已停止 +[2025-11-12 00:23:04] 正在停止MongoDB服务... +[2025-11-12 00:23:05] Windows服务停止MongoDB失败: 发生系统错误 5。 + +拒绝访问。 + + +[2025-11-12 00:23:05] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5: + +拒绝访问。 + + +[2025-11-12 00:23:06] 已终止所有MongoDB进程 +[2025-11-12 00:23:08] MongoDB服务已停止 +[2025-11-12 00:23:08] 正在停止MongoDB服务... +[2025-11-12 00:23:08] Windows服务停止MongoDB失败: 发生系统错误 5。 + +拒绝访问。 + + +[2025-11-12 00:23:09] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5: + +拒绝访问。 + + +[2025-11-12 00:23:09] 已终止所有MongoDB进程 +[2025-11-12 00:23:11] MongoDB服务已停止 +[2025-11-12 00:26:55] HalloChat服务器管理器启动 +[2025-11-12 00:27:20] 检测到依赖未安装,正在尝试安装... +[2025-11-12 00:27:22] 正在启动服务器... +[2025-11-12 00:27:22] 服务器启动命令已发送 +[2025-11-12 00:27:23] +[2025-11-12 00:27:23] > hallo-chat-server@2.1.1-alpha start +[2025-11-12 00:27:23] > node src/index.js +[2025-11-12 00:27:23] +[2025-11-12 00:27:23] info: 服务已启动,端口:7932 {"timestamp":"2025-11-12 00:27:23"} +[2025-11-12 00:27:23] 服务已启动,端口:7932 +[2025-11-12 00:27:23] info: MongoDB数据库连接成功 {"timestamp":"2025-11-12 00:27:23"} +[2025-11-12 00:27:23] MongoDB数据库连接成功 +[2025-11-12 00:32:47] 正在停止服务器... +[2025-11-12 00:32:47] 服务器已停止 +[2025-11-12 00:32:48] 正在停止MongoDB服务... +[2025-11-12 00:32:48] Windows服务停止MongoDB失败: 发生系统错误 5。 + +拒绝访问。 + + +[2025-11-12 00:32:49] 使用sc命令停止MongoDB服务失败: [SC] OpenService 失败 5: + +拒绝访问。 + + +[2025-11-12 00:32:49] 已终止所有MongoDB进程 +[2025-11-12 00:32:51] MongoDB服务已停止 +[2025-11-12 00:36:41] HalloChat服务器管理器启动 +[2025-11-12 00:36:45] 正在停止MongoDB服务... +[2025-11-12 00:36:45] 尝试以管理员权限停止MongoDB服务... +[2025-11-12 00:36:45] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框 +[2025-11-12 00:36:46] 已终止所有MongoDB进程 +[2025-11-12 00:36:48] MongoDB服务已停止 +[2025-11-12 00:36:54] 正在停止MongoDB服务... +[2025-11-12 00:36:54] 尝试以管理员权限停止MongoDB服务... +[2025-11-12 00:36:54] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框 +[2025-11-12 00:36:55] 已终止所有MongoDB进程 +[2025-11-12 00:36:57] MongoDB服务已停止 +[2025-11-12 00:36:57] 正在停止MongoDB服务... +[2025-11-12 00:36:57] 尝试以管理员权限停止MongoDB服务... +[2025-11-12 00:36:58] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框 +[2025-11-12 00:36:58] 已终止所有MongoDB进程 +[2025-11-12 00:37:00] MongoDB服务已停止 +[2025-11-12 00:37:08] 正在停止MongoDB服务... +[2025-11-12 00:37:08] 尝试以管理员权限停止MongoDB服务... +[2025-11-12 00:37:08] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框 +[2025-11-12 00:37:09] 已终止所有MongoDB进程 +[2025-11-12 00:37:11] MongoDB服务已停止 +[2025-11-12 00:41:21] HalloChat服务器管理器启动 +[2025-11-12 00:41:24] 正在启动MongoDB服务... +[2025-11-12 00:41:24] Windows服务启动MongoDB失败: 发生系统错误 5。 + +拒绝访问。 + + +[2025-11-12 00:41:24] 使用sc命令启动MongoDB服务失败: [SC] StartService: OpenService 失败 5: + +拒绝访问。 + + +[2025-11-12 00:41:24] 尝试直接启动MongoDB进程... +[2025-11-12 00:41:24] 找到MongoDB安装: C:\Program Files\MongoDB\Server\7.0\bin\mongod.exe +[2025-11-12 00:41:24] 创建MongoDB数据目录: C:\ProgramData\MongoDB\data\db +[2025-11-12 00:41:25] 直接启动MongoDB成功 +[2025-11-12 00:41:33] 正在停止MongoDB服务... +[2025-11-12 00:41:33] 尝试以管理员权限停止MongoDB服务... +[2025-11-12 00:41:33] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框 +[2025-11-12 00:41:34] 终止MongoDB进程PID: 59468 +[2025-11-12 00:41:34] 终止直接启动的MongoDB进程 +[2025-11-12 00:41:34] 第1次尝试终止所有MongoDB进程 +[2025-11-12 00:41:34] 已尝试终止所有MongoDB进程 +[2025-11-12 00:41:38] 第2次尝试终止所有MongoDB进程 +[2025-11-12 00:41:38] 已尝试终止所有MongoDB进程 +[2025-11-12 00:41:42] 第3次尝试终止所有MongoDB进程 +[2025-11-12 00:41:42] 已尝试终止所有MongoDB进程 +[2025-11-12 00:41:44] 等待MongoDB进程完全终止... +[2025-11-12 00:41:48] MongoDB似乎仍在运行,等待并再次检查...(1/5) +[2025-11-12 00:41:50] MongoDB似乎仍在运行,等待并再次检查...(2/5) +[2025-11-12 00:41:53] MongoDB似乎仍在运行,等待并再次检查...(3/5) +[2025-11-12 00:41:55] MongoDB似乎仍在运行,等待并再次检查...(4/5) +[2025-11-12 00:41:57] MongoDB似乎仍在运行,等待并再次检查...(5/5) +[2025-11-12 00:41:59] MongoDB似乎仍在运行,尝试最后一次强力终止... +[2025-11-12 00:42:02] 警告:无法确认MongoDB是否已完全停止,请手动检查 +[2025-11-12 00:42:10] 正在停止MongoDB服务... +[2025-11-12 00:42:10] 尝试以管理员权限停止MongoDB服务... +[2025-11-12 00:42:10] 已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框 +[2025-11-12 00:42:10] 第1次尝试终止所有MongoDB进程 +[2025-11-12 00:42:11] 已尝试终止所有MongoDB进程 +[2025-11-12 00:42:14] 第2次尝试终止所有MongoDB进程 +[2025-11-12 00:42:15] 已尝试终止所有MongoDB进程 +[2025-11-12 00:42:18] 第3次尝试终止所有MongoDB进程 +[2025-11-12 00:42:19] 已尝试终止所有MongoDB进程 +[2025-11-12 00:42:21] 等待MongoDB进程完全终止... +[2025-11-12 00:42:24] MongoDB似乎仍在运行,等待并再次检查...(1/5) +[2025-11-12 00:42:27] MongoDB似乎仍在运行,等待并再次检查...(2/5) +[2025-11-12 00:42:29] MongoDB似乎仍在运行,等待并再次检查...(3/5) +[2025-11-12 00:42:32] MongoDB似乎仍在运行,等待并再次检查...(4/5) +[2025-11-12 00:42:35] MongoDB服务已成功停止 diff --git a/server/manager/server_manager.py b/server/manager/server_manager.py index c8a18ed..b60c327 100644 --- a/server/manager/server_manager.py +++ b/server/manager/server_manager.py @@ -18,6 +18,7 @@ import tkinter as tk from tkinter import ttk, messagebox, scrolledtext from threading import Thread, Lock +import re class ServerManager: @@ -28,6 +29,13 @@ def __init__(self, root): self.root.resizable(True, True) self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + # 设置软件图标为服务端控制台根目录的HalloChat.ico + try: + icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'HalloChat.ico') + self.root.iconbitmap(icon_path) + except Exception as e: + print(f"设置图标失败: {str(e)}") # 静默失败,不影响主要功能 + # 设置中文字体支持 self.configure_styles() @@ -73,8 +81,8 @@ def show_startup_notice(self): # 添加提示文本 notice_text = ( "使用须知:\n"\ - "1. MongoDB 全程使用Homebrew管理MongoDB社区版\n"\ - "2. 代码中部分注释由AI辅助生成,以提高可读性" + "本软件为测试版本 有些许bug\n"\ + "代码中部分注释由AI辅助生成,以提高可读性" ) notice_label = ttk.Label(content_frame, text=notice_text, style="Notice.TLabel", justify=tk.LEFT) @@ -199,6 +207,30 @@ def create_widgets(self): control_frame = ttk.Frame(main_frame, padding="10") control_frame.pack(fill=tk.X, pady=(0, 20)) + # 进度条区域 - 用于显示依赖安装进度 + self.progress_frame = ttk.LabelFrame(main_frame, text="安装进度", padding="10") + self.progress_frame.pack(fill=tk.X, pady=(0, 20)) + self.progress_frame.pack_forget() # 初始隐藏 + + # 进度条 + self.progress_var = tk.DoubleVar() + self.progress_bar = ttk.Progressbar( + self.progress_frame, + variable=self.progress_var, + maximum=100, + length=0, # 自适应长度 + mode='determinate' + ) + self.progress_bar.pack(fill=tk.X, pady=(0, 10)) + + # 进度文本 + self.progress_text_var = tk.StringVar(value="准备安装依赖...") + self.progress_text = ttk.Label( + self.progress_frame, + textvariable=self.progress_text_var + ) + self.progress_text.pack(anchor=tk.W) + # 安装依赖按钮 self.install_deps_button = ttk.Button( control_frame, @@ -343,6 +375,7 @@ def start_server(self): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + encoding='utf-8', # 明确指定UTF-8编码 cwd=current_dir, shell=False if sys.platform != 'win32' else True ) @@ -362,10 +395,31 @@ def start_server(self): messagebox.showerror("错误", f"启动服务器失败: {str(e)}") def install_dependencies(self): - """安装Node.js依赖""" + """安装Node.js依赖,带进度条显示""" + import re + + def update_progress_bar(progress, text): + """更新进度条和文本""" + # 在主线程中更新UI + def update(): + self.progress_var.set(progress) + self.progress_text_var.set(text) + self.root.update_idletasks() # 立即刷新UI + + if self.root.winfo_exists(): + self.root.after(0, update) + try: self.log("开始安装依赖...") + # 显示进度条区域 + if self.root.winfo_exists(): + self.root.after(0, lambda: self.progress_frame.pack(fill=tk.X, pady=(0, 20))) + self.root.update_idletasks() + + # 初始化进度 + update_progress_bar(0, "准备安装...") + # 禁用按钮防止重复点击 self.install_deps_button.config(state=tk.DISABLED) @@ -386,36 +440,227 @@ def install_dependencies(self): except: pass # 如果找不到,就使用默认的npm - install_process = subprocess.Popen( - [npm_path, "install"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - cwd=current_dir, - shell=False if sys.platform != 'win32' else True - ) - - # 读取安装进度 - for line in iter(install_process.stdout.readline, ''): - if line: - self.log(f"[npm] {line.strip()}") - - # 等待安装完成 - install_process.wait() + # 使用--progress参数启用进度输出 + try: + install_process = subprocess.Popen( + [npm_path, "install", "--progress=true"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding='utf-8', # 明确指定UTF-8编码 + cwd=current_dir, + shell=False if sys.platform != 'win32' else True + ) + + # 用于跟踪已安装的包数量 + installed_count = 0 + + # 解析npm输出的正则表达式 + # 匹配npm进度条输出、包安装信息等 + progress_regex = re.compile(r'progress: ([\d.]+)%') + package_regex = re.compile(r'(added|updated)\s+(\d+)\s+packages?') + fetching_regex = re.compile(r'fetching\s+from\s+(.+)') + installing_regex = re.compile(r'installing\s+(.+)') + error_regex = re.compile(r'(error|fail|warning|warn)', re.IGNORECASE) + + # 读取安装进度 + start_time = time.time() + last_progress_update = time.time() + + for line in iter(install_process.stdout.readline, ''): + if not line or not self.root.winfo_exists(): + # 如果窗口已关闭或无输出,中断安装 + if install_process.poll() is None: + self.log("检测到窗口关闭或中断,终止安装进程...") + try: + # 尝试优雅地终止进程 + if sys.platform == 'win32': + install_process.terminate() + # 给进程一点时间终止 + time.sleep(1) + if install_process.poll() is None: + install_process.kill() # 强制终止 + else: + install_process.kill() + except Exception as kill_error: + self.log(f"终止安装进程时出错: {str(kill_error)}") + break + + line = line.strip() + if line: + # 记录所有输出 + self.log(f"[npm] {line}") + + # 检查是否包含错误信息 + if error_regex.search(line): + # 对于错误或警告信息,特别处理 + error_level = "警告" if any(w in line.lower() for w in ['warning', 'warn']) else "错误" + update_progress_bar(self.progress_var.get(), f"{error_level}: {line[:100]}...") + + # 尝试匹配进度百分比 + progress_match = progress_regex.search(line) + if progress_match: + progress = float(progress_match.group(1)) + update_progress_bar(progress, f"安装进度: {progress:.1f}%") + last_progress_update = time.time() + + # 尝试匹配包安装信息 + package_match = package_regex.search(line) + if package_match: + installed_count = int(package_match.group(2)) + # 估算进度,基于假设的包总数(实际中可能需要动态计算) + estimated_progress = min(50 + (installed_count * 0.5), 90) + update_progress_bar(estimated_progress, f"已安装 {installed_count} 个包...") + last_progress_update = time.time() + + # 尝试匹配正在获取的包 + fetching_match = fetching_regex.search(line.lower()) + if fetching_match: + package_name = fetching_match.group(1) + update_progress_bar(self.progress_var.get(), f"正在获取: {package_name}") + + # 尝试匹配正在安装的包 + installing_match = installing_regex.search(line.lower()) + if installing_match: + package_name = installing_match.group(1) + update_progress_bar(self.progress_var.get(), f"正在安装: {package_name}") + + # 检查是否超时(超过30分钟) + if time.time() - start_time > 30 * 60: + self.log("安装依赖超时(超过30分钟),终止安装...") + update_progress_bar(self.progress_var.get(), "安装超时,正在终止...") + if install_process.poll() is None: + try: + if sys.platform == 'win32': + install_process.terminate() + time.sleep(1) + if install_process.poll() is None: + install_process.kill() + else: + install_process.kill() + except Exception as kill_error: + self.log(f"终止超时进程时出错: {str(kill_error)}") + raise TimeoutError("依赖安装超时,请检查网络连接或尝试手动安装") + + # 如果长时间没有进度更新,显示活动状态 + if time.time() - last_progress_update > 10: # 10秒无更新 + current_progress = self.progress_var.get() + # 小幅度增加进度以显示活动状态 + if current_progress < 95: + new_progress = min(current_progress + 0.5, 95) + update_progress_bar(new_progress, f"安装进行中...") + last_progress_update = time.time() + + # 等待安装完成并更新最终进度 + update_progress_bar(95, "安装完成,正在清理...") + + # 设置等待超时,避免无限等待 + try: + # 使用wait但设置超时 + return_code = install_process.wait(timeout=30) # 30秒超时 + except subprocess.TimeoutExpired: + self.log("等待安装完成超时,强制终止进程...") + if install_process.poll() is None: + try: + if sys.platform == 'win32': + install_process.terminate() + time.sleep(1) + if install_process.poll() is None: + install_process.kill() + else: + install_process.kill() + except Exception as kill_error: + self.log(f"强制终止进程时出错: {str(kill_error)}") + raise TimeoutError("等待安装完成超时") + + except (KeyboardInterrupt, SystemExit): + # 处理用户中断或系统退出 + self.log("检测到用户中断或系统退出,终止安装...") + update_progress_bar(self.progress_var.get(), "安装已中断") + if 'install_process' in locals() and install_process.poll() is None: + try: + if sys.platform == 'win32': + install_process.terminate() + time.sleep(1) + if install_process.poll() is None: + install_process.kill() + else: + install_process.kill() + except Exception as kill_error: + self.log(f"终止进程时出错: {str(kill_error)}") + raise + except Exception as process_error: + # 捕获进程相关的错误 + self.log(f"安装进程执行出错: {str(process_error)}") + update_progress_bar(self.progress_var.get(), f"执行错误: {str(process_error)[:50]}...") + if 'install_process' in locals() and install_process.poll() is None: + try: + if sys.platform == 'win32': + install_process.terminate() + time.sleep(1) + if install_process.poll() is None: + install_process.kill() + else: + install_process.kill() + except Exception as kill_error: + self.log(f"终止错误进程时出错: {str(kill_error)}") + raise if install_process.returncode == 0: + update_progress_bar(100, "依赖安装成功!") self.log("依赖安装成功") - messagebox.showinfo("成功", "依赖安装完成") + # 延迟一下再显示成功消息,让用户看到100%的进度 + if self.root.winfo_exists(): + self.root.after(500, lambda: messagebox.showinfo("成功", "依赖安装完成")) else: self.log(f"依赖安装失败,退出码: {install_process.returncode}") - messagebox.showerror("错误", "依赖安装失败,请查看日志获取详细信息") + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showerror("错误", "依赖安装失败,请查看日志获取详细信息")) + except TimeoutError as te: + # 处理超时错误 + error_msg = f"安装超时: {str(te)}" + self.log(error_msg) + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showerror("安装超时", error_msg)) + except KeyboardInterrupt: + # 处理用户中断 + self.log("安装已被用户中断") + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showinfo("已中断", "依赖安装已被中断")) + except subprocess.SubprocessError as se: + # 处理子进程相关错误 + error_msg = f"安装进程错误: {str(se)}" + self.log(error_msg) + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showerror("进程错误", error_msg)) + except IOError as ioe: + # 处理IO错误 + error_msg = f"输入/输出错误: {str(ioe)}" + self.log(error_msg) + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showerror("IO错误", f"文件读写错误,可能是权限问题: {str(ioe)}")) + except OSError as ose: + # 处理操作系统错误 + error_msg = f"系统错误: {str(ose)}" + self.log(error_msg) + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showerror("系统错误", f"操作系统错误: {str(ose)}\n可能需要管理员权限或检查磁盘空间")) except Exception as e: - self.log(f"安装依赖时出错: {str(e)}") - messagebox.showerror("错误", f"安装依赖时出错: {str(e)}") + # 处理其他所有错误 + error_msg = f"安装依赖时出错: {str(e)}" + self.log(error_msg) + if self.root.winfo_exists(): + self.root.after(0, lambda: messagebox.showerror("错误", error_msg)) finally: + # 隐藏进度条区域 + if self.root.winfo_exists(): + self.root.after(0, lambda: self.progress_frame.pack_forget()) # 重新启用按钮 - self.install_deps_button.config(state=tk.NORMAL) + if self.root.winfo_exists(): + self.root.after(0, lambda: self.install_deps_button.config(state=tk.NORMAL)) + # 确保资源释放 + self.log("依赖安装过程完成(无论成功或失败)") def _install_dependencies_silent(self): """静默安装依赖(用于自动检查时)""" @@ -508,10 +753,19 @@ def read_server_logs(self): self.log(f"读取日志时出错: {str(e)}") def log(self, message): - """添加日志到日志窗口""" + """添加日志到日志窗口并保存到文件""" timestamp = time.strftime("%Y-%m-%d %H:%M:%S") log_message = f"[{timestamp}] {message}" + # 尝试将日志写入文件 + try: + log_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "server_manager.log") + with open(log_file_path, "a", encoding="utf-8") as log_file: + log_file.write(log_message + "\n") + except Exception as e: + # 如果写入日志文件失败,不影响主要功能,但在控制台打印错误 + print(f"写入日志文件失败: {str(e)}") + # 在主线程中更新UI self.root.after(0, lambda: self._append_log(log_message)) @@ -579,26 +833,13 @@ def is_mongodb_running(self): return self.is_port_in_use(self.mongodb_port) def download_mongodb(self): - """使用Homebrew下载MongoDB""" + """下载MongoDB,支持Windows、macOS和Ubuntu Linux平台""" try: self.log("开始下载MongoDB...") - # 检查是否是macOS系统 - if sys.platform != 'darwin': - messagebox.showerror("错误", "此功能仅支持macOS系统") - return - # 禁用下载按钮 self.download_mongodb_button.config(state=tk.DISABLED) - # 执行Homebrew命令 - commands = [ - ["brew", "update"], - ["bash", "-c", "export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles && brew update"], - ["brew", "tap", "mongodb/brew"], - ["brew", "install", "mongodb-community@7.0"] - ] - # 创建进度条窗口 progress_window = tk.Toplevel(self.root) progress_window.title("下载MongoDB") @@ -608,7 +849,7 @@ def download_mongodb(self): progress_window.grab_set() # 创建进度条 - progress_label = ttk.Label(progress_window, text="正在执行命令...", padding=20) + progress_label = ttk.Label(progress_window, text="正在准备下载...", padding=20) progress_label.pack(fill=tk.X) progress = ttk.Progressbar(progress_window, length=480, mode='determinate') @@ -616,44 +857,341 @@ def download_mongodb(self): progress['value'] = 0 progress_window.update() - # 执行命令序列 - success = True - for i, cmd in enumerate(commands): - cmd_str = ' '.join(cmd) - progress_label.config(text=f"正在执行: {cmd_str}") - progress['value'] = (i + 1) * 25 + success = False + + # 根据操作系统选择不同的下载方式 + if sys.platform == 'darwin': # macOS + # 执行Homebrew命令 + commands = [ + ["brew", "update"], + ["bash", "-c", "export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles && brew update"], + ["brew", "tap", "mongodb/brew"], + ["brew", "install", "mongodb-community@7.0"] + ] + + # 执行命令序列 + success = True + for i, cmd in enumerate(commands): + cmd_str = ' '.join(cmd) + progress_label.config(text=f"正在执行: {cmd_str}") + progress['value'] = (i + 1) * 25 + progress_window.update() + + try: + self.log(f"执行命令: {cmd_str}") + + # 对于包含环境变量的命令,使用shell=True + shell = len(cmd) > 1 and cmd[0] == "bash" and cmd[1] == "-c" + + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + shell=shell, + timeout=600 # 设置10分钟超时 + ) + + self.log(f"命令输出: {result.stdout}") + + if result.returncode != 0: + self.log(f"命令执行失败,返回码: {result.returncode}") + success = False + break + + except subprocess.TimeoutExpired: + self.log(f"命令执行超时: {cmd_str}") + success = False + break + except Exception as e: + self.log(f"执行命令时出错: {str(e)}") + success = False + break + + elif sys.platform == 'win32': # Windows + import requests + import tempfile + import shutil + import time + import ctypes + + # MongoDB Windows安装程序下载链接 (MongoDB 7.0 Community Edition) + download_url = "https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-7.0.0-signed.msi" + + # 显示下载进度 + progress_label.config(text="正在下载MongoDB安装程序...") + progress['value'] = 10 progress_window.update() try: - self.log(f"执行命令: {cmd_str}") + # 创建临时目录 + temp_dir = tempfile.mkdtemp() + msi_path = os.path.join(temp_dir, "mongodb-installer.msi") + + self.log(f"开始下载MongoDB安装程序: {download_url}") + + # 下载MongoDB安装程序 + response = requests.get(download_url, stream=True) + total_size = int(response.headers.get('content-length', 0)) + downloaded_size = 0 + + with open(msi_path, 'wb') as file: + for data in response.iter_content(chunk_size=8192): + file.write(data) + downloaded_size += len(data) + if total_size > 0: + percent = (downloaded_size / total_size) * 80 + 10 # 10-90% + progress['value'] = percent + progress_window.update() - # 对于包含环境变量的命令,使用shell=True - shell = len(cmd) > 1 and cmd[0] == "bash" and cmd[1] == "-c" + self.log(f"MongoDB安装程序下载完成: {msi_path}") + progress_label.config(text="正在启动安装向导...") + progress['value'] = 90 + progress_window.update() + + # 检查是否以管理员权限运行 + def is_admin(): + try: + return ctypes.windll.shell32.IsUserAnAdmin() + except: + return False + + # 启动MongoDB安装向导 + if is_admin(): + self.log("以管理员权限启动MongoDB安装向导") + subprocess.run(["msiexec", "/i", msi_path]) + else: + self.log("提示用户以管理员权限运行安装向导") + messagebox.showinfo("提示", "请以管理员权限运行MongoDB安装向导") + subprocess.run(["msiexec", "/i", msi_path]) - result = subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - shell=shell, - timeout=600 # 设置10分钟超时 - ) + # 等待安装完成(用户手动关闭安装向导) + progress_label.config(text="等待安装完成...") + progress['value'] = 95 + progress_window.update() - self.log(f"命令输出: {result.stdout}") + # 让用户确认安装是否完成 + if messagebox.askyesno("确认", "MongoDB安装完成了吗?"): + success = True - if result.returncode != 0: - self.log(f"命令执行失败,返回码: {result.returncode}") + except Exception as e: + self.log(f"Windows下载安装MongoDB出错: {str(e)}") + messagebox.showerror("错误", f"下载或安装MongoDB时出错: {str(e)}") + success = False + finally: + # 清理临时文件 + if 'temp_dir' in locals(): + try: + shutil.rmtree(temp_dir) + except: + pass + + elif sys.platform.startswith('linux'): # Linux (Ubuntu) + import re + + # 检查是否为Ubuntu系统 + try: + with open('/etc/os-release', 'r') as f: + os_release = f.read() + if 'Ubuntu' not in os_release: + self.log("检测到非Ubuntu Linux系统,此功能专为Ubuntu设计") + messagebox.showerror("错误", "此MongoDB安装功能专为Ubuntu设计") + success = False + return + + # 提取Ubuntu版本 + version_match = re.search(r'VERSION_ID="(\d+\.\d+)"', os_release) + if version_match: + ubuntu_version = version_match.group(1) + self.log(f"检测到Ubuntu {ubuntu_version}") + else: + self.log("无法确定Ubuntu版本") + except Exception as e: + self.log(f"检测Ubuntu系统信息时出错: {str(e)}") + # 继续尝试安装 + + # Ubuntu安装步骤 + ubuntu_commands = [ + # 更新包列表 + ["sudo", "apt-get", "update"], + # 安装必要的依赖 + ["sudo", "apt-get", "install", "-y", "gnupg"], + # 添加MongoDB GPG密钥 + ["sudo", "wget", "-qO-", "https://www.mongodb.org/static/pgp/server-7.0.asc", "|", "sudo", "apt-key", "add", "-"], + # 创建MongoDB源列表文件 + ["sudo", "echo", "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu $(lsb_release -cs)/mongodb-org/7.0 multiverse", "|", "sudo", "tee", "/etc/apt/sources.list.d/mongodb-org-7.0.list"], + # 更新包列表 + ["sudo", "apt-get", "update"], + # 安装MongoDB + ["sudo", "apt-get", "install", "-y", "mongodb-org"] + ] + + # 执行Ubuntu安装命令 + success = True + for i, cmd in enumerate(ubuntu_commands): + cmd_str = ' '.join(cmd) + progress_label.config(text=f"正在执行: {cmd_str}") + progress['value'] = (i + 1) * (100 / len(ubuntu_commands)) + progress_window.update() + + try: + self.log(f"执行命令: {cmd_str}") + + # 对于需要管道的命令,使用shell=True + shell = '|' in cmd_str + + # 对于包含echo和tee的命令,使用不同的方式处理 + if "echo" in cmd_str and "tee" in cmd_str: + # 提取要写入的内容和目标文件 + content_match = re.search(r'echo "([^"]*)"', cmd_str) + file_match = re.search(r'tee (\S+)', cmd_str) + if content_match and file_match: + content = content_match.group(1) + target_file = file_match.group(1) + # 使用echo命令通过shell写入文件 + shell_cmd = f"echo '{content}' | sudo tee {target_file}" + self.log(f"使用shell命令: {shell_cmd}") + result = subprocess.run( + shell_cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=300 + ) + else: + raise Exception("无法解析echo和tee命令") + else: + # 对于常规命令,分解命令参数 + if shell: + # 对于包含管道的命令,使用shell=True + result = subprocess.run( + cmd_str, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=300 + ) + else: + # 对于不包含管道的命令,正常执行 + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=300 + ) + + self.log(f"命令输出: {result.stdout}") + + if result.returncode != 0: + self.log(f"命令执行失败,返回码: {result.returncode}") + # 对于某些非关键错误,尝试继续执行 + if i not in [0, 3, 4]: # 不跳过更新和源列表配置 + self.log("继续尝试后续命令...") + else: + success = False + break + + except subprocess.TimeoutExpired: + self.log(f"命令执行超时: {cmd_str}") success = False break + except Exception as e: + self.log(f"执行命令时出错: {str(e)}") + success = False + break + + # 启动MongoDB服务 + if success: + progress_label.config(text="正在启动MongoDB服务...") + progress['value'] = 90 + progress_window.update() + + try: + # 启动服务 + subprocess.run( + ["sudo", "systemctl", "start", "mongod"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) - except subprocess.TimeoutExpired: - self.log(f"命令执行超时: {cmd_str}") - success = False - break - except Exception as e: - self.log(f"执行命令时出错: {str(e)}") - success = False - break + # 设置开机自启 + subprocess.run( + ["sudo", "systemctl", "enable", "mongod"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) + + # 检查MongoDB是否成功启动 + time.sleep(3) + result = subprocess.run( + ["sudo", "systemctl", "is-active", "mongod"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + if "active" in result.stdout: + self.log("MongoDB服务已成功启动") + success = True + else: + self.log("MongoDB服务启动失败") + # 尝试直接启动mongod + self.log("尝试直接启动mongod...") + try: + subprocess.run( + ["mongod", "--dbpath", "/var/lib/mongodb", "--logpath", "/var/log/mongodb/mongod.log", "--fork"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) + # 检查是否成功 + time.sleep(2) + if subprocess.run(["pgrep", "mongod"]).returncode == 0: + self.log("直接启动mongod成功") + success = True + else: + self.log("直接启动mongod失败") + success = False + except Exception as e: + self.log(f"直接启动mongod时出错: {str(e)}") + success = False + + except Exception as e: + self.log(f"启动MongoDB服务时出错: {str(e)}") + success = False + + # 检查MongoDB是否安装成功 + if success: + try: + result = subprocess.run( + ["mongosh", "--version"], # MongoDB 6.0+ 使用mongosh而不是mongo + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + if result.returncode == 0: + self.log(f"MongoDB Shell版本: {result.stdout.splitlines()[0]}") + else: + # 尝试旧版命令 + result = subprocess.run( + ["mongo", "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + if result.returncode == 0: + self.log(f"MongoDB版本: {result.stdout.splitlines()[0]}") + except Exception as e: + self.log(f"检查MongoDB版本时出错: {str(e)}") + + else: + messagebox.showerror("错误", f"不支持的操作系统: {sys.platform}") + success = False # 关闭进度窗口 progress_window.destroy() @@ -675,6 +1213,19 @@ def download_mongodb(self): # 重新启用下载按钮 self.download_mongodb_button.config(state=tk.NORMAL) + def _is_command_available(self, command): + """检查命令是否可用""" + try: + if sys.platform == 'win32': + # Windows系统使用where命令检查 + subprocess.run(['where', command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True) + else: + # Unix系统使用which命令检查 + subprocess.run(['which', command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + return True + except (subprocess.SubprocessError, FileNotFoundError): + return False + def start_mongodb(self): """启动MongoDB服务""" with self.mongodb_lock: @@ -709,6 +1260,7 @@ def start_mongodb(self): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + encoding='utf-8', # 明确指定UTF-8编码 shell=False ) # 等待MongoDB启动 @@ -726,95 +1278,241 @@ def start_mongodb(self): return False elif sys.platform == 'win32': # Windows + # 方法1: 尝试使用net start命令启动MongoDB服务 try: - # 尝试使用net start命令启动MongoDB服务 - subprocess.run( - ["net", "start", "MongoDB"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=True, - shell=True - ) - self.log("Windows服务启动MongoDB成功") - self.mongodb_running = True - self.update_ui_state() - return True - except subprocess.SubprocessError: - self.log("Windows服务启动MongoDB失败,尝试直接启动") - # 尝试直接启动mongod - try: - # 查找MongoDB安装路径 - mongo_path = "mongod" # 默认在PATH中 - # 检查常见的安装路径 - for path in [ - "C:\\Program Files\\MongoDB\\Server\\5.0\\bin\\mongod.exe", - "C:\\Program Files\\MongoDB\\Server\\4.4\\bin\\mongod.exe", - "C:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongod.exe" - ]: - if os.path.exists(path): - mongo_path = path - break - - self.mongodb_process = subprocess.Popen( - [mongo_path], + if self._is_command_available('net'): + result = subprocess.run( + ["net", "start", "MongoDB"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, shell=True ) - # 等待MongoDB启动 - time.sleep(3) - if self.is_mongodb_running(): - self.log("直接启动MongoDB成功") + if result.returncode == 0: + self.log("Windows服务启动MongoDB成功") self.mongodb_running = True self.update_ui_state() return True else: - self.log("直接启动MongoDB失败") - return False - except Exception as e: - self.log(f"直接启动MongoDB时出错: {str(e)}") - return False - - elif sys.platform.startswith('linux'): # Linux + self.log(f"Windows服务启动MongoDB失败: {result.stdout}") + else: + self.log("net命令不可用,跳过服务启动尝试") + except Exception as e: + self.log(f"使用net命令启动MongoDB服务时出错: {str(e)}") + + # 方法2: 尝试使用sc命令启动MongoDB服务 try: - # 尝试使用systemctl启动MongoDB - subprocess.run( - ["sudo", "systemctl", "start", "mongodb"], + if self._is_command_available('sc'): + result = subprocess.run( + ["sc", "start", "MongoDB"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + shell=True + ) + if result.returncode == 0 or "SUCCESS" in result.stdout: + self.log("使用sc命令启动MongoDB服务成功") + self.mongodb_running = True + self.update_ui_state() + return True + else: + self.log(f"使用sc命令启动MongoDB服务失败: {result.stdout}") + else: + self.log("sc命令不可用,跳过服务启动尝试") + except Exception as e: + self.log(f"使用sc命令启动MongoDB服务时出错: {str(e)}") + + # 方法3: 尝试直接启动mongod + try: + self.log("尝试直接启动MongoDB进程...") + # 查找MongoDB安装路径,支持最新版本 + mongo_path = "mongod" # 默认在PATH中 + # 检查常见的安装路径,包括最新版本 + for path in [ + "C:\\Program Files\\MongoDB\\Server\\7.0\\bin\\mongod.exe", + "C:\\Program Files\\MongoDB\\Server\\6.0\\bin\\mongod.exe", + "C:\\Program Files\\MongoDB\\Server\\5.0\\bin\\mongod.exe", + "C:\\Program Files\\MongoDB\\Server\\4.4\\bin\\mongod.exe", + "C:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongod.exe" + ]: + if os.path.exists(path): + mongo_path = path + self.log(f"找到MongoDB安装: {path}") + break + + # 确保数据目录存在 + data_dir = os.path.join(os.environ.get('ProgramData', 'C:\\ProgramData'), 'MongoDB', 'data', 'db') + if not os.path.exists(data_dir): + try: + os.makedirs(data_dir, exist_ok=True) + self.log(f"创建MongoDB数据目录: {data_dir}") + except Exception as e: + self.log(f"创建数据目录失败: {str(e)}") + + # 启动MongoDB进程 + startup_info = subprocess.STARTUPINFO() + startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + self.mongodb_process = subprocess.Popen( + [mongo_path, f"--dbpath={data_dir}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - check=True + encoding='utf-8', # 明确指定UTF-8编码 + shell=True, + startupinfo=startup_info ) - self.log("使用systemctl启动MongoDB成功") - self.mongodb_running = True - self.update_ui_state() - return True - except subprocess.SubprocessError: - self.log("使用systemctl启动MongoDB失败,尝试直接启动") - # 尝试直接启动mongod + + # 添加超时处理,最多等待10秒 + max_wait_time = 10 + wait_time = 0 + check_interval = 1 + + while wait_time < max_wait_time: + time.sleep(check_interval) + wait_time += check_interval + if self.is_mongodb_running(): + self.log("直接启动MongoDB成功") + self.mongodb_running = True + self.update_ui_state() + return True + + # 检查进程是否已经退出 + if self.mongodb_process.poll() is not None: + self.log("MongoDB进程已退出,启动失败") + # 尝试读取错误输出 + if self.mongodb_process.stdout: + error_output = self.mongodb_process.stdout.read() + if error_output: + self.log(f"MongoDB错误输出: {error_output}") + break + + self.log("直接启动MongoDB超时失败") + return False + + except Exception as e: + self.log(f"直接启动MongoDB时出错: {str(e)}") + return False + + elif sys.platform.startswith('linux'): # Linux (Ubuntu) + try: + # 检查Ubuntu系统中的MongoDB服务名称 + # Ubuntu中MongoDB服务可能是mongodb或mongod + service_name = "mongodb" try: - self.mongodb_process = subprocess.Popen( - ["mongod"], + # 检查mongod服务是否存在 + result = subprocess.run( + ["sudo", "systemctl", "list-units", "--full", "-all", "|", "grep", "mongod"], + shell=True, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - shell=False + stderr=subprocess.PIPE ) - # 等待MongoDB启动 - time.sleep(3) + if result.returncode == 0: + service_name = "mongod" + self.log(f"检测到MongoDB服务名称: {service_name}") + except: + self.log("使用默认MongoDB服务名称: mongodb") + + # 尝试使用systemctl启动MongoDB + self.log(f"尝试使用systemctl启动MongoDB服务 ({service_name})...") + result = subprocess.run( + ["sudo", "systemctl", "start", service_name], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) + + if result.returncode == 0: + self.log(f"使用systemctl启动{service_name}成功") + self.mongodb_running = True + self.update_ui_state() + return True + else: + self.log(f"使用systemctl启动{service_name}失败: {result.stdout}") + + # 检查MongoDB是否已经运行 if self.is_mongodb_running(): - self.log("直接启动MongoDB成功") + self.log("MongoDB已经在运行") self.mongodb_running = True self.update_ui_state() return True - else: - self.log("直接启动MongoDB失败") + + # 尝试直接启动mongod + self.log("尝试直接启动mongod...") + try: + # 确保数据目录存在 + data_dir = "/var/lib/mongodb" + if not os.path.exists(data_dir): + try: + os.makedirs(data_dir, exist_ok=True) + # 设置权限 + subprocess.run(["sudo", "chown", "mongodb:mongodb", data_dir], check=False) + self.log(f"创建MongoDB数据目录: {data_dir}") + except Exception as e: + self.log(f"创建数据目录失败: {str(e)}") + # 使用临时目录作为备选 + data_dir = "/tmp/mongodb_data" + os.makedirs(data_dir, exist_ok=True) + self.log(f"使用临时数据目录: {data_dir}") + + # 确保日志目录存在 + log_dir = "/var/log/mongodb" + log_file = os.path.join(log_dir, "mongod.log") + if not os.path.exists(log_dir): + try: + os.makedirs(log_dir, exist_ok=True) + # 设置权限 + subprocess.run(["sudo", "chown", "mongodb:mongodb", log_dir], check=False) + open(log_file, 'a').close() + subprocess.run(["sudo", "chown", "mongodb:mongodb", log_file], check=False) + except Exception as e: + self.log(f"创建日志目录失败: {str(e)}") + log_file = os.path.join("/tmp", "mongod.log") + self.log(f"使用临时日志文件: {log_file}") + + # 直接启动mongod进程 + self.mongodb_process = subprocess.Popen( + ["mongod", "--dbpath", data_dir, "--logpath", log_file], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding='utf-8' # 明确指定UTF-8编码 + ) + + # 等待MongoDB启动 + max_wait_time = 10 + wait_time = 0 + check_interval = 1 + + while wait_time < max_wait_time: + time.sleep(check_interval) + wait_time += check_interval + if self.is_mongodb_running(): + self.log("直接启动MongoDB成功") + self.mongodb_running = True + self.update_ui_state() + return True + + # 检查进程是否已经退出 + if self.mongodb_process.poll() is not None: + self.log("MongoDB进程已退出,启动失败") + # 尝试读取错误输出 + if self.mongodb_process.stdout: + error_output = self.mongodb_process.stdout.read() + if error_output: + self.log(f"MongoDB错误输出: {error_output}") + break + + self.log("直接启动MongoDB超时失败") return False - except Exception as e: - self.log(f"直接启动MongoDB时出错: {str(e)}") - return False + + except Exception as e: + self.log(f"直接启动MongoDB时出错: {str(e)}") + return False + + except Exception as e: + self.log(f"启动MongoDB时出错: {str(e)}") + return False # 如果没有找到对应的操作系统处理方法 messagebox.showerror("错误", "不支持的操作系统,请手动启动MongoDB") @@ -851,38 +1549,108 @@ def stop_mongodb(self): self.log("使用brew服务停止MongoDB失败,检查是否有直接启动的进程") elif sys.platform == 'win32': # Windows + # 尝试以管理员权限停止MongoDB服务 + self.log("尝试以管理员权限停止MongoDB服务...") + + # 方法1: 使用PowerShell以管理员权限停止MongoDB服务 try: - # 尝试使用net stop命令停止MongoDB服务 - subprocess.run( - ["net", "stop", "MongoDB"], + # 创建PowerShell命令来停止服务 + powershell_command = """ + # 尝试停止MongoDB服务 + try { + $service = Get-Service -Name MongoDB -ErrorAction SilentlyContinue + if ($service) { + Stop-Service -Name MongoDB -Force + Write-Output "MongoDB服务停止成功" + } else { + Write-Output "MongoDB服务不存在" + } + } catch { + Write-Output "停止MongoDB服务时出错: $_" + } + """ + + # 使用PowerShell以管理员权限运行 + result = subprocess.run( + ["powershell", "-Command", + "Start-Process", "powershell", + "-ArgumentList", f"-NoProfile -ExecutionPolicy Bypass -Command \"{powershell_command}\"", + "-Verb", "RunAs", + "-Wait", + "-PassThru"], stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=True, - shell=True + stderr=subprocess.PIPE, + text=True ) - self.log("Windows服务停止MongoDB成功") - except subprocess.SubprocessError: - self.log("Windows服务停止MongoDB失败,检查是否有直接启动的进程") - elif sys.platform.startswith('linux'): # Linux + self.log("已尝试以管理员权限停止MongoDB服务,请查看权限提示对话框") + except Exception as e: + self.log(f"以管理员权限停止MongoDB服务时出错: {str(e)}") + + # 方法2: 尝试使用net stop命令停止MongoDB服务(备用方案) + try: + if self._is_command_available('net'): + result = subprocess.run( + ["net", "stop", "MongoDB"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + shell=True + ) + if result.returncode == 0: + self.log("Windows服务停止MongoDB成功") + else: + self.log(f"Windows服务停止MongoDB失败: {result.stdout}") + except Exception as e: + self.log(f"使用net命令停止MongoDB服务时出错: {str(e)}") + + # 方法3: 尝试使用sc命令停止MongoDB服务(备用方案) + try: + if self._is_command_available('sc'): + result = subprocess.run( + ["sc", "stop", "MongoDB"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + shell=True + ) + if result.returncode == 0 or "SUCCESS" in result.stdout: + self.log("使用sc命令停止MongoDB服务成功") + else: + self.log(f"使用sc命令停止MongoDB服务失败: {result.stdout}") + except Exception as e: + self.log(f"使用sc命令停止MongoDB服务时出错: {str(e)}") + + elif sys.platform.startswith('linux'): # Linux (Ubuntu) try: - # 尝试使用systemctl停止MongoDB - subprocess.run( - ["sudo", "systemctl", "stop", "mongodb"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=True - ) - self.log("使用systemctl停止MongoDB成功") - except subprocess.SubprocessError: - self.log("使用systemctl停止MongoDB失败,检查是否有直接启动的进程") + # 检查Ubuntu系统中的MongoDB服务名称 + service_names = ["mongod", "mongodb"] # 尝试常见的服务名称 + stopped = False + + for service_name in service_names: + self.log(f"尝试使用systemctl停止{service_name}服务...") + result = subprocess.run( + ["sudo", "systemctl", "stop", service_name], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) + + if result.returncode == 0: + self.log(f"使用systemctl停止{service_name}成功") + stopped = True + break + + if not stopped: + self.log("使用systemctl停止MongoDB服务失败,检查是否有直接启动的进程") + except Exception as e: + self.log(f"使用systemctl停止MongoDB服务时出错: {str(e)}") # 检查并终止直接启动的MongoDB进程 if self.mongodb_process: if sys.platform == 'win32': subprocess.call(['taskkill', '/F', '/T', '/PID', str(self.mongodb_process.pid)]) + self.log(f"终止MongoDB进程PID: {self.mongodb_process.pid}") else: self.mongodb_process.terminate() try: @@ -892,18 +1660,132 @@ def stop_mongodb(self): self.mongodb_process = None self.log("终止直接启动的MongoDB进程") - # 等待MongoDB停止 - time.sleep(2) + # Windows系统: 尝试终止所有MongoDB进程 + if sys.platform == 'win32': + # 重试机制:最多重试3次 + retry_count = 0 + max_retries = 3 + + while retry_count < max_retries: + try: + # 查找所有mongod.exe进程 + result = subprocess.run( + ['tasklist', '/FI', 'IMAGENAME eq mongod.exe', '/NH'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + shell=True + ) + + if 'mongod.exe' in result.stdout: + self.log(f"第{retry_count+1}次尝试终止所有MongoDB进程") + # 终止所有MongoDB进程,使用更强力的参数 + subprocess.run( + ['taskkill', '/F', '/IM', 'mongod.exe', '/T'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True + ) + self.log("已尝试终止所有MongoDB进程") + + # 等待进程终止 + time.sleep(2) + else: + self.log("没有检测到MongoDB进程") + break + except Exception as e: + self.log(f"终止MongoDB进程时出错: {str(e)}") + + retry_count += 1 + if retry_count < max_retries: + time.sleep(1) # 重试间隔 + + # Linux/macOS系统: 尝试终止所有MongoDB进程 + else: + try: + # 查找并终止所有mongod进程 + subprocess.run(['pkill', '-f', 'mongod'], check=False) + self.log("已尝试终止所有MongoDB进程") + except Exception as e: + self.log(f"终止MongoDB进程时出错: {str(e)}") - # 更新状态 - self.mongodb_running = False - self.update_ui_state() + # 增加等待时间,确保进程完全终止 + self.log("等待MongoDB进程完全终止...") + time.sleep(3) - self.log("MongoDB服务已停止") + # 验证MongoDB是否真正停止:检查端口和进程 + is_really_stopped = False + max_checks = 5 + check_count = 0 + + while check_count < max_checks: + # 检查端口是否被占用 + port_in_use = self.is_port_in_use(self.mongodb_port) + + # 检查进程是否存在 + process_exists = False + if sys.platform == 'win32': + result = subprocess.run( + ['tasklist', '/FI', 'IMAGENAME eq mongod.exe', '/NH'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + shell=True + ) + process_exists = 'mongod.exe' in result.stdout + else: + try: + result = subprocess.run( + ['pgrep', '-f', 'mongod'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + process_exists = result.returncode == 0 + except: + pass + + if not port_in_use and not process_exists: + is_really_stopped = True + break + + self.log(f"MongoDB似乎仍在运行,等待并再次检查...({check_count+1}/{max_checks})") + time.sleep(2) + check_count += 1 + + # 更新状态 + if is_really_stopped: + self.mongodb_running = False + self.update_ui_state() + self.log("MongoDB服务已成功停止") + else: + # 如果仍然检测到MongoDB运行,最后尝试一次强力终止 + if sys.platform == 'win32': + self.log("MongoDB似乎仍在运行,尝试最后一次强力终止...") + subprocess.run(['taskkill', '/F', '/IM', 'mongod.exe', '/T'], shell=True) + else: + subprocess.run(['pkill', '-9', '-f', 'mongod'], check=False) + + time.sleep(2) + # 最后再次检查 + port_in_use = self.is_port_in_use(self.mongodb_port) + if not port_in_use: + self.mongodb_running = False + self.update_ui_state() + self.log("MongoDB服务已成功停止") + else: + self.log("警告:无法确认MongoDB是否已完全停止,请手动检查") + messagebox.showwarning("警告", "无法确认MongoDB是否已完全停止,请手动检查端口和进程") except Exception as e: self.log(f"停止MongoDB失败: {str(e)}") messagebox.showerror("错误", f"停止MongoDB失败: {str(e)}") + # 即使出现异常,也尝试更新状态 + try: + if not self.is_port_in_use(self.mongodb_port): + self.mongodb_running = False + self.update_ui_state() + except: + pass def update_ui_state(self): """更新UI按钮状态""" @@ -1017,11 +1899,31 @@ def main(): 主函数入口 注意:此控制台使用Homebrew管理MongoDB社区版,部分代码注释由AI辅助生成 """ + # 在控制台根目录写入启动日志 + try: + log_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(log_dir, "server_manager.log") + current_time = time.strftime("%Y-%m-%d %H:%M:%S") + with open(log_file, "a", encoding="utf-8") as f: + f.write(f"[{current_time}] HalloChat服务器管理器启动\n") + except Exception as e: + print(f"写入日志失败: {str(e)}") + # 检查Node.js是否安装 try: subprocess.run(["node", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) except (subprocess.SubprocessError, FileNotFoundError): - print("错误: 未找到Node.js,请先安装Node.js") + error_message = "错误: 未找到Node.js,请先安装Node.js" + print(error_message) + # 将错误写入日志文件 + try: + log_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(log_dir, "server_manager.log") + current_time = time.strftime("%Y-%m-%d %H:%M:%S") + with open(log_file, "a", encoding="utf-8") as f: + f.write(f"[{current_time}] {error_message}\n") + except Exception as e: + print(f"写入错误日志失败: {str(e)}") sys.exit(1) # 检查npm是否安装 @@ -1042,7 +1944,17 @@ def main(): subprocess.run([npm_path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) except (subprocess.SubprocessError, FileNotFoundError): - print("错误: 未找到npm,请先安装Node.js和npm") + error_message = "错误: 未找到npm,请先安装npm" + print(error_message) + # 将错误写入日志文件 + try: + log_dir = os.path.dirname(os.path.abspath(__file__)) + log_file = os.path.join(log_dir, "server_manager.log") + current_time = time.strftime("%Y-%m-%d %H:%M:%S") + with open(log_file, "a", encoding="utf-8") as f: + f.write(f"[{current_time}] {error_message}\n") + except Exception as e: + print(f"写入错误日志失败: {str(e)}") sys.exit(1) # 创建并运行GUI diff --git a/server/scripts/migrate-add-uid.js b/server/scripts/migrate-add-uid.js new file mode 100644 index 0000000..1c24b82 --- /dev/null +++ b/server/scripts/migrate-add-uid.js @@ -0,0 +1,91 @@ +/** + * 数据库迁移脚本:为现有用户添加 UID 字段 + * + * 运行方式: + * node scripts/migrate-add-uid.js + */ + +const mongoose = require('mongoose'); +const User = require('../src/models/user.model'); + +// 从环境变量或配置文件读取数据库连接 +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/hallochat'; + +async function migrateUsers() { + try { + console.log('[迁移] 连接数据库...'); + await mongoose.connect(MONGODB_URI, { + useNewUrlParser: true, + useUnifiedTopology: true + }); + console.log('[迁移] 数据库连接成功!'); + + // 获取所有没有 UID 的用户 + const usersWithoutUID = await User.find({ uid: { $exists: false } }); + console.log(`[迁移] 找到 ${usersWithoutUID.length} 个需要迁移的用户`); + + if (usersWithoutUID.length === 0) { + console.log('[迁移] 所有用户已经有 UID,无需迁移'); + return; + } + + // 检查是否存在管理员用户(通过 role 或 username 判断) + const adminUser = usersWithoutUID.find( + user => user.role === 'admin' || user.username.toLowerCase() === 'admin' + ); + + let successCount = 0; + let failCount = 0; + + // 为管理员用户设置特殊 UID + if (adminUser) { + try { + adminUser.uid = 'ADMIN'; + await adminUser.save(); + console.log(`[迁移] ✅ 管理员用户 ${adminUser.username} 已设置 UID: ADMIN`); + successCount++; + } catch (error) { + console.error(`[迁移] ❌ 管理员用户 ${adminUser.username} UID 设置失败:`, error.message); + failCount++; + } + } + + // 为其他用户生成 UID + for (const user of usersWithoutUID) { + if (user.uid) continue; // 跳过已经设置过的(如管理员) + + try { + const uid = await User.generateUniqueUID(); + user.uid = uid; + await user.save(); + console.log(`[迁移] ✅ 用户 ${user.username} 已生成 UID: ${uid}`); + successCount++; + } catch (error) { + console.error(`[迁移] ❌ 用户 ${user.username} UID 生成失败:`, error.message); + failCount++; + } + } + + console.log('\n[迁移] ==================== 迁移完成 ===================='); + console.log(`[迁移] 成功: ${successCount} 个用户`); + console.log(`[迁移] 失败: ${failCount} 个用户`); + console.log('[迁移] ===================================================\n'); + + } catch (error) { + console.error('[迁移] 发生错误:', error); + } finally { + await mongoose.disconnect(); + console.log('[迁移] 数据库连接已关闭'); + } +} + +// 运行迁移 +migrateUsers() + .then(() => { + console.log('[迁移] 脚本执行完成'); + process.exit(0); + }) + .catch(error => { + console.error('[迁移] 脚本执行失败:', error); + process.exit(1); + }); diff --git a/server/src/models/user.model.js b/server/src/models/user.model.js index c6c0632..05462ef 100644 --- a/server/src/models/user.model.js +++ b/server/src/models/user.model.js @@ -37,6 +37,16 @@ const userSchema = new Schema({ default: () => uuidv1() }, + // 用户唯一标识符(以服务器为单位,用于识别和验证) + uid: { + type: String, + required: true, + unique: true, + uppercase: true, + trim: true, + index: true + }, + random_id: { type: String, required: true, @@ -132,6 +142,45 @@ userSchema.index({ email: 1 }); userSchema.index({ status: 1 }); userSchema.index({ lastLogin: 1 }); userSchema.index({ random_id: 1 }); +userSchema.index({ uid: 1 }, { unique: true }); + +// UID 生成器(以服务器为单位) +function generateUID() { + const timestamp = Date.now().toString(36).toUpperCase(); // 时间戳 + const random = Math.random().toString(36).substr(2, 6).toUpperCase(); // 随机字符 + return `${timestamp}${random}`; +} + +// 静态方法:生成唯一 UID +userSchema.statics.generateUniqueUID = async function() { + let uid; + let exists = true; + let attempts = 0; + const maxAttempts = 10; + + while (exists && attempts < maxAttempts) { + uid = generateUID(); + exists = await this.findOne({ uid }); + attempts++; + } + + if (exists) { + throw new Error('无法生成唯一 UID,请稍后重试'); + } + + return uid; +}; + +// 静态方法:检查 UID 是否存在 +userSchema.statics.isUIDExists = async function(uid) { + const user = await this.findOne({ uid }); + return !!user; +}; + +// 静态方法:通过 UID 查找用户 +userSchema.statics.findByUID = async function(uid) { + return await this.findOne({ uid }).select('-password'); +}; // 密码加密中间件 userSchema.pre('save', async function(next) { @@ -161,6 +210,7 @@ userSchema.methods.getPublicProfile = function() { id: this._id, username: this.username, email: this.email, + uid: this.uid, avatar: this.avatar, status: this.status, role: this.role, @@ -219,16 +269,22 @@ userSchema.statics.registerUser = async function(userData) { throw error; } + // 生成唯一 UID + const uid = await this.generateUniqueUID(); + // 创建用户 - 密码会通过pre('save')中间件自动哈希 const user = new this({ username, email, - password // 明文密码,由pre('save')中间件处理哈希 + password, // 明文密码,由pre('save')中间件处理哈希 + uid }); // 保存用户 await user.save(); + console.log(`[用户注册] 成功创建用户: ${username}, UID: ${uid}`); + // 返回用户的公开信息 return user.getPublicProfile(); }; @@ -253,10 +309,14 @@ userSchema.statics.getFriends = async function(userId) { }; // 用户登录验证 -async function loginUser(usernameOrEmail, plainPassword) { - // 支持通过用户名或邮箱登录 +async function loginUser(usernameOrEmailOrUID, plainPassword) { + // 支持通过用户名、邮箱或 UID 登录 const user = await User.findOne({ - $or: [{ username: usernameOrEmail }, { email: usernameOrEmail }] + $or: [ + { username: usernameOrEmailOrUID }, + { email: usernameOrEmailOrUID }, + { uid: usernameOrEmailOrUID.toUpperCase() } + ] }); if (!user) throw new Error('用户不存在'); @@ -267,10 +327,13 @@ async function loginUser(usernameOrEmail, plainPassword) { user.lastLogin = new Date(); await user.save(); + console.log(`[用户登录] ${user.username} (UID: ${user.uid}) 登录成功`); + return { id: user._id, username: user.username, email: user.email, + uid: user.uid, userUuid: user.user_uuid, randomId: user.random_id, avatar: user.avatar,