diff --git a/.github/ISSUE_TEMPLATE/i18n_request.yml b/.github/ISSUE_TEMPLATE/i18n_request.yml index a1f9a6b2..451e8b62 100644 --- a/.github/ISSUE_TEMPLATE/i18n_request.yml +++ b/.github/ISSUE_TEMPLATE/i18n_request.yml @@ -55,4 +55,4 @@ body: label: 软件版本 / Verge Version description: 请提供你使用的 Verge 具体版本 / Please provide the specific version of Verge you are using validations: - required: true \ No newline at end of file + required: true diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml new file mode 100644 index 00000000..1eef2560 --- /dev/null +++ b/.github/workflows/fmt.yml @@ -0,0 +1,51 @@ +# Copyright 2019-2024 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: Check Formatting + +on: + pull_request: + +jobs: + rustfmt: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: install Rust stable and rustfmt + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: run cargo fmt + run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check + + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm i -g --force corepack + - uses: actions/setup-node@v4 + with: + node-version: "lts/*" + cache: "pnpm" + - run: pnpm i --frozen-lockfile + - run: pnpm format:check + + # taplo: + # name: taplo (.toml files) + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + + # - name: install Rust stable + # uses: dtolnay/rust-toolchain@stable + + # - name: install taplo-cli + # uses: taiki-e/install-action@v2 + # with: + # tool: taplo-cli + + # - run: taplo fmt --check --diff diff --git a/.husky/pre-commit b/.husky/pre-commit index 97749242..4f437094 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,15 +2,23 @@ #pnpm pretty-quick --staged -# 运行 clippy fmt -cd src-tauri -cargo fmt - -if [ $? -ne 0 ]; then - echo "rustfmt failed to format the code. Please fix the issues and try again." - exit 1 +if git diff --cached --name-only | grep -q '^src/'; then + pnpm format:check + if [ $? -ne 0 ]; then + echo "Code format check failed in src/. Please fix formatting issues." + exit 1 + fi +fi + +if git diff --cached --name-only | grep -q '^src-tauri/'; then + cd src-tauri + cargo fmt + if [ $? -ne 0 ]; then + echo "rustfmt failed to format the code. Please fix the issues and try again." + exit 1 + fi + cd .. fi -cd .. git add . diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..34a8e568 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +# README.md +# UPDATELOG.md +# CONTRIBUTING.md + +pnpm-lock.yaml + +src-tauri/target/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6ca6fdd2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": false, + "trailingComma": "none", + "experimentalOperatorPosition": "start" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c19b67e7..7ad87050 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,12 +33,15 @@ npm install pnpm -g ``` ### Install Dependencies + Install node packages + ```shell pnpm install ``` Install apt packages ONLY for Ubuntu + ```shell apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf ``` @@ -105,20 +108,25 @@ pnpm portable If you changed the rust code, it's recommanded to execute code style formatting and quailty checks. -1. Code style formatting +1. Code quailty checks ```bash +# For rust backend +$ clash-verge-rev: pnpm clippy +# For frontend (not yet). +``` + +2. Code style formatting + +```bash +# For rust backend $ clash-verge-rev: cd src-tauri $ clash-verge-rev/src-tauri: cargo fmt +# For frontend +$ clash-verge-rev: pnpm format:check +$ clash-verge-rev: pnpm format ``` -2. Code quailty checks - -```bash -$ clash-verge-rev: pnpm clippy -``` - - Once you have made your changes: 1. Fork the repository. diff --git a/README.md b/README.md index 724674f7..6aa3321b 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple). #### 我应当怎样选择发行版 -| 版本 | 特征 | 链接 | -|:-----|:-----|:-----| -|Stable|正式版,高可靠性,适合日常使用。|[Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | -|Alpha|早期测试版,功能未完善,可能存在缺陷。|[Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha)| -|AutoBuild|滚动更新版,持续集成更新,适合开发测试。|[AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild)| +| 版本 | 特征 | 链接 | +| :-------- | :--------------------------------------- | :------------------------------------------------------------------------------------- | +| Stable | 正式版,高可靠性,适合日常使用。 | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) | +| Alpha | 早期测试版,功能未完善,可能存在缺陷。 | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) | +| AutoBuild | 滚动更新版,持续集成更新,适合开发测试。 | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) | -#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看 +#### 安装说明和常见问题,请到 [文档页](https://clash-verge-rev.github.io/) 查看 --- @@ -49,11 +49,12 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple). - 解锁流媒体及 ChatGPT - 官网:[https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6) - #### 本项目的构建与发布环境由 [YXVM](https://yxvm.com/aff.php?aff=827) 独立服务器全力支持, + 感谢提供 独享资源、高性能、高速网络 的强大后端环境。如果你觉得下载够快、使用够爽,那是因为我们用了好服务器! 🧩 YXVM 独立服务器优势: + - 🌎 优质网络,回程优化,下载快到飞起 - 🔧 物理机独享资源,非VPS可比,性能拉满 - 🧠 适合跑代理、搭建 WEB 站 CDN 站 、搞 CI/CD 或任何高负载应用 diff --git a/UPDATELOG.md b/UPDATELOG.md index 217bf638..4583c7b9 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -3,141 +3,153 @@ 尽管外部控制密钥已自动补全默认值且不允许为空。仍然推荐自行修改外部控制密钥。 #### ⚠️ 已知问题 - - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 - - MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - - 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。 - - MacOS 下卸载服务后需手动重启软件才能与内核通信。 + +- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 +- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 +- 窗口状态管理器已确定上游存在缺陷,暂时移除。当前不再内置窗口大小和位置记忆。 +- MacOS 下卸载服务后需手动重启软件才能与内核通信。 ### 2.3.0 相对于 2.2.3 + #### 🐞 修复问题 - - 首页"代理模式"快速切换导致的卡死问题 - - 解锁测试报错信息 - - Macos 快捷键关闭窗口无法启用自动轻量模式 - - 静默启动异常窗口创建和关闭流程 - - Windows 错误的全局快捷键 `Ctrl+Q` 注册 - - Vless URL 解码时网络类型错误 - - 切换自定义代理地址导致系统代理状态异常 - - Macos TUN 默认无效网卡名称 - - 托盘更改订阅后 UI 不同步的问题 - - 修复提权漏洞,改用带认证的 IPC 通信 - - 编辑器中连字符问题 - - 安装服务模式后无法立即开启 TUN 模式 - - 同步更新多语言翻译 - - 修复 .window-state.json 无法删除的问题 - - 无法修改配置更新 HTTP 请求超时 - - 修复 getDelayFix 钩子问题 - - 使用外部扩展脚本覆写代理组时首页无法显示代理组 - - 导出诊断 Verge 版本与设置页面不同步 - - 切换语言时可能造成设置页面无法加载 + +- 首页"代理模式"快速切换导致的卡死问题 +- 解锁测试报错信息 +- Macos 快捷键关闭窗口无法启用自动轻量模式 +- 静默启动异常窗口创建和关闭流程 +- Windows 错误的全局快捷键 `Ctrl+Q` 注册 +- Vless URL 解码时网络类型错误 +- 切换自定义代理地址导致系统代理状态异常 +- Macos TUN 默认无效网卡名称 +- 托盘更改订阅后 UI 不同步的问题 +- 修复提权漏洞,改用带认证的 IPC 通信 +- 编辑器中连字符问题 +- 安装服务模式后无法立即开启 TUN 模式 +- 同步更新多语言翻译 +- 修复 .window-state.json 无法删除的问题 +- 无法修改配置更新 HTTP 请求超时 +- 修复 getDelayFix 钩子问题 +- 使用外部扩展脚本覆写代理组时首页无法显示代理组 +- 导出诊断 Verge 版本与设置页面不同步 +- 切换语言时可能造成设置页面无法加载 #### ✨ 新增功能 - - Mihomo(Meta)内核升级至 1.19.10 - - 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好 - - 关闭系统代理时关闭已建立的网络连接 - - 托盘显示当前轻量模式状态 - - Webdav 请求加入 UA - - Webdav 支持目录重定向 - - Webdav 备份目录检查和文件上传重试机制 - - 系统代理守卫可检查意外设置变更并恢复 - - 定时自动订阅更新也能自动回退使用代理 - - 订阅请求超时机制,防止订阅更新的时候卡死 - - 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示 - - 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死 - - 更新依赖,替换弃用元素 - - 首页当前节点增加排序功能 - - DNS 覆写下增加 Hosts 设置功能 - - 修复服务模式安装后无法立即开启 TUN 模式的问题 - - 支持手动卸载服务模式,回退到 Sidecar 模式 - - 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持 - - 卸载服务的按钮 - - 添加了Zashboard的一键跳转URL - - 使用操作系统默认的窗口管理器 - - 切换、升级、重启内核的状态管理 - - 更精细化控制自动日志清理,新增1天选项 - - Winodws 快捷键名称改为 `Clash Verge` - - 配置加载阶段自动补全 external-controller secret 字段。 + +- Mihomo(Meta)内核升级至 1.19.10 +- 允许代理主机地址设置为非 127.0.0.1 对 WSL 代理友好 +- 关闭系统代理时关闭已建立的网络连接 +- 托盘显示当前轻量模式状态 +- Webdav 请求加入 UA +- Webdav 支持目录重定向 +- Webdav 备份目录检查和文件上传重试机制 +- 系统代理守卫可检查意外设置变更并恢复 +- 定时自动订阅更新也能自动回退使用代理 +- 订阅请求超时机制,防止订阅更新的时候卡死 +- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示 +- 添加网络管理器以优化网络请求处理,防止资源竞争导致的启动时 UI 卡死 +- 更新依赖,替换弃用元素 +- 首页当前节点增加排序功能 +- DNS 覆写下增加 Hosts 设置功能 +- 修复服务模式安装后无法立即开启 TUN 模式的问题 +- 支持手动卸载服务模式,回退到 Sidecar 模式 +- 添加了土耳其语,日本语,德语,西班牙语,繁体中文的支持 +- 卸载服务的按钮 +- 添加了Zashboard的一键跳转URL +- 使用操作系统默认的窗口管理器 +- 切换、升级、重启内核的状态管理 +- 更精细化控制自动日志清理,新增1天选项 +- Winodws 快捷键名称改为 `Clash Verge` +- 配置加载阶段自动补全 external-controller secret 字段。 #### 🚀 优化改进 - - 系统代理 Bypass 设置 - - Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题 - - 切换到规则页面时自动刷新规则数据 - - 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 - - 编辑非激活订阅的时候不在触发当前订阅配置重载 - - 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程 - - 优化系统代理设置更新逻辑 - - 重构前端通知系统分离通知线程防止前端卡死 - - 优化网络请求和错误处理 - - 重构通知系统 - - 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题 - - MacOS 下默认关闭托盘速率显示 - - 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验 - - 优化了其他语言的翻译问题 - - Mihomo 内核默认日志等级为 warn - - Clash Verge Rev 应用默认日志等级为 warn - - 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题 - - 对轮询检测机制进行了优化,引入洗牌算法来增强随机性 - - 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息 - - 优化窗口状态初始化逻辑和添加缺失的权限设置 - - 异步化配置:优化端口查找和配置保存逻辑 - - 重构事件通知机制到独立线程,避免前端卡死 - - 优化端口设置,每个端口可随机设置端口号 - - 优化了保存机制,使用平滑函数,防止客户端卡死 - - 优化端口设置退出和保存机制 - - 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖 - - 修改 端口检测范围 (1111-65536) - - 配置文件缺失 secret 字段时自动填充默认值 set-your-secret - - 优化异步处理,防止部分组件 UI 阻塞 - - 关闭 DNS 启用 - - 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204 + +- 系统代理 Bypass 设置 +- Windows 下使用 Startup 文件夹的方式实现开机自启,解决管理员模式下开机自启的各种问题 +- 切换到规则页面时自动刷新规则数据 +- 重构更新失败回退机制,使用后端完成更新失败后回退到使用 Clash 代理再次尝试更新 +- 编辑非激活订阅的时候不在触发当前订阅配置重载 +- 改进核心功能防止主进程阻塞、改进MihomoManager实现,以及优化窗口创建流程 +- 优化系统代理设置更新逻辑 +- 重构前端通知系统分离通知线程防止前端卡死 +- 优化网络请求和错误处理 +- 重构通知系统 +- 使用异步方法重构 UI 启动逻辑,解决启动软件过程中的各种卡死问题 +- MacOS 下默认关闭托盘速率显示 +- 优化服务操作流程,提升系统服务相关操作的稳定性和用户体验 +- 优化了其他语言的翻译问题 +- Mihomo 内核默认日志等级为 warn +- Clash Verge Rev 应用默认日志等级为 warn +- 重构了原来的 IP 信息请求重试机制,采用轮询检测,解决了 Network Error 和超时问题 +- 对轮询检测机制进行了优化,引入洗牌算法来增强随机性 +- 对获取系统信息的流程进行了优化,并添加了去重检测机制,确保剔除重复的信息 +- 优化窗口状态初始化逻辑和添加缺失的权限设置 +- 异步化配置:优化端口查找和配置保存逻辑 +- 重构事件通知机制到独立线程,避免前端卡死 +- 优化端口设置,每个端口可随机设置端口号 +- 优化了保存机制,使用平滑函数,防止客户端卡死 +- 优化端口设置退出和保存机制 +- 强制为 Mihomo 配置补全并覆盖 external-controller-cors 字段,默认不允许跨域和仅本地请求,提升 cors 安全性,升级配置时自动覆盖 +- 修改 端口检测范围 (1111-65536) +- 配置文件缺失 secret 字段时自动填充默认值 set-your-secret +- 优化异步处理,防止部分组件 UI 阻塞 +- 关闭 DNS 启用 +- 延迟测试链接更换为 Https 协议 https://cp.cloudflare.com/generate_204 #### 🗑️ 移除内容 - - 窗口状态管理器 - - Webdav 跨平台备份恢复限制 + +- 窗口状态管理器 +- Webdav 跨平台备份恢复限制 ## v2.2.3 #### 已知问题 - - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 - - MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙 - - MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 - - Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS + +- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 +- MacOS 自定义图标与速率显示推荐图标尺寸为 256x256。其他尺寸(可能)会导致不正常图标和速率间隙 +- MacOS 下 墙贴主要为浅色,Tray 图标深色时图标闪烁;彩色 Tray 速率颜色淡 +- Linux 下 Clash Verge Rev 内存占用显著高于 Windows / MacOS ### 2.2.3 相对于 2.2.2 + #### 修复了: - - 首页“当前代理”因为重复刷新导致的CPU占用过高的问题 - - “开机自启”和“DNS覆写”开关跳动问题 - - 自定义托盘图标未能应用更改 - - MacOS 自定义托盘图标显示速率时图标和文本间隙过大 - - MacOS 托盘速率显示不全 - - Linux 在系统服务模式下无法拉起 Mihomo 内核 - - 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃 - - 相同节点名称可能导致的页面渲染出错 - - URL Schemes被截断的问题 - - 首页流量统计卡更好的时间戳范围 - - 静默启动无法触发自动轻量化计时器 + +- 首页“当前代理”因为重复刷新导致的CPU占用过高的问题 +- “开机自启”和“DNS覆写”开关跳动问题 +- 自定义托盘图标未能应用更改 +- MacOS 自定义托盘图标显示速率时图标和文本间隙过大 +- MacOS 托盘速率显示不全 +- Linux 在系统服务模式下无法拉起 Mihomo 内核 +- 使用异步操作,避免获取系统信息和切换代理模式可能带来的崩溃 +- 相同节点名称可能导致的页面渲染出错 +- URL Schemes被截断的问题 +- 首页流量统计卡更好的时间戳范围 +- 静默启动无法触发自动轻量化计时器 #### 新增了: - - Mihomo(Meta)内核升级至 1.19.4 - - Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 - - 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 - - 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置 - - 检测是否以管理员模式运行软件,如果是提示无法使用开机自启 - - 代理组显示节点数量 - - 统一运行模式检测,支持管理员模式下开启TUN模式 - - 托盘切换代理模式会根据设置自动断开之前连接 - - 如订阅获取失败回退使用Clash内核代理再次尝试 + +- Mihomo(Meta)内核升级至 1.19.4 +- Clash Verge Rev 从现在开始不再强依赖系统服务和管理权限 +- 支持根据用户偏好选择Sidecar(用户空间)模式或安装服务 +- 增加载入初始配置文件的错误提示,防止切换到错误的订阅配置 +- 检测是否以管理员模式运行软件,如果是提示无法使用开机自启 +- 代理组显示节点数量 +- 统一运行模式检测,支持管理员模式下开启TUN模式 +- 托盘切换代理模式会根据设置自动断开之前连接 +- 如订阅获取失败回退使用Clash内核代理再次尝试 #### 移除了: - - 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。 + +- 实时保存窗口位置和大小。这个功能可能会导致窗口异常大小和位置,还需观察。 #### 优化了: - - 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 - - 前端统一刷新应用数据,优化数据获取和刷新逻辑 - - 优化首页流量图表代码,调整图表文字边距 - - MacOS 托盘速率更好的显示样式和更新逻辑 - - 首页仅在有流量图表时显示流量图表区域 - - 更新DNS默认覆写配置 - - 移除测试目录,简化资源初始化逻辑 + +- 重构了后端内核管理逻辑,更轻量化和有效的管理内核,提高了性能和稳定性 +- 前端统一刷新应用数据,优化数据获取和刷新逻辑 +- 优化首页流量图表代码,调整图表文字边距 +- MacOS 托盘速率更好的显示样式和更新逻辑 +- 首页仅在有流量图表时显示流量图表区域 +- 更新DNS默认覆写配置 +- 移除测试目录,简化资源初始化逻辑 ## v2.2.2 @@ -148,23 +160,29 @@ 代号释义: 本次发布在功能上的大幅扩展。新首页设计为用户带来全新交互体验,DNS 覆写功能增强网络控制能力,解锁测试页面助力内容访问自由度提升,轻量模式提供灵活使用选择。此外,macOS 应用菜单集成、sidecar 模式、诊断信息导出等新特性进一步丰富了软件的适用场景。这些新增功能显著拓宽了 Clash Verge 的功能边界,为用户提供了更强大的工具和可能性。 #### 已知问题 - - 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 + +- 仅在Ubuntu 22.04/24.04,Fedora 41 **Gnome桌面环境** 做过简单测试,不保证其他其他Linux发行版可用,将在未来做进一步适配和调优 ### 2.2.2 相对于 2.2.1(已下架不再提供) + #### 修复了: - - 弹黑框的问题(原因是服务崩溃触发重装机制) - - MacOS进入轻量模式以后隐藏Dock图标 - - 增加轻量模式缺失的tray翻译 - - Linux下的窗口边框被削掉的问题 + +- 弹黑框的问题(原因是服务崩溃触发重装机制) +- MacOS进入轻量模式以后隐藏Dock图标 +- 增加轻量模式缺失的tray翻译 +- Linux下的窗口边框被削掉的问题 #### 新增了: - - 加强服务检测和重装逻辑 - - 增强内核与服务保活机制 - - 增加服务模式下的僵尸进程清理机制 - - 新增当服务模式多次尝试失败后自动回退至用户空间模式 + +- 加强服务检测和重装逻辑 +- 增强内核与服务保活机制 +- 增加服务模式下的僵尸进程清理机制 +- 新增当服务模式多次尝试失败后自动回退至用户空间模式 ### 2.2.1 相对于 2.2.0(已下架不再提供) + #### 修复了: + 1. **首页** - 修复 Direct 模式首页无法渲染 - 修复 首页启用轻量模式导致 ClashVergeRev 从托盘退出 @@ -181,6 +199,7 @@ - 修复 MacOS 轻量模式下 Dock 栏图标无法隐藏。 #### 新增了: + 1. **首页** - 首页文本过长自动截断 2. **轻量模式** @@ -197,7 +216,9 @@ ## 2.2.0(已下架不再提供) #### 新增功能 + 1. **首页** + - 新增首页功能,默认启动页面改为首页。 - 首页流量图卡片显示上传/下载名称。 - 首页支持轻量模式切换。 @@ -205,17 +226,21 @@ - 限制首页配置文件卡片URL长度。 2. **DNS 设置与覆写** + - 新增 DNS 覆写功能。 - 默认启用 DNS 覆写。 3. **解锁测试** + - 新增解锁测试页面。 4. **轻量模式** + - 新增轻量模式及设置。 - 添加自动轻量模式定时器。 5. **系统支持** + - Mihomo(meta)内核升级 1.19.3 - macOS 支持 CMD+W 关闭窗口。 - 新增 macOS 应用菜单。 @@ -228,7 +253,9 @@ - 新增代理命令。 #### 修复 + 1. **系统** + - 修复 Windows 热键崩溃。 - 修复 macOS 无框标题。 - 修复 macOS 静默启动崩溃。 @@ -241,7 +268,9 @@ - 修复构建失败问题。 #### 优化 + 1. **性能** + - 重构后端,巨幅性能优化。 - 优化首页组件性能。 - 优化流量图表资源使用。 @@ -254,6 +283,7 @@ - 优化修改verge配置性能。 2. **重构** + - 重构后端,巨幅性能优化。 - 优化定时器管理。 - 重构 MihomoManager 处理流量。 diff --git a/package.json b/package.json index 6472e732..ea726270 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "publish-version": "node scripts/publish-version.mjs", "prepare": "husky", "fmt": "cargo fmt --manifest-path ./src-tauri/Cargo.toml", - "clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml" + "clippy": "cargo clippy --manifest-path ./src-tauri/Cargo.toml", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/scripts/check-unused-i18n.js b/scripts/check-unused-i18n.js index 381c6590..95958df4 100644 --- a/scripts/check-unused-i18n.js +++ b/scripts/check-unused-i18n.js @@ -1,21 +1,21 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const LOCALES_DIR = path.resolve(__dirname, '../src/locales'); +const LOCALES_DIR = path.resolve(__dirname, "../src/locales"); const SRC_DIRS = [ - path.resolve(__dirname, '../src'), - path.resolve(__dirname, '../src-tauri') + path.resolve(__dirname, "../src"), + path.resolve(__dirname, "../src-tauri"), ]; -const exts = ['.js', '.ts', '.tsx', '.jsx', '.vue', '.rs']; +const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"]; // 递归获取所有文件 function getAllFiles(dir, exts) { let files = []; - fs.readdirSync(dir).forEach(file => { + fs.readdirSync(dir).forEach((file) => { const full = path.join(dir, file); if (fs.statSync(full).isDirectory()) { files = files.concat(getAllFiles(full, exts)); @@ -28,21 +28,21 @@ function getAllFiles(dir, exts) { // 读取所有源码内容为一个大字符串 function getAllSourceContent() { - const files = SRC_DIRS.flatMap(dir => getAllFiles(dir, exts)); - return files.map(f => fs.readFileSync(f, 'utf8')).join('\n'); + const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts)); + return files.map((f) => fs.readFileSync(f, "utf8")).join("\n"); } // 白名单 key,不检查这些 key 是否被使用 const WHITELIST_KEYS = [ - 'theme.light', - 'theme.dark', - 'theme.system', - "Already Using Latest Core Version" + "theme.light", + "theme.dark", + "theme.system", + "Already Using Latest Core Version", ]; // 主流程 function processI18nFile(i18nPath, lang, allSource) { - const i18n = JSON.parse(fs.readFileSync(i18nPath, 'utf8')); + const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8")); const keys = Object.keys(i18n); const used = {}; @@ -50,7 +50,7 @@ function processI18nFile(i18nPath, lang, allSource) { let checked = 0; const total = keys.length; - keys.forEach(key => { + keys.forEach((key) => { if (WHITELIST_KEYS.includes(key)) { used[key] = i18n[key]; } else { @@ -65,8 +65,10 @@ function processI18nFile(i18nPath, lang, allSource) { checked++; if (checked % 20 === 0 || checked === total) { const percent = ((checked / total) * 100).toFixed(1); - process.stdout.write(`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`); - if (checked === total) process.stdout.write('\n'); + process.stdout.write( + `\r[${lang}] Progress: ${checked}/${total} (${percent}%)`, + ); + if (checked === total) process.stdout.write("\n"); } }); @@ -74,25 +76,27 @@ function processI18nFile(i18nPath, lang, allSource) { console.log(`\n[${lang}] Unused keys:`, unused); // 备份原文件 - const oldPath = i18nPath + '.old'; + const oldPath = i18nPath + ".old"; fs.renameSync(i18nPath, oldPath); // 写入精简后的 i18n 文件(保留原文件名) - fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), 'utf8'); - console.log(`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`); + fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8"); + console.log( + `[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`, + ); console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`); } function main() { // 支持 zhtw.json、zh-tw.json、zh_CN.json 等 - const files = fs.readdirSync(LOCALES_DIR).filter(f => - /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith('.old') - ); + const files = fs + .readdirSync(LOCALES_DIR) + .filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old")); const allSource = getAllSourceContent(); - files.forEach(file => { - const lang = path.basename(file, '.json'); + files.forEach((file) => { + const lang = path.basename(file, ".json"); processI18nFile(path.join(LOCALES_DIR, file), lang, allSource); }); } -main(); \ No newline at end of file +main(); diff --git a/scripts/publish-version.mjs b/scripts/publish-version.mjs index e105e4b7..e5f4158f 100644 --- a/scripts/publish-version.mjs +++ b/scripts/publish-version.mjs @@ -38,7 +38,9 @@ async function run() { let tag = null; if (versionArg === "alpha") { // 读取 package.json 里的主版本 - const pkg = await import(path.join(rootDir, "package.json"), { assert: { type: "json" } }); + const pkg = await import(path.join(rootDir, "package.json"), { + assert: { type: "json" }, + }); tag = `v${pkg.default.version}-alpha`; } else if (isSemver(versionArg)) { // 1.2.3 或 v1.2.3 @@ -61,4 +63,4 @@ async function run() { } } -run(); \ No newline at end of file +run(); diff --git a/scripts/release-version.mjs b/scripts/release-version.mjs index de463d22..13314265 100644 --- a/scripts/release-version.mjs +++ b/scripts/release-version.mjs @@ -1,5 +1,3 @@ - - /** * CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json. * @@ -51,7 +49,9 @@ function generateShortTimestamp() { * @returns {boolean} */ function isValidVersion(version) { - return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(version); + return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test( + version, + ); } /** @@ -69,8 +69,8 @@ function normalizeVersion(version) { * @returns {string} */ function getBaseVersion(version) { - let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, ''); - base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, ''); + let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, ""); + base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, ""); return base; } @@ -85,10 +85,21 @@ async function updatePackageVersion(newVersion) { const data = await fs.readFile(packageJsonPath, "utf8"); const packageJson = JSON.parse(data); - console.log("[INFO]: Current package.json version is: ", packageJson.version); - packageJson.version = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; - await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), "utf8"); - console.log(`[INFO]: package.json version updated to: ${packageJson.version}`); + console.log( + "[INFO]: Current package.json version is: ", + packageJson.version, + ); + packageJson.version = newVersion.startsWith("v") + ? newVersion.slice(1) + : newVersion; + await fs.writeFile( + packageJsonPath, + JSON.stringify(packageJson, null, 2), + "utf8", + ); + console.log( + `[INFO]: package.json version updated to: ${packageJson.version}`, + ); } catch (error) { console.error("Error updating package.json version:", error); throw error; @@ -105,12 +116,17 @@ async function updateCargoVersion(newVersion) { try { const data = await fs.readFile(cargoTomlPath, "utf8"); const lines = data.split("\n"); - const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; + const versionWithoutV = newVersion.startsWith("v") + ? newVersion.slice(1) + : newVersion; const baseVersion = getBaseVersion(versionWithoutV); const updatedLines = lines.map((line) => { if (line.trim().startsWith("version =")) { - return line.replace(/version\s*=\s*"[^"]+"/, `version = "${baseVersion}"`); + return line.replace( + /version\s*=\s*"[^"]+"/, + `version = "${baseVersion}"`, + ); } return line; }); @@ -133,12 +149,21 @@ async function updateTauriConfigVersion(newVersion) { try { const data = await fs.readFile(tauriConfigPath, "utf8"); const tauriConfig = JSON.parse(data); - const versionWithoutV = newVersion.startsWith("v") ? newVersion.slice(1) : newVersion; + const versionWithoutV = newVersion.startsWith("v") + ? newVersion.slice(1) + : newVersion; const baseVersion = getBaseVersion(versionWithoutV); - console.log("[INFO]: Current tauri.conf.json version is: ", tauriConfig.version); + console.log( + "[INFO]: Current tauri.conf.json version is: ", + tauriConfig.version, + ); tauriConfig.version = baseVersion; - await fs.writeFile(tauriConfigPath, JSON.stringify(tauriConfig, null, 2), "utf8"); + await fs.writeFile( + tauriConfigPath, + JSON.stringify(tauriConfig, null, 2), + "utf8", + ); console.log(`[INFO]: tauri.conf.json version updated to: ${baseVersion}`); } catch (error) { console.error("Error updating tauri.conf.json version:", error); @@ -210,4 +235,3 @@ program .argument("", "version tag or full version") .action(main) .parse(process.argv); - diff --git a/src-tauri/rustfmt.toml b/src-tauri/rustfmt.toml index 11eda882..baaa750e 100644 --- a/src-tauri/rustfmt.toml +++ b/src-tauri/rustfmt.toml @@ -11,4 +11,3 @@ merge_derives = true use_try_shorthand = false use_field_init_shorthand = false force_explicit_abi = true -imports_granularity = "Crate" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0d52e46b..04f10958 100755 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -11,15 +11,9 @@ "icons/icon.icns", "icons/icon.ico" ], - "resources": [ - "resources", - "resources/locales/*" - ], + "resources": ["resources", "resources/locales/*"], "publisher": "Clash Verge Rev", - "externalBin": [ - "sidecar/verge-mihomo", - "sidecar/verge-mihomo-alpha" - ], + "externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"], "copyright": "GNU General Public License v3.0", "category": "DeveloperTool", "shortDescription": "Clash Verge Rev", @@ -50,28 +44,18 @@ }, "deep-link": { "desktop": { - "schemes": [ - "clash", - "clash-verge" - ] + "schemes": ["clash", "clash-verge"] } } }, "app": { "security": { - "capabilities": [ - "desktop-capability", - "migrated" - ], + "capabilities": ["desktop-capability", "migrated"], "assetProtocol": { - "scope": [ - "$APPDATA/**", - "$RESOURCE/../**", - "**" - ], + "scope": ["$APPDATA/**", "$RESOURCE/../**", "**"], "enable": true }, "csp": null } } -} \ No newline at end of file +} diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index eeb68458..aa12ded7 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -4,9 +4,9 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; user-select: none; diff --git a/src/components/base/NoticeManager.tsx b/src/components/base/NoticeManager.tsx index 0fb261f4..b7fedf98 100644 --- a/src/components/base/NoticeManager.tsx +++ b/src/components/base/NoticeManager.tsx @@ -1,7 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { Snackbar, Alert, IconButton, Box } from '@mui/material'; -import { CloseRounded } from '@mui/icons-material'; -import { subscribeNotices, hideNotice, NoticeItem } from '@/services/noticeService'; +import React, { useState, useEffect } from "react"; +import { Snackbar, Alert, IconButton, Box } from "@mui/material"; +import { CloseRounded } from "@mui/icons-material"; +import { + subscribeNotices, + hideNotice, + NoticeItem, +} from "@/services/noticeService"; export const NoticeManager: React.FC = () => { const [currentNotices, setCurrentNotices] = useState([]); @@ -23,49 +27,49 @@ export const NoticeManager: React.FC = () => { return ( {currentNotices.map((notice) => ( - handleClose(notice.id)} - > - - - } - > - {notice.message} - + handleClose(notice.id)} + > + + + } + > + {notice.message} + ))} ); -}; \ No newline at end of file +}; diff --git a/src/components/base/base-search-box.tsx b/src/components/base/base-search-box.tsx index ff35d9ba..a01789fd 100644 --- a/src/components/base/base-search-box.tsx +++ b/src/components/base/base-search-box.tsx @@ -157,7 +157,7 @@ export const BaseSearchBox = (props: SearchProps) => { ), - } + }, }} /> diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx index 172c0f57..6c4dc89e 100644 --- a/src/components/connection/connection-detail.tsx +++ b/src/components/connection/connection-detail.tsx @@ -107,7 +107,14 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => { {information.map((each) => (
{each.label} - : {each.value} + + : {each.value} +
))} diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index cefb4f2f..029f174c 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -25,7 +25,7 @@ export const ClashInfoCard = () => { // 使用备忘录组件内容,减少重新渲染 const cardContent = useMemo(() => { if (!clashConfig) return null; - + return ( diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index 19881182..8faafceb 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -24,11 +24,14 @@ export const ClashModeCard = () => { const currentMode = clashConfig?.mode?.toLowerCase(); // 模式图标映射 - const modeIcons = useMemo(() => ({ - rule: , - global: , - direct: - }), []); + const modeIcons = useMemo( + () => ({ + rule: , + global: , + direct: , + }), + [], + ); // 切换模式的处理函数 const onChangeMode = useLockFn(async (mode: string) => { @@ -68,18 +71,19 @@ export const ClashModeCard = () => { "&:active": { transform: "translateY(1px)", }, - "&::after": mode === currentMode - ? { - content: '""', - position: "absolute", - bottom: -16, - left: "50%", - width: 2, - height: 16, - bgcolor: "primary.main", - transform: "translateX(-50%)", - } - : {}, + "&::after": + mode === currentMode + ? { + content: '""', + position: "absolute", + bottom: -16, + left: "50%", + width: 2, + height: 16, + bgcolor: "primary.main", + transform: "translateX(-50%)", + } + : {}, }); // 描述样式 @@ -143,12 +147,10 @@ export const ClashModeCard = () => { overflow: "visible", }} > - - {t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)} + + {t( + `${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`, + )} diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 57a14e1d..ce4914bf 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -105,7 +105,7 @@ export const CurrentProxyCard = () => { // 添加排序类型状态 const [sortType, setSortType] = useState(() => { const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE); - return savedSortType ? Number(savedSortType) as ProxySortType : 0; + return savedSortType ? (Number(savedSortType) as ProxySortType) : 0; }); // 定义状态类型 @@ -156,7 +156,8 @@ export const CurrentProxyCard = () => { primaryKeywords.some((keyword) => group.name.toLowerCase().includes(keyword.toLowerCase()), ), - ) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0]; + ) || + proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0]; return primaryGroup?.name || ""; }; @@ -200,11 +201,13 @@ export const CurrentProxyCard = () => { // 只保留 Selector 类型的组用于选择 const filteredGroups = proxies.groups .filter((g: { name: string; type?: string }) => g.type === "Selector") - .map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({ - name: g.name, - now: g.now || "", - all: g.all.map((p: { name: string }) => p.name), - })); + .map( + (g: { name: string; now: string; all: Array<{ name: string }> }) => ({ + name: g.name, + now: g.now || "", + all: g.all.map((p: { name: string }) => p.name), + }), + ); let newProxy = ""; let newDisplayProxy = null; @@ -230,12 +233,12 @@ export const CurrentProxyCard = () => { if (selectorGroup) { newGroup = selectorGroup.name; newProxy = selectorGroup.now || selectorGroup.all[0] || ""; - newDisplayProxy = proxies.records?.[newProxy] || null; + newDisplayProxy = proxies.records?.[newProxy] || null; - if (!isGlobalMode && !isDirectMode) { - localStorage.setItem(STORAGE_KEY_GROUP, newGroup); - if (newProxy) { - localStorage.setItem(STORAGE_KEY_PROXY, newProxy); + if (!isGlobalMode && !isDirectMode) { + localStorage.setItem(STORAGE_KEY_GROUP, newGroup); + if (newProxy) { + localStorage.setItem(STORAGE_KEY_PROXY, newProxy); } } } @@ -280,7 +283,9 @@ export const CurrentProxyCard = () => { localStorage.setItem(STORAGE_KEY_GROUP, newGroup); setState((prev) => { - const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup); + const group = prev.proxyData.groups.find( + (g: { name: string }) => g.name === newGroup, + ); if (group) { return { ...prev, @@ -368,14 +373,16 @@ export const CurrentProxyCard = () => { }, [state.displayProxy]); // 获取当前节点的延迟(增加非空校验) - const currentDelay = currentProxy && state.selection.group - ? delayManager.getDelayFix(currentProxy, state.selection.group) - : -1; + const currentDelay = + currentProxy && state.selection.group + ? delayManager.getDelayFix(currentProxy, state.selection.group) + : -1; // 信号图标(增加非空校验) - const signalInfo = currentProxy && state.selection.group - ? getSignalIcon(currentDelay) - : { icon: , text: "未初始化", color: "text.secondary" }; + const signalInfo = + currentProxy && state.selection.group + ? getSignalIcon(currentDelay) + : { icon: , text: "未初始化", color: "text.secondary" }; // 自定义渲染选择框中的值 const renderProxyValue = useCallback( @@ -384,7 +391,7 @@ export const CurrentProxyCard = () => { const delayValue = delayManager.getDelayFix( state.proxyData.records[selected], - state.selection.group + state.selection.group, ); return ( @@ -441,7 +448,7 @@ export const CurrentProxyCard = () => { return list; }, - [sortType, state.proxyData.records, state.selection.group] + [sortType, state.proxyData.records, state.selection.group], ); // 计算要显示的代理选项(增加非空校验) @@ -452,11 +459,11 @@ export const CurrentProxyCard = () => { if (isGlobalMode && proxies?.global) { const options = proxies.global.all .filter((p: any) => { - const name = typeof p === 'string' ? p : p.name; + const name = typeof p === "string" ? p : p.name; return name !== "DIRECT" && name !== "REJECT"; }) .map((p: any) => ({ - name: typeof p === 'string' ? p : p.name + name: typeof p === "string" ? p : p.name, })); return sortProxies(options); @@ -464,7 +471,7 @@ export const CurrentProxyCard = () => { // 规则模式 const group = state.selection.group - ? state.proxyData.groups.find(g => g.name === state.selection.group) + ? state.proxyData.groups.find((g) => g.name === state.selection.group) : null; if (group) { @@ -473,7 +480,14 @@ export const CurrentProxyCard = () => { } return []; - }, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]); + }, [ + isDirectMode, + isGlobalMode, + proxies, + state.proxyData, + state.selection.group, + sortProxies, + ]); // 获取排序图标 const getSortIcon = () => { @@ -660,12 +674,14 @@ export const CurrentProxyCard = () => { {isDirectMode ? null : proxyOptions.map((proxy, index) => { - const delayValue = state.proxyData.records[proxy.name] && state.selection.group - ? delayManager.getDelayFix( - state.proxyData.records[proxy.name], - state.selection.group, - ) - : -1; + const delayValue = + state.proxyData.records[proxy.name] && + state.selection.group + ? delayManager.getDelayFix( + state.proxyData.records[proxy.name], + state.selection.group, + ) + : -1; return ( { )} ); -}; +}; diff --git a/src/components/home/enhanced-card.tsx b/src/components/home/enhanced-card.tsx index 814e4be1..68548a54 100644 --- a/src/components/home/enhanced-card.tsx +++ b/src/components/home/enhanced-card.tsx @@ -38,7 +38,7 @@ export const EnhancedCard = ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", - display: "block" + display: "block", }; return ( @@ -62,13 +62,15 @@ export const EnhancedCard = ({ borderColor: "divider", }} > - + {typeof title === "string" ? ( - ) : ( - - {title} - + {title} )} diff --git a/src/components/home/enhanced-traffic-graph.tsx b/src/components/home/enhanced-traffic-graph.tsx index 2f93a6ee..1da33898 100644 --- a/src/components/home/enhanced-traffic-graph.tsx +++ b/src/components/home/enhanced-traffic-graph.tsx @@ -30,7 +30,7 @@ ChartJS.register( PointElement, LineElement, Tooltip, - Filler + Filler, ); // 流量数据项接口 @@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number }; /** * 增强型流量图表组件 */ -export const EnhancedTrafficGraph = memo(forwardRef( - (props, ref) => { +export const EnhancedTrafficGraph = memo( + forwardRef((props, ref) => { const theme = useTheme(); const { t } = useTranslation(); @@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(forwardRef( const [timeRange, setTimeRange] = useState(10); const [chartStyle, setChartStyle] = useState<"line" | "area">("area"); const [displayData, setDisplayData] = useState([]); - + // 数据缓冲区 const dataBufferRef = useRef([]); // 根据时间范围计算保留的数据点数量 const getMaxPointsByTimeRange = useCallback( (minutes: TimeRange): number => minutes * 60, - [] + [], ); // 最大数据点数量 const MAX_BUFFER_SIZE = useMemo( () => getMaxPointsByTimeRange(10), - [getMaxPointsByTimeRange] + [getMaxPointsByTimeRange], ); // 颜色配置 @@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef( text: theme.palette.text.primary, tooltipBorder: theme.palette.divider, }), - [theme] + [theme], ); // 切换时间范围 - const handleTimeRangeClick = useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - setTimeRange((prevRange) => { - return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; - }); - }, []); - - // 点击图表主体或图例时切换样式 - const handleToggleStyleClick = useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - setChartStyle((prev) => (prev === "line" ? "area" : "line")); - }, []); + const handleTimeRangeClick = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + setTimeRange((prevRange) => { + return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1; + }); + }, + [], + ); + // 点击图表主体或图例时切换样式 + const handleToggleStyleClick = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + setChartStyle((prev) => (prev === "line" ? "area" : "line")); + }, + [], + ); // 初始化数据缓冲区 useEffect(() => { @@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef( let nameValue: string; try { if (isNaN(date.getTime())) { - console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`); + console.warn( + `Initial data generation: Invalid date for timestamp ${pointTime}`, + ); nameValue = "??:??:??"; } else { nameValue = date.toLocaleTimeString("en-US", { @@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef( }); } } catch (e) { - console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime); + console.error( + "Error in toLocaleTimeString during initial data gen:", + e, + "Date:", + date, + "Timestamp:", + pointTime, + ); nameValue = "Err:Time"; } @@ -142,55 +156,66 @@ export const EnhancedTrafficGraph = memo(forwardRef( timestamp: pointTime, name: nameValue, }; - } + }, ); dataBufferRef.current = initialBuffer; - + // 更新显示数据 const pointsToShow = getMaxPointsByTimeRange(timeRange); setDisplayData(initialBuffer.slice(-pointsToShow)); }, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]); // 添加数据点方法 - const appendData = useCallback((data: ITrafficItem) => { - const safeData = { - up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, - down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, - }; + const appendData = useCallback( + (data: ITrafficItem) => { + const safeData = { + up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0, + down: + typeof data.down === "number" && !isNaN(data.down) ? data.down : 0, + }; - const timestamp = data.timestamp || Date.now(); - const date = new Date(timestamp); + const timestamp = data.timestamp || Date.now(); + const date = new Date(timestamp); - let nameValue: string; - try { - if (isNaN(date.getTime())) { - console.warn(`appendData: Invalid date for timestamp ${timestamp}`); - nameValue = "??:??:??"; - } else { - nameValue = date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); + let nameValue: string; + try { + if (isNaN(date.getTime())) { + console.warn(`appendData: Invalid date for timestamp ${timestamp}`); + nameValue = "??:??:??"; + } else { + nameValue = date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + } + } catch (e) { + console.error( + "Error in toLocaleTimeString in appendData:", + e, + "Date:", + date, + "Timestamp:", + timestamp, + ); + nameValue = "Err:Time"; } - } catch (e) { - console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp); - nameValue = "Err:Time"; - } - // 带时间标签的新数据点 - const newPoint: DataPoint = { - ...safeData, - name: nameValue, - timestamp: timestamp, - }; + // 带时间标签的新数据点 + const newPoint: DataPoint = { + ...safeData, + name: nameValue, + timestamp: timestamp, + }; - const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; - dataBufferRef.current = newBuffer; + const newBuffer = [...dataBufferRef.current.slice(1), newPoint]; + dataBufferRef.current = newBuffer; - const pointsToShow = getMaxPointsByTimeRange(timeRange); - setDisplayData(newBuffer.slice(-pointsToShow)); - }, [timeRange, getMaxPointsByTimeRange]); + const pointsToShow = getMaxPointsByTimeRange(timeRange); + setDisplayData(newBuffer.slice(-pointsToShow)); + }, + [timeRange, getMaxPointsByTimeRange], + ); // 监听时间范围变化 useEffect(() => { @@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef( // 切换图表样式 const toggleStyle = useCallback(() => { - setChartStyle((prev) => prev === "line" ? "area" : "line"); + setChartStyle((prev) => (prev === "line" ? "area" : "line")); }, []); // 暴露方法给父组件 @@ -212,30 +237,31 @@ export const EnhancedTrafficGraph = memo(forwardRef( appendData, toggleStyle, }), - [appendData, toggleStyle] + [appendData, toggleStyle], ); - const formatYAxis = useCallback((value: number | string): string => { - if (typeof value !== 'number') return String(value); + if (typeof value !== "number") return String(value); const [num, unit] = parseTraffic(value); return `${num}${unit}`; }, []); - const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => { - const dataPoint = displayData[index as number]; - if (dataPoint && dataPoint.name) { - const parts = dataPoint.name.split(":"); - return `${parts[0]}:${parts[1]}`; - } - if(typeof tickValue === 'string') { - const parts = tickValue.split(":"); - if (parts.length >= 2) return `${parts[0]}:${parts[1]}`; - return tickValue; - } - return ''; - }, [displayData]); - + const formatXLabel = useCallback( + (tickValue: string | number, index: number, ticks: any[]) => { + const dataPoint = displayData[index as number]; + if (dataPoint && dataPoint.name) { + const parts = dataPoint.name.split(":"); + return `${parts[0]}:${parts[1]}`; + } + if (typeof tickValue === "string") { + const parts = tickValue.split(":"); + if (parts.length >= 2) return `${parts[0]}:${parts[1]}`; + return tickValue; + } + return ""; + }, + [displayData], + ); // 获取当前时间范围文本 const getTimeRangeText = useCallback(() => { @@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef( }, [timeRange, t]); const chartData = useMemo(() => { - const labels = displayData.map(d => d.name); + const labels = displayData.map((d) => d.name); return { labels, datasets: [ { label: t("Upload"), - data: displayData.map(d => d.up), + data: displayData.map((d) => d.up), borderColor: colors.up, backgroundColor: chartStyle === "area" ? colors.up : colors.up, fill: chartStyle === "area", @@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef( }, { label: t("Download"), - data: displayData.map(d => d.down), + data: displayData.map((d) => d.down), borderColor: colors.down, backgroundColor: chartStyle === "area" ? colors.down : colors.down, fill: chartStyle === "area", @@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef( pointRadius: 0, pointHoverRadius: 4, borderWidth: 2, - } - ] + }, + ], }; }, [displayData, colors.up, colors.down, t, chartStyle]); - const chartOptions = useMemo(() => ({ - responsive: true, - maintainAspectRatio: false, - animation: false as false, - scales: { - x: { - display: true, - type: 'category' as const, - labels: displayData.map(d => d.name), - ticks: { + const chartOptions = useMemo( + () => ({ + responsive: true, + maintainAspectRatio: false, + animation: false as false, + scales: { + x: { display: true, - color: colors.text, - font: { size: 10 }, - callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined { - let labelToFormat: string | undefined = undefined; + type: "category" as const, + labels: displayData.map((d) => d.name), + ticks: { + display: true, + color: colors.text, + font: { size: 10 }, + callback: function ( + this: Scale, + tickValue: string | number, + index: number, + ticks: Tick[], + ): string | undefined { + let labelToFormat: string | undefined = undefined; - const currentDisplayTick = ticks[index]; - if (currentDisplayTick && typeof currentDisplayTick.label === 'string') { - labelToFormat = currentDisplayTick.label; - } else { - const sourceLabels = displayData.map(d => d.name); - if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) { - labelToFormat = sourceLabels[tickValue]; - } else if (typeof tickValue === 'string') { - labelToFormat = tickValue; + const currentDisplayTick = ticks[index]; + if ( + currentDisplayTick && + typeof currentDisplayTick.label === "string" + ) { + labelToFormat = currentDisplayTick.label; + } else { + const sourceLabels = displayData.map((d) => d.name); + if ( + typeof tickValue === "number" && + tickValue >= 0 && + tickValue < sourceLabels.length + ) { + labelToFormat = sourceLabels[tickValue]; + } else if (typeof tickValue === "string") { + labelToFormat = tickValue; + } } - } - if (typeof labelToFormat !== 'string') { - return undefined; - } + if (typeof labelToFormat !== "string") { + return undefined; + } - const parts: string[] = labelToFormat.split(':'); - return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat; + const parts: string[] = labelToFormat.split(":"); + return parts.length >= 2 + ? `${parts[0]}:${parts[1]}` + : labelToFormat; + }, + autoSkip: true, + maxTicksLimit: Math.max( + 5, + Math.floor(displayData.length / (timeRange * 2)), + ), + minRotation: 0, + maxRotation: 0, + }, + grid: { + display: true, + drawOnChartArea: false, + drawTicks: true, + tickLength: 2, + color: colors.text, }, - autoSkip: true, - maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))), - minRotation: 0, - maxRotation: 0, }, - grid: { - display: true, - drawOnChartArea: false, - drawTicks: true, - tickLength: 2, - color: colors.text, - + y: { + beginAtZero: true, + ticks: { + color: colors.text, + font: { size: 10 }, + callback: formatYAxis, + }, + grid: { + display: true, + drawTicks: true, + tickLength: 3, + color: colors.grid, + }, }, }, - y: { - beginAtZero: true, - ticks: { - color: colors.text, - font: { size: 10 }, - callback: formatYAxis, - }, - grid: { - display: true, - drawTicks: true, - tickLength: 3, - color: colors.grid, - - }, - } - }, - plugins: { - tooltip: { - enabled: true, - mode: 'index' as const, - intersect: false, - backgroundColor: colors.tooltipBg, - titleColor: colors.text, - bodyColor: colors.text, - borderColor: colors.tooltipBorder, - borderWidth: 1, - cornerRadius: 4, - padding: 8, - callbacks: { - title: (tooltipItems: any[]) => { - return `${t("Time")}: ${tooltipItems[0].label}`; + plugins: { + tooltip: { + enabled: true, + mode: "index" as const, + intersect: false, + backgroundColor: colors.tooltipBg, + titleColor: colors.text, + bodyColor: colors.text, + borderColor: colors.tooltipBorder, + borderWidth: 1, + cornerRadius: 4, + padding: 8, + callbacks: { + title: (tooltipItems: any[]) => { + return `${t("Time")}: ${tooltipItems[0].label}`; + }, + label: (context: any): string => { + const label = context.dataset.label || ""; + const value = context.parsed.y; + const [num, unit] = parseTraffic(value); + return `${label}: ${num} ${unit}/s`; + }, }, - label: (context: any): string => { - const label = context.dataset.label || ''; - const value = context.parsed.y; - const [num, unit] = parseTraffic(value); - return `${label}: ${num} ${unit}/s`; - } - } + }, + legend: { + display: false, + }, }, - legend: { - display: false - } - }, - layout: { - padding: { - top: 16, - right: 7, - left: 3, - } - } - }), [colors, t, formatYAxis, timeRange, displayData]); - + layout: { + padding: { + top: 16, + right: 7, + left: 3, + }, + }, + }), + [colors, t, formatYAxis, timeRange, displayData], + ); return ( ( {displayData.length > 0 && ( )} - - + + ( fontSize={11} fontWeight="bold" onClick={handleTimeRangeClick} - style={{ cursor: "pointer", pointerEvents: 'all' }} + style={{ cursor: "pointer", pointerEvents: "all" }} > {getTimeRangeText()} - + ( fontSize={12} fontWeight="bold" onClick={handleToggleStyleClick} - style={{ cursor: "pointer", pointerEvents: 'all' }} + style={{ cursor: "pointer", pointerEvents: "all" }} > {t("Upload")} @@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef( fontSize={12} fontWeight="bold" onClick={handleToggleStyleClick} - style={{ cursor: "pointer", pointerEvents: 'all' }} + style={{ cursor: "pointer", pointerEvents: "all" }} > {t("Download")} @@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef( ); - }, -)); + }), +); EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph"; diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 81cc065e..29999f31 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据 const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新 // 统计卡片组件 - 使用memo优化 -const CompactStatCard = memo(({ - icon, - title, - value, - unit, - color, - onClick, -}: StatCardProps) => { - const theme = useTheme(); +const CompactStatCard = memo( + ({ icon, title, value, unit, color, onClick }: StatCardProps) => { + const theme = useTheme(); - // 获取调色板颜色 - 使用useMemo避免重复计算 - const colorValue = useMemo(() => { - const palette = theme.palette; - if ( - color in palette && - palette[color as keyof typeof palette] && - "main" in (palette[color as keyof typeof palette] as PaletteColor) - ) { - return (palette[color as keyof typeof palette] as PaletteColor).main; - } - return palette.primary.main; - }, [theme.palette, color]); + // 获取调色板颜色 - 使用useMemo避免重复计算 + const colorValue = useMemo(() => { + const palette = theme.palette; + if ( + color in palette && + palette[color as keyof typeof palette] && + "main" in (palette[color as keyof typeof palette] as PaletteColor) + ) { + return (palette[color as keyof typeof palette] as PaletteColor).main; + } + return palette.primary.main; + }, [theme.palette, color]); - return ( - - {/* 图标容器 */} - - {icon} - - - {/* 文本内容 */} - - - {title} - - - - {value} - - - {unit} - + {/* 图标容器 */} + + {icon} - - - ); -}); + + {/* 文本内容 */} + + + {title} + + + + {value} + + + {unit} + + + + + ); + }, +); // 添加显示名称 CompactStatCard.displayName = "CompactStatCard"; @@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => { down: data.down, timestamp: now, }); - } catch { } + } catch {} return; } lastUpdateRef.current.traffic = now; const safeUp = isNaN(data.up) ? 0 : data.up; const safeDown = isNaN(data.down) ? 0 : data.down; try { - setStats(prev => ({ + setStats((prev) => ({ ...prev, - traffic: { up: safeUp, down: safeDown } + traffic: { up: safeUp, down: safeDown }, })); - } catch { } + } catch {} try { trafficRef.current?.appendData({ up: safeUp, down: safeDown, timestamp: now, }); - } catch { } + } catch {} } } catch (err) { console.error("[Traffic] 解析数据错误:", err, event.data); @@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => { try { const data = JSON.parse(event.data) as MemoryUsage; if (data && typeof data.inuse === "number") { - setStats(prev => ({ + setStats((prev) => ({ ...prev, memory: { inuse: isNaN(data.inuse) ? 0 : data.inuse, oslimit: data.oslimit, - } + }, })); } } catch (err) { @@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => { // 清理现有连接的函数 const cleanupSockets = () => { - Object.values(socketRefs.current).forEach(socket => { + Object.values(socketRefs.current).forEach((socket) => { if (socket) { socket.close(); } @@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => { cleanupSockets(); // 创建新连接 - console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`); - socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, { - onmessage: handleTrafficUpdate, - onopen: (event) => { - console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); + console.log( + `[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`, + ); + socketRefs.current.traffic = createAuthSockette( + `${server}/traffic`, + secret, + { + onmessage: handleTrafficUpdate, + onopen: (event) => { + console.log( + `[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, + event, + ); + }, + onerror: (event) => { + console.error( + `[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, + event, + ); + setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } })); + }, + onclose: (event) => { + console.log( + `[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, + event.code, + event.reason, + ); + if (event.code !== 1000 && event.code !== 1001) { + console.warn( + `[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`, + ); + setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } })); + } + }, }, - onerror: (event) => { - console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event); - setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } })); - }, - onclose: (event) => { - console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason); - if (event.code !== 1000 && event.code !== 1001) { - console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`); - setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } })); - } - }, - }); + ); - console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`); + console.log( + `[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`, + ); socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, { onmessage: handleMemoryUpdate, onopen: (event) => { - console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event); + console.log( + `[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, + event, + ); }, onerror: (event) => { - console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event); - setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); + console.error( + `[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, + event, + ); + setStats((prev) => ({ + ...prev, + memory: { inuse: 0, oslimit: undefined }, + })); }, onclose: (event) => { - console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason); + console.log( + `[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, + event.code, + event.reason, + ); if (event.code !== 1000 && event.code !== 1001) { - console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`); - setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } })); + console.warn( + `[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`, + ); + setStats((prev) => ({ + ...prev, + memory: { inuse: 0, oslimit: undefined }, + })); } }, }); @@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => { useEffect(() => { return () => { try { - Object.values(socketRefs.current).forEach(socket => { + Object.values(socketRefs.current).forEach((socket) => { if (socket) socket.close(); }); socketRefs.current = { traffic: null, memory: null }; - } catch { } + } catch {} }; }, []); @@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => { const [up, upUnit] = parseTraffic(stats.traffic.up); const [down, downUnit] = parseTraffic(stats.traffic.down); const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse); - const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal); - const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal); + const [uploadTotal, uploadTotalUnit] = parseTraffic( + connections.uploadTotal, + ); + const [downloadTotal, downloadTotalUnit] = parseTraffic( + connections.downloadTotal, + ); return { - up, upUnit, down, downUnit, inuse, inuseUnit, - uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit, - connectionsCount: connections.count + up, + upUnit, + down, + downUnit, + inuse, + inuseUnit, + uploadTotal, + uploadTotalUnit, + downloadTotal, + downloadTotalUnit, + connectionsCount: connections.count, }; }, [stats, connections]); @@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => { }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); // 使用useMemo计算统计卡片配置 - const statCards = useMemo(() => [ - { - icon: , - title: t("Upload Speed"), - value: parsedData.up, - unit: `${parsedData.upUnit}/s`, - color: "secondary" as const, - }, - { - icon: , - title: t("Download Speed"), - value: parsedData.down, - unit: `${parsedData.downUnit}/s`, - color: "primary" as const, - }, - { - icon: , - title: t("Active Connections"), - value: parsedData.connectionsCount, - unit: "", - color: "success" as const, - }, - { - icon: , - title: t("Uploaded"), - value: parsedData.uploadTotal, - unit: parsedData.uploadTotalUnit, - color: "secondary" as const, - }, - { - icon: , - title: t("Downloaded"), - value: parsedData.downloadTotal, - unit: parsedData.downloadTotalUnit, - color: "primary" as const, - }, - { - icon: , - title: t("Memory Usage"), - value: parsedData.inuse, - unit: parsedData.inuseUnit, - color: "error" as const, - onClick: isDebug ? handleGarbageCollection : undefined, - }, - ], [t, parsedData, isDebug, handleGarbageCollection]); + const statCards = useMemo( + () => [ + { + icon: , + title: t("Upload Speed"), + value: parsedData.up, + unit: `${parsedData.upUnit}/s`, + color: "secondary" as const, + }, + { + icon: , + title: t("Download Speed"), + value: parsedData.down, + unit: `${parsedData.downUnit}/s`, + color: "primary" as const, + }, + { + icon: , + title: t("Active Connections"), + value: parsedData.connectionsCount, + unit: "", + color: "success" as const, + }, + { + icon: , + title: t("Uploaded"), + value: parsedData.uploadTotal, + unit: parsedData.uploadTotalUnit, + color: "secondary" as const, + }, + { + icon: , + title: t("Downloaded"), + value: parsedData.downloadTotal, + unit: parsedData.downloadTotalUnit, + color: "primary" as const, + }, + { + icon: , + title: t("Memory Usage"), + value: parsedData.inuse, + unit: parsedData.inuseUnit, + color: "error" as const, + onClick: isDebug ? handleGarbageCollection : undefined, + }, + ], + [t, parsedData, isDebug, handleGarbageCollection], + ); return ( diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index 1762002d..8a02d1c8 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -78,12 +78,16 @@ const truncateStyle = { maxWidth: "calc(100% - 28px)", overflow: "hidden", textOverflow: "ellipsis", - whiteSpace: "nowrap" + whiteSpace: "nowrap", }; // 提取独立组件减少主组件复杂度 -const ProfileDetails = ({ current, onUpdateProfile, updating }: { - current: ProfileItem; +const ProfileDetails = ({ + current, + onUpdateProfile, + updating, +}: { + current: ProfileItem; onUpdateProfile: () => void; updating: boolean; }) => { @@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: { if (!current.extra || !current.extra.total) return 1; return Math.min( Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1, - 100 + 100, ); }, [current.extra, usedTraffic]); @@ -109,19 +113,24 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: { {current.url && ( - + {t("From")}: {current.home ? ( current.home && openWebUrl(current.home)} - sx={{ + sx={{ display: "inline-flex", alignItems: "center", minWidth: 0, maxWidth: "calc(100% - 40px)", - ml: 0.5 + ml: 0.5, }} title={parseUrl(current.url)} > @@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: { textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0, - flex: 1 + flex: 1, }} > {parseUrl(current.url)} ) : ( @@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: { whiteSpace: "nowrap", minWidth: 0, flex: 1, - ml: 0.5 + ml: 0.5, }} title={parseUrl(current.url)} > @@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: { {t("Used / Total")}:{" "} - {parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)} + {parseTraffic(usedTraffic)} /{" "} + {parseTraffic(current.extra.total)} @@ -240,7 +255,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: { // 提取空配置组件 const EmptyProfile = ({ onClick }: { onClick: () => void }) => { const { t } = useTranslation(); - + return ( void }) => { ); }; -export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => { +export const HomeProfileCard = ({ + current, + onProfileUpdated, +}: HomeProfileCardProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const { refreshAll } = useAppData(); // 更新当前订阅 const [updating, setUpdating] = useState(false); - + const onUpdateProfile = useLockFn(async () => { if (!current?.uid) return; setUpdating(true); try { await updateProfile(current.uid, current.option); - showNotice('success', t("Update subscription successfully"), 1000); + showNotice("success", t("Update subscription successfully"), 1000); onProfileUpdated?.(); - + // 刷新首页数据 refreshAll(); } catch (err: any) { - showNotice('error', err.message || err.toString(), 3000); + showNotice("error", err.message || err.toString(), 3000); } finally { setUpdating(false); } @@ -302,9 +320,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr // 卡片标题 const cardTitle = useMemo(() => { if (!current) return t("Profiles"); - + if (!current.home) return current.name; - + return ( {current.name} @@ -345,7 +363,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr // 卡片操作按钮 const cardAction = useMemo(() => { if (!current) return null; - + return ( - - + + - + {t("Proxy Provider")} - - + + - + {t("Rule Providers")} - + {Object.entries(ruleProviders || {}).map(([key, item]) => { const provider = item as RuleProviderItem; const time = dayjs(provider.updatedAt); const isUpdating = updating[key]; - + return ( { const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; - const hoverColor = mode === "light" - ? alpha(primary.main, 0.1) - : alpha(primary.main, 0.2); - + const hoverColor = + mode === "light" + ? alpha(primary.main, 0.1) + : alpha(primary.main, 0.2); + return { backgroundColor: bgcolor, "&:hover": { backgroundColor: hoverColor, - borderColor: alpha(primary.main, 0.3) - } + borderColor: alpha(primary.main, 0.3), + }, }; - } + }, ]} > + { title={key} sx={{ display: "flex", alignItems: "center" }} > - {key} + {key} {provider.ruleCount} - - - {t("Update At")}: {time.fromNow()} + + + {t("Update At")}: + {time.fromNow()} } @@ -219,30 +232,32 @@ export const ProviderButton = () => { {provider.vehicleType} - - {provider.behavior} - + {provider.behavior} } /> - + updateProvider(key)} disabled={isUpdating} sx={{ - animation: isUpdating ? "spin 1s linear infinite" : "none", + animation: isUpdating + ? "spin 1s linear infinite" + : "none", "@keyframes spin": { "0%": { transform: "rotate(0deg)" }, - "100%": { transform: "rotate(360deg)" } - } + "100%": { transform: "rotate(360deg)" }, + }, }} title={t("Update Provider") as string} > @@ -254,7 +269,7 @@ export const ProviderButton = () => { })} - +