下载非内核

This commit is contained in:
zhongluofeng
2026-07-22 18:26:26 +08:00
parent 1e31ee8da9
commit f7d3c13f35
24 changed files with 2609 additions and 2532 deletions
+31 -4
View File
@@ -25,7 +25,9 @@ const toNavModule = (meta: ModuleMeta): NavModule => ({
icon: meta.icon
})
const activeModule = ref('proxy')
/** 上次激活的模块 IDlocalStorage 持久化) */
const LAST_MODULE_KEY = 'thing_last_module'
const activeModule = ref('')
const activeComponent = shallowRef<Component | null>(null)
@@ -64,11 +66,13 @@ const handleModuleChange = (moduleId: string) => {
prevConfig?.lifecycle?.onDeactivate?.()
activeModule.value = moduleId
localStorage.setItem(LAST_MODULE_KEY, moduleId)
loadModule(moduleId)
}
const handleSearch = (moduleId: string) => {
activeModule.value = moduleId
localStorage.setItem(LAST_MODULE_KEY, moduleId)
loadModule(moduleId)
}
@@ -81,6 +85,8 @@ const getFallbackModule = () => {
}
watch(() => appStore.enabledModules.length, () => {
// 启动期间 activeModule 尚未确定,跳过
if (!activeModule.value) return
const enabledIds = appStore.enabledModules.map(m => m.id)
if (activeModule.value !== 'settings' && !enabledIds.includes(activeModule.value)) {
const fallback = getFallbackModule()
@@ -89,9 +95,30 @@ watch(() => appStore.enabledModules.length, () => {
}
})
onMounted(() => {
loadModule(activeModule.value)
appStore.init().catch(e => console.error('App init error:', e))
/** 计算启动时应打开的默认模块:优先上次记忆,其次排序第一个 */
const resolveDefaultModule = (): string => {
const enabledIds = appStore.enabledModules.map(m => m.id)
const allMetas = moduleRegistry.getAllMetas()
// 尝试上次记忆的模块
const lastModule = localStorage.getItem(LAST_MODULE_KEY)
if (lastModule) {
const meta = allMetas.find(m => m.id === lastModule)
if (meta && (meta.builtin || enabledIds.includes(lastModule))) {
return lastModule
}
}
// 回退到排序第一个可用模块(settings 除外)
const first = availableModules.value.find(m => m.id !== 'settings')
return first?.id || 'settings'
}
onMounted(async () => {
await appStore.init().catch(e => console.error('App init error:', e))
const defaultModule = resolveDefaultModule()
activeModule.value = defaultModule
loadModule(defaultModule)
})
</script>
+42 -10
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onUnmounted } from 'vue'
import { Search, Minus, Square, X, Settings, ChevronRight } from '@lucide/vue'
import { Input } from '@/components/ui/input'
import { getCurrentWindow } from '@tauri-apps/api/window'
@@ -72,17 +72,46 @@ const maximize = async () => {
// 期间用 CSS 覆盖 :hover 样式,避免按钮残留高亮(webview 隐藏时 mouseleave 不触发)
const hoverSuppressed = ref(true)
// 恢复 hover 的定时器引用,便于在鼠标进入时提前恢复
let restoreHoverTimer: ReturnType<typeof setTimeout> | null = null
/** 窗口重新显示后恢复 hover 状态 */
const restoreHover = (delay = 200) => {
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
restoreHoverTimer = setTimeout(() => {
hoverSuppressed.value = false
}, delay)
}
/** 鼠标移动时恢复 hover(比定时器更可靠,webview 隐藏时 mouseleave 不触发,:hover 被冻结) */
const handleFirstMouseMove = () => {
if (hoverSuppressed.value) {
hoverSuppressed.value = false
if (restoreHoverTimer) {
clearTimeout(restoreHoverTimer)
restoreHoverTimer = null
}
}
window.removeEventListener('mousemove', handleFirstMouseMove)
}
/** 注册鼠标移动监听(窗口即将隐藏时调用,为下次显示做准备) */
const armMouseMoveRestore = () => {
window.removeEventListener('mousemove', handleFirstMouseMove)
window.addEventListener('mousemove', handleFirstMouseMove)
}
const close = async () => {
// 隐藏前立即抑制 hover,避免冻结的 :hover 状态残留到下次显示
hoverSuppressed.value = true
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
// 注册鼠标移动监听,窗口重新显示后第一次移动即恢复 hover
armMouseMoveRestore()
await tauriWindow?.hide()
}
// 窗口重新获得焦点时,先保持抑制(防止冻结的 hover 显示),
// 短暂延迟后恢复 hover(让真实鼠标位置重新接管)
if (tauriWindow) {
tauriWindow.onFocusChanged(({ payload: focused }) => {
if (focused) {
@@ -90,10 +119,10 @@ if (tauriWindow) {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
// 100ms 后恢复 hover,足够让浏览器重置伪类状态
setTimeout(() => {
hoverSuppressed.value = false
}, 100)
// 200ms 后恢复 hover,足够让浏览器重置伪类状态
restoreHover(200)
// 同时注册鼠标移动监听作为后备(定时器可能不够,鼠标移动更可靠)
armMouseMoveRestore()
} else {
hoverSuppressed.value = true
}
@@ -101,9 +130,12 @@ if (tauriWindow) {
}
// 初始时窗口已显示,恢复 hover
setTimeout(() => {
hoverSuppressed.value = false
}, 200)
restoreHover(300)
onUnmounted(() => {
window.removeEventListener('mousemove', handleFirstMouseMove)
if (restoreHoverTimer) clearTimeout(restoreHoverTimer)
})
const handleBlur = () => {
setTimeout(() => {
File diff suppressed because it is too large Load Diff
+7 -29
View File
@@ -1,12 +1,11 @@
import type { ModuleConfig } from '@/types/module'
import type { SearchIndexItem } from '@/stores/searchIndex'
import { invoke } from '@tauri-apps/api/core'
const searchItems: SearchIndexItem[] = [
{
title: '下载任务',
description: '查看与管理下载任务',
keywords: ['下载', 'download', '任务', 'task', 'aria2']
keywords: ['下载', 'download', '任务', 'task']
},
{
title: '添加下载',
@@ -15,8 +14,8 @@ const searchItems: SearchIndexItem[] = [
},
{
title: '下载设置',
description: '配置下载目录、速度限制与 RPC',
keywords: ['设置', 'setting', 'rpc', '速度', '端口', '目录']
description: '配置下载目录、并发数与速度限制',
keywords: ['设置', 'setting', '速度', '目录', '并发']
},
{
title: '浏览器扩展',
@@ -29,39 +28,18 @@ export const moduleConfig: ModuleConfig = {
id: 'downloader',
name: '下载器',
icon: 'downloader',
description: '基于 aria2 的多线程 HTTP 下载管理',
description: '多线程 HTTP 下载管理',
category: 'network',
defaultEnabled: true,
loader: () => import('./DownloaderModule.vue'),
searchItems,
// 进程由 Aria2Manager 通过 ProcessManager 统一管理(id='downloader'),
// executable/args 在运行时由后端确定,此处仅声明 hasProcess 以便禁用时自动停止。
process: {
name: 'aria2c',
executable: '',
autoStart: false,
restartOnCrash: true,
maxRestarts: 3
},
// 下载引擎在进程内运行,无需外部进程管理
lifecycle: {
// 启用模块时若用户开启了"自动启动",则随模块启用而运行 aria2
onEnable: async () => {
try {
const s = await invoke<{ autoStart?: boolean }>('downloader_get_settings')
if (s.autoStart) {
await invoke('downloader_start')
}
} catch {
/* 忽略:可能内核未安装 */
}
// 引擎在应用启动时已自动运行,模块启用时无需额外操作
},
// 禁用模块时停止 aria2 进程(cleanup_on_exit 会在应用退出时调用)
onDisable: async () => {
try {
await invoke('downloader_stop')
} catch {
/* 忽略:可能进程未运行 */
}
// 模块禁用时不停止引擎(引擎在应用退出时统一清理)
}
},
order: 50
+29 -14
View File
@@ -322,6 +322,7 @@ const init = async () => {
startAutoSwitch()
}
}
store.initialized = true
}
onMounted(() => {
@@ -883,12 +884,19 @@ const saveSettingsForm = async () => {
<CardContent class="space-y-3 text-sm">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">状态</span>
<Badge v-if="store.kernel?.exists" variant="default" class="gap-1 bg-emerald-500 hover:bg-emerald-500">
<Check class="size-3" />已安装
</Badge>
<Badge v-else variant="destructive" class="gap-1">
<AlertCircle class="size-3" />未安装
</Badge>
<template v-if="!store.initialized">
<span class="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 class="size-3 animate-spin" />加载中...
</span>
</template>
<template v-else>
<Badge v-if="store.kernel?.exists" variant="default" class="gap-1 bg-emerald-500 hover:bg-emerald-500">
<Check class="size-3" />已安装
</Badge>
<Badge v-else variant="destructive" class="gap-1">
<AlertCircle class="size-3" />未安装
</Badge>
</template>
</div>
<div v-if="store.kernel?.exists" class="flex items-center justify-between">
<span class="text-muted-foreground">当前版本</span>
@@ -933,9 +941,9 @@ const saveSettingsForm = async () => {
</div>
</div>
<!-- 首次安装区块仅在内核未安装且不在安装中时显示 -->
<!-- 首次安装区块仅在内核未安装且不在安装中且已初始化时显示 -->
<div
v-if="!store.kernel?.exists && !store.installProgress"
v-if="store.initialized && !store.kernel?.exists && !store.installProgress"
class="space-y-2 pt-2 border-t"
>
<div class="space-y-1.5">
@@ -1035,12 +1043,19 @@ const saveSettingsForm = async () => {
<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>
<template v-if="!store.initialized">
<span class="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 class="size-3 animate-spin" />加载中...
</span>
</template>
<template v-else>
<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>
</template>
</div>
<div class="flex items-center justify-between">
<span class="text-muted-foreground">PID</span>
+162 -321
View File
@@ -8,285 +8,141 @@ const logger = createLogger('downloader')
// ===== 与 Rust 端对应的数据结构(camelCase =====
export type TaskStatus = 'queued' | 'active' | 'paused' | 'complete' | 'error'
export interface Segment {
index: number
start: number
end: number
completed: number
}
export interface DownloadTask {
id: string
url: string
filename: string
dir: string
status: TaskStatus
totalSize: number
completedSize: number
speed: number
supportsResume: boolean
segments: Segment[]
error: string | null
createdAt: number
headers: Record<string, string>
}
export interface DownloaderSettings {
rpcPort: number
rpcSecret: string
downloadDir: string
maxConcurrent: number
maxConnectionPerServer: number
split: number
maxConnections: number
continueDownload: boolean
autoStart: boolean
speedLimit: number
kernelMirrors: string[]
}
/** 内核安装进度事件载荷,对应 Rust 端 InstallProgress */
export interface InstallProgress {
/** downloading | extracting | replacing | done | error */
stage: string
percent: number
downloadedBytes: number
totalBytes: number | null
message: string
}
export interface KernelInfo {
path: string
exists: boolean
version: string | null
}
export interface KernelUpdateInfo {
currentVersion: string | null
latestVersion: string
downloadUrl: string
hasUpdate: boolean
globalSpeedLimit: number
extensionPort: number
extensionSecret: string
deleteFilesOnRemove: boolean
}
export interface DownloaderStatus {
running: boolean
pid: number | null
restartCount: number
}
export interface RpcInfo {
rpcUrl: string
rpcSecret: string
extensionPath: string | null
export interface ExtensionInfo {
url: string
port: number
secret: string
hasSecret: boolean
}
/** aria2 任务文件信息 */
export interface Aria2File {
path: string
length: number
completedLength: number
selected: boolean
/** 下载进度事件载荷 */
interface ProgressPayload {
id: string
completedSize: number
totalSize: number
speed: number
status: TaskStatus
}
/** aria2 任务状态(tellActive/tellWaiting/tellStopped 返回项) */
export interface Aria2Task {
gid: string
status: 'active' | 'waiting' | 'paused' | 'complete' | 'removed' | 'error' | string
totalLength: string
completedLength: string
downloadSpeed: string
uploadSpeed: string
connections: string
dir: string
files?: Aria2File[]
bittorrent?: { info?: { name?: string } } | null
errorCode?: string
errorMessage?: string
}
/** 全局统计 */
export interface GlobalStat {
downloadSpeed: string
uploadSpeed: string
numActive: string
numWaiting: string
numStopped: string
numStoppedTotal: string
}
export interface Aria2Version {
version: string
enabledFeatures?: string[]
/** 下载完成事件载荷 */
interface CompletePayload {
id: string
filename: string
status: TaskStatus
error: string | null
}
export const useDownloaderStore = defineStore('downloader', () => {
const kernel = ref<KernelInfo | null>(null)
const status = ref<DownloaderStatus>({ running: false, pid: null, restartCount: 0 })
const version = ref<string>('')
const tasks = ref<DownloadTask[]>([])
const status = ref<DownloaderStatus>({ running: false })
const settings = ref<DownloaderSettings | null>(null)
const rpcInfo = ref<RpcInfo | null>(null)
const extensionInfo = ref<ExtensionInfo | null>(null)
const activeTasks = ref<Aria2Task[]>([])
const waitingTasks = ref<Aria2Task[]>([])
const stoppedTasks = ref<Aria2Task[]>([])
const globalStat = ref<GlobalStat | null>(null)
// ===== 任务历史持久化(localStorage =====
// 即使 aria2 未启动,也能展示最近一次的任务快照
const HISTORY_KEY = 'thing.downloader.taskHistory'
const HISTORY_MAX = 200 // 最多保留 200 条历史记录
/** 将当前任务快照保存到 localStorage(合并 active+waiting+stopped,按 gid 去重) */
const persistHistory = () => {
try {
const map = new Map<string, Aria2Task>()
// 先读已有历史,作为基底
const raw = localStorage.getItem(HISTORY_KEY)
if (raw) {
const existing: Aria2Task[] = JSON.parse(raw)
for (const t of existing) map.set(t.gid, t)
}
// 用最新任务覆盖(active/waiting/stopped 都是最新的)
for (const t of activeTasks.value) map.set(t.gid, t)
for (const t of waitingTasks.value) map.set(t.gid, t)
for (const t of stoppedTasks.value) map.set(t.gid, t)
// 限制条数:优先保留 stopped(已完成/错误),其次 waiting,最后 active
const all = Array.from(map.values())
const priority = { complete: 0, error: 0, removed: 1, active: 2, waiting: 2, paused: 2 } as Record<string, number>
all.sort((a, b) => (priority[a.status] ?? 3) - (priority[b.status] ?? 3))
const trimmed = all.slice(0, HISTORY_MAX)
localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmed))
} catch (e) {
logger.error('保存任务历史失败: ' + e)
}
}
/** 从 localStorage 加载任务历史,填充到 stoppedTasks(作为历史展示) */
const loadHistory = () => {
try {
const raw = localStorage.getItem(HISTORY_KEY)
if (!raw) return
const history: Aria2Task[] = JSON.parse(raw)
if (!Array.isArray(history)) return
// 仅在没有实时任务时填充(避免覆盖实时数据)
if (stoppedTasks.value.length === 0) {
stoppedTasks.value = history
}
} catch (e) {
logger.error('加载任务历史失败: ' + e)
}
}
// ===== 内核安装进度 =====
const installing = ref(false)
const installProgress = ref<InstallProgress | null>(null)
// 事件监听器
let progressUnlisten: UnlistenFn | null = null
let completeUnlisten: UnlistenFn | null = null
let addedUnlisten: UnlistenFn | null = null
/** 内核信息(同时尝试从 resource 提取到 cores/ */
const refreshKernel = async () => {
// ===== 任务列表 =====
const refreshTasks = async () => {
try {
kernel.value = await invoke<KernelInfo>('downloader_kernel_info')
tasks.value = await invoke<DownloadTask[]>('downloader_get_tasks')
} catch (e) {
logger.error('获取内核信息失败: ' + e)
logger.error('获取任务列表失败: ' + e)
}
return kernel.value
return tasks.value
}
/** 刷新进程状态 */
const refreshStatus = async () => {
try {
status.value = await invoke<DownloaderStatus>('downloader_status')
} catch (e) {
logger.error('获取进程状态失败: ' + e)
}
return status.value
}
const start = async () => {
await invoke('downloader_start')
await refreshStatus()
}
const stop = async () => {
await invoke('downloader_stop')
await refreshStatus()
}
const restart = async () => {
await invoke('downloader_restart')
await refreshStatus()
}
/** 等待 aria2 RPC 就绪(轮询 version 接口,最多等 10 秒) */
const waitForApi = async (timeoutMs = 10000): Promise<boolean> => {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
await invoke<Aria2Version>('downloader_version')
return true
} catch {
await new Promise((r) => setTimeout(r, 500))
}
}
return false
}
/** 获取 aria2 版本(仅运行时可用) */
const refreshVersion = async () => {
try {
const v = await invoke<Aria2Version>('downloader_version')
version.value = v.version
} catch {
version.value = ''
/** 根据进度事件更新单个任务(不触发完整刷新) */
const updateTaskProgress = (payload: ProgressPayload) => {
const task = tasks.value.find((t) => t.id === payload.id)
if (task) {
task.completedSize = payload.completedSize
task.totalSize = payload.totalSize
task.speed = payload.speed
task.status = payload.status
}
}
// ---------- 任务列表 ----------
const refreshActive = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_active')
activeTasks.value = res ?? []
} catch (e) {
logger.error('获取活跃任务失败: ' + e)
}
return activeTasks.value
/** 下载完成事件处理 */
const handleTaskComplete = async (_payload: CompletePayload) => {
// 刷新任务列表以获取最终状态
await refreshTasks()
}
const refreshWaiting = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_waiting')
waitingTasks.value = res ?? []
} catch (e) {
logger.error('获取等待任务失败: ' + e)
}
return waitingTasks.value
// ===== 任务操作 =====
const addTask = async (
url: string,
filename?: string,
dir?: string,
headers?: Record<string, string>
): Promise<string> => {
const id = await invoke<string>('downloader_add_task', {
url,
filename: filename || null,
dir: dir || null,
headers: headers || null
})
await refreshTasks()
return id
}
const refreshStopped = async () => {
try {
const res = await invoke<Aria2Task[]>('downloader_get_stopped')
stoppedTasks.value = res ?? []
} catch (e) {
logger.error('获取已完成任务失败: ' + e)
}
return stoppedTasks.value
const pauseTask = async (id: string) => {
await invoke('downloader_pause_task', { id })
await refreshTasks()
}
/** 刷新全部任务(active + waiting + stopped */
const refreshAllTasks = async () => {
await Promise.all([refreshActive(), refreshWaiting(), refreshStopped()])
// 刷新后持久化历史快照
persistHistory()
const resumeTask = async (id: string) => {
await invoke('downloader_resume_task', { id })
await refreshTasks()
}
const refreshGlobalStat = async () => {
try {
globalStat.value = await invoke<GlobalStat>('downloader_get_global_stat')
} catch (e) {
logger.error('获取全局统计失败: ' + e)
}
return globalStat.value
const removeTask = async (id: string, deleteFiles = false) => {
await invoke('downloader_remove_task', { id, deleteFiles })
await refreshTasks()
}
// ---------- 任务操作 ----------
const addUri = async (uris: string[], options?: Record<string, unknown>) => {
const opts = options ? (JSON.parse(JSON.stringify(options)) as unknown) : undefined
return await invoke<string>('downloader_add_uri', { uris, options: opts })
}
const pauseTask = async (gid: string) => {
await invoke('downloader_pause', { gid })
}
const unpauseTask = async (gid: string) => {
await invoke('downloader_unpause', { gid })
}
const removeTask = async (gid: string) => {
await invoke('downloader_remove', { gid })
}
const changeGlobalOption = async (options: Record<string, string>) => {
await invoke('downloader_change_global_option', { options })
}
// ---------- 设置 ----------
// ===== 设置 =====
const loadSettings = async () => {
settings.value = await invoke<DownloaderSettings>('downloader_get_settings')
return settings.value
@@ -297,107 +153,92 @@ export const useDownloaderStore = defineStore('downloader', () => {
settings.value = s
}
// ---------- RPC 信息 ----------
const loadRpcInfo = async () => {
rpcInfo.value = await invoke<RpcInfo>('downloader_get_rpc_info')
return rpcInfo.value
}
// ---------- 内核更新 / 安装 ----------
const checkKernelUpdate = async (): Promise<KernelUpdateInfo> => {
return await invoke<KernelUpdateInfo>('downloader_check_kernel_update')
}
const updateKernel = async (mirrorPrefix: string = '') => {
await invoke('downloader_update_kernel', { mirrorPrefix })
await refreshKernel()
}
/**
* 首次安装内核:调用后端 install_kernel,监听 downloader-kernel-install-progress 事件更新进度
* @param mirrorPrefix 镜像源前缀(空串=GitHub 直连)
*/
const installKernel = async (mirrorPrefix: string = ''): Promise<void> => {
if (installing.value) return
installing.value = true
installProgress.value = {
stage: 'downloading',
percent: 0,
downloadedBytes: 0,
totalBytes: null,
message: '准备开始下载...'
// ===== 状态 =====
const refreshStatus = async () => {
try {
status.value = await invoke<DownloaderStatus>('downloader_status')
} catch (e) {
logger.error('获取状态失败: ' + e)
}
return status.value
}
// ===== 扩展信息 =====
const loadExtensionInfo = async () => {
extensionInfo.value = await invoke<ExtensionInfo>('downloader_get_extension_info')
return extensionInfo.value
}
// ===== 事件监听 =====
const startEventListeners = async () => {
if (progressUnlisten && completeUnlisten && addedUnlisten) return
if (!progressUnlisten) {
progressUnlisten = await listen<InstallProgress>('downloader-kernel-install-progress', (e) => {
installProgress.value = e.payload
progressUnlisten = await listen<ProgressPayload>('download-progress', (e) => {
updateTaskProgress(e.payload)
})
}
try {
await invoke('downloader_install_kernel', { mirrorPrefix })
await refreshKernel()
} catch (e) {
logger.error('内核安装失败: ' + e)
throw e
} finally {
installing.value = false
if (progressUnlisten) {
progressUnlisten()
progressUnlisten = null
}
if (!completeUnlisten) {
completeUnlisten = await listen<CompletePayload>('download-complete', (e) => {
handleTaskComplete(e.payload)
})
}
if (!addedUnlisten) {
addedUnlisten = await listen<{ id: string }>('download-added', () => {
// 新任务加入(可能是扩展通过 HTTP API 添加的),刷新任务列表
refreshTasks()
})
}
}
const clearInstallProgress = () => {
installProgress.value = null
const stopEventListeners = () => {
if (progressUnlisten) {
progressUnlisten()
progressUnlisten = null
}
if (completeUnlisten) {
completeUnlisten()
completeUnlisten = null
}
if (addedUnlisten) {
addedUnlisten()
addedUnlisten = null
}
}
/** 用系统资源管理器打开目录(Rust 端绕过 opener scope 限制) */
// ===== 初始化 =====
const init = async () => {
await Promise.all([refreshStatus(), loadSettings(), loadExtensionInfo(), refreshTasks()])
await startEventListeners()
}
// ===== 工具函数 =====
const openDir = (path: string) => invoke<void>('downloader_open_dir', { path })
return {
// state
kernel,
tasks,
status,
version,
settings,
rpcInfo,
activeTasks,
waitingTasks,
stoppedTasks,
globalStat,
installing,
installProgress,
// kernel & process
refreshKernel,
refreshStatus,
start,
stop,
restart,
waitForApi,
refreshVersion,
extensionInfo,
// tasks
refreshActive,
refreshWaiting,
refreshStopped,
refreshAllTasks,
refreshGlobalStat,
addUri,
refreshTasks,
addTask,
pauseTask,
unpauseTask,
resumeTask,
removeTask,
changeGlobalOption,
// settings
loadSettings,
saveSettings,
// rpc info
loadRpcInfo,
// kernel update / install
checkKernelUpdate,
updateKernel,
installKernel,
clearInstallProgress,
openDir,
loadHistory,
persistHistory
// status
refreshStatus,
// extension
loadExtensionInfo,
// events
startEventListeners,
stopEventListeners,
// init
init,
// utils
openDir
}
})
+4
View File
@@ -98,6 +98,9 @@ export const useProxyStore = defineStore('proxy', () => {
const settings = ref<ProxySettings | null>(null)
const systemProxy = ref(false)
/** 是否已完成首次加载(避免初始 null/false 导致闪烁误导状态) */
const initialized = ref(false)
// ===== 内核安装进度 =====
const installing = ref(false)
const installProgress = ref<InstallProgress | null>(null)
@@ -325,6 +328,7 @@ export const useProxyStore = defineStore('proxy', () => {
proxies,
settings,
systemProxy,
initialized,
installing,
installProgress,
// kernel & process