bug修复调整

This commit is contained in:
zhongluofeng
2026-08-31 18:05:27 +08:00
parent 28e0c4664a
commit 6b9f71da08
35 changed files with 1004 additions and 296 deletions
+14 -2
View File
@@ -16,8 +16,16 @@ export const commands = {
* 调用返回前会触发应用退出。
*/
updateInstall: (downloadedPath: string) => __TAURI_INVOKE<null>("update_install", { downloadedPath }),
/** 更新 ThingHK 内核:下载 thing-hk_{v}.zip → 停止监控内核 → 覆盖内核文件 */
updateThinghk: () => __TAURI_INVOKE<null>("update_thinghk"),
/**
* 应用 ThingHK 内核更新:下载阶段已由下载模块完成,本命令仅做
* need_stop(等待前端停止监控内核并确认)→ 解压 → 替换。
* 进度通过 UPDATE_PROGRESS 事件上报,前端据 need_stop 弹出确认对话框。
*/
updateThinghkApply: (zipPath: string) => __TAURI_INVOKE<null>("update_thinghk_apply", { zipPath }),
/** 前端已停止监控内核,确认继续解压替换(唤醒 need_stop 等待) */
updateThinghkConfirm: () => __TAURI_INVOKE<null>("update_thinghk_confirm"),
/** 取消 ThingHK 内核更新(need_stop 等待阶段有效:唤醒 apply 以「已取消」返回,zip 保留便于重试) */
updateThinghkCancel: () => __TAURI_INVOKE<null>("update_thinghk_cancel"),
proxyActivateProfile: (id: string) => __TAURI_INVOKE<null>("proxy_activate_profile", { id }),
/** 应用内核更新:下载阶段已由下载模块完成,本方法仅做 need_stop → 解压 → 替换。 */
proxyApplyKernelUpdate: (zipPath: string) => __TAURI_INVOKE<KernelInfo>("proxy_apply_kernel_update", { zipPath }),
@@ -110,6 +118,10 @@ export const commands = {
/**
* 运行系统命令(不设置 CREATE_NO_WINDOW,使 cmd/powershell/regedit 等显示自身窗口)
* 适用于内置系统工具:regedit、shutdown、cmd、powershell、taskmgr 等。
* - 控制台类交互程序(cmd/powershell/pwsh)额外设置 CREATE_NEW_CONSOLE
* 否则从 GUI 宿主启动时无可见控制台窗口(表现为"点击没反应")。
* - .msc 控制台文件(如 devmgmt.msc)不可被 CreateProcess 直接执行,
* 改由 mmc 打开(路径解析到 System32,不受当前工作目录影响)。
*/
quickpanelRunSystemCommand: (command: string, args: string[]) => __TAURI_INVOKE<null>("quickpanel_run_system_command", { command, args }),
/** 列出目录下的压缩包文件(供批量解压面板使用)。 */
+4
View File
@@ -61,6 +61,10 @@ export const EVENTS = {
osdContentSize: 'osd-content-size',
osdSystemUiActive: 'osd-system-ui-active',
osdSystemUiInactive: 'osd-system-ui-inactive',
/** 前台出现全屏应用(游戏):OSD 应隐藏以避免游戏掉帧 */
osdGameActive: 'osd-game-active',
/** 全屏应用退出前台:OSD 可恢复显示 */
osdGameInactive: 'osd-game-inactive',
osdStartDrag: 'osd-start-drag',
osdEndDrag: 'osd-end-drag',
monitorReady: 'monitor-ready',
+6
View File
@@ -9,8 +9,14 @@ const logger = createLogger('main')
// 禁用 WebView 默认右键菜单(桌面应用体验,主窗口与独立窗口共用)
document.addEventListener('contextmenu', (e) => e.preventDefault())
// 良性通知过滤:ResizeObserver 回调引发的布局变化在同一帧内级联时,
// 浏览器会派发此 ErrorEvent(规范定义为"通知"而非异常,无可操作信息)。
// 监控数据每秒刷新、reka-ui 组件挂载时高发,直接忽略避免污染日志。
const BENIGN_RESIZE_OBSERVER_RE = /^ResizeObserver loop (completed with undelivered notifications|limit exceeded)/i
// 全局未捕获异常日志
window.addEventListener('error', (event) => {
if (event.message && BENIGN_RESIZE_OBSERVER_RE.test(event.message)) return
logger.error(`全局JS错误: ${event.message} @ ${event.filename}:${event.lineno}`)
})
+15 -1
View File
@@ -6,7 +6,7 @@ import {
ArrowDown, ArrowUp, Wifi, FolderOpen, Copy, ListChecks,
Monitor as MonitorIcon, GripVertical, SlidersHorizontal,
Eye, EyeOff, MousePointerClick, Plus, PencilLine,
CircuitBoard, BatteryFull,
CircuitBoard, BatteryFull, Gamepad2,
} from '@lucide/vue'
import type { LucideIcon } from '@lucide/vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -2156,6 +2156,20 @@ watch(() => store.status?.ready, (ready, prev) => {
@update:model-value="updateOsdConfig('clickThrough', Boolean($event))"
/>
</div>
<!-- 游戏全屏自动隐藏 -->
<div class="flex items-center justify-between">
<div class="flex flex-col gap-0.5">
<Label class="text-sm flex items-center gap-1.5 cursor-pointer">
<Gamepad2 class="size-3.5 text-muted-foreground" />
游戏全屏时自动隐藏
</Label>
<span class="text-[11px] text-muted-foreground">检测到全屏应用游戏前台时隐藏悬浮窗退出后自动恢复避免游戏掉帧</span>
</div>
<Switch
:model-value="osdConfig.gameAutoHide"
@update:model-value="updateOsdConfig('gameAutoHide', Boolean($event))"
/>
</div>
</div>
<Separator />
+7
View File
@@ -512,6 +512,13 @@ onMounted(async () => {
console.error('[OSD] 启动置顶监视失败:', e)
}
// 启动游戏全屏监视(前台全屏应用时通知主窗口隐藏 OSD,避免游戏掉帧)
try {
await invoke('osd_start_game_watch')
} catch (e) {
console.error('[OSD] 启动游戏全屏监视失败:', e)
}
// 监听主窗口推送的 OSD 配置(低频通道)
unlistenFns.push(await listen<OsdStatePayload>(EVENTS.osdStateUpdate, (e) => {
config.value = e.payload.config
+98 -112
View File
@@ -5,11 +5,10 @@ import { getCurrentWindow, LogicalSize, Effect, EffectState } from '@tauri-apps/
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts
import { commands } from '@/lib/bindings'
import type { ArchiveInfo, DeleteResult, ExtractResult, FileEntry, RenamePreview, RenameResult } from '@/lib/bindings'
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, ChevronLeft, History, FolderOpen, Ruler, Trash2, Terminal, Archive as ArchiveIcon, Regex, FileText, Settings } from '@lucide/vue'
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, ChevronLeft, ChevronDown, History, FolderOpen, Ruler, Trash2, Terminal, Archive as ArchiveIcon, Regex, FileText, Settings } from '@lucide/vue'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { Switch } from '@/components/ui/switch'
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getAllHistoryItems, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
import HistoryPicker from './HistoryPicker.vue'
@@ -143,14 +142,32 @@ const renameResults = ref<RenameResult[] | null>(null)
const renameOkCount = ref(0)
let renameTimer: ReturnType<typeof setTimeout> | null = null
// ===== 历史分区(从 results 中分离历史项与其他结果) =====
// 展示顺序:目录操作(批量解压/重命名/删除)> 历史 > 更多历史 > 其他
const dirActionItems = computed(() => results.value.filter(r => r.group === '目录操作'))
const historyItems = computed(() => results.value.filter(r => r.group === '历史'))
const otherItems = computed(() => results.value.filter(r => r.group !== '历史' && r.group !== '目录操作'))
// Accordion 中的更多历史项(不参与键盘上下导航,仅鼠标点击)
const moreHistoryItems = ref<QPItem[]>([])
const moreHistoryCount = ref(0)
// ===== 结果分区(从 results 中分离历史项与其他结果) =====
// 展示顺序:目录操作(批量解压/重命名/删除)> 历史(折叠分组)> 其他
// 目录操作为置顶行为项;历史在空查询时置顶但默认折叠,按 Tab 展开
const dirActionItems = ref<QPItem[]>([])
const otherItems = computed(() => results.value)
// 历史项单独从 localStorage 加载(不再混入 results),默认折叠
const historyItems = ref<QPItem[]>([])
const historyExpanded = ref(false)
// 键盘导航扁平化序号:目录操作 + 历史(展开时)+ 其他
const dirCount = computed(() => dirActionItems.value.length)
const historyCount = computed(() => historyItems.value.length)
const otherCount = computed(() => otherItems.value.length)
const otherNavStart = computed(() => dirCount.value + (historyExpanded.value ? historyCount.value : 0))
const navTotal = computed(() => otherNavStart.value + otherCount.value)
// 键盘导航项(selectedIndex 指向该扁平数组):目录操作 + 历史(展开时)+ 其他
const navItems = computed<QPItem[]>(() => [
...dirActionItems.value,
...(historyExpanded.value ? historyItems.value : []),
...otherItems.value,
])
function toggleHistory() {
collapseSubActions()
historyExpanded.value = !historyExpanded.value
}
// ===== 历史频率(localStorage 持久化,用于排序加权) =====
const HISTORY_KEY = STORAGE_KEYS.quickpanelHistory
@@ -192,24 +209,25 @@ async function doSearch() {
const seq = ++searchSeq
const q = query.value.trim()
if (!q) {
// 空查询:当前目录文件操作(若检测到 Explorer 目录)+ 历史置顶 + 系统相关条目
// 空查询:目录操作(若检测到 Explorer 目录)+ 历史置顶(默认折叠)+ 系统相关条目
// (程序相关设置不参与默认展示;所有 Provider 空查询零 IPC,首屏即时)
const items = await aggregateSearch('')
if (seq !== searchSeq) return // 过期请求丢弃
const dirItems = getExplorerActions()
results.value = applyHistoryBoost([...dirItems, ...items])
dirActionItems.value = getExplorerActions()
results.value = applyHistoryBoost(items)
// 历史置顶但默认折叠,按 Tab 展开(每次显示重置为折叠态)
historyItems.value = getAllHistoryItems()
historyExpanded.value = false
selectedIndex.value = 0
// 加载更多历史(Accordion 折叠区,不参与键盘导航)
moreHistoryItems.value = getMoreHistoryItems()
moreHistoryCount.value = getMoreHistoryCount()
// 后台加载应用图标(含历史中的图标)
void loadAppIconsForResults(results.value)
void loadAppIconsForResults(moreHistoryItems.value)
void loadAppIconsForResults(historyItems.value)
return
}
// 非空查询:清空历史分区
moreHistoryItems.value = []
moreHistoryCount.value = 0
// 非空查询:清空历史分区与目录操作
dirActionItems.value = []
historyItems.value = []
historyExpanded.value = false
loading.value = true
try {
const items = await aggregateSearch(q)
@@ -680,7 +698,7 @@ async function confirmDelete() {
// 子动作展开/收起
function toggleSubActions(idx: number) {
const item = results.value[idx]
const item = navItems.value[idx]
if (!item?.subActions?.length) return
if (subActionExpanded.value === idx) {
subActionExpanded.value = null
@@ -697,7 +715,7 @@ function collapseSubActions() {
// 当前展开的子动作列表
function currentSubActions(): QPSubAction[] {
if (subActionExpanded.value === null) return []
return results.value[subActionExpanded.value]?.subActions || []
return navItems.value[subActionExpanded.value]?.subActions || []
}
// ===== 键盘导航 =====
@@ -725,7 +743,7 @@ function onKeydown(e: KeyboardEvent) {
const expanded = subActionExpanded.value !== null
const subs = currentSubActions()
const expandedItem = expanded ? results.value[subActionExpanded.value!] : undefined
const expandedItem = expanded ? navItems.value[subActionExpanded.value!] : undefined
if (expanded) {
// 子动作导航模式
@@ -760,7 +778,7 @@ function onKeydown(e: KeyboardEvent) {
// 结果列表导航模式
if (e.key === 'ArrowDown') {
e.preventDefault()
selectedIndex.value = Math.min(selectedIndex.value + 1, results.value.length - 1)
selectedIndex.value = Math.min(selectedIndex.value + 1, navTotal.value - 1)
scrollSelectedIntoView()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
@@ -768,17 +786,23 @@ function onKeydown(e: KeyboardEvent) {
scrollSelectedIntoView()
} else if (e.key === 'Enter') {
e.preventDefault()
const item = results.value[selectedIndex.value]
const item = navItems.value[selectedIndex.value]
if (item) executeItem(item)
} else if (e.key === 'Escape') {
e.preventDefault()
hideWindow()
} else if (e.key === 'Tab') {
// Tab 展开子动作
const item = results.value[selectedIndex.value]
if (item?.subActions?.length) {
if (!query.value.trim()) {
// 默认视图:Tab 展开/收起历史分组
e.preventDefault()
toggleSubActions(selectedIndex.value)
toggleHistory()
} else {
// 搜索视图:Tab 展开子动作
const item = navItems.value[selectedIndex.value]
if (item?.subActions?.length) {
e.preventDefault()
toggleSubActions(selectedIndex.value)
}
}
}
}
@@ -1393,58 +1417,35 @@ onUnmounted(() => {
</div>
</template>
<!-- 历史置顶可键盘导航索引偏移 dirActionItems.length -->
<template v-for="(item, idx) in historyItems" :key="item.id">
<!-- 历史分组置顶默认折叠 Tab 展开 -->
<div v-if="historyCount > 0" class="qp-history-section">
<div
class="qp-item"
:class="{ 'qp-item-selected': (idx + dirActionItems.length) === selectedIndex }"
@click="executeItem(item)"
@mouseenter="onItemHover(idx + dirActionItems.length)"
class="qp-item qp-history-trigger"
@click="toggleHistory"
>
<img
v-if="item.iconUrl"
:src="item.iconUrl"
class="qp-app-icon shrink-0"
alt=""
/>
<component
v-else
:is="groupIcon(item.group)"
class="size-4 text-muted-foreground shrink-0"
:class="isAppLike(item) ? '' : 'mt-0.5'"
/>
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
<p class="text-sm truncate">{{ item.title }}</p>
<p v-if="!isAppLike(item) && item.subtitle" class="text-xs text-muted-foreground truncate">{{ item.subtitle }}</p>
<History class="size-4 text-muted-foreground shrink-0" />
<div class="flex-1 min-w-0">
<p class="text-sm">历史</p>
<p class="text-xs text-muted-foreground">{{ historyCount }} 条最近记录</p>
</div>
<span class="qp-group-badge" :class="groupBadgeClass(item.group)">{{ item.group }}</span>
<CornerDownLeft
v-if="(idx + dirActionItems.length) === selectedIndex"
class="h-3.5 w-3.5 text-primary shrink-0"
<kbd class="qp-kbd shrink-0" @click.stop="toggleHistory">Tab</kbd>
<ChevronDown
v-if="historyExpanded"
class="h-3.5 w-3.5 text-muted-foreground shrink-0"
/>
<ChevronRight
v-else
class="h-3.5 w-3.5 text-muted-foreground shrink-0"
/>
</div>
</template>
<!-- 更多历史 Accordion固定在历史下方不参与键盘导航 -->
<Accordion
v-if="moreHistoryCount > 0"
type="single"
collapsible
class="qp-more-history"
>
<AccordionItem value="more" class="border-0">
<AccordionTrigger class="qp-more-trigger">
<span class="flex items-center gap-2">
<History class="h-3.5 w-3.5 text-muted-foreground" />
更多历史{{ moreHistoryCount }}
</span>
</AccordionTrigger>
<AccordionContent class="qp-more-content">
<!-- 展开后的历史项索引偏移 dirCount可键盘导航 -->
<template v-if="historyExpanded">
<template v-for="(item, idx) in historyItems" :key="item.id">
<div
v-for="item in moreHistoryItems"
:key="item.id"
class="qp-item qp-more-item"
class="qp-item"
:class="{ 'qp-item-selected': (dirCount + idx) === selectedIndex }"
@click="executeItem(item)"
@mouseenter="onItemHover(dirCount + idx)"
>
<img
v-if="item.iconUrl"
@@ -1463,18 +1464,22 @@ onUnmounted(() => {
<p v-if="!isAppLike(item) && item.subtitle" class="text-xs text-muted-foreground truncate">{{ item.subtitle }}</p>
</div>
<span class="qp-group-badge" :class="groupBadgeClass(item.group)">{{ item.group }}</span>
<CornerDownLeft
v-if="(dirCount + idx) === selectedIndex"
class="h-3.5 w-3.5 text-primary shrink-0"
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</template>
</template>
</div>
<!-- 其他结果设置/应用/系统等可键盘导航索引偏移 dirActionItems.length + historyItems.length -->
<!-- 其他结果设置/应用/系统等可键盘导航索引偏移 otherNavStart -->
<template v-for="(item, idx) in otherItems" :key="item.id">
<div
class="qp-item"
:class="{ 'qp-item-selected': (idx + dirActionItems.length + historyItems.length) === selectedIndex }"
:class="{ 'qp-item-selected': (otherNavStart + idx) === selectedIndex }"
@click="executeItem(item)"
@mouseenter="onItemHover(idx + dirActionItems.length + historyItems.length)"
@mouseenter="onItemHover(otherNavStart + idx)"
>
<img
v-if="item.iconUrl"
@@ -1494,21 +1499,21 @@ onUnmounted(() => {
</div>
<span class="qp-group-badge" :class="groupBadgeClass(item.group)">{{ item.group }}</span>
<ChevronRight
v-if="item.subActions?.length && (idx + dirActionItems.length + historyItems.length) !== selectedIndex"
v-if="item.subActions?.length && (otherNavStart + idx) !== selectedIndex"
class="h-3.5 w-3.5 text-muted-foreground shrink-0"
/>
<kbd
v-else-if="item.subActions?.length && (idx + dirActionItems.length + historyItems.length) === selectedIndex"
v-else-if="item.subActions?.length && (otherNavStart + idx) === selectedIndex"
class="qp-kbd shrink-0"
@click.stop="toggleSubActions(idx + dirActionItems.length + historyItems.length)"
@click.stop="toggleSubActions(otherNavStart + idx)"
>Tab</kbd>
<CornerDownLeft
v-else-if="(idx + dirActionItems.length + historyItems.length) === selectedIndex"
v-else-if="(otherNavStart + idx) === selectedIndex"
class="h-3.5 w-3.5 text-primary shrink-0"
/>
</div>
<!-- 子动作展开面板 -->
<div v-if="subActionExpanded === (idx + dirActionItems.length + historyItems.length) && item.subActions?.length" class="qp-sub-panel">
<div v-if="subActionExpanded === (otherNavStart + idx) && item.subActions?.length" class="qp-sub-panel">
<div
v-for="(sub, sIdx) in item.subActions"
:key="sub.id"
@@ -1529,7 +1534,8 @@ onUnmounted(() => {
<!-- 底部提示 -->
<div class="qp-footer">
<span><kbd></kbd><kbd></kbd> 导航</span>
<span v-if="subActionExpanded === null"><kbd>Tab</kbd> 子动作</span>
<span v-if="!query.trim()"><kbd>Tab</kbd> 历史</span>
<span v-else-if="subActionExpanded === null"><kbd>Tab</kbd> 子动作</span>
<span v-else><kbd>1-9</kbd> 快捷执行</span>
<span><kbd>Enter</kbd> 执行</span>
<span><kbd>Esc</kbd> {{ subActionExpanded !== null ? '收起' : '关闭' }}</span>
@@ -1995,35 +2001,15 @@ onUnmounted(() => {
background: var(--accent);
}
/* 更多历史 Accordion */
.qp-more-history {
/* 历史折叠分组 */
.qp-history-section {
margin: 0 6px 4px;
}
.qp-more-trigger {
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
color: var(--muted-foreground);
min-height: 28px;
border-radius: var(--radius);
/* 覆盖 reka-ui 默认 py-4 */
padding-top: 6px;
padding-bottom: 6px;
}
.qp-more-trigger:hover {
background: var(--muted);
}
.qp-more-content {
/* 覆盖 AccordionContent 默认 pb-4 */
padding-top: 0;
padding-bottom: 2px;
}
.qp-more-item {
.qp-history-trigger {
min-height: 36px;
padding: 6px 12px;
font-size: 13px;
}
.qp-item-selected:hover {
@@ -4,7 +4,6 @@
*/
import type { QPItem, QPProvider } from './types'
import { appRankFromPath } from './utils'
import { HistoryProvider } from './history'
import { CommandProvider } from './command'
import { CustomCommandProvider } from './customCommand'
import { AppProvider } from './app'
@@ -21,7 +20,6 @@ let providers: QPProvider[] | null = null
export function getProviders(): QPProvider[] {
if (!providers) {
providers = [
new HistoryProvider(),
new CommandProvider(),
new CustomCommandProvider(),
new AppProvider(),
+5 -32
View File
@@ -1,18 +1,15 @@
/**
* history Provider:最近交互记录。
* 记录持久化到 localStorage空查询时置顶展示最近几条;点击历史项时
* 记录持久化到 localStorage供 QuickPanel 顶部折叠分组加载;点击历史项时
* 重新聚合搜索恢复原 action。
*/
import { STORAGE_KEYS } from '@/lib/constants'
import type { HistoryEntry, QPItem, QPProvider } from './types'
import type { HistoryEntry, QPItem } from './types'
import { aggregateSearch } from './aggregate'
const HISTORY_ITEMS_KEY = STORAGE_KEYS.quickpanelHistoryItems
const HISTORY_MAX = 50
/** 空查询时默认展示的历史条数(置顶部分) */
export const HISTORY_PREVIEW_COUNT = 3
function loadHistoryEntries(): HistoryEntry[] {
try {
const raw = localStorage.getItem(HISTORY_ITEMS_KEY)
@@ -76,32 +73,8 @@ export function clearHistory() {
localStorage.removeItem(HISTORY_ITEMS_KEY)
}
/** 获取置顶的历史项(最近 N 条),用于空查询时在结果列表顶部显示 */
export function getTopHistoryItems(): QPItem[] {
/** 获取全部历史项(最近优先),供顶部可折叠的历史分组使用 */
export function getAllHistoryItems(): QPItem[] {
const entries = loadHistoryEntries()
return entries.slice(0, HISTORY_PREVIEW_COUNT).map(buildHistoryItem)
}
/** 获取置顶历史之后的剩余历史项,用于 Accordion 折叠显示 */
export function getMoreHistoryItems(): QPItem[] {
const entries = loadHistoryEntries()
return entries.slice(HISTORY_PREVIEW_COUNT).map(buildHistoryItem)
}
/** 获取剩余历史数量(用于 Accordion 标题显示) */
export function getMoreHistoryCount(): number {
const entries = loadHistoryEntries()
return Math.max(0, entries.length - HISTORY_PREVIEW_COUNT)
}
export class HistoryProvider implements QPProvider {
id = 'history'
label = '历史'
priority = 99 // 最高优先级,空查询时显示在最前
async search(query: string): Promise<QPItem[]> {
if (query.trim()) return [] // 历史只在空查询时显示
// 只返回置顶3条,剩余由 Accordion 承载
return getTopHistoryItems()
}
return entries.map(buildHistoryItem)
}
+1 -4
View File
@@ -14,10 +14,7 @@ export { loadAppIconsForResults, invalidateAppIconCache } from './app'
export { setFileIndexReady } from './file'
export { invalidateCustomCommandsCache } from './customCommand'
export {
HISTORY_PREVIEW_COUNT,
recordHistoryItem,
clearHistory,
getTopHistoryItems,
getMoreHistoryItems,
getMoreHistoryCount,
getAllHistoryItems,
} from './history'
@@ -54,6 +54,14 @@ const SYSTEM_COMMANDS: SystemCommandDef[] = [
command: 'taskmgr',
args: [],
},
{
id: 'sys-devmgmt',
title: '设备管理器',
subtitle: 'devmgmt.msc',
keywords: ['devmgmt', '设备管理', '硬件', '驱动', 'sheb'],
command: 'devmgmt.msc',
args: [],
},
{
id: 'sys-explorer',
title: '资源管理器',
+240 -7
View File
@@ -10,6 +10,8 @@ import { useAppStore, type Theme, type EffectType, type ModuleInfo } from '@/sto
import { useSearchStore } from '@/stores/searchStore'
import { useProcessStore } from '@/stores/processStore'
import { useDownloaderStore } from '@/stores/downloaderStore'
import { useMonitorStore } from '@/stores/monitorStore'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { getModuleIcon } from '@/modules/icons'
import { commands, type UpdateCheckResult } from '@/lib/bindings'
import { EVENTS } from '@/lib/constants'
@@ -44,7 +46,6 @@ const appUpdating = ref(false)
/** ThingHK 内核更新中 */
const kernelUpdating = ref(false)
const progress = ref<UpdateProgress | null>(null)
const thinghkExists = ref(false)
let progressUnlisten: UnlistenFn | null = null
const installTypeText = computed(() =>
@@ -215,19 +216,203 @@ const cancelUpdateDownload = async () => {
cancelAppDownload?.()
}
/** 更新 ThingHK 内核:后端先停止监控内核再覆盖文件 */
/** 更新 ThingHK 内核:下载模块下载 → apply 命令 need_stop 等待确认 → 停止内核 → 解压替换。
* 与代理模块 mihomo 内核更新同模式。 */
const thinghkExists = ref(false)
/** 需要停止监控内核的确认弹窗状态(need_stop 阶段由后端 event 触发) */
const thinghkConfirmState = ref<{ open: boolean; wasRunning: boolean; busy: boolean; resolved: boolean }>({
open: false,
wasRunning: false,
busy: false,
resolved: false,
})
const monitorStore = useMonitorStore()
/** 下载中可取消(沿用应用更新下载的取消交互) */
const kernelDownloadCancellable = ref(false)
/** 取消 ThingHK 内核包下载的唤醒回调 */
let cancelKernelDownload: (() => void) | null = null
/** 停止 Kernel 确认弹窗中处理中标记(防止重复触发) */
let handlingThinghkNeedStop = false
const handleThinghkNeedStop = () => {
if (handlingThinghkNeedStop) return
handlingThinghkNeedStop = true
try {
// 若程序被最小化/隐藏到后台,先弹到前台再显示确认框
invoke('quickpanel_focus_main_window').catch(() => { /* 忽略 */ })
const wasRunning = monitorStore.status?.running ?? false
thinghkConfirmState.value = { open: true, wasRunning, busy: false, resolved: false }
} finally {
handlingThinghkNeedStop = false
}
}
/** 确认停止内核并唤醒后端 apply 继续解压替换 */
const onThinghkNeedStopConfirm = async () => {
const s = thinghkConfirmState.value
if (s.resolved) return
// 置 resolved 防止 AlertDialog 的 update:open(false) 兜底逻辑把确认误判为取消
s.resolved = true
s.busy = true
try {
if (s.wasRunning) {
await monitorStore.stop()
}
await invoke('update_thinghk_confirm')
} catch (e) {
toast.error('停止监控内核失败', { description: String(e) })
// 停止失败则中止更新,避免替换阶段因 exe 占用而报错
try { await invoke('update_thinghk_cancel') } catch { /* 忽略 */ }
} finally {
s.busy = false
s.open = false
}
}
/** 取消 ThingHK 内核更新(中止后端等待中的 apply 流程,zip 保留便于重试) */
const onThinghkNeedStopCancel = () => {
const s = thinghkConfirmState.value
if (s.resolved) return
s.resolved = true
s.open = false
invoke('update_thinghk_cancel').catch(() => { /* 忽略 */ })
}
/** AlertDialog 关闭事件的兜底判定:cancel/action 点击会先 update:open(false) 再 click
* 仅在此前未被 click 处理器 resolve 时才当作取消(遮罩/Esc 关闭),否则会误判确认/取消。 */
const onThinghkNeedStopOpenChange = (open: boolean) => {
const s = thinghkConfirmState.value
if (!open && !s.resolved) {
setTimeout(() => {
if (!thinghkConfirmState.value.resolved) onThinghkNeedStopCancel()
}, 0)
}
thinghkConfirmState.value.open = open
}
/** 取消 ThingHK 内核包下载(仅下载阶段) */
const cancelKernelUpdateDownload = () => {
if (!kernelDownloadCancellable.value) return
cancelKernelDownload?.()
}
/** 更新 ThingHK 内核 */
const updateThinghkKernel = async () => {
if (kernelUpdating.value) return
const result = updateResult.value
if (!result) return
// 从 release assets 中定位 ThingHK 内核包(zip);找不到则提示手动下载
const asset = result.assets.find(a => {
const n = a.name.toLowerCase()
return (n.includes('thing-hk') || n.includes('thinghk')) && n.endsWith('.zip')
})
if (!asset) {
toast.error('未找到 ThingHK 内核更新包', {
description: '请确认 release 资产中已上传 ThingHK 内核 zip 包',
})
return
}
kernelUpdating.value = true
progress.value = {
stage: 'downloading',
percent: 0,
downloadedBytes: 0,
totalBytes: asset.size || null,
message: '准备开始下载...',
}
let taskId: string | null = null
let downloadProgressFn: UnlistenFn | null = null
// 用对象持有完成事件解绑函数,避免闭包内赋值导致的 TS 类型收窄问题(同 proxyStore
const completeHolder: { fn: UnlistenFn | null } = { fn: null }
let downloadOk = false
try {
await commands.updateThinghk()
// 确保下载模块事件监听已注册(下载器 UI 与这里共用事件流)
try { await downloaderStore.startEventListeners() } catch { /* 忽略 */ }
taskId = await downloaderStore.addTask(asset.browserDownloadUrl, asset.name, undefined, {}, false)
// 下载进度 → 更新进度条
downloadProgressFn = await listen<{
id: string; completedSize: number; totalSize: number; speed: number; status: string
}>('download-progress', (e) => {
if (e.payload.id !== taskId || !kernelUpdating.value) return
const pct = e.payload.totalSize > 0
? Math.round((e.payload.completedSize / e.payload.totalSize) * 100)
: 0
progress.value = {
stage: 'downloading',
percent: pct,
downloadedBytes: e.payload.completedSize,
totalBytes: e.payload.totalSize,
message: e.payload.speed > 0
? `正在下载... ${fmtSpeed(e.payload.speed)}`
: '正在下载...',
}
})
// 等待下载完成 / 失败 / 取消
kernelDownloadCancellable.value = true
const dlResult = await new Promise<{ ok: boolean; error?: string }>((resolve) => {
let settled = false
const finish = (r: { ok: boolean; error?: string }) => {
if (settled) return
settled = true
cancelKernelDownload = null
resolve(r)
}
// 任务添加后瞬间进入终态(如探测即失败)
const initial = downloaderStore.tasks.find(t => t.id === taskId)
if (initial) {
if (initial.status === 'complete') { finish({ ok: true }); return }
if (initial.status === 'error') { finish({ ok: false, error: initial.error || '下载失败' }); return }
}
cancelKernelDownload = () => finish({ ok: false, error: '已取消下载' })
listen<{ id: string; status: string; error: string | null }>('download-complete', (e) => {
if (e.payload.id === taskId) {
if (e.payload.status === 'complete') finish({ ok: true })
else finish({ ok: false, error: e.payload.error || '下载失败' })
}
}).then(fn => { completeHolder.fn = fn })
})
kernelDownloadCancellable.value = false
if (!dlResult.ok) throw new Error(dlResult.error || '下载失败')
downloadOk = true
// 取下载文件路径 → 移除任务记录(保留文件,apply 命令内部会解压并清理)
const dlTask = downloaderStore.tasks.find(t => t.id === taskId)
if (!dlTask) throw new Error('下载任务未找到')
const zipPath = dlTask.dir + '/' + dlTask.filename
try {
await downloaderStore.removeTask(taskId, false)
taskId = null
} catch { /* 任务清理失败不阻断更新 */ }
// 应用阶段:apply 内部 need_stop 等待前端停止内核并确认 → 解压替换 → 完成
await invoke('update_thinghk_apply', { zipPath })
await loadAppInfo()
toast.success('ThingHK 内核更新完成')
} catch (e) {
console.error('[updater] ThingHK 更新失败', e)
toast.error('ThingHK 内核更新失败', { description: String(e) })
const msg = String(e)
if (msg.includes('已取消下载')) {
toast.info('已取消更新下载')
} else if (msg.includes('更新已取消')) {
toast.info('已取消内核更新')
} else {
toast.error('ThingHK 内核更新失败', { description: msg })
}
// 清理下载任务:下载失败/取消时删除半成品文件;apply 失败保留 zip 便于重试
if (taskId) {
try { await downloaderStore.removeTask(taskId, !downloadOk) } catch { /* 忽略 */ }
}
} finally {
kernelUpdating.value = false
progress.value = null
kernelDownloadCancellable.value = false
cancelKernelDownload = null
if (downloadProgressFn) downloadProgressFn()
if (completeHolder.fn) completeHolder.fn()
}
}
@@ -240,10 +425,14 @@ onMounted(() => {
// 监听更新进度事件(应用更新与 ThingHK 内核更新共用)
listen<UpdateProgress>(EVENTS.updateProgress, (e) => {
progress.value = e.payload
if (e.payload.stage === 'done') {
const p = e.payload
if (p.stage === 'need_stop') {
// ThingHK 内核更新:解压替换前需停止监控内核,弹窗确认(若在后台自动弹到前台)
handleThinghkNeedStop()
} else if (p.stage === 'done') {
kernelUpdating.value = false
progress.value = null
} else if (e.payload.stage === 'error') {
} else if (p.stage === 'error') {
// ThingHK 内核更新失败(后端 emit);应用更新失败走命令 reject 路径
kernelUpdating.value = false
}
@@ -407,6 +596,16 @@ const onDragEnd = () => {
@update:model-value="(checked: boolean) => appStore.toggleAutoStart(checked)"
/>
</div>
<div class="flex items-center justify-between py-2 border-t border-border/60 mt-2">
<div class="space-y-1">
<Label class="text-base font-medium">静默启动</Label>
<p class="text-sm text-muted-foreground">启动后不打开主界面,静默驻留托盘(托盘左键可呼出)</p>
</div>
<Switch
:model-value="appStore.silentAutoStart"
@update:model-value="(checked: boolean) => appStore.toggleSilentAutoStart(checked)"
/>
</div>
</CardContent>
</Card>
@@ -675,8 +874,42 @@ const onDragEnd = () => {
<span>{{ progress.message }}</span>
<span class="font-mono">{{ progress.percent }}%</span>
</div>
<Progress :model-value="progress.percent" />
<div class="flex items-center gap-2">
<Progress :model-value="progress.percent" class="flex-1" />
<Button
v-if="kernelDownloadCancellable"
variant="ghost"
size="sm"
class="h-5 px-1.5 text-xs"
@click="cancelKernelUpdateDownload"
>
<X class="size-3 mr-0.5" />
取消
</Button>
</div>
</div>
<!-- 停止监控内核确认(need_stop 阶段:与 mihomo 同模式) -->
<AlertDialog :open="thinghkConfirmState.open" @update:open="onThinghkNeedStopOpenChange">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>停止监控内核后继续</AlertDialogTitle>
<AlertDialogDescription>
{{
thinghkConfirmState.wasRunning
? '内核更新包已下载。安装新内核前需要停止监控内核,点击「停止并继续」将自动停止监控并完成安装。'
: '内核更新包已下载。即将安装新内核,点击「继续」完成安装。'
}}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="onThinghkNeedStopCancel">取消</AlertDialogCancel>
<AlertDialogAction :disabled="thinghkConfirmState.busy" @click="onThinghkNeedStopConfirm">
{{ thinghkConfirmState.busy ? '正在停止...' : (thinghkConfirmState.wasRunning ? '停止并继续' : '继续') }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
+13
View File
@@ -57,6 +57,7 @@ export const useAppStore = defineStore('app', () => {
const theme = ref<Theme>('system')
const effect = ref<EffectType>('mica')
const isAutoStart = ref(false)
const silentAutoStart = ref(false)
const isInitialized = ref(false)
const modules = ref<ModuleInfo[]>(initModulesFromRegistry())
const moduleOrder = ref<string[]>(initModuleOrder())
@@ -88,6 +89,8 @@ export const useAppStore = defineStore('app', () => {
if (settings.theme) theme.value = settings.theme
if (settings.effect) effect.value = settings.effect
if (typeof settings.isAutoStart === 'boolean') isAutoStart.value = settings.isAutoStart
if (typeof settings.silentAutoStart === 'boolean') silentAutoStart.value = settings.silentAutoStart
if (settings.modules) {
const savedModules = settings.modules as Array<{ id: string; enabled: boolean }>
savedModules.forEach(sm => {
@@ -127,6 +130,7 @@ export const useAppStore = defineStore('app', () => {
theme: theme.value,
effect: effect.value,
isAutoStart: isAutoStart.value,
silentAutoStart: silentAutoStart.value,
modules: modulesData,
moduleOrder: moduleOrder.value
}))
@@ -323,6 +327,11 @@ export const useAppStore = defineStore('app', () => {
}
}
const toggleSilentAutoStart = (checked?: boolean) => {
silentAutoStart.value = checked !== undefined ? checked : !silentAutoStart.value
saveSettings()
}
const applyTheme = async () => {
const root = document.documentElement
root.classList.remove('dark')
@@ -472,6 +481,8 @@ export const useAppStore = defineStore('app', () => {
isInitialized.value = true
} finally {
// 静默启动:仅当未开启时才显示主窗口(开启后启动静默驻留托盘,托盘左键可呼出)
if (silentAutoStart.value) return
try {
const tauriWindow = getCurrentWindow()
await tauriWindow.show()
@@ -486,6 +497,7 @@ export const useAppStore = defineStore('app', () => {
effect,
systemDark,
isAutoStart,
silentAutoStart,
isInitialized,
modules,
moduleOrder,
@@ -497,6 +509,7 @@ export const useAppStore = defineStore('app', () => {
setTheme,
setEffect,
toggleAutoStart,
toggleSilentAutoStart,
applyTheme,
applyEffect,
init,
+21
View File
@@ -185,6 +185,8 @@ export interface OsdConfig {
/** 悬浮窗窗口保存位置(null=使用百分比计算默认位置) */
overlayX?: number | null
overlayY?: number | null
/** 游戏全屏时自动隐藏悬浮窗(前台全屏应用会因置顶透明窗口掉帧,默认开启) */
gameAutoHide: boolean
}
/** OSD 悬浮窗窗口 label(与 Tauri 窗口创建对应,见 constants::WINDOWS */
@@ -274,6 +276,7 @@ function defaultOsdConfig(): OsdConfig {
alert: { ...DEFAULT_ALERT_CONFIG, maxValues: { ...DEFAULT_ALERT_CONFIG.maxValues } },
overlayX: null,
overlayY: null,
gameAutoHide: true,
}
}
@@ -340,6 +343,10 @@ export const useMonitorStore = defineStore('monitor', () => {
*/
const osdConfig = ref<OsdConfig>(loadOsdConfig())
/** 前台是否为全屏应用(游戏)。由 Rust 侧 osd-game-active/inactive 事件驱动,
* 用于游戏时隐藏 OSD(透明置顶窗口会占用 DWM 合成路径导致游戏掉帧) */
const gameFullscreen = ref(false)
/** OSD 配置防抖保存:滑块/输入连续变化时合并为一次 localStorage 写入(避免每帧全量序列化) */
let osdSaveTimer: ReturnType<typeof setTimeout> | null = null
/** OSD 配置防抖推送定时器(initOsd 内注册的 deep watch 使用,dispose 时需清理) */
@@ -367,6 +374,8 @@ export const useMonitorStore = defineStore('monitor', () => {
/** 推送 OSD 数据到所有 OSD 窗口(高频通道:仅显示项 key→value 映射 + 网速,每秒一次) */
async function pushOsdState() {
if (!osdConfig.value.overlayEnabled) return
// 游戏全屏自动隐藏期间停止推送:OSD 窗口已隐藏,推送只会白白消耗 IPC 和 WebView JS 时间片
if (gameFullscreen.value && osdConfig.value.gameAutoHide) return
try {
const map = sensorKeyMap.value
const data: Record<string, number | null> = {}
@@ -1190,12 +1199,24 @@ export const useMonitorStore = defineStore('monitor', () => {
if (enabled) {
// 开启时若显示项为空则不创建窗口
if (osdConfig.value.overlayItems.length === 0) return
// 游戏全屏自动隐藏期间不创建窗口(退出全屏时由 osd-game-inactive 统一恢复)
if (gameFullscreen.value && osdConfig.value.gameAutoHide) return
ensureOverlayWindow().catch(e => logger.error('[OSD] 创建悬浮窗失败: ' + e))
} else {
hideOverlayWindow().catch(e => logger.error('[OSD] 隐藏悬浮窗失败: ' + e))
}
}))
// 游戏中切换"自动隐藏"开关:立即生效(关闭时恢复显示,开启时立即隐藏)
osdWatchStops.push(watch(() => osdConfig.value.gameAutoHide, (enabled) => {
if (!osdConfig.value.overlayEnabled || !gameFullscreen.value) return
if (enabled) {
hideOverlayWindow().catch(e => logger.error('[OSD] 游戏全屏隐藏悬浮窗失败: ' + e))
} else {
ensureOverlayWindow().catch(e => logger.error('[OSD] 恢复悬浮窗失败: ' + e))
}
}))
// 显示项变化时:无项则隐藏窗口,有项且开关开则确保窗口存在
osdWatchStops.push(watch(() => osdConfig.value.overlayItems.length, (len) => {
if (!osdConfig.value.overlayEnabled) return