细节调整及优化(26.8.3)
This commit is contained in:
+169
-113
@@ -7,12 +7,14 @@ import {
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { appDataDir } from '@tauri-apps/api/path'
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { useProxyStore, type ProxyNode } from '@/stores/proxyStore'
|
||||
import { useModuleTabs } from '@/lib/use-module-tabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { EVENTS } from '@/lib/constants'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -66,7 +68,14 @@ const onConfirmCancel = () => {
|
||||
confirmState.value.resolve?.(false)
|
||||
}
|
||||
const onConfirmOpenChange = (open: boolean) => {
|
||||
if (!open) onConfirmCancel()
|
||||
// reka-ui 的 AlertDialogAction/Cancel 点击时会先触发 update:open(false)(自动关闭),
|
||||
// 再触发各自的 @click。若关闭事件立即按「取消」处理,会把确认误判为取消(确认按钮点了没反应)。
|
||||
// 因此关闭时的取消判定推迟到当前事件循环的 click 处理器执行完毕后再进行。
|
||||
if (!open && !confirmState.value.resolved) {
|
||||
setTimeout(() => {
|
||||
if (!confirmState.value.resolved) onConfirmCancel()
|
||||
}, 0)
|
||||
}
|
||||
confirmState.value.open = open
|
||||
}
|
||||
|
||||
@@ -90,16 +99,13 @@ const testingGroups = ref<Set<string>>(new Set())
|
||||
const loadingProxies = ref(false)
|
||||
const checkingUpdate = ref(false)
|
||||
const updatingKernel = ref(false)
|
||||
const kernelUpdateInfo = ref<{ latestVersion: string; hasUpdate: boolean } | null>(null)
|
||||
const kernelUpdateInfo = ref<{ latestVersion: string; hasUpdate: boolean; downloadUrl: string } | null>(null)
|
||||
|
||||
// 自动切换节点(从 settings 持久化)
|
||||
// 自动切换节点(从 settings 持久化;执行由后端调度)
|
||||
const autoSwitchEnabled = ref(false)
|
||||
const autoSwitchInterval = ref(5) // 分钟
|
||||
const autoSwitchTargetGroup = ref('') // 目标代理组
|
||||
const autoSwitchRegion = ref('') // 空=全部,否则按地区过滤
|
||||
let autoSwitchTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 自动切换执行中标志(防重入:测速超时时上一轮未结束,间隔触发会重叠) */
|
||||
let autoSwitchRunning = false
|
||||
|
||||
// 从 store.settings 同步自动切换设置
|
||||
const syncAutoSwitchSettings = () => {
|
||||
@@ -127,6 +133,9 @@ const saveAutoSwitchSettings = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 后端自动切换事件监听句柄(模块卸载时关闭)
|
||||
let autoSwitchUnlisten: UnlistenFn[] = []
|
||||
|
||||
// 手风琴展开项
|
||||
const accordionValue = ref<string>('')
|
||||
|
||||
@@ -328,10 +337,6 @@ const init = async () => {
|
||||
await store.waitForApi()
|
||||
store.refreshVersion()
|
||||
loadProxiesWithError()
|
||||
// 若自动切换已开启,恢复定时器(静默,不弹通知、不立即执行)
|
||||
if (autoSwitchEnabled.value) {
|
||||
startAutoSwitch(false, false)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 无论初始化链路是否出错,都结束"加载中"占位,避免模块永久卡死
|
||||
@@ -350,12 +355,17 @@ onMounted(() => {
|
||||
}, 3000)
|
||||
// 页面重新可见时立即刷新一次系统代理状态(切回标签页/从托盘返回主窗口)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
// 监听后端自动切换节点完成事件(后台执行,不依赖模块激活)
|
||||
listen<{ switched?: boolean; group?: string; name?: string; delay?: number }>(EVENTS.proxyAutoSwitch, onProxyAutoSwitch)
|
||||
.then(fn => autoSwitchUnlisten.push(fn))
|
||||
.catch(err => logger.error('注册自动切换事件监听失败: ' + err))
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusTimer) clearInterval(statusTimer)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
stopAutoSwitch()
|
||||
autoSwitchUnlisten.forEach(fn => fn())
|
||||
autoSwitchUnlisten = []
|
||||
})
|
||||
|
||||
/** 页面可见性变化时刷新系统代理状态(低成本感知外部修改) */
|
||||
@@ -370,11 +380,6 @@ watch(running, async (val, old) => {
|
||||
await store.waitForApi()
|
||||
await store.refreshVersion()
|
||||
await loadProxiesWithError()
|
||||
// 自动切换若已开启,mihomo 启动/重启后恢复定时器(静默,不弹通知)
|
||||
// (handleStop 会停掉旧定时器,此处统一接管启动路径,避免开关显示开但功能静默失效)
|
||||
if (autoSwitchEnabled.value) {
|
||||
startAutoSwitch(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -417,7 +422,6 @@ const handleStart = async () => {
|
||||
const handleStop = async () => {
|
||||
stopping.value = true
|
||||
try {
|
||||
stopAutoSwitch()
|
||||
await store.stop()
|
||||
toast.success('mihomo 已停止')
|
||||
} catch (e) {
|
||||
@@ -448,6 +452,12 @@ const handleRestart = async () => {
|
||||
|
||||
// ===== 系统代理 =====
|
||||
const onToggleSystemProxy = async (on: boolean) => {
|
||||
// 停机时禁止开启(正常情况下开关已禁用,此处兜底防止外部调用)
|
||||
if (on && !running.value) {
|
||||
toast.warning('请先启动 mihomo 再开启系统代理')
|
||||
store.refreshSystemProxy()
|
||||
return
|
||||
}
|
||||
sysProxyLoading.value = true
|
||||
try {
|
||||
await store.toggleSystemProxy(on)
|
||||
@@ -485,46 +495,26 @@ const quickSwitchNode = async (name: string) => {
|
||||
try {
|
||||
await store.selectProxy(mainGroupName.value, name)
|
||||
toast.success('节点已切换', { description: name })
|
||||
// 测速新节点
|
||||
store.testDelay(name).then(delay => {
|
||||
toast.success(`${name}`, { description: `延迟 ${delay}ms` })
|
||||
// 测速新节点:用 testDelayBatch 以更新 history,保证节点 Badge 显示与结果一致
|
||||
store.testDelayBatch([name]).then(() => {
|
||||
const delay = store.proxies[name]?.history?.[0]?.delay
|
||||
if (delay && delay > 0) {
|
||||
toast.success(`${name}`, { description: `延迟 ${delay}ms` })
|
||||
}
|
||||
}).catch(() => {})
|
||||
} catch (e) {
|
||||
toast.error('切换节点失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 自动切换节点 =====
|
||||
const startAutoSwitch = (notify = true, immediate = true) => {
|
||||
stopAutoSwitch()
|
||||
if (!autoSwitchEnabled.value) return
|
||||
const ms = autoSwitchInterval.value * 60 * 1000
|
||||
autoSwitchTimer = setInterval(runAutoSwitch, ms)
|
||||
// 仅用户主动开启时提示;模块挂载/内核重启恢复定时器时静默,避免每次切换都弹通知
|
||||
if (notify) {
|
||||
toast.success('自动切换已开启', {
|
||||
description: `每 ${autoSwitchInterval.value} 分钟测试并切换至最优节点`
|
||||
})
|
||||
}
|
||||
// 立即执行一次(用户主动开启/调整时立即生效;进入模块恢复时跳过,避免每次进入都测速切换)
|
||||
if (immediate) {
|
||||
runAutoSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
const stopAutoSwitch = () => {
|
||||
if (autoSwitchTimer) {
|
||||
clearInterval(autoSwitchTimer)
|
||||
autoSwitchTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 自动切换节点(执行由后端调度,前端仅负责维护设置并刷新/提示) =====
|
||||
const onToggleAutoSwitch = (on: boolean) => {
|
||||
autoSwitchEnabled.value = on
|
||||
if (on) {
|
||||
startAutoSwitch()
|
||||
toast.success('自动切换已开启', {
|
||||
description: `每 ${autoSwitchInterval.value} 分钟测试并切换至最优节点`
|
||||
})
|
||||
} else {
|
||||
stopAutoSwitch()
|
||||
toast.info('自动切换已关闭')
|
||||
}
|
||||
saveAutoSwitchSettings()
|
||||
@@ -532,52 +522,21 @@ const onToggleAutoSwitch = (on: boolean) => {
|
||||
|
||||
const onAutoSwitchIntervalChange = (val: unknown) => {
|
||||
autoSwitchInterval.value = Number(val) || 5
|
||||
if (autoSwitchEnabled.value) {
|
||||
startAutoSwitch()
|
||||
}
|
||||
saveAutoSwitchSettings()
|
||||
}
|
||||
|
||||
const runAutoSwitch = async () => {
|
||||
if (autoSwitchRunning) return
|
||||
autoSwitchRunning = true
|
||||
/** 后端自动切换完成后刷新节点列表并提示(后台亦可运行,不依赖模块激活) */
|
||||
const onProxyAutoSwitch = async (e: { payload: { switched?: boolean; group?: string; name?: string; delay?: number } }) => {
|
||||
const p = e.payload
|
||||
try {
|
||||
const groupName = autoSwitchTargetGroup.value || mainGroupName.value
|
||||
if (!groupName || !running.value) return
|
||||
const nodes = filteredNodes.value
|
||||
if (!nodes.length) return
|
||||
|
||||
// 使用 testDelayBatch 测速,它会更新 store.proxies[name].history,
|
||||
// 确保 UI 显示的延迟与选优结果一致
|
||||
await store.testDelayBatch(nodes)
|
||||
|
||||
// 从更新后的 history 读取最新延迟
|
||||
const results = nodes.map(name => ({
|
||||
name,
|
||||
delay: store.proxies[name]?.history?.[0]?.delay ?? 0
|
||||
}))
|
||||
|
||||
// 找到有效延迟中最低的
|
||||
const valid = results.filter(r => r.delay > 0)
|
||||
if (!valid.length) {
|
||||
toast.warning('所有节点均超时,未切换')
|
||||
return
|
||||
}
|
||||
valid.sort((a, b) => a.delay - b.delay)
|
||||
const best = valid[0]
|
||||
|
||||
// 如果当前节点不是最优,则切换
|
||||
const currentNow = store.proxies[groupName]?.now ?? ''
|
||||
if (currentNow !== best.name) {
|
||||
await store.selectProxy(groupName, best.name)
|
||||
toast.success('已自动切换到最优节点', {
|
||||
description: `${best.name} (${best.delay}ms)`
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('自动切换失败: ' + e)
|
||||
} finally {
|
||||
autoSwitchRunning = false
|
||||
await store.loadProxies()
|
||||
} catch (err) {
|
||||
logger.error('自动切换后刷新节点失败: ' + err)
|
||||
}
|
||||
if (p?.switched && p.name && p.delay) {
|
||||
toast.success('已自动切换到最优节点', {
|
||||
description: `${p.name} (${p.delay}ms)`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +545,7 @@ const handleCheckUpdate = async () => {
|
||||
checkingUpdate.value = true
|
||||
try {
|
||||
const info = await store.checkKernelUpdate()
|
||||
kernelUpdateInfo.value = { latestVersion: info.latestVersion, hasUpdate: info.hasUpdate }
|
||||
kernelUpdateInfo.value = { latestVersion: info.latestVersion, hasUpdate: info.hasUpdate, downloadUrl: info.downloadUrl }
|
||||
if (info.hasUpdate) {
|
||||
toast.info('发现新版本', { description: `最新: ${info.latestVersion}` })
|
||||
} else {
|
||||
@@ -607,31 +566,26 @@ const handleUpdateKernel = async () => {
|
||||
/** 是否展开"更新内核"区块(下载源 + 进度) */
|
||||
const updateExpanded = ref(false)
|
||||
|
||||
/** 开始更新:停止 mihomo → 调用 updateKernel(复用 installProgress 进度机制) */
|
||||
/** 开始更新:确保有下载 URL → 调用 updateKernel(复用 installProgress 进度机制)。
|
||||
* 下载阶段允许 mihomo 运行(可通过当前系统代理下载),
|
||||
* 解压替换前由 need_stop 阶段弹窗要求停止 mihomo */
|
||||
const handleStartUpdate = async () => {
|
||||
if (store.installing) return
|
||||
// 确认停止 mihomo
|
||||
if (running.value) {
|
||||
const ok = await showConfirm({
|
||||
title: '更新内核',
|
||||
description: '更新内核需要先停止 mihomo,确认继续?',
|
||||
confirmText: '继续更新'
|
||||
})
|
||||
if (!ok) return
|
||||
updatingKernel.value = true
|
||||
// 使用检查更新时获取的下载 URL(缺失时先补查一次,避免后端二次请求 GitHub)
|
||||
let url = kernelUpdateInfo.value?.downloadUrl ?? ''
|
||||
if (!url) {
|
||||
try {
|
||||
await store.stop()
|
||||
const info = await store.checkKernelUpdate()
|
||||
url = info.downloadUrl
|
||||
kernelUpdateInfo.value = { latestVersion: info.latestVersion, hasUpdate: info.hasUpdate, downloadUrl: info.downloadUrl }
|
||||
} catch (e) {
|
||||
toast.error('停止 mihomo 失败', { description: String(e) })
|
||||
updatingKernel.value = false
|
||||
toast.error('获取更新信息失败', { description: String(e) })
|
||||
return
|
||||
} finally {
|
||||
updatingKernel.value = false
|
||||
}
|
||||
}
|
||||
toast.info('开始下载更新...')
|
||||
try {
|
||||
await store.updateKernel(selectedMirrorPrefix.value)
|
||||
await store.updateKernel(selectedMirrorPrefix.value, url)
|
||||
} catch (e) {
|
||||
toast.error('内核更新失败', { description: String(e) })
|
||||
}
|
||||
@@ -641,7 +595,9 @@ const handleStartUpdate = async () => {
|
||||
const installStageText = computed(() => {
|
||||
const stage = store.installProgress?.stage
|
||||
switch (stage) {
|
||||
case 'checking': return '正在检查'
|
||||
case 'downloading': return '正在下载'
|
||||
case 'need_stop': return '等待停止 mihomo'
|
||||
case 'extracting': return '正在解压'
|
||||
case 'replacing': return '正在安装'
|
||||
case 'done': return '安装完成'
|
||||
@@ -654,6 +610,7 @@ const installStageColor = computed(() => {
|
||||
const stage = store.installProgress?.stage
|
||||
if (stage === 'done') return 'text-emerald-500'
|
||||
if (stage === 'error') return 'text-destructive'
|
||||
if (stage === 'need_stop') return 'text-amber-500'
|
||||
return 'text-primary'
|
||||
})
|
||||
|
||||
@@ -672,6 +629,25 @@ const installPercentDisplay = computed(() => {
|
||||
|
||||
const installHasTotal = computed(() => store.installProgress?.totalBytes != null)
|
||||
|
||||
/** 停止下载进行中标记(防止重复点击) */
|
||||
const stoppingDownload = ref(false)
|
||||
|
||||
/** 停止下载:通知后端中止,并立即回退到下载方式卡片(保留 updateExpanded 供重新选择) */
|
||||
const handleStopDownload = async () => {
|
||||
if (stoppingDownload.value || !store.installing) return
|
||||
stoppingDownload.value = true
|
||||
try {
|
||||
await store.cancelKernelInstall()
|
||||
// 立即复位 UI 状态回退到下载方式卡片;后端下载循环稍后中止,updateKernel 会静默结束
|
||||
store.installing = false
|
||||
store.clearInstallProgress()
|
||||
} catch (e) {
|
||||
toast.error('停止下载失败', { description: String(e) })
|
||||
} finally {
|
||||
stoppingDownload.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatMB = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||
|
||||
// ===== 首次安装内核 =====
|
||||
@@ -709,12 +685,55 @@ const handleInstallKernel = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** need_stop 弹窗处理中标记(防止重复触发) */
|
||||
let handlingNeedStop = false
|
||||
|
||||
/**
|
||||
* 下载完成、解压替换前:弹窗提示用户停止 mihomo,确认后停止 mihomo 并唤醒后端继续安装。
|
||||
* 取消则中止整个安装流程(后端正在等待确认,通过取消唤醒)。
|
||||
*/
|
||||
const handleNeedStop = async () => {
|
||||
if (handlingNeedStop) return
|
||||
handlingNeedStop = true
|
||||
try {
|
||||
const wasRunning = running.value
|
||||
const ok = await showConfirm({
|
||||
title: '停止 mihomo 后继续',
|
||||
description: wasRunning
|
||||
? '下载已完成。安装新内核前需要停止 mihomo,点击「停止并继续」将自动停止 mihomo 并完成安装。'
|
||||
: '下载已完成。即将安装新内核,点击「继续」完成安装。',
|
||||
confirmText: wasRunning ? '停止并继续' : '继续'
|
||||
})
|
||||
if (!ok) {
|
||||
// 用户取消:中止安装(后端在等待确认,置取消标志唤醒其返回)
|
||||
await store.cancelKernelInstall()
|
||||
return
|
||||
}
|
||||
if (wasRunning) {
|
||||
await store.stop()
|
||||
}
|
||||
await store.confirmInstall()
|
||||
} catch (e) {
|
||||
toast.error('停止 mihomo 失败', { description: String(e) })
|
||||
// 停止失败则中止安装,避免替换阶段因 exe 占用而报错
|
||||
try {
|
||||
await store.cancelKernelInstall()
|
||||
} catch {
|
||||
// 忽略:cancelKernelInstall 内部已记录日志
|
||||
}
|
||||
} finally {
|
||||
handlingNeedStop = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听安装/更新进度终态,弹 toast 并延时清空进度
|
||||
// 同时处理更新场景下的 updateExpanded 清理(与 installProgress 同步清除,避免更新区块闪烁)
|
||||
watch(
|
||||
() => store.installProgress?.stage,
|
||||
(stage) => {
|
||||
if (stage === 'done') {
|
||||
if (stage === 'need_stop') {
|
||||
handleNeedStop()
|
||||
} else if (stage === 'done') {
|
||||
toast.success('内核安装完成', {
|
||||
description: store.installProgress?.message
|
||||
})
|
||||
@@ -899,12 +918,35 @@ watch(() => store.settings, syncLocalSettings, { immediate: true })
|
||||
|
||||
const saveSettingsForm = async () => {
|
||||
if (!store.settings) return
|
||||
const prev = store.settings
|
||||
try {
|
||||
await store.saveSettings({
|
||||
...store.settings,
|
||||
...localSettings.value
|
||||
})
|
||||
toast.success('设置已保存')
|
||||
|
||||
// 网络相关字段(端口/接口/密钥)变更需重启 mihomo 才生效,运行实例仍在旧值上;
|
||||
// 提示用户重启,避免后续代理 API 调用打到新地址而失败
|
||||
const networkChanged =
|
||||
localSettings.value.mixedPort !== prev.mixedPort ||
|
||||
localSettings.value.externalController !== prev.externalController ||
|
||||
localSettings.value.secret !== prev.secret
|
||||
if (networkChanged && running.value) {
|
||||
toast.warning('端口/接口/密钥已保存,重启 mihomo 后生效(期间代理 API 使用新地址可能暂时不可用)')
|
||||
} else {
|
||||
toast.success('设置已保存')
|
||||
}
|
||||
|
||||
// 纯模式变更(网络字段未变)在运行中即时生效,与概览页行为一致,
|
||||
// 避免「UI 显示新模式、运行实例仍是旧模式」的不一致
|
||||
if (!networkChanged && localSettings.value.mode !== prev.mode && running.value) {
|
||||
try {
|
||||
await invokePatchConfigs({ mode: localSettings.value.mode })
|
||||
await store.loadProxies()
|
||||
} catch (modeErr) {
|
||||
logger.error('运行中应用模式失败,重启后生效: ' + modeErr)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('保存失败', { description: String(e) })
|
||||
}
|
||||
@@ -1128,12 +1170,12 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span :class="installStageColor" class="flex items-center gap-1.5 font-medium">
|
||||
<Loader2
|
||||
v-if="['downloading', 'extracting', 'replacing'].includes(store.installProgress.stage)"
|
||||
v-if="['checking', 'downloading', 'extracting', 'replacing'].includes(store.installProgress.stage)"
|
||||
key="stage-loading"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
<Check v-else-if="store.installProgress.stage === 'done'" key="stage-done" class="size-3" />
|
||||
<AlertCircle v-else-if="store.installProgress.stage === 'error'" key="stage-error" class="size-3" />
|
||||
<AlertCircle v-else-if="['need_stop', 'error'].includes(store.installProgress.stage)" key="stage-warn" class="size-3" />
|
||||
{{ installStageText }}
|
||||
</span>
|
||||
<span v-if="installHasTotal && store.installProgress.stage === 'downloading'" class="font-mono text-muted-foreground">
|
||||
@@ -1141,7 +1183,7 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
v-if="installHasTotal || store.installProgress.stage !== 'downloading'"
|
||||
v-if="installHasTotal || (store.installProgress.stage !== 'downloading' && store.installProgress.stage !== 'checking')"
|
||||
key="progress-bar"
|
||||
:model-value="installPercentDisplay"
|
||||
class="h-2"
|
||||
@@ -1163,6 +1205,20 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
</template>
|
||||
<template v-else>{{ store.installProgress.message }}</template>
|
||||
</p>
|
||||
<!-- 停止下载:仅在进行中的检查/下载阶段显示,点击后回退到下载方式卡片 -->
|
||||
<div
|
||||
v-if="['checking', 'downloading'].includes(store.installProgress.stage)"
|
||||
class="flex justify-end"
|
||||
>
|
||||
<Button
|
||||
size="sm" variant="outline" class="h-7 text-xs"
|
||||
:disabled="stoppingDownload"
|
||||
@click="handleStopDownload"
|
||||
>
|
||||
<Loader2 v-if="stoppingDownload" class="size-3 animate-spin" />
|
||||
<Square v-else class="size-3" />停止下载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1371,7 +1427,7 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="store.systemProxy"
|
||||
:disabled="sysProxyLoading"
|
||||
:disabled="sysProxyLoading || !running"
|
||||
@update:model-value="onToggleSystemProxy"
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -1627,7 +1683,7 @@ tabsStore.registerSave(saveSettingsForm)
|
||||
</Tabs>
|
||||
|
||||
<!-- 通用确认对话框 -->
|
||||
<AlertDialog :model-value="confirmState.open" @update:model-value="onConfirmOpenChange">
|
||||
<AlertDialog :open="confirmState.open" @update:open="onConfirmOpenChange">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ confirmState.opts.title }}</AlertDialogTitle>
|
||||
|
||||
Reference in New Issue
Block a user