From a2a387fb6cbb5f44f4fdf6ece7c50922d80e3abe Mon Sep 17 00:00:00 2001 From: SANYOU-hash Date: Wed, 12 Nov 2025 01:13:46 +0800 Subject: [PATCH 01/26] =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8F=B0=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20=E4=BF=AE=E5=A4=8D=20=E5=A2=9E=E8=82=8Clinux?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=20mongb=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/manager/HalloChat.ico | Bin 0 -> 33240 bytes server/manager/requirements.txt | 3 +- server/manager/server_manager.log | 178 +++++ server/manager/server_manager.py | 1240 +++++++++++++++++++++++++---- 4 files changed, 1256 insertions(+), 165 deletions(-) create mode 100644 server/manager/HalloChat.ico create mode 100644 server/manager/server_manager.log diff --git a/server/manager/HalloChat.ico b/server/manager/HalloChat.ico new file mode 100644 index 0000000000000000000000000000000000000000..62fedc784a4f64ec23d8297a2f2904b6eea7ca42 GIT binary patch literal 33240 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.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 From 8ae95c194e0a964d8bd8bc0164ea41f24a5aec5d Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Thu, 20 Nov 2025 17:27:33 +0800 Subject: [PATCH 02/26] =?UTF-8?q?=E5=9C=A8=E5=AD=A6=E6=A0=A1=E6=9C=BA?= =?UTF-8?q?=E6=88=BF=E6=8F=90=E4=BA=A4Readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8823ef1..d9b6809 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**本仓库近期因为一些原因重建,丢失了原有所有的commit记录,仅有Gitee仓库留有完整备份。现在已经重建成功,欢迎提交。** +**本仓库近期因为一些原因重建,丢失了原有所有的commit记录,仅有Gitee仓库留有完整备份。现在已经重建成功,欢迎大神为我们的项目进行代码贡献。** **注意:** 1. 本项目为测试版本,存在已知问题和功能缺失,且不定期更新,如果有发现问题请及时提交issues联系我,感谢支持。 From 0d850e51aaddff2d7763b5f8e19897751482a320 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Sat, 22 Nov 2025 16:17:51 +0800 Subject: [PATCH 03/26] Update contact emails in README.md Updated contact email addresses for the project team and developer. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d9b6809..63acbc8 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 - 项目仓库:[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. 企业微信:(正在配置中) ## 项目概述 @@ -290,8 +290,8 @@ JWT_REFRESH_SECRET=your_refresh_secret - 项目团队企业微信(要求提供手机号,用于加入项目团队) 当你做好了以上准备,你可以通过以下方式联系我们: - 1. hallochatdev@hanjiang88luntan.eu.org(项目开发团队) - 2. d16671480856@163.com(项目开发者Ink-dark/墨染柒DarkSeven) + 1. dev@hallochat.cn(项目开发团队) + 2. moranqidarkseven@hallochoat.cn(项目开发者Ink-dark/墨染柒DarkSeven) 3. 企业微信:(因企业微信限制,目前暂无法直接提供添加方式) From 244f0f36b41cb4cc5cb593de566d678f07a3cfa9 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Sat, 6 Dec 2025 20:25:24 +0800 Subject: [PATCH 04/26] =?UTF-8?q?feat(=E8=81=8A=E5=A4=A9):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=9C=AC=E5=9C=B0=E6=B6=88=E6=81=AF=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=92=8C=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加本地存储功能以保存聊天消息,包括消息的增删改查 修改ContactList组件以使用静态联系人数据 在ChatWindow中添加模拟历史消息数据 --- client/src/components/ChatWindow.js | 57 ++++++++++++++++++++++++----- client/src/components/MainWindow.js | 15 +++++--- client/src/services/chatService.js | 45 ++++++++++++++++++++++- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/client/src/components/ChatWindow.js b/client/src/components/ChatWindow.js index 356773a..d5815ba 100644 --- a/client/src/components/ChatWindow.js +++ b/client/src/components/ChatWindow.js @@ -129,15 +129,54 @@ function ChatWindow({ currentUser, contact }) { }); // 加载历史消息 - 将函数移到内部以避免依赖问题 - const loadHistoryMessages = async () => { - try { - // 这里应该调用API获取历史消息 - // 暂时使用模拟数据 - console.log('加载历史消息:', contact.id); - } catch (err) { - setError('加载历史消息失败: ' + err.message); - } - }; +132→ const loadHistoryMessages = async () => { +133→ try { +134→ // 使用模拟历史消息数据 +135→ const mockHistoryMessages = [ +136→ { +137→ id: 'msg1', +138→ senderId: contact.id, +139→ receiverId: currentUser.id, +140→ content: '你好!最近怎么样?', +141→ type: 'text', +142→ timestamp: Date.now() - 3600000, +143→ isRead: true, +144→ isDelivered: true, +145→ status: 'delivered', +146→ syncStatus: 'synced' +147→ }, +148→ { +149→ id: 'msg2', +150→ senderId: currentUser.id, +151→ receiverId: contact.id, +152→ content: '我很好,谢谢!你呢?', +153→ type: 'text', +154→ timestamp: Date.now() - 3500000, +155→ isRead: true, +156→ isDelivered: true, +157→ status: 'delivered', +158→ syncStatus: 'synced' +159→ }, +160→ { +161→ id: 'msg3', +162→ senderId: contact.id, +163→ receiverId: currentUser.id, +164→ content: '我也不错,最近在忙什么?', +165→ type: 'text', +166→ timestamp: Date.now() - 3400000, +167→ isRead: true, +168→ isDelivered: true, +169→ status: 'delivered', +170→ syncStatus: 'synced' +171→ } +172→ ]; +173→ +174→ // 将模拟消息添加到状态中 +175→ setMessages(mockHistoryMessages); +176→ } catch (err) { +177→ setError('加载历史消息失败: ' + err.message); +178→ } +179→ }; loadHistoryMessages(); diff --git a/client/src/components/MainWindow.js b/client/src/components/MainWindow.js index 5f6c303..8e52391 100644 --- a/client/src/components/MainWindow.js +++ b/client/src/components/MainWindow.js @@ -54,16 +54,19 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { { + contacts={[ + { id: 'user2', username: '好友1', onlineStatus: true, isStarred: false, isPinned: false, type: 'user' }, + { id: 'user3', username: '好友2', onlineStatus: false, isStarred: true, isPinned: false, type: 'user' }, + { id: 'user4', username: '好友3', onlineStatus: true, isStarred: false, isPinned: true, type: 'user' }, + ]} + onSelectContact={(contact) => { setSelectedContact(contact); setSelectedGroup(null); setActiveView('chat'); }} - onGroupSelect={(group) => { - setSelectedGroup(group); - setSelectedContact(null); - setActiveView('group-chat'); - }} + onStartEncryptedChat={() => console.log('开始加密聊天')} + onCreateGroup={() => console.log('创建群组')} + onCreateChannel={() => console.log('创建频道')} /> diff --git a/client/src/services/chatService.js b/client/src/services/chatService.js index 06f5a2f..427797a 100644 --- a/client/src/services/chatService.js +++ b/client/src/services/chatService.js @@ -16,6 +16,44 @@ 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); + } + // 设置服务器地址 setServerAddress(address) { this.serverAddress = address; @@ -31,6 +69,8 @@ class ChatService { // 监听消息 this.socket.on('message', (messageData) => { const message = new Message(messageData); + // 保存接收的消息到本地存储 + this.saveMessageToLocal(message); this.messageHandlers.forEach(handler => handler(message)); }); @@ -72,7 +112,7 @@ class ChatService { // 发送消息 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 +123,9 @@ class ChatService { syncStatus: 'pending' }); + // 保存消息到本地存储 + this.saveMessageToLocal(message); + // 发送消息到服务器 this.socket.emit('message', { content, From 77d1c2250c3eda9441208fa2bee783fa3159faca Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Sat, 6 Dec 2025 20:27:37 +0800 Subject: [PATCH 05/26] =?UTF-8?q?feat(=E8=81=8A=E5=A4=A9=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?):=20=E6=B7=BB=E5=8A=A0=E6=B6=88=E6=81=AF=E9=87=8D=E5=8F=91?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E5=92=8CWebSocket=E8=BF=9E=E6=8E=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现未发送消息的自动重发功能,当连接恢复时自动同步pending状态的消息 优化WebSocket连接配置,添加重连机制和相关事件监听 --- client/src/services/chatService.js | 86 +++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/client/src/services/chatService.js b/client/src/services/chatService.js index 427797a..86264ed 100644 --- a/client/src/services/chatService.js +++ b/client/src/services/chatService.js @@ -54,6 +54,48 @@ class ChatService { 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; @@ -63,7 +105,13 @@ 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传输 }); // 监听消息 @@ -104,9 +152,43 @@ 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); }); } From a5d4c3c25a6f094d78f3a80e81ff6cbae556d70c Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Sat, 6 Dec 2025 20:36:56 +0800 Subject: [PATCH 06/26] =?UTF-8?q?style(ChatWindow):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E6=B6=88=E6=81=AF=E5=8A=A0=E8=BD=BD=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=9A=84=E7=BC=A9=E8=BF=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ChatWindow.js | 96 ++++++++++++++--------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/client/src/components/ChatWindow.js b/client/src/components/ChatWindow.js index d5815ba..379b85b 100644 --- a/client/src/components/ChatWindow.js +++ b/client/src/components/ChatWindow.js @@ -129,54 +129,54 @@ function ChatWindow({ currentUser, contact }) { }); // 加载历史消息 - 将函数移到内部以避免依赖问题 -132→ const loadHistoryMessages = async () => { -133→ try { -134→ // 使用模拟历史消息数据 -135→ const mockHistoryMessages = [ -136→ { -137→ id: 'msg1', -138→ senderId: contact.id, -139→ receiverId: currentUser.id, -140→ content: '你好!最近怎么样?', -141→ type: 'text', -142→ timestamp: Date.now() - 3600000, -143→ isRead: true, -144→ isDelivered: true, -145→ status: 'delivered', -146→ syncStatus: 'synced' -147→ }, -148→ { -149→ id: 'msg2', -150→ senderId: currentUser.id, -151→ receiverId: contact.id, -152→ content: '我很好,谢谢!你呢?', -153→ type: 'text', -154→ timestamp: Date.now() - 3500000, -155→ isRead: true, -156→ isDelivered: true, -157→ status: 'delivered', -158→ syncStatus: 'synced' -159→ }, -160→ { -161→ id: 'msg3', -162→ senderId: contact.id, -163→ receiverId: currentUser.id, -164→ content: '我也不错,最近在忙什么?', -165→ type: 'text', -166→ timestamp: Date.now() - 3400000, -167→ isRead: true, -168→ isDelivered: true, -169→ status: 'delivered', -170→ syncStatus: 'synced' -171→ } -172→ ]; -173→ -174→ // 将模拟消息添加到状态中 -175→ setMessages(mockHistoryMessages); -176→ } catch (err) { -177→ setError('加载历史消息失败: ' + err.message); -178→ } -179→ }; + const loadHistoryMessages = async () => { + try { + // 使用模拟历史消息数据 + const mockHistoryMessages = [ + { + id: 'msg1', + senderId: contact.id, + receiverId: currentUser.id, + content: '你好!最近怎么样?', + type: 'text', + timestamp: Date.now() - 3600000, + isRead: true, + isDelivered: true, + status: 'delivered', + syncStatus: 'synced' + }, + { + id: 'msg2', + senderId: currentUser.id, + receiverId: contact.id, + content: '我很好,谢谢!你呢?', + type: 'text', + timestamp: Date.now() - 3500000, + isRead: true, + isDelivered: true, + status: 'delivered', + syncStatus: 'synced' + }, + { + id: 'msg3', + senderId: contact.id, + receiverId: currentUser.id, + content: '我也不错,最近在忙什么?', + type: 'text', + timestamp: Date.now() - 3400000, + isRead: true, + isDelivered: true, + status: 'delivered', + syncStatus: 'synced' + } + ]; + + // 将模拟消息添加到状态中 + setMessages(mockHistoryMessages); + } catch (err) { + setError('加载历史消息失败: ' + err.message); + } + }; loadHistoryMessages(); From 7d6a04a996bcc43f671e60423b7fea77d943129d Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Sat, 6 Dec 2025 20:49:08 +0800 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E9=AA=8C=E8=AF=81=E5=92=8C=E6=A0=B7=E5=BC=8F=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ServerSelectionWindow 的 bodyStyle 迁移到 styles.body 以符合新版 antd 规范 - 移除 App 组件中不必要的 Layout 组件,改用 div 布局 - 重构 Login 组件表单处理逻辑,使用 Form 内置验证替代手动验证 - 简化错误处理逻辑,移除冗余的字段检查 --- client/src/App.js | 6 +-- client/src/components/Login.js | 40 ++++++++++--------- .../src/components/ServerSelectionWindow.js | 10 +++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/client/src/App.js b/client/src/App.js index 25b314f..f7e0902 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,6 +1,6 @@ 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 './App.css'; @@ -99,7 +99,7 @@ function App() { return ( - +
{activeView === 'main' && isAuthenticated && currentUser ? ( )} - +
); } diff --git a/client/src/components/Login.js b/client/src/components/Login.js index 6a0d81b..d54d354 100644 --- a/client/src/components/Login.js +++ b/client/src/components/Login.js @@ -56,18 +56,12 @@ const Login = ({ onLoginSuccess }) => { loadLastUsedServer(); }, [selectedServer]); - 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 (!selectedServer) { @@ -122,14 +116,12 @@ const Login = ({ onLoginSuccess }) => { } }; - const handleRegister = async () => { + const handleRegister = async (values) => { setIsLoading(true); setError(''); try { - if (!username || !password || !email) { - throw new Error('请填写所有必填字段'); - } + const { username, email, password } = values; if (password !== confirmPassword) { throw new Error('两次输入的密码不一致'); @@ -156,11 +148,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); } @@ -254,9 +245,20 @@ const Login = ({ onLoginSuccess }) => { ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致')); + }, + }), + ]} > - setConfirmPassword(e.target.value)} /> + + + {/* 登录/注册表单 */} + {isRegistering ? ( +
+ + } + placeholder="请输入您的邮箱" + size="large" + value={email} + onChange={(e) => setEmail(e.target.value)} + /> + + + } + placeholder="请输入用户名" + size="large" + value={username} + onChange={(e) => setUsername(e.target.value)} + /> + + + } + placeholder="请输入密码" + 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('两次输入的密码不一致')); + }, + }), + ]} + > + } + placeholder="请再次输入密码" + size="large" + /> + + + + +
+ 已有账号? +
+
+ ) : ( +
+ + } + placeholder="请输入用户名" + size="large" + value={username} + onChange={(e) => setUsername(e.target.value)} + /> + + + } + placeholder="请输入密码" + size="large" + value={password} + onChange={(e) => setPassword(e.target.value)} + /> + + + setRememberMe(e.target.checked)}> + 记住我 + + + + + +
+ 没有账号? +
+
+ )} + + {/* 服务器选择窗口 */} + setShowServerSelection(false)} + onServerSelected={handleServerSelected} + /> - - {/* 登录/注册表单 */} - {isRegistering ? ( -
- - setEmail(e.target.value)} /> - - - setUsername(e.target.value)} /> - - - setPassword(e.target.value)} /> - - ({ - validator(_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致')); - }, - }), - ]} - > - - - - - -
- 已有账号? -
-
- ) : ( -
- - setUsername(e.target.value)} /> - - - setPassword(e.target.value)} /> - - - setRememberMe(e.target.checked)}> - 记住我 - - - - - -
- 没有账号? -
-
- )} - - {/* 服务器选择窗口 */} - setShowServerSelection(false)} - onServerSelected={handleServerSelected} - /> ); }; diff --git a/client/src/components/ServerSelectionWindow.js b/client/src/components/ServerSelectionWindow.js index ce59bbe..217ed5e 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, @@ -29,7 +29,8 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { useEffect(() => { if (visible) { loadSavedServers(); - discoverLocalServers(); + // 移除局域网发现,因为它可能导致卡顿 + // discoverLocalServers(); } }, [visible]); @@ -241,12 +242,13 @@ 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 +259,10 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { } onClose(); - }; + }, [selectedServer, onServerSelected, onClose]); - // 服务器表格列定义 - const serverColumns = [ + // 服务器表格列定义 - 使用 useMemo 优化 + const serverColumns = useMemo(() => [ { title: '服务器名称', dataIndex: 'name', @@ -399,13 +401,13 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { ), }, - ]; + ], [selectedServer, serverStatuses, handleServerSelect]); - // 合并局域网服务器和已保存服务器 - 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 ( { padding: '16px 24px' } }} + destroyOnClose={false} + maskClosable={true} + keyboard={true} > {error && } @@ -451,7 +456,7 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { columns={serverColumns} dataSource={combinedServers} pagination={{ - pageSize: 5, + pageSize: 10, showSizeChanger: true, pageSizeOptions: ['5', '10', '20'], showTotal: (total) => `共 ${total} 个服务器` @@ -467,6 +472,8 @@ const ServerSelectionWindow = ({ visible, onClose, onServerSelected }) => { } })} scroll={{ x: 'max-content', y: 'calc(60vh - 120px)' }} + size="middle" + loading={false} /> ) : (
@@ -527,4 +534,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 From 13b0208e068c4e08851110156bd797f509e242f7 Mon Sep 17 00:00:00 2001 From: SANYOU-hash Date: Sat, 3 Jan 2026 16:38:09 +0800 Subject: [PATCH 11/26] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=87=AA=E8=BF=B0?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加开发者介绍 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 63acbc8..9a2ac4b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,14 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 // 版权所有 © 2025 Ink-dark(墨染柒DarkSeven) // 遵循 MIT 开源许可证 ``` +## 项目开发者 +### 墨染染 +- 邮箱:moranqidarkseven@hallochat.cn + +### SANYOU (LUCA.NEX) +- 邮箱:you.san1@icloud.com +- 个人网站:lucanex.top + ## 联系我们 - 项目仓库:[HalloChat](https://github.com/Ink-dark/HalloChat) - 问题反馈:[Issues](https://github.com/Ink-dark/HalloChat/issues) From 90cb2c2fd297ae7a87ad53d75450860fc8593b5d Mon Sep 17 00:00:00 2001 From: SANYOU-hash Date: Sat, 3 Jan 2026 16:47:53 +0800 Subject: [PATCH 12/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a2ac4b..35cfe73 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 ### SANYOU (LUCA.NEX) - 邮箱:you.san1@icloud.com -- 个人网站:lucanex.top +- 个人网站:[lucanex.top](https://www.lucanex.top/) ## 联系我们 - 项目仓库:[HalloChat](https://github.com/Ink-dark/HalloChat) From d30c099f57abbd4e2b3e6dec830adea09d4eb017 Mon Sep 17 00:00:00 2001 From: Ink-dark Date: Sat, 3 Jan 2026 09:05:24 +0000 Subject: [PATCH 13/26] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Readme=20=E4=B8=AD?= =?UTF-8?q?=20=E9=83=A8=E5=88=86=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ink-dark --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35cfe73..f5b5856 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ HalloChat 是一款实时聊天应用,客户端版本:v0.2.0 // 遵循 MIT 开源许可证 ``` ## 项目开发者 -### 墨染染 +### 墨染柒DarkSeven - 邮箱:moranqidarkseven@hallochat.cn +- 个人博客:[墨染柒的个人博客](Ink-dark.github.io) ### SANYOU (LUCA.NEX) - 邮箱:you.san1@icloud.com From f7b01953594e1446c1b01f50a4813c86e2369042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=B5=A9=E5=B8=85=EF=BC=88SYSANYOU=EF=BC=89?= Date: Mon, 19 Jan 2026 01:35:37 +0800 Subject: [PATCH 14/26] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E6=94=AF=E6=8C=81=EF=BC=9A=E7=AE=80=E4=BD=93=E4=B8=AD?= =?UTF-8?q?=E6=96=87=20=E7=B9=81=E4=BD=93=E4=B8=AD=E6=96=87=20=E4=BF=84?= =?UTF-8?q?=E6=96=87=20=E8=8B=B1=E6=96=87=20=EF=BC=88=E5=90=8E=E7=BB=AD?= =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E6=94=AF=E6=8C=81=E5=85=B6=E4=BB=96=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package.json | 2 + client/src/components/Login.css | 80 +++++++++++++++++++ client/src/components/Login.js | 117 ++++++++++++++++++---------- client/src/components/MainWindow.js | 12 +-- client/src/components/Settings.js | 93 +++++++++++++--------- client/src/i18n/config.js | 71 +++++++++++++++++ client/src/i18n/locales/en-US.json | 95 ++++++++++++++++++++++ client/src/i18n/locales/ru-RU.json | 95 ++++++++++++++++++++++ client/src/i18n/locales/zh-CN.json | 95 ++++++++++++++++++++++ client/src/i18n/locales/zh-TW.json | 95 ++++++++++++++++++++++ client/src/index.js | 1 + 11 files changed, 676 insertions(+), 80 deletions(-) create mode 100644 client/src/i18n/config.js create mode 100644 client/src/i18n/locales/en-US.json create mode 100644 client/src/i18n/locales/ru-RU.json create mode 100644 client/src/i18n/locales/zh-CN.json create mode 100644 client/src/i18n/locales/zh-TW.json diff --git a/client/package.json b/client/package.json index 677eee8..a653d54 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", diff --git a/client/src/components/Login.css b/client/src/components/Login.css index ef6d336..ab75490 100644 --- a/client/src/components/Login.css +++ b/client/src/components/Login.css @@ -57,6 +57,86 @@ 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; diff --git a/client/src/components/Login.js b/client/src/components/Login.js index 76db0ef..575aff0 100644 --- a/client/src/components/Login.js +++ b/client/src/components/Login.js @@ -1,14 +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 { UserOutlined, LockOutlined, MailOutlined, CloudServerOutlined, CheckCircleOutlined } from '@ant-design/icons'; +import { Form, Input, Button, Checkbox, Alert, message, Select } from 'antd'; +import { UserOutlined, LockOutlined, MailOutlined, CloudServerOutlined, CheckCircleOutlined, GlobalOutlined } 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'); @@ -65,7 +69,7 @@ const Login = ({ onLoginSuccess }) => { // 检查是否已选择服务器 if (!selectedServer) { - throw new Error('请先选择服务器'); + throw new Error(t('login.pleaseSelectServer')); } const fullAddress = `${selectedServer.address}:${selectedServer.port}`; @@ -125,7 +129,7 @@ const Login = ({ onLoginSuccess }) => { // 检查是否已选择服务器 if (!selectedServer) { - throw new Error('请先选择服务器'); + throw new Error(t('login.pleaseSelectServer')); } const fullAddress = `${selectedServer.address}:${selectedServer.port}`; @@ -157,7 +161,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})`); }; // 打开服务器选择窗口 @@ -165,25 +169,58 @@ const Login = ({ onLoginSuccess }) => { setShowServerSelection(true); }; + // 处理语言切换 + const handleLanguageChange = (language) => { + i18n.changeLanguage(language); + }; + + // 获取语言显示名称 + const getLanguageLabel = (lang) => { + const labels = { + 'zh-CN': '简体中文', + 'zh-TW': '繁體中文', + 'en-US': 'English', + 'ru-RU': 'Русский' + }; + return labels[lang] || lang; + }; + return (
+ {/* 语言切换按钮 */} +
+ +
+ {/* Logo 和标题区域 */}
HalloChat Logo
-

欢迎使用 HalloChat

-

安全、快速、可靠的即时通讯平台

+

{t('login.title')}

+

{t('login.subtitle')}

- {error && } + {error && } {/* 服务器选择区域 */}
- 服务器配置 + {t('login.serverConfig')}
{/* 当前选择的服务器 */} @@ -191,18 +228,18 @@ const Login = ({ onLoginSuccess }) => {
- 服务器名称: + {t('login.serverName')}: {selectedServer.name}
- 服务器地址: + {t('login.serverAddress')}: {selectedServer.address}:{selectedServer.port}
) : (
- 未选择服务器,请先选择服务器 + {t('login.noServerSelected')}
)} @@ -213,7 +250,7 @@ const Login = ({ onLoginSuccess }) => { icon={} className="server-select-btn" > - {selectedServer ? '更换服务器' : '选择服务器'} + {selectedServer ? t('login.changeServer') : t('login.selectServer')}
@@ -222,12 +259,12 @@ const Login = ({ onLoginSuccess }) => {
} - placeholder="请输入您的邮箱" + placeholder={t('login.email')} size="large" value={email} onChange={(e) => setEmail(e.target.value)} @@ -235,15 +272,15 @@ const Login = ({ onLoginSuccess }) => { } - placeholder="请输入用户名" + placeholder={t('login.username')} size="large" value={username} onChange={(e) => setUsername(e.target.value)} @@ -251,18 +288,18 @@ const Login = ({ onLoginSuccess }) => { } - placeholder="请输入密码" + placeholder={t('login.password')} size="large" value={password} onChange={(e) => setPassword(e.target.value)} @@ -270,23 +307,23 @@ const Login = ({ onLoginSuccess }) => { ({ validator(_, value) { if (!value || getFieldValue('password') === value) { return Promise.resolve(); } - return Promise.reject(new Error('两次输入的密码不一致')); + return Promise.reject(new Error(t('login.passwordNotMatch'))); }, }), ]} > } - placeholder="请再次输入密码" + placeholder={t('login.confirmPassword')} size="large" /> @@ -298,26 +335,26 @@ const Login = ({ onLoginSuccess }) => { loading={isLoading} className="login-submit-btn" > - 立即注册 + {t('login.registerButton')}
- 已有账号? + {t('login.hasAccount')}
) : (
} - placeholder="请输入用户名" + placeholder={t('login.username')} size="large" value={username} onChange={(e) => setUsername(e.target.value)} @@ -325,12 +362,12 @@ const Login = ({ onLoginSuccess }) => { } - placeholder="请输入密码" + placeholder={t('login.password')} size="large" value={password} onChange={(e) => setPassword(e.target.value)} @@ -338,7 +375,7 @@ const Login = ({ onLoginSuccess }) => { setRememberMe(e.target.checked)}> - 记住我 + {t('login.rememberMe')} @@ -349,11 +386,11 @@ const Login = ({ onLoginSuccess }) => { loading={isLoading} className="login-submit-btn" > - 立即登录 + {t('login.loginButton')}
- 没有账号? + {t('login.noAccount')}
)} diff --git a/client/src/components/MainWindow.js b/client/src/components/MainWindow.js index 8e52391..2846b68 100644 --- a/client/src/components/MainWindow.js +++ b/client/src/components/MainWindow.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import ContactList from './ContactList'; import ChatWindow from './ChatWindow'; import GroupChatWindow from './GroupChatWindow'; @@ -8,6 +9,7 @@ import Notification from './Notification'; import './MainWindow.css'; const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { + const { t } = useTranslation(); const [activeView, setActiveView] = useState('contacts'); const [selectedContact, setSelectedContact] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); @@ -50,7 +52,7 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { className="settings-btn" onClick={() => setShowSettings(!showSettings)} > - 设置 + {t('main.settings')} {
{!currentUser && (
-

请先登录

-

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

+

{t('main.pleaseLogin')}

+

{t('main.redirectingToLogin')}

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

欢迎回来,{currentUser.username}

-

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

+

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

+

{t('main.selectContactToChat')}

)} diff --git a/client/src/components/Settings.js b/client/src/components/Settings.js index 8447c9b..7c313ca 100644 --- a/client/src/components/Settings.js +++ b/client/src/components/Settings.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import './Settings.css'; /** @@ -6,6 +7,7 @@ import './Settings.css'; * 在多窗口架构中,设置的更改会通过props传递到主应用 */ const Settings = ({ currentUser, onLogout, onSettingsChange }) => { + const { t, i18n } = useTranslation(); const [settings, setSettings] = useState({ sidebarStyle: 'default', chatListStarred: false, @@ -68,27 +70,48 @@ const Settings = ({ currentUser, onLogout, onSettingsChange }) => { } }; + /** + * 处理语言变更 + */ + const handleLanguageChange = (language) => { + i18n.changeLanguage(language); + }; + return (
-

设置

- {currentUser &&

当前用户: {currentUser.username}

} +

{t('settings.title')}

+ {currentUser &&

{t('settings.currentUser')}: {currentUser.username}

} + +
+

{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..5c36c93 --- /dev/null +++ b/client/src/i18n/locales/en-US.json @@ -0,0 +1,95 @@ +{ + "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" + }, + "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..dca7f66 --- /dev/null +++ b/client/src/i18n/locales/ru-RU.json @@ -0,0 +1,95 @@ +{ + "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": "Ошибка" + }, + "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..2712529 --- /dev/null +++ b/client/src/i18n/locales/zh-CN.json @@ -0,0 +1,95 @@ +{ + "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": "错误提示" + }, + "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..07990e3 --- /dev/null +++ b/client/src/i18n/locales/zh-TW.json @@ -0,0 +1,95 @@ +{ + "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": "錯誤提示" + }, + "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 客户端启动中...'); From 71b964b9abb29edce94fee3aad57131c65211ebd Mon Sep 17 00:00:00 2001 From: sanyou-hash Date: Mon, 19 Jan 2026 01:51:09 +0800 Subject: [PATCH 15/26] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E8=BF=9B=E5=85=A5=E6=B5=8B=E8=AF=95=20=EF=BC=88?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E5=8F=91=E5=B8=83=E6=97=B6=E5=8F=96=E6=B6=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Login.js | 55 ++++++++++++++++++++++-------- client/src/i18n/locales/en-US.json | 5 ++- client/src/i18n/locales/ru-RU.json | 5 ++- client/src/i18n/locales/zh-CN.json | 5 ++- client/src/i18n/locales/zh-TW.json | 5 ++- 5 files changed, 57 insertions(+), 18 deletions(-) diff --git a/client/src/components/Login.js b/client/src/components/Login.js index 575aff0..b49b816 100644 --- a/client/src/components/Login.js +++ b/client/src/components/Login.js @@ -35,6 +35,9 @@ const Login = ({ onLoginSuccess }) => { const [isLoading, setIsLoading] = useState(false); const [isRegistering, setIsRegistering] = useState(false); const [email, setEmail] = useState(''); + + // 管理员模式状态 + const [adminMode, setAdminMode] = useState(false); useEffect(() => { // 从localStorage加载上次使用的服务器信息 @@ -67,6 +70,32 @@ const Login = ({ onLoginSuccess }) => { try { 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(t('login.pleaseSelectServer')); @@ -174,17 +203,6 @@ const Login = ({ onLoginSuccess }) => { i18n.changeLanguage(language); }; - // 获取语言显示名称 - const getLanguageLabel = (lang) => { - const labels = { - 'zh-CN': '简体中文', - 'zh-TW': '繁體中文', - 'en-US': 'English', - 'ru-RU': 'Русский' - }; - return labels[lang] || lang; - }; - return (
@@ -374,9 +392,18 @@ const Login = ({ onLoginSuccess }) => { /> - setRememberMe(e.target.checked)}> - {t('login.rememberMe')} - +
+ setRememberMe(e.target.checked)}> + {t('login.rememberMe')} + + setAdminMode(e.target.checked)} + style={{ color: '#667eea' }} + > + 🔧 {t('login.adminMode')} + +
{/* Logo 和标题区域 */} diff --git a/client/src/i18n/locales/en-US.json b/client/src/i18n/locales/en-US.json index 2fb140a..15e5139 100644 --- a/client/src/i18n/locales/en-US.json +++ b/client/src/i18n/locales/en-US.json @@ -30,7 +30,10 @@ "errorTitle": "Error", "adminMode": "Admin Mode", "adminModeSuccess": "Admin mode login successful", - "adminCodeError": "Admin code is incorrect" + "adminCodeError": "Admin code is incorrect", + "selectBgColor": "Select Background Color", + "bgColorChanged": "Background color changed", + "languageSetting": "Language Setting" }, "settings": { "title": "Settings", diff --git a/client/src/i18n/locales/ru-RU.json b/client/src/i18n/locales/ru-RU.json index 767a41f..0930fcc 100644 --- a/client/src/i18n/locales/ru-RU.json +++ b/client/src/i18n/locales/ru-RU.json @@ -30,7 +30,10 @@ "errorTitle": "Ошибка", "adminMode": "Режим администратора", "adminModeSuccess": "Вход в режиме администратора выполнен успешно", - "adminCodeError": "Неверный код администратора" + "adminCodeError": "Неверный код администратора", + "selectBgColor": "Выберите цвет фона", + "bgColorChanged": "Цвет фона изменен", + "languageSetting": "Настройка языка" }, "settings": { "title": "Настройки", diff --git a/client/src/i18n/locales/zh-CN.json b/client/src/i18n/locales/zh-CN.json index 0a1bb39..436e1ca 100644 --- a/client/src/i18n/locales/zh-CN.json +++ b/client/src/i18n/locales/zh-CN.json @@ -30,7 +30,10 @@ "errorTitle": "错误提示", "adminMode": "管理员模式", "adminModeSuccess": "管理员模式登录成功", - "adminCodeError": "管理员代码错误" + "adminCodeError": "管理员代码错误", + "selectBgColor": "选择背景颜色", + "bgColorChanged": "背景颜色已更改", + "languageSetting": "语言设置" }, "settings": { "title": "设置", diff --git a/client/src/i18n/locales/zh-TW.json b/client/src/i18n/locales/zh-TW.json index bc96b0a..72a6719 100644 --- a/client/src/i18n/locales/zh-TW.json +++ b/client/src/i18n/locales/zh-TW.json @@ -30,7 +30,10 @@ "errorTitle": "錯誤提示", "adminMode": "管理員模式", "adminModeSuccess": "管理員模式登錄成功", - "adminCodeError": "管理員代碼錯誤" + "adminCodeError": "管理員代碼錯誤", + "selectBgColor": "選擇背景顏色", + "bgColorChanged": "背景顏色已更改", + "languageSetting": "語言設置" }, "settings": { "title": "設置", From 65afa010110da9689e616e94c72ea21d142eef69 Mon Sep 17 00:00:00 2001 From: sanyou-hash Date: Tue, 20 Jan 2026 01:48:10 +0800 Subject: [PATCH 17/26] =?UTF-8?q?=E5=AF=B9ui=E8=BF=9B=E8=A1=8C=E5=A4=A7?= =?UTF-8?q?=E5=9E=8B=E6=94=B9=E5=8A=A8=20=E6=B7=BB=E5=8A=A0uid=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/.npmrc | 4 +- client/package.json | 3 + client/src/components/ChatWindow.css | 340 +++++++++----------------- client/src/components/ChatWindow.js | 262 +++++--------------- client/src/components/ContactList.css | 276 +++++++++++++++++++-- client/src/components/ContactList.js | 254 +++++++++---------- client/src/components/MainWindow.css | 7 +- client/src/components/MainWindow.js | 193 ++++++++++----- client/src/components/Settings.css | 210 ++++++++++++++++ client/src/components/Settings.js | 73 +++++- client/src/services/contactService.js | 93 +++++++ client/src/services/messageService.js | 102 ++++++++ server/scripts/migrate-add-uid.js | 91 +++++++ server/src/models/user.model.js | 71 +++++- 14 files changed, 1320 insertions(+), 659 deletions(-) create mode 100644 client/src/services/contactService.js create mode 100644 client/src/services/messageService.js create mode 100644 server/scripts/migrate-add-uid.js 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/package.json b/client/package.json index a653d54..c212062 100644 --- a/client/package.json +++ b/client/package.json @@ -68,6 +68,9 @@ "react-app/jest" ] }, + "overrides": { + "typescript": "^4.9.5" + }, "browserslist": { "production": [ ">0.2%", 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 379b85b..c2b403f 100644 --- a/client/src/components/ChatWindow.js +++ b/client/src/components/ChatWindow.js @@ -1,4 +1,12 @@ import React, { useState, useEffect, useRef } from 'react'; +import { + PhoneOutlined, + VideoCameraOutlined, + AppstoreOutlined, + PaperClipOutlined, + SmileOutlined, + SendOutlined +} from '@ant-design/icons'; import Message from '../models/message'; import chatService from '../services/chatService'; import './ChatWindow.css'; @@ -13,6 +21,15 @@ function ChatWindow({ currentUser, contact }) { const [isGroupChat, setIsGroupChat] = useState(false); const messageMenuRef = useRef(null); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); // 初始化聊天类型(单聊或群聊) useEffect(() => { @@ -52,126 +69,35 @@ 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 { - // 使用模拟历史消息数据 const mockHistoryMessages = [ { id: 'msg1', senderId: contact.id, receiverId: currentUser.id, - content: '你好!最近怎么样?', + content: 'Can you send me the files?', type: 'text', timestamp: Date.now() - 3600000, isRead: true, isDelivered: true, status: 'delivered', syncStatus: 'synced' - }, - { - id: 'msg2', - senderId: currentUser.id, - receiverId: contact.id, - content: '我很好,谢谢!你呢?', - type: 'text', - timestamp: Date.now() - 3500000, - isRead: true, - isDelivered: true, - status: 'delivered', - syncStatus: 'synced' - }, - { - id: 'msg3', - senderId: contact.id, - receiverId: currentUser.id, - content: '我也不错,最近在忙什么?', - type: 'text', - timestamp: Date.now() - 3400000, - isRead: true, - isDelivered: true, - status: 'delivered', - syncStatus: 'synced' } ]; - - // 将模拟消息添加到状态中 setMessages(mockHistoryMessages); } catch (err) { setError('加载历史消息失败: ' + err.message); @@ -186,27 +112,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 { @@ -228,121 +148,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..a1d9f1a 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,38 @@ const ContactList = ({ onStartEncryptedChat, onCreateGroup, onCreateChannel, + onShowSettings, + onLogout, + isLoading = false, + serverConnected = false, settings = {} }) => { - const handleSettingChange = (contactId, settings) => { - // 处理联系人设置变更的逻辑 - console.log('Contact settings changed:', contactId, settings); - }; - - 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; - } - - 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([]); + const [showDropdown, setShowDropdown] = useState(false); + const dropdownRef = useRef(null); + + // 点击外部关闭下拉菜单 + 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 +57,98 @@ 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(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/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 2846b68..4d02fe1 100644 --- a/client/src/components/MainWindow.js +++ b/client/src/components/MainWindow.js @@ -5,7 +5,8 @@ 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 }) => { @@ -15,6 +16,9 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { const [selectedGroup, setSelectedGroup] = useState(null); 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, @@ -22,6 +26,15 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { theme: 'light' }); + // 模拟数据(用于演示模式) + const mockContacts = [ + { 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) { @@ -32,6 +45,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]); + const handleLogout = () => { setShowLogout(true); }; @@ -46,69 +123,65 @@ const MainWindow = ({ currentUser, onLoginSuccess, onLogout }) => { return (
- -
- - { - setSelectedContact(contact); - setSelectedGroup(null); - setActiveView('chat'); - }} - onStartEncryptedChat={() => console.log('开始加密聊天')} - onCreateGroup={() => console.log('创建群组')} - onCreateChannel={() => console.log('创建频道')} + onLogout={handleConfirmLogout} + onSettingsChange={setSettings} + onBack={() => setShowSettings(false)} /> -
- -
- {!currentUser && ( -
-

{t('main.pleaseLogin')}

-

{t('main.redirectingToLogin')}

+ ) : ( + <> +
+ { + setSelectedContact(contact); + setSelectedGroup(null); + setActiveView('chat'); + }} + onStartEncryptedChat={() => console.log('开始加密聊天')} + onCreateGroup={() => console.log('创建群组')} + onCreateChannel={() => console.log('创建频道')} + onShowSettings={() => setShowSettings(true)} + onLogout={handleLogout} + />
- )} - - {currentUser && activeView === 'contacts' && ( -
-

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

-

{t('main.selectContactToChat')}

+ +
+ {!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/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 7c313ca..75a8c2a 100644 --- a/client/src/components/Settings.js +++ b/client/src/components/Settings.js @@ -1,12 +1,13 @@ 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', @@ -17,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', @@ -78,10 +84,31 @@ const Settings = ({ currentUser, onLogout, onSettingsChange }) => { }; return ( -
-

{t('settings.title')}

- {currentUser &&

{t('settings.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')}

handleSettingChange('notificationSound', e.target.checked)} + /> + 声音提醒 + + + + + + + +