Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eac325b75 | ||
|
|
bba146fd1a | ||
|
|
7cbc80176d |
@@ -2,22 +2,22 @@
|
||||
|
||||
# FrpcPilot
|
||||
|
||||
**frp 可视化桌面客户端 —— 告别手写 `frpc.toml`**
|
||||
**frp 可视化桌面客户端 —— 告别手写** **`frpc.toml`**
|
||||
|
||||
下载 frpc、配置服务器、编排隧道、一键启动并实时查看日志,全部在同一个窗口里完成。
|
||||
|
||||
[](https://github.com/SparksFly91/FrpcPilot/releases)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/SparksFly91/FrpcPilot/releases)
|
||||
[](./LICENSE)
|
||||
[](#-下载安装)
|
||||
[](https://v2.tauri.app/)
|
||||
[](https://vuejs.org/)
|
||||
[](https://v2.tauri.app/)
|
||||
[](https://vuejs.org/)
|
||||
[](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 三平台,并内置应用内自动更新。
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
## 🖼️ 界面预览
|
||||
|
||||
| 仪表盘 | 服务器 |
|
||||
| --- | --- |
|
||||
| ------------------------------- | ------------------------------- |
|
||||
|  |  |
|
||||
|
||||
| 隧道列表 | Frpc 下载 |
|
||||
| --- | --- |
|
||||
| --------------------------------- | -------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
| 设置 · 常规 | 设置 · 外观 |
|
||||
| --- | --- |
|
||||
| ---------------------------------- | ------------------------------- |
|
||||
|  |  |
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+10
@@ -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
|
||||
|
||||
Generated
+112
-5
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "复制失败,请手动复制"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
Vendored
+1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user