feat: 新增常规设置页面,支持关闭行为与语言切换
新增常规设置路由和页面,实现关闭行为(每次询问/保持选择)与应用语言的持久化配置;封装卡片式单选组组件复用在关闭弹窗和设置页;修改关闭逻辑,根据关闭行为决定是否显示确认弹窗,同步更新多语言文案和类型定义。
This commit is contained in:
+12
-1
@@ -159,4 +159,15 @@ Layout布局content区域的磨砂玻璃卡片圆角样式我要改动一下:
|
|||||||
#### 3.8 系统托盘图标BUG修改
|
#### 3.8 系统托盘图标BUG修改
|
||||||
|
|
||||||
- 开发阶段每次修改代码热更新之后,系统托盘图标就会新增一个
|
- 开发阶段每次修改代码热更新之后,系统托盘图标就会新增一个
|
||||||
- 需要修复确保只有一个系统托盘图标
|
- 需要修复确保只有一个系统托盘图标
|
||||||
|
|
||||||
|
#### 3.9 新增常规设置
|
||||||
|
|
||||||
|
- 设置页增加一个子页面-常规,第一项设置内容是关闭行为设置
|
||||||
|
- 目前的关闭行为弹窗分为最小化到系统托盘和直接退出两种,需要增加一个单选组,里面是每次询问和保持以上选择(默认每次询问)
|
||||||
|
- 每次询问即每次关闭都需要弹窗供用户选择,保持以上选择则是保持用户选择的关闭方式,持久生效
|
||||||
|
- 常规设置页面需要同步关闭弹窗里面的内容,使用户可以再次修改设置
|
||||||
|
|
||||||
|
- 常规设置页面再增加一个语言切换设置,保持跟Header栏同步
|
||||||
|
|
||||||
|
开发过程中使用到的组件尽可能全部使用Antdv库里面的,另外我引入的有tailwindcss原子化组件、auto-import和components依赖可以简化引入,开发之前尽可能先阅读一下package.json,看一下有哪些依赖可以简化代码开发
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 卡片式单选组:标题 + 描述纵向排列,选中时整卡高亮。关闭弹窗与「设置 - 常规」共用 -->
|
||||||
|
<a-radio-group v-model:value="model" class="card-radio-group" :disabled="disabled">
|
||||||
|
<a-radio
|
||||||
|
v-for="option in options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
:value="option.value"
|
||||||
|
class="card-radio"
|
||||||
|
>
|
||||||
|
<span class="card-radio-title">{{ option.label }}</span>
|
||||||
|
<span v-if="option.description" class="card-radio-desc">{{ option.description }}</span>
|
||||||
|
</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends string | number">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
options: { value: T; label: string; description?: string }[]
|
||||||
|
disabled?: boolean
|
||||||
|
}>(),
|
||||||
|
{ disabled: false },
|
||||||
|
)
|
||||||
|
|
||||||
|
const model = defineModel<T>({ required: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.card-radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-radio {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||||
|
transition: border-color 0.2s ease, background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(99, 102, 241, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中态高亮
|
||||||
|
&.ant-radio-wrapper-checked {
|
||||||
|
border-color: #6366f1;
|
||||||
|
background-color: rgba(99, 102, 241, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-radio) {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(> span:last-child) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding-inline-end: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-radio {
|
||||||
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
|
|
||||||
|
&.ant-radio-wrapper-checked {
|
||||||
|
border-color: #6366f1;
|
||||||
|
background-color: rgba(99, 102, 241, 0.16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-radio-title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-radio-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,29 +2,27 @@
|
|||||||
<a-modal
|
<a-modal
|
||||||
:open="store.closeConfirmOpen"
|
:open="store.closeConfirmOpen"
|
||||||
:title="t('closeDialog.title')"
|
:title="t('closeDialog.title')"
|
||||||
:width="380"
|
:width="400"
|
||||||
centered
|
centered
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:closable="!store.quitting"
|
:closable="!store.quitting"
|
||||||
:keyboard="!store.quitting"
|
:keyboard="!store.quitting"
|
||||||
@cancel="store.cancelClose()"
|
@cancel="store.cancelClose()"
|
||||||
>
|
>
|
||||||
<a-radio-group v-model:value="store.closeChoice" class="close-choice">
|
<!-- 关闭方式:最小化到系统托盘 / 直接退出 -->
|
||||||
<a-radio value="tray" class="close-option">
|
<CardRadioGroup v-model="draftChoice" :options="choiceOptions" :disabled="store.quitting" />
|
||||||
<span class="close-option-title">{{ t("closeDialog.tray") }}</span>
|
|
||||||
<span class="close-option-desc">{{ t("closeDialog.trayDesc") }}</span>
|
<a-divider class="!my-3" />
|
||||||
</a-radio>
|
|
||||||
<a-radio value="exit" class="close-option">
|
<!-- 关闭行为:每次询问 / 保持以上选择 -->
|
||||||
<span class="close-option-title">{{ t("closeDialog.exit") }}</span>
|
<div class="mb-2 text-xs opacity-60">{{ t("closeDialog.behavior") }}</div>
|
||||||
<span class="close-option-desc">{{ t("closeDialog.exitDesc") }}</span>
|
<CardRadioGroup v-model="draftBehavior" :options="behaviorOptions" :disabled="store.quitting" />
|
||||||
</a-radio>
|
|
||||||
</a-radio-group>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button :disabled="store.quitting" @click="store.cancelClose()">
|
<a-button :disabled="store.quitting" @click="store.cancelClose()">
|
||||||
{{ t("common.cancel") }}
|
{{ t("common.cancel") }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" :loading="store.quitting" @click="store.confirmClose()">
|
<a-button type="primary" :loading="store.quitting" @click="onConfirm">
|
||||||
{{ t("common.confirm") }}
|
{{ t("common.confirm") }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,68 +31,40 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "vue-i18n"
|
import { useI18n } from "vue-i18n"
|
||||||
import { useWindowStore } from "@/stores/window"
|
import { useWindowStore, type CloseBehavior, type CloseChoice } from "@/stores/window"
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useWindowStore()
|
const store = useWindowStore()
|
||||||
|
|
||||||
|
/** 弹窗内的临时选择,点击「确定」后才写回设置(避免误触单选框又取消时被改掉) */
|
||||||
|
const draftChoice = ref<CloseChoice>("tray")
|
||||||
|
const draftBehavior = ref<CloseBehavior>("ask")
|
||||||
|
|
||||||
|
const choiceOptions = computed(() => [
|
||||||
|
{ value: "tray" as const, label: t("closeDialog.tray"), description: t("closeDialog.trayDesc") },
|
||||||
|
{ value: "exit" as const, label: t("closeDialog.exit"), description: t("closeDialog.exitDesc") },
|
||||||
|
])
|
||||||
|
|
||||||
|
const behaviorOptions = computed(() => [
|
||||||
|
{ value: "ask" as const, label: t("closeDialog.ask"), description: t("closeDialog.askDesc") },
|
||||||
|
{
|
||||||
|
value: "remember" as const,
|
||||||
|
label: t("closeDialog.remember"),
|
||||||
|
description: t("closeDialog.rememberDesc"),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 每次打开弹窗时,用当前设置初始化临时选择
|
||||||
|
watch(
|
||||||
|
() => store.closeConfirmOpen,
|
||||||
|
(open) => {
|
||||||
|
if (!open) return
|
||||||
|
draftChoice.value = store.closeChoice
|
||||||
|
draftBehavior.value = store.closeBehavior
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function onConfirm() {
|
||||||
|
void store.confirmClose(draftChoice.value, draftBehavior.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.close-choice {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
margin-inline-end: 0;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
|
||||||
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: rgba(99, 102, 241, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中态高亮
|
|
||||||
&.ant-radio-wrapper-checked {
|
|
||||||
border-color: #6366f1;
|
|
||||||
background-color: rgba(99, 102, 241, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-radio) {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(> span:last-child) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
padding-inline-end: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .close-option {
|
|
||||||
border-color: rgba(255, 255, 255, 0.12);
|
|
||||||
|
|
||||||
&.ant-radio-wrapper-checked {
|
|
||||||
border-color: #6366f1;
|
|
||||||
background-color: rgba(99, 102, 241, 0.16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-option-title {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-option-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -39,7 +39,12 @@
|
|||||||
"tray": "Minimize to system tray",
|
"tray": "Minimize to system tray",
|
||||||
"trayDesc": "Keep Frpilot running in the background; reopen it from the system tray",
|
"trayDesc": "Keep Frpilot running in the background; reopen it from the system tray",
|
||||||
"exit": "Quit directly",
|
"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": {
|
"system": {
|
||||||
"cpu": "CPU usage",
|
"cpu": "CPU usage",
|
||||||
@@ -73,9 +78,20 @@
|
|||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"placeholder": "The settings page is under construction.",
|
"placeholder": "The settings page is under construction.",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"general": "General",
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"about": "About"
|
"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": {
|
"appearance": {
|
||||||
"title": "Appearance",
|
"title": "Appearance",
|
||||||
"subtitle": "Customize the theme mode and window background",
|
"subtitle": "Customize the theme mode and window background",
|
||||||
|
|||||||
@@ -39,7 +39,12 @@
|
|||||||
"tray": "最小化到系统托盘",
|
"tray": "最小化到系统托盘",
|
||||||
"trayDesc": "保持 Frpilot 在后台运行,可从系统托盘随时打开",
|
"trayDesc": "保持 Frpilot 在后台运行,可从系统托盘随时打开",
|
||||||
"exit": "直接退出",
|
"exit": "直接退出",
|
||||||
"exitDesc": "停止穿透并完全退出 Frpilot"
|
"exitDesc": "停止穿透并完全退出 Frpilot",
|
||||||
|
"behavior": "关闭行为",
|
||||||
|
"ask": "每次询问",
|
||||||
|
"askDesc": "每次关闭都弹出此对话框,由你选择关闭方式",
|
||||||
|
"remember": "保持以上选择",
|
||||||
|
"rememberDesc": "记住上面的关闭方式,之后关闭不再询问"
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"cpu": "CPU 占用",
|
"cpu": "CPU 占用",
|
||||||
@@ -73,9 +78,20 @@
|
|||||||
"title": "设置",
|
"title": "设置",
|
||||||
"placeholder": "设置页面开发中,敬请期待。",
|
"placeholder": "设置页面开发中,敬请期待。",
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"general": "常规",
|
||||||
"appearance": "外观设置",
|
"appearance": "外观设置",
|
||||||
"about": "关于"
|
"about": "关于"
|
||||||
},
|
},
|
||||||
|
"general": {
|
||||||
|
"title": "常规",
|
||||||
|
"subtitle": "关闭行为与应用语言等基础设置",
|
||||||
|
"closeBehavior": "关闭行为",
|
||||||
|
"closeBehaviorTip": "设置点击关闭按钮时的处理方式,与关闭弹窗中的选项保持一致",
|
||||||
|
"closeAction": "关闭方式",
|
||||||
|
"closeActionTip": "选择「保持以上选择」后生效,关闭窗口时将直接按此方式处理,同时也作为关闭弹窗的默认选项",
|
||||||
|
"language": "语言",
|
||||||
|
"languageTip": "切换界面显示语言,与顶部栏的语言切换保持同步"
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"title": "外观设置",
|
"title": "外观设置",
|
||||||
"subtitle": "自定义窗口的主题模式与背景图片",
|
"subtitle": "自定义窗口的主题模式与背景图片",
|
||||||
|
|||||||
@@ -36,9 +36,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: "setting",
|
path: "setting",
|
||||||
name: "Setting",
|
name: "Setting",
|
||||||
component: () => import("@/views/Setting.vue"),
|
component: () => import("@/views/Setting.vue"),
|
||||||
redirect: "/setting/appearance",
|
redirect: "/setting/general",
|
||||||
meta: { title: "settings.title" },
|
meta: { title: "settings.title" },
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "general",
|
||||||
|
name: "General",
|
||||||
|
component: () => import("@/views/setting/General.vue"),
|
||||||
|
meta: { title: "settings.menu.general" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "appearance",
|
path: "appearance",
|
||||||
name: "Appearance",
|
name: "Appearance",
|
||||||
|
|||||||
+37
-11
@@ -5,26 +5,34 @@ import { useFrpcStore } from "@/stores/frpc"
|
|||||||
/** 关闭窗口时用户的选择:最小化到系统托盘 / 直接退出 */
|
/** 关闭窗口时用户的选择:最小化到系统托盘 / 直接退出 */
|
||||||
export type CloseChoice = "tray" | "exit"
|
export type CloseChoice = "tray" | "exit"
|
||||||
|
|
||||||
|
/** 关闭行为:每次询问 / 保持以上选择 */
|
||||||
|
export type CloseBehavior = "ask" | "remember"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 窗口生命周期:关闭确认、最小化到系统托盘、退出软件。
|
* 窗口生命周期:关闭确认、最小化到系统托盘、退出软件。
|
||||||
*
|
*
|
||||||
* - 顶部栏关闭按钮与系统原生关闭(如 Alt+F4)都会先弹出确认对话框;
|
* - 顶部栏关闭按钮与系统原生关闭(如 Alt+F4)都会先经过 requestClose;
|
||||||
* - 选择「最小化到系统托盘」时隐藏窗口,程序保持后台运行;
|
* - 关闭行为为「每次询问」时弹出确认对话框,选择「最小化到系统托盘」会隐藏窗口、
|
||||||
* - 选择「直接退出」时若穿透正在运行会先停止,再退出整个软件。
|
* 程序保持后台运行,选择「直接退出」会先停止穿透再退出整个软件;
|
||||||
|
* - 关闭行为为「保持以上选择」时不弹窗,直接按记住的关闭方式处理。
|
||||||
|
*
|
||||||
|
* 关闭行为与关闭方式均持久化,可在「设置 - 常规」中再次修改,与关闭弹窗内容保持一致。
|
||||||
*/
|
*/
|
||||||
export const useWindowStore = defineStore("window", () => {
|
export const useWindowStore = defineStore("window", () => {
|
||||||
const frpcStore = useFrpcStore()
|
const frpcStore = useFrpcStore()
|
||||||
|
|
||||||
/** 关闭确认对话框是否打开 */
|
/** 关闭确认对话框是否打开 */
|
||||||
const closeConfirmOpen = ref(false)
|
const closeConfirmOpen = ref(false)
|
||||||
/** 当前选中的关闭方式 */
|
/** 记住的关闭方式(弹窗默认选中项;「保持以上选择」时直接生效,持久化) */
|
||||||
const closeChoice = ref<CloseChoice>("tray")
|
const closeChoice = useStorage<CloseChoice>("frpilot:close-choice", "tray")
|
||||||
|
/** 关闭行为:每次询问 / 保持以上选择(持久化,默认每次询问) */
|
||||||
|
const closeBehavior = useStorage<CloseBehavior>("frpilot:close-behavior", "ask")
|
||||||
/** 是否正在退出(停止穿透 + 退出软件) */
|
/** 是否正在退出(停止穿透 + 退出软件) */
|
||||||
const quitting = ref(false)
|
const quitting = ref(false)
|
||||||
|
|
||||||
let initialized = false
|
let initialized = false
|
||||||
|
|
||||||
/** 应用启动时调用一次:拦截系统原生关闭,改为弹出确认对话框 */
|
/** 应用启动时调用一次:拦截系统原生关闭,改为按关闭行为处理 */
|
||||||
async function init() {
|
async function init() {
|
||||||
if (initialized || !isTauri()) return
|
if (initialized || !isTauri()) return
|
||||||
initialized = true
|
initialized = true
|
||||||
@@ -36,9 +44,16 @@ export const useWindowStore = defineStore("window", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 请求关闭窗口:弹出确认对话框(默认选中最小化到系统托盘) */
|
/**
|
||||||
|
* 请求关闭窗口:
|
||||||
|
* - 「每次询问」弹出确认对话框;
|
||||||
|
* - 「保持以上选择」不弹窗,直接按记住的方式处理。
|
||||||
|
*/
|
||||||
function requestClose() {
|
function requestClose() {
|
||||||
closeChoice.value = "tray"
|
if (closeBehavior.value === "remember") {
|
||||||
|
void applyClose(closeChoice.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
closeConfirmOpen.value = true
|
closeConfirmOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +62,19 @@ export const useWindowStore = defineStore("window", () => {
|
|||||||
closeConfirmOpen.value = false
|
closeConfirmOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确认关闭:按所选方式处理 */
|
/** 确认关闭:持久化所选行为与方式,再按所选方式处理 */
|
||||||
async function confirmClose() {
|
async function confirmClose(
|
||||||
const choice = closeChoice.value
|
choice: CloseChoice = closeChoice.value,
|
||||||
|
behavior: CloseBehavior = closeBehavior.value,
|
||||||
|
) {
|
||||||
|
closeChoice.value = choice
|
||||||
|
closeBehavior.value = behavior
|
||||||
closeConfirmOpen.value = false
|
closeConfirmOpen.value = false
|
||||||
|
await applyClose(choice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按所选关闭方式执行:最小化到托盘 / 退出软件 */
|
||||||
|
async function applyClose(choice: CloseChoice) {
|
||||||
if (choice === "tray") {
|
if (choice === "tray") {
|
||||||
await minimizeToTray()
|
await minimizeToTray()
|
||||||
} else {
|
} else {
|
||||||
@@ -85,11 +109,13 @@ export const useWindowStore = defineStore("window", () => {
|
|||||||
return {
|
return {
|
||||||
closeConfirmOpen,
|
closeConfirmOpen,
|
||||||
closeChoice,
|
closeChoice,
|
||||||
|
closeBehavior,
|
||||||
quitting,
|
quitting,
|
||||||
init,
|
init,
|
||||||
requestClose,
|
requestClose,
|
||||||
cancelClose,
|
cancelClose,
|
||||||
confirmClose,
|
confirmClose,
|
||||||
|
applyClose,
|
||||||
minimizeToTray,
|
minimizeToTray,
|
||||||
showWindow,
|
showWindow,
|
||||||
quitApp,
|
quitApp,
|
||||||
|
|||||||
Vendored
+1
@@ -45,6 +45,7 @@ declare module 'vue' {
|
|||||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
|
CardRadioGroup: typeof import('./../components/CardRadioGroup.vue')['default']
|
||||||
CloseConfirmModal: typeof import('./../components/CloseConfirmModal.vue')['default']
|
CloseConfirmModal: typeof import('./../components/CloseConfirmModal.vue')['default']
|
||||||
FrpCard: typeof import('./../components/frp/FrpCard.vue')['default']
|
FrpCard: typeof import('./../components/frp/FrpCard.vue')['default']
|
||||||
MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default']
|
MacTitleBar: typeof import('./../components/MacTitleBar.vue')['default']
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h } from "vue"
|
import { h } from "vue"
|
||||||
import { BgColorsOutlined, InfoCircleOutlined } from "@ant-design/icons-vue"
|
import { BgColorsOutlined, InfoCircleOutlined, SlidersOutlined } from "@ant-design/icons-vue"
|
||||||
import { useI18n } from "vue-i18n"
|
import { useI18n } from "vue-i18n"
|
||||||
import type { MenuProps } from "ant-design-vue"
|
import type { MenuProps } from "ant-design-vue"
|
||||||
|
|
||||||
@@ -31,6 +31,11 @@ const router = useRouter()
|
|||||||
const selectedKeys = computed(() => [route.path])
|
const selectedKeys = computed(() => [route.path])
|
||||||
|
|
||||||
const menuItems = computed<MenuProps["items"]>(() => [
|
const menuItems = computed<MenuProps["items"]>(() => [
|
||||||
|
{
|
||||||
|
key: "/setting/general",
|
||||||
|
icon: () => h(SlidersOutlined),
|
||||||
|
label: t("settings.menu.general"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "/setting/appearance",
|
key: "/setting/appearance",
|
||||||
icon: () => h(BgColorsOutlined),
|
icon: () => h(BgColorsOutlined),
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 关闭行为:与关闭弹窗内容保持一致,可在此再次修改 -->
|
||||||
|
<section class="mb-6">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<CardRadioGroup v-model="windowStore.closeChoice" :options="choiceOptions" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-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>
|
||||||
|
<a-radio-group
|
||||||
|
v-model:value="appStore.language"
|
||||||
|
:options="languageRadios"
|
||||||
|
option-type="button"
|
||||||
|
button-style="solid"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "vue-i18n"
|
||||||
|
import { languageOptions } from "@/i18n"
|
||||||
|
import { useAppStore } from "@/stores/app"
|
||||||
|
import { useWindowStore } from "@/stores/window"
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const windowStore = useWindowStore()
|
||||||
|
|
||||||
|
const behaviorOptions = computed(() => [
|
||||||
|
{ value: "ask" as const, label: t("closeDialog.ask"), description: t("closeDialog.askDesc") },
|
||||||
|
{
|
||||||
|
value: "remember" as const,
|
||||||
|
label: t("closeDialog.remember"),
|
||||||
|
description: t("closeDialog.rememberDesc"),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const choiceOptions = computed(() => [
|
||||||
|
{ value: "tray" as const, label: t("closeDialog.tray"), description: t("closeDialog.trayDesc") },
|
||||||
|
{ value: "exit" as const, label: t("closeDialog.exit"), description: t("closeDialog.exitDesc") },
|
||||||
|
])
|
||||||
|
|
||||||
|
/** 语言选项与顶部栏共用 languageOptions,保证两端一致 */
|
||||||
|
const languageRadios = languageOptions.map((item) => ({ label: item.label, value: item.key }))
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user