{
'bg-primary text-primary-foreground shadow-md': activeModule === module.id,
'hover:bg-secondary/50': activeModule !== module.id
}"
- @click="emit('change', module.id)"
+ @click="activeModule !== module.id && emit('change', module.id)"
>
@@ -60,7 +60,7 @@ const displayModules = computed(() => {
'bg-primary text-primary-foreground shadow-md': activeModule === 'settings',
'hover:bg-secondary/50': activeModule !== 'settings'
}"
- @click="emit('change', 'settings')"
+ @click="activeModule !== 'settings' && emit('change', 'settings')"
>
diff --git a/src/components/ui/empty/Empty.vue b/src/components/ui/empty/Empty.vue
new file mode 100644
index 0000000..2637b3c
--- /dev/null
+++ b/src/components/ui/empty/Empty.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/empty/EmptyContent.vue b/src/components/ui/empty/EmptyContent.vue
new file mode 100644
index 0000000..d19ee00
--- /dev/null
+++ b/src/components/ui/empty/EmptyContent.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/empty/EmptyDescription.vue b/src/components/ui/empty/EmptyDescription.vue
new file mode 100644
index 0000000..e4ed5fb
--- /dev/null
+++ b/src/components/ui/empty/EmptyDescription.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/empty/EmptyHeader.vue b/src/components/ui/empty/EmptyHeader.vue
new file mode 100644
index 0000000..ac21d8c
--- /dev/null
+++ b/src/components/ui/empty/EmptyHeader.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/empty/EmptyMedia.vue b/src/components/ui/empty/EmptyMedia.vue
new file mode 100644
index 0000000..c68397c
--- /dev/null
+++ b/src/components/ui/empty/EmptyMedia.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/empty/EmptyTitle.vue b/src/components/ui/empty/EmptyTitle.vue
new file mode 100644
index 0000000..90c950d
--- /dev/null
+++ b/src/components/ui/empty/EmptyTitle.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/empty/index.ts b/src/components/ui/empty/index.ts
new file mode 100644
index 0000000..ce0c489
--- /dev/null
+++ b/src/components/ui/empty/index.ts
@@ -0,0 +1,26 @@
+import type { VariantProps } from "class-variance-authority"
+import { cva } from "class-variance-authority"
+
+export { default as Empty } from "./Empty.vue"
+export { default as EmptyContent } from "./EmptyContent.vue"
+export { default as EmptyDescription } from "./EmptyDescription.vue"
+export { default as EmptyHeader } from "./EmptyHeader.vue"
+export { default as EmptyMedia } from "./EmptyMedia.vue"
+export { default as EmptyTitle } from "./EmptyTitle.vue"
+
+export const emptyMediaVariants = cva(
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+)
+
+export type EmptyMediaVariants = VariantProps
diff --git a/src/components/ui/skeleton/Skeleton.vue b/src/components/ui/skeleton/Skeleton.vue
new file mode 100644
index 0000000..8d527f9
--- /dev/null
+++ b/src/components/ui/skeleton/Skeleton.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/src/components/ui/skeleton/index.ts b/src/components/ui/skeleton/index.ts
new file mode 100644
index 0000000..e5ce72c
--- /dev/null
+++ b/src/components/ui/skeleton/index.ts
@@ -0,0 +1 @@
+export { default as Skeleton } from "./Skeleton.vue"
diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts
index 22d9afd..432b8cc 100644
--- a/src/lib/bindings.ts
+++ b/src/lib/bindings.ts
@@ -54,7 +54,11 @@ export const commands = {
quickpanelShowWindow: () => __TAURI_INVOKE("quickpanel_show_window"),
/** 锁定屏幕(Windows: rundll32 user32.dll,LockWorkStation,CREATE_NO_WINDOW 避免黑窗) */
quickpanelLockScreen: () => __TAURI_INVOKE("quickpanel_lock_screen"),
- /** 初始化文件索引数据库(应用启动时调用) */
+ /**
+ * 初始化文件索引数据库(应用启动时调用)。
+ * 若存在上次构建的索引(last_built_dirs 非空),自动恢复 notify 增量监听,
+ * 无需重建即可继续自动同步文件变更。
+ */
quickpanelInitFileIndex: () => __TAURI_INVOKE("quickpanel_init_file_index"),
/** 构建文件索引(全量重建,阻塞操作建议在 spawn_blocking 调用) */
quickpanelBuildFileIndex: () => __TAURI_INVOKE("quickpanel_build_file_index"),
@@ -96,6 +100,35 @@ export const commands = {
* 适用于内置系统工具:regedit、shutdown、cmd、powershell、taskmgr 等。
*/
quickpanelRunSystemCommand: (command: string, args: string[]) => __TAURI_INVOKE("quickpanel_run_system_command", { command, args }),
+ /** 列出目录下的压缩包文件(供批量解压面板使用)。 */
+ quickpanelListArchives: (dir: string) => __TAURI_INVOKE("quickpanel_list_archives", { dir }),
+ /** 列出目录下的全部条目(供批量重命名/删除面板使用,不含子目录递归)。 */
+ quickpanelListDir: (dir: string) => __TAURI_INVOKE("quickpanel_list_dir", { dir }),
+ /**
+ * 批量解压。`files` 为压缩包路径列表,`dest_dir` 为目标目录,
+ * `password` 为统一解压密码(可空),`into_subfolder` 是否解压到同名子文件夹。
+ * 每完成一个文件通过 `quickpanel-extract-progress` 事件推送进度。
+ */
+ quickpanelBatchExtract: (files: string[], destDir: string, password: string | null, intoSubfolder: boolean) => __TAURI_INVOKE("quickpanel_batch_extract", { files, destDir, password, intoSubfolder }),
+ /**
+ * 正则批量重命名预览:对每个文件名应用 `pattern → replacement`,
+ * 仅返回有匹配的文件,`newName` 为替换结果。
+ */
+ quickpanelPreviewRename: (files: string[], pattern: string, replacement: string) => __TAURI_INVOKE("quickpanel_preview_rename", { files, pattern, replacement }),
+ /** 执行重命名。同一目录下若目标已存在则跳过该项。 */
+ quickpanelApplyRename: (items: RenameItem[]) => __TAURI_INVOKE("quickpanel_apply_rename", { items }),
+ /**
+ * 批量删除文件/目录。`force=false` 时移动至回收站;`force=true` 时先递归清除
+ * 只读属性再永久删除(可绕过只读/部分占用导致的删除失败,但被其他进程真正
+ * 锁定的文件仍会失败并返回原因)。
+ */
+ quickpanelDeleteFiles: (paths: string[], force: boolean) => __TAURI_INVOKE("quickpanel_delete_files", { paths, force }),
+ /**
+ * 显示主窗口并强制置为前台。
+ * Tauri 的 set_focus 在 Windows 上受前台锁定限制,主窗口被其他应用遮挡时无法到前台;
+ * 改用原生 SetForegroundWindow + BringWindowToTop(模拟 Alt 键重置前台锁定)。
+ */
+ quickpanelFocusMainWindow: () => __TAURI_INVOKE("quickpanel_focus_main_window"),
clipboardGetHistory: (limit: number | null, offset: number | null, kind: string | null) => __TAURI_INVOKE("clipboard_get_history", { limit, offset, kind }),
clipboardGetPinned: () => __TAURI_INVOKE("clipboard_get_pinned"),
clipboardSearch: (query: string, limit: number | null, offset: number | null) => __TAURI_INVOKE("clipboard_search", { query, limit, offset }),
@@ -206,6 +239,12 @@ export type AppRecord = {
path: string,
};
+export type ArchiveInfo = {
+ name: string,
+ path: string,
+ size: number,
+};
+
/** 前端可见的捕获数据 */
export type CaptureData = {
pngBase64: string,
@@ -280,6 +319,14 @@ export type CustomCommand = {
args?: string[],
};
+/** 批量删除单个条目的结果。 */
+export type DeleteResult = {
+ name: string,
+ path: string,
+ ok: boolean,
+ error: string,
+};
+
/** 下载任务 */
export type DownloadTask = {
/** 任务 ID(自增 hex 字符串) */
@@ -350,6 +397,20 @@ export type ExistingTaskInfo = {
status: TaskStatus,
};
+export type ExtractResult = {
+ name: string,
+ path: string,
+ ok: boolean,
+ error: string,
+};
+
+export type FileEntry = {
+ name: string,
+ path: string,
+ isDir: boolean,
+ size: number,
+};
+
/** 单个文件记录(返回给前端) */
export type FileRecord = {
path: string,
@@ -449,6 +510,26 @@ export type QuickPanelSettings = {
customCommands?: CustomCommand[],
};
+export type RenameItem = {
+ path: string,
+ oldName: string,
+ newName: string,
+};
+
+export type RenamePreview = {
+ path: string,
+ oldName: string,
+ newName: string,
+ error: string,
+};
+
+export type RenameResult = {
+ oldName: string,
+ newName: string,
+ ok: boolean,
+ error: string,
+};
+
export type ScreenRect = {
x: number,
y: number,
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index ac43708..5028e91 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -28,6 +28,7 @@ export const EVENTS = {
quickpanelShow: 'quickpanel-show',
quickpanelHide: 'quickpanel-hide',
quickpanelExecuteCommand: 'quickpanel-execute-command',
+ quickpanelExtractProgress: 'quickpanel-extract-progress',
// 截图
screenshotBegin: 'screenshot-begin',
screenshotOverlayReady: 'screenshot-overlay-ready',
@@ -56,6 +57,8 @@ export const EVENTS = {
// 其他
processStatusChanged: 'process-status-changed',
downloadAdded: 'download-added',
+ /** 浏览器扩展通过 HTTP API 新增下载(置前主窗口并跳到下载画面) */
+ downloadExtensionAdded: 'download-extension-added',
} as const
/** localStorage 存储键 */
@@ -66,6 +69,14 @@ export const STORAGE_KEYS = {
quickpanelSettings: 'thing_quickpanel_settings',
quickpanelHistory: 'thing_quickpanel_history',
quickpanelHistoryItems: 'thing_quickpanel_history_items',
+ quickpanelPwdHistory: 'thing_quickpanel_pwd_history',
+ quickpanelPwdFavs: 'thing_quickpanel_pwd_favs',
+ quickpanelRenameMatchHistory: 'thing_quickpanel_rename_match_history',
+ quickpanelRenameMatchFavs: 'thing_quickpanel_rename_match_favs',
+ quickpanelRenameReplaceHistory: 'thing_quickpanel_rename_replace_history',
+ quickpanelRenameReplaceFavs: 'thing_quickpanel_rename_replace_favs',
+ quickpanelDeleteFilterHistory: 'thing_quickpanel_delete_filter_history',
+ quickpanelDeleteFilterFavs: 'thing_quickpanel_delete_filter_favs',
currencyRates: 'thing_quickpanel_currency_rates',
monitorOsdConfig: 'thing_monitor_osd_config',
screenshotHistory: 'thing_screenshot_history',
diff --git a/src/lib/trayEvents.ts b/src/lib/trayEvents.ts
index 4b64b66..3d69f66 100644
--- a/src/lib/trayEvents.ts
+++ b/src/lib/trayEvents.ts
@@ -10,3 +10,6 @@ import { ref } from 'vue'
/** 待打开新建下载对话框(由托盘"新建下载"触发) */
export const pendingNewDownload = ref(false)
+
+/** 待切换到下载任务列表页(由浏览器扩展新增下载触发,不弹对话框直接看任务) */
+export const pendingShowDownloadTasks = ref(false)
diff --git a/src/modules/clipboard/ClipboardModule.vue b/src/modules/clipboard/ClipboardModule.vue
index 6025c8c..088d603 100644
--- a/src/modules/clipboard/ClipboardModule.vue
+++ b/src/modules/clipboard/ClipboardModule.vue
@@ -16,7 +16,6 @@ import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
-import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
} from '@/components/ui/dialog'
@@ -27,6 +26,10 @@ import {
import {
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
} from '@/components/ui/pagination'
+import { Skeleton } from '@/components/ui/skeleton'
+import {
+ Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,
+} from '@/components/ui/empty'
const store = useClipboardStore()
@@ -74,6 +77,12 @@ const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const openDetail = async (item: ClipboardItem) => {
+ // reka-ui Dialog 打开时会把当前活动元素记为 triggerElement,关闭时对其无 preventScroll 地 focus,
+ // 导致历史列表的 ScrollAreaViewport(tabindex=0) 被聚焦并滚回顶部。打开前 blur,避免记录滚动容器。
+ const active = document.activeElement
+ if (active instanceof HTMLElement) {
+ active.blur()
+ }
detailOpen.value = true
detailLoading.value = true
detail.value = null
@@ -283,9 +292,17 @@ const historyList = computed(() => store.history)
const pinnedList = computed(() => store.pinned)
onMounted(async () => {
- await store.init()
- await Promise.all([loadPage(), store.refreshPinned()])
- form.value = { ...store.settings }
+ // 首次进入:异步加载并显示骨架屏。再次进入时 store 已缓存历史/固定/设置,
+ // 直接即时渲染缓存数据,后台并行静默刷新,避免每次切换都出现骨架屏/空态闪烁。
+ const isFirst = !store.initialized
+ if (isFirst) store.loading = true
+ try {
+ await Promise.all([store.init(), loadPage(), store.refreshPinned()])
+ store.initialized = true
+ form.value = { ...store.settings }
+ } finally {
+ store.loading = false
+ }
})
onUnmounted(() => {
@@ -369,13 +386,33 @@ onUnmounted(() => {
-
-
-
暂无历史记录,复制内容后将自动收录
+
+
+
+
+
+
+
+ 暂无历史记录
+
+
+ 复制内容后将自动收录,图片与文件也会被记录
+
+
{
-
-
-
-
- 复制
-
-
-
-
-
- {{ item.pinned ? '取消固定' : '固定' }}
-
-
-
-
-
- 删除
-
+
+
+
@@ -426,14 +448,17 @@ onUnmounted(() => {
-
-
-
暂无固定条目
-
鼠标悬停历史条目,点击图钉按钮即可固定
-
+
+
+
+
+
+ 暂无固定条目
+
+
+ 鼠标悬停历史条目,点击图钉按钮即可固定
+
+
@@ -446,30 +471,15 @@ onUnmounted(() => {
-
+
@@ -443,10 +997,321 @@ onUnmounted(() => {
autocomplete="off"
/>
Esc
+
+
+
+
+
+
+
批量解压
+
{{ explorerDir }}
+
+
+
+
+
+
+ 已选 {{ archiveSelected.size }} / {{ archives.length }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
批量重命名
+
{{ explorerDir }}
+
+
+
+
+
+
+ 已选 {{ (renamePreviews ?? []).filter(p => !p.error && renameSelected.has(p.path)).length }} / {{ (renamePreviews ?? []).filter(p => !p.error).length }}
+
+
+
+
+
+
+
{{ renameResults.length }} 个文件重命名失败(已成功 {{ renameOkCount }} 个)
+
+
+
+
+
+
+
+
+
+
批量删除
+
{{ explorerDir }}
+
+
+
+
+
{{ deleteResults.length }} 项删除失败,可修正后重试
+
+
+
+
+ 已选 {{ deleteSelected.size }} / {{ deleteFiltered.length }}
+
+
+
+
+
+
+
+
搜索中…
@@ -462,8 +1327,8 @@ onUnmounted(() => {
命令 · 计算 · 系统 · 网页
-
-
+
+
{
{{ item.title }}
{{ item.subtitle }}
- {{ item.group }}
+ {{ item.group }}
{
+
+
+
+
![]()
+
+
+
{{ item.title }}
+
{{ item.subtitle }}
+
+
{{ item.group }}
+
+
+
+
{
{{ item.title }}
{{ item.subtitle }}
-
{{ item.group }}
+
{{ item.group }}
-
+
![]()
{
{{ item.title }}
{{ item.subtitle }}
- {{ item.group }}
+ {{ item.group }}
Tab
-
+
@@ -669,6 +1567,22 @@ onUnmounted(() => {
font-family: inherit;
}
+.qp-settings-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ color: var(--muted-foreground);
+ transition: background 0.15s, color 0.15s;
+}
+
+.qp-settings-btn:hover {
+ background: var(--muted);
+ color: var(--foreground);
+}
+
.qp-results {
flex: 1;
min-height: 0;
@@ -687,6 +1601,339 @@ onUnmounted(() => {
padding: 20px;
}
+/* ===== 目录操作面板(批量解压 / 批量重命名) ===== */
+.qp-action-head {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 6px;
+}
+
+.qp-back-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ border: none;
+ background: var(--muted);
+ color: var(--foreground);
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: background 0.1s;
+}
+
+.qp-back-btn:hover {
+ background: var(--accent);
+}
+
+.qp-action-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--foreground);
+ line-height: 1.3;
+}
+
+.qp-action-dir {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ line-height: 1.3;
+}
+
+.qp-fa-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.qp-fa-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 2px 0;
+}
+
+.qp-fa-row-head {
+ justify-content: space-between;
+ padding: 2px 4px;
+}
+
+.qp-fa-scroll {
+ max-height: 168px;
+ overflow-y: auto;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ background: var(--muted);
+ padding: 2px;
+}
+
+.qp-fa-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 8px;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: background 0.08s;
+}
+
+.qp-fa-item:hover {
+ background: var(--accent);
+}
+
+.qp-fa-checked {
+ background: var(--accent);
+}
+
+.qp-fa-checkbox {
+ accent-color: var(--primary);
+ width: 13px;
+ height: 13px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.qp-fa-size {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ flex-shrink: 0;
+ font-variant-numeric: tabular-nums;
+}
+
+.qp-fa-form {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-top: 6px;
+}
+
+.qp-fa-label {
+ width: 56px;
+ flex-shrink: 0;
+ font-size: 12px;
+ color: var(--muted-foreground);
+ text-align: right;
+}
+
+.qp-fa-input-group {
+ position: relative;
+ flex: 1;
+ min-width: 0;
+}
+
+.qp-fa-input {
+ display: block;
+ width: 100%;
+ min-width: 0;
+ height: 28px;
+ padding: 0 8px;
+ font-size: 13px;
+ color: var(--foreground);
+ background: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ outline: none;
+ transition: border-color 0.1s;
+}
+
+.qp-fa-input:focus {
+ border-color: var(--primary);
+}
+
+.qp-fa-input::placeholder {
+ color: var(--muted-foreground);
+}
+
+.qp-fa-radios {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ flex: 1;
+}
+
+.qp-fa-radio {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 12px;
+ color: var(--foreground);
+ cursor: pointer;
+}
+
+.qp-fa-radio input {
+ accent-color: var(--primary);
+ cursor: pointer;
+}
+
+.qp-fa-primary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ height: 30px;
+ padding: 0 16px;
+ font-size: 13px;
+ font-weight: 500;
+ color: white;
+ background: var(--primary);
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: opacity 0.12s;
+}
+
+.qp-fa-primary:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.qp-fa-primary:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+
+.qp-fa-primary.qp-fa-danger {
+ background: var(--destructive);
+}
+
+.qp-fa-delete-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex: 1;
+}
+
+.qp-fa-force {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--muted-foreground);
+ cursor: pointer;
+ user-select: none;
+}
+
+.qp-fa-hint {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ font-size: 10px;
+ color: var(--muted-foreground);
+ background: var(--muted);
+ border: 1px solid var(--border);
+ cursor: help;
+}
+
+.qp-fa-progress-wrap {
+ position: relative;
+ height: 20px;
+ margin-top: 4px;
+ background: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.qp-fa-progress {
+ height: 100%;
+ background: var(--primary);
+ opacity: 0.85;
+ transition: width 0.2s ease;
+}
+
+.qp-fa-progress-text {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ color: var(--foreground);
+}
+
+.qp-fa-results {
+ margin-top: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+/* 删除结果区:限高,避免遮挡下方可重试的列表 */
+.qp-fa-results-scroll {
+ max-height: 96px;
+}
+
+.qp-fa-results-sum {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ padding: 0 2px;
+}
+
+.qp-fa-result {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 8px;
+ border-radius: 5px;
+ font-size: 12px;
+}
+
+.qp-fa-ok {
+ color: var(--foreground);
+}
+
+.qp-fa-err {
+ color: var(--destructive);
+}
+
+.qp-fa-summary {
+ flex: 1;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ line-height: 1.4;
+}
+
+.qp-fa-empty-tip {
+ padding: 10px;
+ text-align: center;
+ font-size: 12px;
+ color: var(--muted-foreground);
+}
+
+.qp-link-btn {
+ background: none;
+ border: none;
+ padding: 2px 4px;
+ font-size: 12px;
+ color: var(--primary);
+ cursor: pointer;
+}
+
+.qp-link-btn:hover {
+ text-decoration: underline;
+}
+
+.mono {
+ font-family: Consolas, 'Cascadia Mono', 'Courier New', monospace;
+}
+
+.qp-fa-scroll::-webkit-scrollbar {
+ width: 6px;
+}
+.qp-fa-scroll::-webkit-scrollbar-thumb {
+ background: var(--muted-foreground);
+ opacity: 0.3;
+ border-radius: 3px;
+}
+.qp-fa-scroll::-webkit-scrollbar-track {
+ background: transparent;
+}
+
.qp-item {
display: flex;
align-items: flex-start;
@@ -751,12 +1998,12 @@ onUnmounted(() => {
.qp-group-badge {
font-size: 10px;
- color: var(--muted-foreground);
- background: var(--muted);
padding: 1px 6px;
border-radius: 3px;
flex-shrink: 0;
margin-top: 2px;
+ /* 颜色由 groupBadgeClass 的 tailwind 类控制(不同类型不同颜色) */
+ line-height: 16px;
}
/* 子动作面板 */
diff --git a/src/modules/quickpanel/QuickPanelModule.vue b/src/modules/quickpanel/QuickPanelModule.vue
index 11823ae..eade138 100644
--- a/src/modules/quickpanel/QuickPanelModule.vue
+++ b/src/modules/quickpanel/QuickPanelModule.vue
@@ -528,7 +528,15 @@ async function changeEngine(v: string) {
文件
- 索引指定目录,快速定位文件
+ 索引指定目录,快速定位文件,支持打开/显示/复制路径/删除,.lnk 按应用处理
+
+
+ 目录操作
+ 文件资源管理器批量处理:批量解压、正则批量重命名(实时预览)、筛选批量删除
+
+
+ 历史
+ 记录最近交互,空查询优先展示
剪贴板
@@ -548,7 +556,7 @@ async function changeEngine(v: string) {
系统
- 锁屏、退出应用
+ 系统命令(注册表、CMD/PowerShell、任务管理器、控制面板、关机/重启/休眠)及锁屏、退出应用
网页
diff --git a/src/modules/tray/TrayMenu.vue b/src/modules/tray/TrayMenu.vue
index 7a19344..5b646f4 100644
--- a/src/modules/tray/TrayMenu.vue
+++ b/src/modules/tray/TrayMenu.vue
@@ -38,8 +38,17 @@ const state = reactive({
const loadingAction = ref(null)
const refreshing = ref(false)
const osdVisible = ref(false)
+const nodeSelectOpen = ref(false)
let unlistenFns: UnlistenFn[] = []
+/** 菜单关闭/失焦前重置状态:关闭节点下拉框并取消所有焦点,避免下次打开时残留 */
+function resetMenuState() {
+ nodeSelectOpen.value = false
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur()
+ }
+}
+
function readOsdVisible(): boolean {
try {
const raw = localStorage.getItem(STORAGE_KEYS.monitorOsdConfig)
@@ -260,6 +269,8 @@ const sortedNodes = computed(() => {
})
async function handleAction(action: string, payload?: Record) {
+ // 点击菜单项即关闭下拉并取消焦点,避免下次打开残留
+ resetMenuState()
try { await invoke('tray_menu_hide') } catch { /* 忽略 */ }
if (action === 'proxy_refresh') refreshing.value = true
@@ -305,6 +316,7 @@ function delayClass(delay: number | null): string {
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault()
+ resetMenuState()
invoke('tray_menu_hide').catch(() => {})
}
}
@@ -410,9 +422,16 @@ onMounted(async () => {
await applyTheme()
Object.assign(state, event.payload)
osdVisible.value = readOsdVisible()
+ // 显示前重置上次残留的下拉/焦点状态(双保险)
+ resetMenuState()
await measureAndShow()
}))
+ // 菜单失焦(点击其他位置自动隐藏)时重置下拉框与焦点,避免下次打开时残留
+ unlistenFns.push(await getCurrentWindow().onFocusChanged(({ payload: focused }) => {
+ if (!focused) resetMenuState()
+ }))
+
// 仅更新状态数据,不重新显示窗口。
// 动作完成后的状态更新不应让已隐藏的菜单重新弹出(measureAndShow 会触发 win.show)。
// 菜单显示统一由右键托盘触发的 tray-menu-show 事件负责。
@@ -465,6 +484,7 @@ onUnmounted(() => {