Compare commits

..
3 Commits
Author SHA1 Message Date
Yuhang Wu 8eac325b75 feat(about): add version info and contact section to about page 2026-10-20 10:22:23 +08:00
Yuhang Wu bba146fd1a feat: 添加微信二维码及相关国际化配置
新增微信图片资源,补充联系方式相关的中英双语文案,新增复制功能的国际化文本,同步更新 README 格式优化
2026-10-20 10:22:14 +08:00
Yuhang Wu 7cbc80176d feat: 新增开机自启功能 2026-10-20 10:03:36 +08:00
17 changed files with 515 additions and 102 deletions
+37 -27
View File
@@ -2,22 +2,22 @@
# FrpcPilot
**frp 可视化桌面客户端 —— 告别手写 `frpc.toml`**
**frp 可视化桌面客户端 —— 告别手写** **`frpc.toml`**
下载 frpc、配置服务器、编排隧道、一键启动并实时查看日志,全部在同一个窗口里完成。
[![Release](https://img.shields.io/github/v/release/SparksFly91/FrpcPilot?style=flat-square&color=6366f1)](https://github.com/SparksFly91/FrpcPilot/releases)
[![License](https://img.shields.io/github/license/SparksFly91/FrpcPilot?style=flat-square&color=6366f1)](./LICENSE)
[![Release](https://img.shields.io/github/v/release/SparksFly91/FrpcPilot?style=flat-square\&color=6366f1)](https://github.com/SparksFly91/FrpcPilot/releases)
[![License](https://img.shields.io/github/license/SparksFly91/FrpcPilot?style=flat-square\&color=6366f1)](./LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-6366f1?style=flat-square)](#-下载安装)
[![Tauri](https://img.shields.io/badge/Tauri-2-24C8DB?style=flat-square&logo=tauri&logoColor=white)](https://v2.tauri.app/)
[![Vue](https://img.shields.io/badge/Vue-3-42B883?style=flat-square&logo=vuedotjs&logoColor=white)](https://vuejs.org/)
[![Tauri](https://img.shields.io/badge/Tauri-2-24C8DB?style=flat-square\&logo=tauri\&logoColor=white)](https://v2.tauri.app/)
[![Vue](https://img.shields.io/badge/Vue-3-42B883?style=flat-square\&logo=vuedotjs\&logoColor=white)](https://vuejs.org/)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-6366f1?style=flat-square)](https://github.com/SparksFly91/FrpcPilot/pulls)
</div>
> 📘 **新手必读**:第一次搭建内网穿透?请阅读 **[《Frp 内网穿透搭建指南》](docs/Frp搭建内网穿透指南.md)** —— 从公网服务端搭建到用 FrpcPilot 一键开启穿透,手把手带你跑通第一个隧道。
---
***
## 🏷️ 关于「FrpcPilot」这个名字
@@ -28,7 +28,7 @@
一句话:**让 frpc 起飞,你只管掌舵。** ✈️
---
***
## 📖 项目简介
@@ -38,68 +38,76 @@
> 当前版本 **v1.2.1**,已支持 Windows / macOS(Apple Silicon)/ Linux 三平台,并内置应用内自动更新。
---
***
## 🖼️ 界面预览
| 仪表盘 | 服务器 |
| --- | --- |
| ------------------------------- | ------------------------------- |
| ![仪表盘](docs/snipastes/仪表盘.webp) | ![服务器](docs/snipastes/服务器.webp) |
| 隧道列表 | Frpc 下载 |
| --- | --- |
| --------------------------------- | -------------------------------------- |
| ![隧道列表](docs/snipastes/隧道列表.webp) | ![Frpc 下载](docs/snipastes/frpc下载.webp) |
| 设置 · 常规 | 设置 · 外观 |
| --- | --- |
| ---------------------------------- | ------------------------------- |
| ![常规设置](docs/snipastes/设置·常规.webp) | ![外观设置](docs/snipastes/设置.webp) |
<p align="center">
<img src="docs/snipastes/关于.webp" alt="关于" width="49%" />
</p>
---
***
## ✨ 功能特性
### 📊 仪表盘
- 一屏总览:服务器数量、隧道数量,以及主机名 / 系统 / 架构 / IP 地址等系统信息
- 选择 frpc 版本后「**一键启动 / 停止**」,运行状态实时联动
- **实时日志**:通过事件流回显 frpc 运行日志(保留最近 500 行,支持一键清空)
- 侧边栏常驻 **CPU / 内存占用** 与运行状态指示(🟢 运行正常 · 🔴 运行异常 · 🟡 未启动)
### 🖥️ 服务器管理
- frps 服务器的新增 / 编辑 / 删除
- 支持服务器地址、端口、鉴权 Token、传输协议、TLS 开关与备注
- 置顶与排序;启用开关(**同一时间仅允许启用一台**服务器)
### 🔀 隧道管理
- 覆盖 **TCP / UDP / HTTP / HTTPS / STCP / XTCP / SUDP / TCPMUX** 等代理类型
- 本地 IP / 端口、远程端口、自定义域名、子域名、URL 前缀
- 高级配置:密钥、加密、压缩、带宽限制、Proxy Protocol、健康检查、负载均衡
- 统一管理启用 / 置顶 / 排序
### 📦 Frpc 下载管理
- 在线拉取 frp 官方版本列表,按平台与架构筛选,支持「所有架构 frpc / frps」开关
- 一键下载并自动解压,实时显示下载 / 解压进度与状态(支持镜像加速)
- 导入本地已有的 `frpc` / `frps`,删除已下载版本、复制下载直链
- **页面级缓存**:应用运行期间仅首次进入时拉取版本列表,避免重复请求触发限流,需要更新时手动点击「刷新」
### 🚀 一键启动与进程管理
- 依据所选版本、已启用的服务器与隧道,自动生成 `frpc.toml` 并拉起独立进程
- 通过 `frpc-log` / `frpc-exit` 事件实时推送日志与进程退出信息
- 应用退出时自动结束 frpc 子进程,避免残留后台进程
### 🗂️ 系统托盘
- 常驻托盘菜单:**显示窗口 / 启动穿透 / 停止穿透 / 退出**
- 左键单击直达主窗口,右键展开菜单;菜单随运行状态与界面语言自动联动
- 「退出」会先停止正在运行的穿透,再退出整个应用
### 🔔 关闭行为
- 关闭窗口时可选「**最小化到系统托盘**」或「**直接退出**」
- 支持「每次询问」与「保持以上选择」两种策略,也可在「设置 - 常规」中随时修改
### 🎨 界面与体验
- **自定义标题栏**:Windows / macOS 双套方案,窗口可拖拽移动;macOS 还原原生红绿灯
- **明暗主题**:一键切换,Tailwind 暗色变体与 Ant Design Vue 暗色算法同步
- **磨砂玻璃视觉**:侧边栏、内容区与卡片统一为圆角 + `backdrop-filter` 半透明面板,透明度可调
@@ -107,23 +115,25 @@
- **国际化**:内置简体中文 / English,语言选择持久化并与组件库语言包联动
### 🔄 应用内自动更新
- 启动时静默检查更新,支持手动检查
- 更新弹窗以 **Markdown** 渲染更新说明并展示下载进度
- 支持「忽略该版本」、一键更新,安装完成后自动重启
### ⚙️ 设置中心
- **常规**:关闭行为、应用语言
- **外观**:主题模式、背景图片、磨砂玻璃透明度(卡片 / 弹窗,实时预览)
- **关于**:项目介绍、技术栈、使用流程、开源地址与版本信息
---
***
## 📥 下载安装
前往 [**Releases**](https://github.com/SparksFly91/FrpcPilot/releases/latest) 下载对应平台的安装包:
前往 **[Releases](https://github.com/SparksFly91/FrpcPilot/releases/latest)** 下载对应平台的安装包:
| 平台 | 安装包 |
| --- | --- |
| -------------------- | -------------------- |
| Windows | `.exe`(NSIS 安装程序) |
| macOS(Apple Silicon) | `.dmg` / `.app` |
| Linux | `.deb` / `.AppImage` |
@@ -132,12 +142,12 @@
> 🧭 **首次使用?** 还不清楚怎么搭建 frps 服务端、怎么配置隧道?请参考 [《Frp 内网穿透搭建指南》](docs/Frp搭建内网穿透指南.md)。
---
***
## 🧱 技术栈
| 分类 | 选型 |
| --- | --- |
| -------- | ----------------------------------------------------------------------------- |
| 桌面框架 | [Tauri 2](https://v2.tauri.app/)(Rust 后端) |
| 前端框架 | Vue 3(`<script setup>`)+ TypeScript |
| 构建工具 | Vite 8 |
@@ -152,7 +162,7 @@
| Tauri 插件 | `updater` / `process` / `dialog` / `opener` / `store` / `prevent-default` |
| 工程化 | `unplugin-auto-import`、`unplugin-vue-components`(按需自动导入)、`tauri-helper`(命令注册) |
---
***
## 📦 环境要求
@@ -164,7 +174,7 @@
- macOS:Xcode Command Line Tools
- Linux:`webkit2gtk`、`libayatana-appindicator`、`librsvg` 等
---
***
## 🚀 快速开始
@@ -186,7 +196,7 @@ pnpm dev
### 常用脚本
| 命令 | 说明 |
| --- | --- |
| ------------------ | ----------------------------------------- |
| `pnpm dev` | 启动 Vite 开发服务器(端口 `1420`) |
| `pnpm tauri dev` | 启动 Tauri 桌面开发环境(自动拉起前端) |
| `pnpm build` | 类型检查(`vue-tsc --noEmit`)+ 构建前端产物到 `dist/` |
@@ -195,7 +205,7 @@ pnpm dev
> ⚠️ `pnpm dev` 端口固定为 `1420` 且 `strictPort: true`,被占用时会直接报错退出。
---
***
## 📁 项目结构
@@ -240,7 +250,7 @@ FrpcPilot/
└── package.json
```
---
***
## 🛠️ 开发指南
@@ -287,19 +297,19 @@ FrpcPilot/
- 推送 `v*` tag 会触发 `.github/workflows/release.yml`:构建 **Windows x64 / macOS Apple Silicon / Linux x64** 产物,签名生成更新产物与 `latest.json`,并自动从 `docs/CHANGELOG.md` 提取对应版本说明作为 Release 正文。
- 发布前请在 `docs/CHANGELOG.md` 中补上 `## v<版本号>` 段落。
---
***
## 🗺️ 路线图
- [x] 系统托盘与关闭到托盘
- [ ] 开机自启
- [x] 开机自启
- [ ] 主题跟随系统
- [ ] frpc 配置文件的导入 / 导出与校验
- [ ] frps 服务端信息的集中管理与状态检测
- [ ] 日志持久化与导出
- [ ] 更多语言与更多内置背景
---
***
## 🤝 参与贡献
@@ -311,7 +321,7 @@ FrpcPilot/
如果 FrpcPilot 对你有帮助,欢迎点一个 ⭐ **Star** 支持一下~
---
***
## 📚 参考链接
@@ -323,7 +333,7 @@ FrpcPilot/
- [VueUse](https://vueuse.org/)
- [frp 官方仓库](https://github.com/fatedier/frp)
---
***
## 📄 License
+1
View File
@@ -12,6 +12,7 @@
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-autostart": "^2.7.0",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.13.3",
+10
View File
@@ -14,6 +14,9 @@ importers:
'@tauri-apps/api':
specifier: ^2
version: 2.12.3
'@tauri-apps/plugin-autostart':
specifier: ^2.7.0
version: 2.7.0
'@tauri-apps/plugin-opener':
specifier: ^2
version: 2.7.1
@@ -535,6 +538,9 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-autostart@2.7.0':
resolution: {integrity: sha512-4uRq+rqKSAh6MWAxgk6FnUPigc8VMG8eDVYTLk3A7OfKs9sLVwfeM8kbUvzjpabRQvyl4zd9tfzXBTrrtuprsA==}
'@tauri-apps/plugin-opener@2.7.1':
resolution: {integrity: sha512-XkXdt30rontti9TII/gBBxFRXWfj8DiprFW7RgBFRBj1OhdfjBNVcFqSEWkkZ4hXafHnuEnB7F1sFsmu6uyywQ==}
@@ -1578,6 +1584,10 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.12.2
'@tauri-apps/cli-win32-x64-msvc': 2.12.2
'@tauri-apps/plugin-autostart@2.7.0':
dependencies:
'@tauri-apps/api': 2.12.3
'@tauri-apps/plugin-opener@2.7.1':
dependencies:
'@tauri-apps/api': 2.12.3
+112 -5
View File
@@ -231,6 +231,20 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17918dba7ecf78b9a14507ec9f984e7977d13fad87dc86d61038980a45d128cf"
dependencies = [
"dirs 6.0.0",
"os_info",
"smappservice-rs",
"thiserror 2.0.21",
"windows-registry",
"windows-result",
]
[[package]]
name = "autocfg"
version = "1.5.1"
@@ -493,6 +507,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cfg_aliases"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.2"
@@ -889,6 +909,15 @@ dependencies = [
"ctutils",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "7.0.0"
@@ -1280,6 +1309,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-helper",
"tauri-plugin-autostart",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tauri-plugin-prevent-default",
@@ -2571,6 +2601,18 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "ntapi"
version = "0.4.3"
@@ -2802,6 +2844,30 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "objc2-security"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
dependencies = [
"bitflags 2.13.2",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-service-management"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b213642d6959cc6023ceb1217aa595eaaf09b8094ce95127c103cab611fe65e8"
dependencies = [
"block2",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
"objc2-security",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.2"
@@ -2923,6 +2989,21 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_info"
version = "3.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b"
dependencies = [
"android_system_properties",
"log",
"nix",
"objc2",
"objc2-foundation",
"objc2-ui-kit",
"windows-sys 0.61.2",
]
[[package]]
name = "osakit"
version = "0.3.1"
@@ -3988,6 +4069,18 @@ dependencies = [
"serde",
]
[[package]]
name = "smappservice-rs"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52703b97a53101cf5d4580e0737aaa634ce1fdcfc123c16b2a9658e358013488"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-service-management",
"thiserror 2.0.21",
]
[[package]]
name = "socket2"
version = "0.6.5"
@@ -4494,7 +4587,7 @@ dependencies = [
"anyhow",
"bytes",
"cookie",
"dirs",
"dirs 7.0.0",
"dunce",
"embed_plist",
"getrandom 0.3.4",
@@ -4544,7 +4637,7 @@ checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc"
dependencies = [
"anyhow",
"cargo_toml",
"dirs",
"dirs 7.0.0",
"glob",
"heck 0.5.0",
"json-patch",
@@ -4629,6 +4722,20 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-autostart"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bce56ceb0a99b8ab5eebc977ba9ffc62391b862648cb9ab76fc8bc7231b8f7e2"
dependencies = [
"auto-launch",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.21",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.8.2"
@@ -4740,7 +4847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "855a336eb389d2c1502244fd48fa2d8cb487a430f691bbaed025195f99a04489"
dependencies = [
"base64 0.22.1",
"dirs",
"dirs 7.0.0",
"flate2",
"futures-util",
"http",
@@ -5277,7 +5384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72"
dependencies = [
"crossbeam-channel",
"dirs",
"dirs 7.0.0",
"libappindicator",
"muda",
"objc2",
@@ -6137,7 +6244,7 @@ dependencies = [
"block2",
"cookie",
"crossbeam-channel",
"dirs",
"dirs 7.0.0",
"dom_query",
"dpi",
"dunce",
+1
View File
@@ -39,6 +39,7 @@ sysinfo = "0.39"
tauri-plugin-dialog = "2.7.2"
tauri-plugin-updater = "2.10.1"
tauri-plugin-process = "2.3.1"
tauri-plugin-autostart = "2"
tauri-plugin-prevent-default = "6.0.1"
reqwest = { version = "0.12", features = ["json", "stream"] }
futures-util = "0.3"
+1
View File
@@ -8,6 +8,7 @@
"opener:default",
"updater:default",
"process:default",
"autostart:default",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-unminimize",
+5
View File
@@ -28,6 +28,11 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init()) // 对话框插件
.plugin(tauri_plugin_updater::Builder::new().build()) // 更新插件
.plugin(tauri_plugin_process::init()) // 进程插件(更新后重启)
// 开机自启插件:读写系统启动项(Windows 注册表 Run 键 / macOS LaunchAgent / Linux XDG autostart)
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
))
.plugin(tauri_plugin_prevent_default::Builder::new().build())
.setup(|app| {
let pool = tauri::async_runtime::block_on(async {
+23
View File
@@ -0,0 +1,23 @@
import { isTauri } from "@/utils/window"
/**
* 开机自启封装(Tauri 官方 autostart 插件)。
*
* 自启状态由系统启动项决定(Windows 注册表 Run 键 / macOS LaunchAgent /
* Linux XDG autostart),这里只做读写,不在前端另行持久化,避免两端不一致。
* 非 Tauri 环境(纯浏览器调试)下调用会被安全忽略。
*/
/** 读取系统启动项:本应用是否已设为开机自启 */
export async function isAutostartEnabled(): Promise<boolean> {
if (!isTauri()) return false
const { isEnabled } = await import("@tauri-apps/plugin-autostart")
return isEnabled()
}
/** 开启 / 关闭开机自启(直接写入系统启动项) */
export async function setAutostartEnabled(enabled: boolean): Promise<void> {
if (!isTauri()) return
const { enable, disable } = await import("@tauri-apps/plugin-autostart")
await (enabled ? enable() : disable())
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+9 -7
View File
@@ -29,7 +29,7 @@ const model = defineModel<T>({ required: true })
.card-radio-group {
display: flex;
flex-direction: column;
gap: 10px;
gap: 6px;
width: 100%;
}
@@ -38,8 +38,8 @@ const model = defineModel<T>({ required: true })
align-items: flex-start;
width: 100%;
margin-inline-end: 0;
padding: 10px 12px;
border-radius: 10px;
padding: 7px 10px;
border-radius: 8px;
border: 1px solid rgba(15, 23, 42, 0.1);
transition: border-color 0.2s ease, background-color 0.2s ease;
@@ -54,13 +54,13 @@ const model = defineModel<T>({ required: true })
}
:deep(.ant-radio) {
margin-top: 2px;
margin-top: 1px;
}
:deep(> span:last-child) {
display: flex;
flex-direction: column;
gap: 2px;
gap: 0;
padding-inline-end: 0;
}
}
@@ -75,12 +75,14 @@ const model = defineModel<T>({ required: true })
}
.card-radio-title {
font-size: 13px;
font-weight: 500;
line-height: 1.5;
}
.card-radio-desc {
font-size: 12px;
line-height: 1.4;
font-size: 11.5px;
line-height: 1.35;
opacity: 0.6;
}
</style>
+10 -2
View File
@@ -7,15 +7,16 @@
:mask-closable="false"
:closable="!store.quitting"
:keyboard="!store.quitting"
:body-style="{ paddingTop: '10px', paddingBottom: '4px' }"
@cancel="store.cancelClose()"
>
<!-- 关闭方式:最小化到系统托盘 / 直接退出 -->
<CardRadioGroup v-model="draftChoice" :options="choiceOptions" :disabled="store.quitting" />
<a-divider class="!my-3" />
<a-divider class="close-divider" />
<!-- 关闭行为:每次询问 / 保持以上选择 -->
<div class="mb-2 text-xs opacity-60">{{ t("closeDialog.behavior") }}</div>
<div class="mb-1.5 text-xs opacity-60">{{ t("closeDialog.behavior") }}</div>
<CardRadioGroup v-model="draftBehavior" :options="behaviorOptions" :disabled="store.quitting" />
<template #footer>
@@ -68,3 +69,10 @@ function onConfirm() {
void store.confirmClose(draftChoice.value, draftBehavior.value)
}
</script>
<style scoped>
/* 弹窗内两组选项之间只留窄窄一段分隔,整体更紧凑 */
.close-divider {
margin: 8px 0;
}
</style>
+20 -6
View File
@@ -84,7 +84,13 @@
},
"general": {
"title": "General",
"subtitle": "Basic settings such as close behavior and app language",
"subtitle": "Basic settings such as launch at startup, close behavior and app language",
"autostart": "Launch at startup",
"autostartTip": "Start FrpcPilot automatically after you sign in. The switch writes straight to the system startup entry.",
"autostartUnsupported": "System startup entries are not accessible here, so this setting is unavailable.",
"autostartEnabled": "Launch at startup enabled",
"autostartDisabled": "Launch at startup disabled",
"autostartFailed": "Failed to update launch at startup: {message}",
"closeBehavior": "Close behavior",
"closeBehaviorTip": "Choose what happens when you click the close button. Kept in sync with the close dialog.",
"closeAction": "Close method",
@@ -257,6 +263,10 @@
"about": {
"title": "About FrpcPilot",
"subtitle": "A lightweight, friendly visual client for frp",
"versionTitle": "Version",
"currentVersion": "Current version",
"checkUpdate": "Check for updates",
"newVersion": "New version v{version} is available",
"introTitle": "Introduction",
"intro": "FrpcPilot is a visual client for frp built with Tauri 2 + Vue 3 + TypeScript. Manage frpc versions, servers and tunnels through a friendly GUI — and say goodbye to hand-writing frpc.toml.",
"flowTitle": "How to use",
@@ -273,10 +283,11 @@
"repoTitle": "Open source",
"repoDesc": "Stars, issues and contributions are all welcome~",
"openRepo": "Open GitHub",
"versionTitle": "Version",
"currentVersion": "Current version",
"checkUpdate": "Check for updates",
"newVersion": "New version v{version} is available"
"contactTitle": "Contact",
"contactDesc": "Questions or ideas? Reach me by email or WeChat, or just open an issue on GitHub.",
"email": "Email",
"wechat": "WeChat",
"wechatTip": "Scan the QR code to add me"
},
"update": {
"title": "New version v{version} is available ✨",
@@ -296,6 +307,9 @@
"developing": "This feature is under construction",
"confirm": "OK",
"cancel": "Cancel",
"save": "Save"
"save": "Save",
"copy": "Copy",
"copySuccess": "Copied to clipboard",
"copyFailed": "Copy failed, please copy manually"
}
}
+20 -6
View File
@@ -84,7 +84,13 @@
},
"general": {
"title": "常规",
"subtitle": "关闭行为与应用语言等基础设置",
"subtitle": "开机自启、关闭行为与应用语言等基础设置",
"autostart": "开机自启",
"autostartTip": "登录系统后自动启动 FrpcPilot,开关状态直接写入系统启动项",
"autostartUnsupported": "当前环境无法读写系统启动项,暂不支持该设置",
"autostartEnabled": "已开启开机自启",
"autostartDisabled": "已关闭开机自启",
"autostartFailed": "设置开机自启失败:{message}",
"closeBehavior": "关闭行为",
"closeBehaviorTip": "设置点击关闭按钮时的处理方式,与关闭弹窗中的选项保持一致",
"closeAction": "关闭方式",
@@ -257,6 +263,10 @@
"about": {
"title": "关于 FrpcPilot",
"subtitle": "轻量、顺手的 frp 可视化桌面客户端",
"versionTitle": "版本信息",
"currentVersion": "当前版本",
"checkUpdate": "检查更新",
"newVersion": "发现新版本 v{version}",
"introTitle": "项目介绍",
"intro": "FrpcPilot 是一个基于 Tauri 2 + Vue 3 + TypeScript 打造的 frp 可视化客户端,帮你用图形界面管理 frpc 版本、服务器与隧道,从此告别手写 frpc.toml。",
"flowTitle": "使用流程",
@@ -273,10 +283,11 @@
"repoTitle": "开源地址",
"repoDesc": "欢迎 Star、提 Issue 或一起参与共建~",
"openRepo": "打开 GitHub",
"versionTitle": "版本信息",
"currentVersion": "当前版本",
"checkUpdate": "检查更新",
"newVersion": "发现新版本 v{version}"
"contactTitle": "联系方式",
"contactDesc": "遇到问题或有新想法,欢迎通过邮箱或微信联系我,也可以在 GitHub 上提 Issue。",
"email": "邮箱",
"wechat": "微信",
"wechatTip": "扫码添加微信"
},
"update": {
"title": "发现新版本 v{version} ✨",
@@ -296,6 +307,9 @@
"developing": "功能开发中,敬请期待",
"confirm": "确定",
"cancel": "取消",
"save": "保存"
"save": "保存",
"copy": "复制",
"copySuccess": "已复制到剪贴板",
"copyFailed": "复制失败,请手动复制"
}
}
+65
View File
@@ -0,0 +1,65 @@
import { defineStore } from "pinia"
import { message } from "ant-design-vue"
import { isTauri } from "@/utils/window"
import { isAutostartEnabled, setAutostartEnabled } from "@/api/autostart"
import { translate } from "@/i18n"
/**
* 开机自启:直接读写系统启动项,状态以系统为准,不做本地持久化。
*
* - 进入设置页时读一次系统启动项,保证开关展示的就是系统真实状态;
* - 切换后重新读取一次,写入失败则回滚开关并提示原因,避免界面与系统不一致;
* - 无法访问系统启动项(浏览器调试 / 插件不可用)时 supported 为 false,开关置灰。
*/
export const useAutostartStore = defineStore("autostart", () => {
/** 系统启动项是否已登记本应用 */
const enabled = ref(false)
/** 读取 / 写入系统启动项期间显示加载态,同时防止重复操作 */
const loading = ref(isTauri())
/** 当前环境能否读写系统启动项 */
const supported = ref(isTauri())
function errMsg(e: unknown) {
return e instanceof Error ? e.message : String(e)
}
/** 读取系统启动项的真实状态(进入设置页时调用) */
async function refresh() {
if (!isTauri()) return
loading.value = true
try {
enabled.value = await isAutostartEnabled()
supported.value = true
} catch {
// 读取失败(如插件未注册、权限缺失):禁用开关,不展示可能错误的状态
supported.value = false
} finally {
loading.value = false
}
}
/** 切换开机自启:写入系统启动项,成功 / 失败都以系统状态为准并给出提示 */
async function toggle(value: boolean) {
if (!supported.value || loading.value || value === enabled.value) return
const previous = enabled.value
loading.value = true
try {
await setAutostartEnabled(value)
// 以系统启动项为准;读回失败时先按目标值展示,下次进入设置页会再次校准
enabled.value = await isAutostartEnabled().catch(() => value)
message.success(
translate(
value ? "settings.general.autostartEnabled" : "settings.general.autostartDisabled",
),
)
} catch (e) {
enabled.value = previous
message.error(translate("settings.general.autostartFailed", { message: errMsg(e) }))
} finally {
loading.value = false
}
}
return { enabled, loading, supported, refresh, toggle }
})
+1
View File
@@ -23,6 +23,7 @@ declare module 'vue' {
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
+121 -15
View File
@@ -3,6 +3,25 @@
<h2 class="mb-1 text-lg font-semibold">{{ t("about.title") }}</h2>
<p class="mb-6 text-xs opacity-60">{{ t("about.subtitle") }}</p>
<!-- 版本信息 / 检查更新:最常用的信息放在最前面 -->
<section class="mb-6">
<div class="mb-3 text-sm font-medium">{{ t("about.versionTitle") }}</div>
<div class="flex flex-wrap items-center gap-3">
<span class="text-xs opacity-60">{{ t("about.currentVersion") }}</span>
<a-tag color="purple" class="!me-0">v{{ version }}</a-tag>
<a-badge :dot="updateStore.hasUpdate">
<a-button size="small" :loading="updateStore.checking" @click="onCheckUpdate">
{{ t("about.checkUpdate") }}
</a-button>
</a-badge>
<span v-if="updateStore.hasUpdate" class="text-xs text-[#6366f1]">
{{ t("about.newVersion", { version: updateStore.info?.version }) }}
</span>
</div>
</section>
<a-divider class="!my-5" />
<!-- 项目介绍 -->
<section class="mb-6">
<div class="mb-2 text-sm font-medium">{{ t("about.introTitle") }}</div>
@@ -42,30 +61,57 @@
<a-divider class="!my-5" />
<!-- 版本信息 / 检查更新 -->
<!-- 联系方式:邮箱可唤起邮件客户端并支持一键复制,微信以二维码图片展示(可点击放大) -->
<section>
<div class="mb-3 text-sm font-medium">{{ t("about.versionTitle") }}</div>
<div class="flex flex-wrap items-center gap-3">
<span class="text-xs opacity-60">{{ t("about.currentVersion") }}</span>
<a-tag color="purple" class="!me-0">v{{ version }}</a-tag>
<a-badge :dot="updateStore.hasUpdate">
<a-button size="small" :loading="updateStore.checking" @click="onCheckUpdate">
{{ t("about.checkUpdate") }}
</a-button>
</a-badge>
<span v-if="updateStore.hasUpdate" class="text-xs text-[#6366f1]">
{{ t("about.newVersion", { version: updateStore.info?.version }) }}
</span>
<div class="mb-3 text-sm font-medium">{{ t("about.contactTitle") }}</div>
<a-card class="glass-card" size="small" :body-style="{ padding: '14px 16px' }">
<div class="flex items-center gap-3">
<MailOutlined class="text-2xl" />
<div class="min-w-0 flex-1">
<div class="text-xs opacity-60">{{ t("about.email") }}</div>
<a
class="contact-value"
:href="`mailto:${CONTACT_EMAIL}`"
@click.prevent="onOpenEmail"
>
{{ CONTACT_EMAIL }}
</a>
</div>
<a-button size="small" :icon="h(CopyOutlined)" @click="onCopy(CONTACT_EMAIL)">
{{ t("common.copy") }}
</a-button>
</div>
<a-divider class="!my-3" />
<div class="flex items-center gap-3">
<WechatOutlined class="text-2xl" />
<div class="min-w-0 flex-1">
<div class="text-xs opacity-60">{{ t("about.wechat") }}</div>
<div class="truncate text-sm font-medium">{{ t("about.wechatTip") }}</div>
</div>
<!-- 微信二维码:点击可用 a-image 内置的预览放大,方便用手机扫码 -->
<a-image :src="wechatQr" :width="100" class="wechat-qr" />
</div>
</a-card>
<p class="mt-2 text-xs opacity-60">{{ t("about.contactDesc") }}</p>
</section>
</div>
</template>
<script setup lang="ts">
import { h } from "vue"
import { GithubOutlined, LinkOutlined } from "@ant-design/icons-vue"
import { message } from "ant-design-vue"
import {
CopyOutlined,
GithubOutlined,
LinkOutlined,
MailOutlined,
WechatOutlined,
} from "@ant-design/icons-vue"
import { useI18n } from "vue-i18n"
import { getAppVersion, openProjectRepo, PROJECT_GITHUB_URL } from "@/api/updater"
import wechatQr from "@/assets/images/wx.webp"
import { getAppVersion, openExternal, openProjectRepo, PROJECT_GITHUB_URL } from "@/api/updater"
import { useUpdateStore } from "@/stores/update"
const { t } = useI18n()
@@ -75,6 +121,9 @@ const version = ref("")
const stack = ["Tauri 2", "Vue 3", "TypeScript", "Rust", "Vite"]
/** 作者邮箱:展示、复制、唤起邮件客户端共用同一份常量 */
const CONTACT_EMAIL = "wu434425608@163.com"
const flowItems = computed(() => [
{ title: t("about.flow.step1Title"), description: t("about.flow.step1Desc") },
{ title: t("about.flow.step2Title"), description: t("about.flow.step2Desc") },
@@ -93,4 +142,61 @@ function onOpenRepo() {
function onCheckUpdate() {
void updateStore.checkForUpdate(false)
}
/** 复制到剪贴板,失败时提示手动复制 */
async function onCopy(text: string) {
try {
await navigator.clipboard.writeText(text)
message.success(t("common.copySuccess"))
} catch {
message.error(t("common.copyFailed"))
}
}
/** 用系统默认邮件客户端写信;没有可用客户端时退化为复制邮箱 */
async function onOpenEmail() {
try {
await openExternal(`mailto:${CONTACT_EMAIL}`)
} catch {
await onCopy(CONTACT_EMAIL)
}
}
</script>
<style scoped>
/* 邮箱可点击:悬停时高亮,提示可以唤起邮件客户端 */
.contact-value {
display: block;
overflow: hidden;
font-size: 13px;
font-weight: 500;
line-height: 1.5;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s ease;
}
.contact-value:hover {
color: #6366f1;
text-decoration: underline;
}
/* 微信二维码:白底圆角,深色模式下也保持足够对比度,便于手机扫描 */
.wechat-qr {
flex-shrink: 0;
padding: 4px;
cursor: zoom-in;
background-color: #fff;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 8px;
}
.wechat-qr :deep(img) {
display: block;
border-radius: 4px;
}
.dark .wechat-qr {
border-color: rgba(255, 255, 255, 0.16);
}
</style>
+52 -7
View File
@@ -1,26 +1,45 @@
<template>
<div class="max-w-2xl">
<h2 class="mb-1 text-lg font-semibold">{{ t("settings.general.title") }}</h2>
<p class="mb-6 text-xs opacity-60">{{ t("settings.general.subtitle") }}</p>
<p class="mb-5 text-xs opacity-60">{{ t("settings.general.subtitle") }}</p>
<!-- 开机自启:开关直接写入系统启动项,状态由系统决定 -->
<section>
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
<div class="text-sm font-medium">{{ t("settings.general.autostart") }}</div>
<p class="mt-1 text-xs opacity-60">{{ autostartTip }}</p>
</div>
<a-switch
class="shrink-0"
:checked="autostartStore.enabled"
:loading="autostartStore.loading"
:disabled="!autostartStore.supported"
@change="onAutostartChange"
/>
</div>
</section>
<a-divider class="general-divider" />
<!-- 关闭行为:与关闭弹窗内容保持一致,可在此再次修改 -->
<section class="mb-6">
<section>
<div class="mb-1 text-sm font-medium">{{ t("settings.general.closeBehavior") }}</div>
<p class="mb-3 text-xs opacity-60">{{ t("settings.general.closeBehaviorTip") }}</p>
<p class="mb-2 text-xs opacity-60">{{ t("settings.general.closeBehaviorTip") }}</p>
<CardRadioGroup v-model="windowStore.closeBehavior" :options="behaviorOptions" />
<!-- 关闭方式:与关闭弹窗一致,可随时修改;选择「保持以上选择」后关闭时直接生效 -->
<div class="mt-4 mb-1 text-sm font-medium">{{ t("settings.general.closeAction") }}</div>
<p class="mb-3 text-xs opacity-60">{{ t("settings.general.closeActionTip") }}</p>
<div class="mt-3 mb-1 text-sm font-medium">{{ t("settings.general.closeAction") }}</div>
<p class="mb-2 text-xs opacity-60">{{ t("settings.general.closeActionTip") }}</p>
<CardRadioGroup v-model="windowStore.closeChoice" :options="choiceOptions" />
</section>
<a-divider />
<a-divider class="general-divider" />
<!-- 语言:与顶部栏语言切换共用同一状态 -->
<section>
<div class="mb-1 text-sm font-medium">{{ t("settings.general.language") }}</div>
<p class="mb-3 text-xs opacity-60">{{ t("settings.general.languageTip") }}</p>
<p class="mb-2 text-xs opacity-60">{{ t("settings.general.languageTip") }}</p>
<a-radio-group
v-model:value="appStore.language"
:options="languageRadios"
@@ -35,10 +54,12 @@
import { useI18n } from "vue-i18n"
import { languageOptions } from "@/i18n"
import { useAppStore } from "@/stores/app"
import { useAutostartStore } from "@/stores/autostart"
import { useWindowStore } from "@/stores/window"
const { t } = useI18n()
const appStore = useAppStore()
const autostartStore = useAutostartStore()
const windowStore = useWindowStore()
const behaviorOptions = computed(() => [
@@ -57,4 +78,28 @@ const choiceOptions = computed(() => [
/** 语言选项与顶部栏共用 languageOptions,保证两端一致 */
const languageRadios = languageOptions.map((item) => ({ label: item.label, value: item.key }))
/** 无法访问系统启动项时(非桌面环境)给出说明,避免开关无故置灰 */
const autostartTip = computed(() =>
autostartStore.supported
? t("settings.general.autostartTip")
: t("settings.general.autostartUnsupported"),
)
/** 切换开机自启:失败时由 store 回滚开关状态并提示原因 */
async function onAutostartChange(checked: boolean | string | number) {
await autostartStore.toggle(Boolean(checked))
}
// 每次进入设置页都读一次系统启动项,用户可能在系统设置里改过
onMounted(() => {
void autostartStore.refresh()
})
</script>
<style scoped>
/* 各组设置之间留白收紧,避免分隔线两侧过于松散 */
.general-divider {
margin: 16px 0;
}
</style>