-
-
- {{ t("closeDialog.tray") }}
- {{ t("closeDialog.trayDesc") }}
-
-
- {{ t("closeDialog.exit") }}
- {{ t("closeDialog.exitDesc") }}
-
-
+
+
+
+
+
+
+ {{ t("closeDialog.behavior") }}
+
{{ t("common.cancel") }}
-
+
{{ t("common.confirm") }}
@@ -33,68 +31,40 @@
-
-
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 8245d97..6d7bee4 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -39,7 +39,12 @@
"tray": "Minimize to system tray",
"trayDesc": "Keep Frpilot running in the background; reopen it from the system tray",
"exit": "Quit directly",
- "exitDesc": "Stop the tunnel and exit Frpilot completely"
+ "exitDesc": "Stop the tunnel and exit Frpilot completely",
+ "behavior": "Close behavior",
+ "ask": "Ask every time",
+ "askDesc": "Show this dialog on every close and let you choose",
+ "remember": "Keep the choice above",
+ "rememberDesc": "Remember the close method above and never ask again"
},
"system": {
"cpu": "CPU usage",
@@ -73,9 +78,20 @@
"title": "Settings",
"placeholder": "The settings page is under construction.",
"menu": {
+ "general": "General",
"appearance": "Appearance",
"about": "About"
},
+ "general": {
+ "title": "General",
+ "subtitle": "Basic settings such as close behavior and app language",
+ "closeBehavior": "Close behavior",
+ "closeBehaviorTip": "Choose what happens when you click the close button. Kept in sync with the close dialog.",
+ "closeAction": "Close method",
+ "closeActionTip": "Takes effect when \"Keep the choice above\" is selected. The window closes this way directly, and it is also the default option in the close dialog.",
+ "language": "Language",
+ "languageTip": "Switch the interface language. In sync with the language switcher in the title bar."
+ },
"appearance": {
"title": "Appearance",
"subtitle": "Customize the theme mode and window background",
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index 4be45c3..077210a 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -39,7 +39,12 @@
"tray": "最小化到系统托盘",
"trayDesc": "保持 Frpilot 在后台运行,可从系统托盘随时打开",
"exit": "直接退出",
- "exitDesc": "停止穿透并完全退出 Frpilot"
+ "exitDesc": "停止穿透并完全退出 Frpilot",
+ "behavior": "关闭行为",
+ "ask": "每次询问",
+ "askDesc": "每次关闭都弹出此对话框,由你选择关闭方式",
+ "remember": "保持以上选择",
+ "rememberDesc": "记住上面的关闭方式,之后关闭不再询问"
},
"system": {
"cpu": "CPU 占用",
@@ -73,9 +78,20 @@
"title": "设置",
"placeholder": "设置页面开发中,敬请期待。",
"menu": {
+ "general": "常规",
"appearance": "外观设置",
"about": "关于"
},
+ "general": {
+ "title": "常规",
+ "subtitle": "关闭行为与应用语言等基础设置",
+ "closeBehavior": "关闭行为",
+ "closeBehaviorTip": "设置点击关闭按钮时的处理方式,与关闭弹窗中的选项保持一致",
+ "closeAction": "关闭方式",
+ "closeActionTip": "选择「保持以上选择」后生效,关闭窗口时将直接按此方式处理,同时也作为关闭弹窗的默认选项",
+ "language": "语言",
+ "languageTip": "切换界面显示语言,与顶部栏的语言切换保持同步"
+ },
"appearance": {
"title": "外观设置",
"subtitle": "自定义窗口的主题模式与背景图片",
diff --git a/src/router/router.ts b/src/router/router.ts
index c75ea1b..a7dc19a 100644
--- a/src/router/router.ts
+++ b/src/router/router.ts
@@ -36,9 +36,15 @@ const routes: RouteRecordRaw[] = [
path: "setting",
name: "Setting",
component: () => import("@/views/Setting.vue"),
- redirect: "/setting/appearance",
+ redirect: "/setting/general",
meta: { title: "settings.title" },
children: [
+ {
+ path: "general",
+ name: "General",
+ component: () => import("@/views/setting/General.vue"),
+ meta: { title: "settings.menu.general" },
+ },
{
path: "appearance",
name: "Appearance",
diff --git a/src/stores/window.ts b/src/stores/window.ts
index 11b2fc9..cb36358 100644
--- a/src/stores/window.ts
+++ b/src/stores/window.ts
@@ -5,26 +5,34 @@ import { useFrpcStore } from "@/stores/frpc"
/** 关闭窗口时用户的选择:最小化到系统托盘 / 直接退出 */
export type CloseChoice = "tray" | "exit"
+/** 关闭行为:每次询问 / 保持以上选择 */
+export type CloseBehavior = "ask" | "remember"
+
/**
* 窗口生命周期:关闭确认、最小化到系统托盘、退出软件。
*
- * - 顶部栏关闭按钮与系统原生关闭(如 Alt+F4)都会先弹出确认对话框;
- * - 选择「最小化到系统托盘」时隐藏窗口,程序保持后台运行;
- * - 选择「直接退出」时若穿透正在运行会先停止,再退出整个软件。
+ * - 顶部栏关闭按钮与系统原生关闭(如 Alt+F4)都会先经过 requestClose;
+ * - 关闭行为为「每次询问」时弹出确认对话框,选择「最小化到系统托盘」会隐藏窗口、
+ * 程序保持后台运行,选择「直接退出」会先停止穿透再退出整个软件;
+ * - 关闭行为为「保持以上选择」时不弹窗,直接按记住的关闭方式处理。
+ *
+ * 关闭行为与关闭方式均持久化,可在「设置 - 常规」中再次修改,与关闭弹窗内容保持一致。
*/
export const useWindowStore = defineStore("window", () => {
const frpcStore = useFrpcStore()
/** 关闭确认对话框是否打开 */
const closeConfirmOpen = ref(false)
- /** 当前选中的关闭方式 */
- const closeChoice = ref("tray")
+ /** 记住的关闭方式(弹窗默认选中项;「保持以上选择」时直接生效,持久化) */
+ const closeChoice = useStorage("frpilot:close-choice", "tray")
+ /** 关闭行为:每次询问 / 保持以上选择(持久化,默认每次询问) */
+ const closeBehavior = useStorage("frpilot:close-behavior", "ask")
/** 是否正在退出(停止穿透 + 退出软件) */
const quitting = ref(false)
let initialized = false
- /** 应用启动时调用一次:拦截系统原生关闭,改为弹出确认对话框 */
+ /** 应用启动时调用一次:拦截系统原生关闭,改为按关闭行为处理 */
async function init() {
if (initialized || !isTauri()) return
initialized = true
@@ -36,9 +44,16 @@ export const useWindowStore = defineStore("window", () => {
})
}
- /** 请求关闭窗口:弹出确认对话框(默认选中最小化到系统托盘) */
+ /**
+ * 请求关闭窗口:
+ * - 「每次询问」弹出确认对话框;
+ * - 「保持以上选择」不弹窗,直接按记住的方式处理。
+ */
function requestClose() {
- closeChoice.value = "tray"
+ if (closeBehavior.value === "remember") {
+ void applyClose(closeChoice.value)
+ return
+ }
closeConfirmOpen.value = true
}
@@ -47,10 +62,19 @@ export const useWindowStore = defineStore("window", () => {
closeConfirmOpen.value = false
}
- /** 确认关闭:按所选方式处理 */
- async function confirmClose() {
- const choice = closeChoice.value
+ /** 确认关闭:持久化所选行为与方式,再按所选方式处理 */
+ async function confirmClose(
+ choice: CloseChoice = closeChoice.value,
+ behavior: CloseBehavior = closeBehavior.value,
+ ) {
+ closeChoice.value = choice
+ closeBehavior.value = behavior
closeConfirmOpen.value = false
+ await applyClose(choice)
+ }
+
+ /** 按所选关闭方式执行:最小化到托盘 / 退出软件 */
+ async function applyClose(choice: CloseChoice) {
if (choice === "tray") {
await minimizeToTray()
} else {
@@ -85,11 +109,13 @@ export const useWindowStore = defineStore("window", () => {
return {
closeConfirmOpen,
closeChoice,
+ closeBehavior,
quitting,
init,
requestClose,
cancelClose,
confirmClose,
+ applyClose,
minimizeToTray,
showWindow,
quitApp,
diff --git a/src/types/components.d.ts b/src/types/components.d.ts
index 192db3f..9c5beee 100644
--- a/src/types/components.d.ts
+++ b/src/types/components.d.ts
@@ -45,6 +45,7 @@ declare module 'vue' {
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+ CardRadioGroup: typeof import('./../components/CardRadioGroup.vue')['default']
CloseConfirmModal: typeof import('./../components/CloseConfirmModal.vue')['default']
FrpCard: typeof import('./../components/frp/FrpCard.vue')['default']
MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default']
diff --git a/src/views/Setting.vue b/src/views/Setting.vue
index ab4f098..754c0ed 100644
--- a/src/views/Setting.vue
+++ b/src/views/Setting.vue
@@ -20,7 +20,7 @@