主界面修改及代理模块初始化
This commit is contained in:
@@ -1,24 +1,617 @@
|
||||
<script setup lang="ts">
|
||||
import { Globe } from '@lucide/vue'
|
||||
import {
|
||||
Globe, Play, Square, RotateCw, Power, Zap, Plus, RefreshCw, Trash2,
|
||||
Check, AlertCircle, Server, Settings as SettingsIcon, ListChecks,
|
||||
Upload, Link2, Loader2
|
||||
} from '@lucide/vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useProxyStore, type ProxyNode } from '@/stores/proxyStore'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
const store = useProxyStore()
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const starting = ref(false)
|
||||
const stopping = ref(false)
|
||||
const restarting = ref(false)
|
||||
const sysProxyLoading = ref(false)
|
||||
const importUrl = ref('')
|
||||
const importName = ref('')
|
||||
const importing = ref(false)
|
||||
const testingGroups = ref<Set<string>>(new Set())
|
||||
|
||||
// 进程状态轮询
|
||||
let statusTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const running = computed(() => store.status.running)
|
||||
|
||||
// 代理组(Selector/URLTest/Fallback/LoadBalance)
|
||||
const GROUP_TYPES = ['Selector', 'URLTest', 'Fallback', 'LoadBalance']
|
||||
const groups = computed<Array<[string, ProxyNode]>>(() => {
|
||||
return Object.entries(store.proxies).filter(([, n]) => GROUP_TYPES.includes(n.type))
|
||||
})
|
||||
|
||||
const modeOptions = [
|
||||
{ value: 'rule', label: '规则' },
|
||||
{ value: 'global', label: '全局' },
|
||||
{ value: 'direct', label: '直连' }
|
||||
]
|
||||
|
||||
const currentProfile = computed(() =>
|
||||
store.settings?.profiles.find(p => p.id === store.settings?.currentProfile) ?? null
|
||||
)
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
||||
}
|
||||
|
||||
const delayColor = (delay: number | undefined) => {
|
||||
if (!delay) return 'text-muted-foreground'
|
||||
if (delay < 150) return 'text-emerald-500'
|
||||
if (delay < 400) return 'text-amber-500'
|
||||
return 'text-red-500'
|
||||
}
|
||||
|
||||
const delayText = (delay: number | undefined) => {
|
||||
if (delay === undefined) return '—'
|
||||
if (delay === 0) return '超时'
|
||||
return `${delay}ms`
|
||||
}
|
||||
|
||||
// ===== 生命周期 =====
|
||||
const init = async () => {
|
||||
await Promise.all([store.loadSettings(), store.refreshKernel(), store.refreshStatus()])
|
||||
if (running.value) {
|
||||
store.refreshVersion()
|
||||
store.loadProxies().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
statusTimer = setInterval(async () => {
|
||||
await store.refreshStatus()
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusTimer) clearInterval(statusTimer)
|
||||
})
|
||||
|
||||
// 状态从停止→运行时,加载节点与版本
|
||||
watch(running, async (val, old) => {
|
||||
if (val && !old) {
|
||||
await store.refreshVersion()
|
||||
await store.loadProxies().catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 进程控制 =====
|
||||
const handleStart = async () => {
|
||||
starting.value = true
|
||||
try {
|
||||
await store.start()
|
||||
toast.success('mihomo 已启动')
|
||||
await store.refreshVersion()
|
||||
await store.loadProxies().catch(() => {})
|
||||
} catch (e) {
|
||||
toast.error('启动失败', { description: String(e) })
|
||||
} finally {
|
||||
starting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
stopping.value = true
|
||||
try {
|
||||
await store.stop()
|
||||
toast.success('mihomo 已停止')
|
||||
} catch (e) {
|
||||
toast.error('停止失败', { description: String(e) })
|
||||
} finally {
|
||||
stopping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestart = async () => {
|
||||
restarting.value = true
|
||||
try {
|
||||
await store.restart()
|
||||
toast.success('mihomo 已重启')
|
||||
await store.refreshVersion()
|
||||
await store.loadProxies().catch(() => {})
|
||||
} catch (e) {
|
||||
toast.error('重启失败', { description: String(e) })
|
||||
} finally {
|
||||
restarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 系统代理 =====
|
||||
const onToggleSystemProxy = async (on: boolean) => {
|
||||
sysProxyLoading.value = true
|
||||
try {
|
||||
await store.toggleSystemProxy(on)
|
||||
toast.success(on ? '系统代理已开启' : '系统代理已关闭')
|
||||
} catch (e) {
|
||||
toast.error('操作失败', { description: String(e) })
|
||||
} finally {
|
||||
sysProxyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 模式切换 =====
|
||||
const changeMode = async (mode: string) => {
|
||||
if (!store.settings || store.settings.mode === mode) return
|
||||
const prev = store.settings.mode
|
||||
store.settings.mode = mode
|
||||
try {
|
||||
await store.saveSettings({ ...store.settings })
|
||||
if (running.value) {
|
||||
await invokePatchConfigs({ mode })
|
||||
}
|
||||
toast.success(`已切换为${modeOptions.find(m => m.value === mode)?.label}模式`)
|
||||
} catch (e) {
|
||||
if (store.settings) store.settings.mode = prev
|
||||
toast.error('模式切换失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
const invokePatchConfigs = (body: Record<string, unknown>) =>
|
||||
invoke('proxy_patch_configs', { body })
|
||||
|
||||
// ===== 节点 =====
|
||||
const selectNode = async (group: string, name: string) => {
|
||||
// 仅 Selector 允许手动选择
|
||||
if (store.proxies[group]?.type !== 'Selector') return
|
||||
try {
|
||||
await store.selectProxy(group, name)
|
||||
} catch (e) {
|
||||
toast.error('切换节点失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
const testGroup = async (groupName: string) => {
|
||||
const group = store.proxies[groupName]
|
||||
if (!group?.all?.length) return
|
||||
testingGroups.value.add(groupName)
|
||||
try {
|
||||
await store.testDelayBatch(group.all)
|
||||
toast.success(`「${groupName}」测速完成`)
|
||||
} catch (e) {
|
||||
toast.error('测速失败', { description: String(e) })
|
||||
} finally {
|
||||
testingGroups.value.delete(groupName)
|
||||
}
|
||||
}
|
||||
|
||||
const nodeDelay = (name: string): number | undefined => {
|
||||
return store.proxies[name]?.history?.[0]?.delay
|
||||
}
|
||||
|
||||
// ===== 订阅 =====
|
||||
const doImport = async () => {
|
||||
if (!importUrl.value.trim()) {
|
||||
toast.warning('请输入订阅地址')
|
||||
return
|
||||
}
|
||||
importing.value = true
|
||||
try {
|
||||
const name = importName.value.trim() || `订阅 ${new Date().toLocaleString()}`
|
||||
await store.importProfile(importUrl.value.trim(), name)
|
||||
toast.success('订阅导入成功')
|
||||
importUrl.value = ''
|
||||
importName.value = ''
|
||||
} catch (e) {
|
||||
toast.error('导入失败', { description: String(e) })
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const doUpdate = async (id: string) => {
|
||||
try {
|
||||
await store.updateProfile(id)
|
||||
toast.success('订阅已更新')
|
||||
} catch (e) {
|
||||
toast.error('更新失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
const doDelete = async (id: string, name: string) => {
|
||||
if (!confirm(`确定删除订阅「${name}」?`)) return
|
||||
try {
|
||||
await store.deleteProfile(id)
|
||||
toast.success('已删除订阅')
|
||||
} catch (e) {
|
||||
toast.error('删除失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
const doActivate = async (id: string) => {
|
||||
try {
|
||||
await store.activateProfile(id)
|
||||
toast.success('已切换订阅,配置已重新生成')
|
||||
if (running.value) {
|
||||
await handleRestart()
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('切换失败', { description: String(e) })
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 设置 =====
|
||||
const localSettings = ref({
|
||||
mixedPort: 7890,
|
||||
externalController: '127.0.0.1:9090',
|
||||
secret: '',
|
||||
logLevel: 'info',
|
||||
allowLan: false,
|
||||
autoStart: false
|
||||
})
|
||||
|
||||
const syncLocalSettings = () => {
|
||||
if (store.settings) {
|
||||
localSettings.value = {
|
||||
mixedPort: store.settings.mixedPort,
|
||||
externalController: store.settings.externalController,
|
||||
secret: store.settings.secret,
|
||||
logLevel: store.settings.logLevel,
|
||||
allowLan: store.settings.allowLan,
|
||||
autoStart: store.settings.autoStart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => store.settings, syncLocalSettings, { immediate: true })
|
||||
|
||||
const savingSettings = ref(false)
|
||||
const saveSettingsForm = async () => {
|
||||
if (!store.settings) return
|
||||
savingSettings.value = true
|
||||
try {
|
||||
await store.saveSettings({
|
||||
...store.settings,
|
||||
...localSettings.value
|
||||
})
|
||||
toast.success('设置已保存')
|
||||
} catch (e) {
|
||||
toast.error('保存失败', { description: String(e) })
|
||||
} finally {
|
||||
savingSettings.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full p-6 overflow-y-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Globe class="h-5 w-5 text-primary" />
|
||||
代理管理模块
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
||||
<Globe class="h-16 w-16 mb-4 opacity-50" />
|
||||
<p>代理管理功能开发中...</p>
|
||||
<p class="text-sm mt-2">支持系统代理切换、规则配置、延迟测速等功能</p>
|
||||
<div class="h-full p-6">
|
||||
<Tabs v-model="activeTab" class="h-full flex flex-col">
|
||||
<TabsList class="grid w-full grid-cols-4 max-w-md">
|
||||
<TabsTrigger value="overview" class="gap-1.5"><Globe class="size-3.5" />概览</TabsTrigger>
|
||||
<TabsTrigger value="proxies" class="gap-1.5"><Server class="size-3.5" />节点</TabsTrigger>
|
||||
<TabsTrigger value="profiles" class="gap-1.5"><ListChecks class="size-3.5" />订阅</TabsTrigger>
|
||||
<TabsTrigger value="settings" class="gap-1.5"><SettingsIcon class="size-3.5" />设置</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 概览 -->
|
||||
<TabsContent value="overview" class="flex-1 mt-4 overflow-y-auto">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<!-- 内核状态 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Server class="size-4 text-primary" />内核
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">状态</span>
|
||||
<span v-if="store.kernel?.exists" class="flex items-center gap-1 text-emerald-500">
|
||||
<Check class="size-3.5" />已安装
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1 text-red-500">
|
||||
<AlertCircle class="size-3.5" />未安装
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">版本</span>
|
||||
<span class="font-mono text-xs">{{ store.kernel?.version ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-muted-foreground shrink-0">路径</span>
|
||||
<span class="font-mono text-xs text-right break-all">{{ store.kernel?.path ?? '—' }}</span>
|
||||
</div>
|
||||
<p v-if="!store.kernel?.exists" class="text-xs text-amber-600 dark:text-amber-500 leading-relaxed">
|
||||
请将 mihomo.exe 放到 <code class="px-1 bg-muted rounded">src-tauri/binaries/</code> 后重启应用,或直接放到上述 cores 目录。
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 运行状态 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Zap class="size-4 text-primary" />运行状态
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">mihomo</span>
|
||||
<span v-if="running" class="flex items-center gap-1 text-emerald-500">
|
||||
<span class="size-2 rounded-full bg-emerald-500" />运行中
|
||||
</span>
|
||||
<span v-else class="flex items-center gap-1 text-muted-foreground">
|
||||
<span class="size-2 rounded-full bg-muted-foreground" />已停止
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">PID</span>
|
||||
<span class="font-mono text-xs">{{ store.status.pid ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">API 版本</span>
|
||||
<span class="font-mono text-xs">{{ store.version || '—' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">重启次数</span>
|
||||
<span class="font-mono text-xs">{{ store.status.restartCount }}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex gap-2">
|
||||
<Button v-if="!running" size="sm" :disabled="starting" @click="handleStart">
|
||||
<Loader2 v-if="starting" class="size-3.5 animate-spin" />
|
||||
<Play v-else class="size-3.5" />启动
|
||||
</Button>
|
||||
<template v-else>
|
||||
<Button size="sm" variant="destructive" :disabled="stopping" @click="handleStop">
|
||||
<Loader2 v-if="stopping" class="size-3.5 animate-spin" />
|
||||
<Square v-else class="size-3.5" />停止
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" :disabled="restarting" @click="handleRestart">
|
||||
<Loader2 v-if="restarting" class="size-3.5 animate-spin" />
|
||||
<RotateCw v-else class="size-3.5" />重启
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 系统代理 -->
|
||||
<Card :class="{ 'opacity-60': sysProxyLoading }">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Power class="size-4 text-primary" />系统代理
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm">Windows 系统代理</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ store.systemProxy ? `指向 127.0.0.1:${store.settings?.mixedPort ?? 7890}` : '已关闭' }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="store.systemProxy"
|
||||
:disabled="sysProxyLoading"
|
||||
@update:model-value="onToggleSystemProxy"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 当前订阅 & 模式 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<ListChecks class="size-4 text-primary" />订阅与模式
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3 text-sm">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-muted-foreground shrink-0">当前订阅</span>
|
||||
<span class="text-right truncate">{{ currentProfile?.name ?? '无' }}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">运行模式</span>
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
v-for="m in modeOptions" :key="m.value"
|
||||
size="xs"
|
||||
:variant="store.settings?.mode === m.value ? 'default' : 'outline'"
|
||||
:disabled="!running && store.settings?.mode !== m.value"
|
||||
@click="changeMode(m.value)"
|
||||
>{{ m.label }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 节点 -->
|
||||
<TabsContent value="proxies" class="flex-1 mt-4 min-h-0">
|
||||
<div v-if="!running" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2">
|
||||
<Server class="size-12 opacity-30" />
|
||||
<p class="text-sm">mihomo 未运行,请先在概览页启动</p>
|
||||
</div>
|
||||
<ScrollArea v-else class="h-full pr-3">
|
||||
<div v-if="!groups.length" class="text-center text-sm text-muted-foreground py-12">
|
||||
暂无代理组,请先在订阅页导入并激活配置
|
||||
</div>
|
||||
<div class="space-y-4 pb-4">
|
||||
<Card v-for="[gname, group] in groups" :key="gname">
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="flex items-center justify-between text-base">
|
||||
<span class="flex items-center gap-2">
|
||||
<Server class="size-4 text-primary" />{{ gname }}
|
||||
<span class="text-xs font-normal text-muted-foreground">{{ group.type }}</span>
|
||||
</span>
|
||||
<Button
|
||||
size="xs" variant="outline"
|
||||
:disabled="testingGroups.has(gname)"
|
||||
@click="testGroup(gname)"
|
||||
>
|
||||
<Loader2 v-if="testingGroups.has(gname)" class="size-3 animate-spin" />
|
||||
<Zap v-else class="size-3" />测速
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-1.5">
|
||||
<button
|
||||
v-for="node in group.all" :key="node"
|
||||
type="button"
|
||||
class="flex items-center justify-between gap-2 rounded-md border px-2.5 py-1.5 text-xs transition-colors hover:bg-accent"
|
||||
:class="group.now === node ? 'border-primary bg-primary/10' : 'border-border'"
|
||||
@click="selectNode(gname, node)"
|
||||
>
|
||||
<span class="truncate text-left">{{ store.proxies[node]?.name ?? node }}</span>
|
||||
<span class="font-mono shrink-0" :class="delayColor(nodeDelay(node))">
|
||||
{{ delayText(nodeDelay(node)) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 订阅 -->
|
||||
<TabsContent value="profiles" class="flex-1 mt-4 overflow-y-auto">
|
||||
<div class="space-y-4 max-w-3xl">
|
||||
<!-- 导入 -->
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Plus class="size-4 text-primary" />导入订阅
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-3">
|
||||
<div class="grid gap-2">
|
||||
<Label for="sub-url">订阅地址</Label>
|
||||
<Input id="sub-url" v-model="importUrl" placeholder="https://example.com/sub.yaml" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sub-name">名称(可选)</Label>
|
||||
<Input id="sub-name" v-model="importName" placeholder="我的订阅" />
|
||||
</div>
|
||||
<Button size="sm" :disabled="importing" @click="doImport">
|
||||
<Loader2 v-if="importing" class="size-3.5 animate-spin" />
|
||||
<Upload v-else class="size-3.5" />导入
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card>
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="text-base">订阅列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="!store.settings?.profiles.length" class="text-center text-sm text-muted-foreground py-8">
|
||||
暂无订阅
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="p in store.settings.profiles" :key="p.id"
|
||||
class="flex items-center gap-3 rounded-md border p-3"
|
||||
:class="store.settings.currentProfile === p.id ? 'border-primary bg-primary/5' : 'border-border'"
|
||||
>
|
||||
<div class="flex-1 min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Link2 class="size-3.5 text-muted-foreground shrink-0" />
|
||||
<span class="font-medium text-sm truncate">{{ p.name }}</span>
|
||||
<span v-if="store.settings.currentProfile === p.id" class="text-xs text-primary">当前</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground truncate">{{ p.url }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ formatSize(p.size) }} · 更新于 {{ p.updatedAt }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<Button size="icon-sm" variant="ghost" title="更新" @click="doUpdate(p.id)">
|
||||
<RefreshCw class="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="store.settings.currentProfile !== p.id"
|
||||
size="icon-sm" variant="ghost" title="切换" @click="doActivate(p.id)"
|
||||
>
|
||||
<Check class="size-3.5" />
|
||||
</Button>
|
||||
<Button size="icon-sm" variant="ghost" title="删除" @click="doDelete(p.id, p.name)">
|
||||
<Trash2 class="size-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- 设置 -->
|
||||
<TabsContent value="settings" class="flex-1 mt-4 overflow-y-auto">
|
||||
<Card class="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<SettingsIcon class="size-4 text-primary" />基础设置
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="mixed-port">混合代理端口</Label>
|
||||
<Input id="mixed-port" v-model.number="localSettings.mixedPort" type="number" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="api-addr">控制接口地址</Label>
|
||||
<Input id="api-addr" v-model="localSettings.externalController" placeholder="127.0.0.1:9090" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="secret">API 密钥(留空则不鉴权)</Label>
|
||||
<Input id="secret" v-model="localSettings.secret" placeholder="可选" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="log-level">日志级别</Label>
|
||||
<Input id="log-level" v-model="localSettings.logLevel" placeholder="info" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
<p class="text-sm">允许局域网连接</p>
|
||||
<p class="text-xs text-muted-foreground">允许其他设备通过本机代理上网</p>
|
||||
</div>
|
||||
<Switch v-model="localSettings.allowLan" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
<p class="text-sm">模块启用时自动启动</p>
|
||||
<p class="text-xs text-muted-foreground">在设置中开启代理模块时自动运行 mihomo</p>
|
||||
</div>
|
||||
<Switch v-model="localSettings.autoStart" />
|
||||
</div>
|
||||
<Button size="sm" :disabled="savingSettings" @click="saveSettingsForm">
|
||||
<Loader2 v-if="savingSettings" class="size-3.5 animate-spin" />
|
||||
<Check v-else class="size-3.5" />保存设置
|
||||
</Button>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
修改端口/接口/密钥后需重启 mihomo 生效。
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user