优化调整
This commit is contained in:
@@ -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<ClipboardItemDetail | null>(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(() => {
|
||||
|
||||
<ScrollArea class="flex-1 min-h-0">
|
||||
<div class="space-y-1.5 pr-2">
|
||||
<div
|
||||
v-if="!historyList.length"
|
||||
class="flex flex-col items-center justify-center text-muted-foreground py-12"
|
||||
>
|
||||
<ClipboardList class="size-12 mb-3 opacity-40" />
|
||||
<p class="text-sm">暂无历史记录,复制内容后将自动收录</p>
|
||||
<!-- 加载骨架:数据异步返回前撑起画面,避免误显示"暂无历史" -->
|
||||
<div v-if="store.loading" class="space-y-1.5">
|
||||
<div
|
||||
v-for="n in 8" :key="n"
|
||||
class="flex items-center gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<Skeleton class="size-4 shrink-0" />
|
||||
<div class="flex-1 space-y-2">
|
||||
<Skeleton class="h-3.5 w-3/4" />
|
||||
<Skeleton class="h-2.5 w-1/4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Empty
|
||||
v-else-if="!historyList.length"
|
||||
class="py-12"
|
||||
>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<ClipboardList class="size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>暂无历史记录</EmptyTitle>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<EmptyDescription>复制内容后将自动收录,图片与文件也会被记录</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Card
|
||||
v-for="item in historyList"
|
||||
:key="item.id"
|
||||
@@ -392,30 +429,15 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-7" @click.stop="handleCopy(item)">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>复制</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-7" @click.stop="handlePin(item)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ item.pinned ? '取消固定' : '固定' }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" @click.stop="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>删除</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button variant="ghost" size="icon" class="size-7" title="复制" @click.stop="handleCopy(item)">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="size-7" :title="item.pinned ? '取消固定' : '固定'" @click.stop="handlePin(item)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -426,14 +448,17 @@ onUnmounted(() => {
|
||||
<!-- 固定 -->
|
||||
<TabsContent value="pinned" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto space-y-1.5 pr-1">
|
||||
<div
|
||||
v-if="!pinnedList.length"
|
||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||
>
|
||||
<Pin class="size-12 mb-3 opacity-40" />
|
||||
<p class="text-sm">暂无固定条目</p>
|
||||
<p class="text-xs mt-1">鼠标悬停历史条目,点击图钉按钮即可固定</p>
|
||||
</div>
|
||||
<Empty v-if="!pinnedList.length" class="h-full py-8">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<Pin class="size-6" />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>暂无固定条目</EmptyTitle>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<EmptyDescription>鼠标悬停历史条目,点击图钉按钮即可固定</EmptyDescription>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Card v-for="item in pinnedList" :key="item.id" class="group hover:shadow-md transition-shadow py-0">
|
||||
<CardContent class="flex items-center gap-3 px-3 py-2">
|
||||
<component :is="kindIcon(item.kind)" class="size-4 text-primary shrink-0" />
|
||||
@@ -446,30 +471,15 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-7" @click.stop="handleCopy(item)">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>复制</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-7" @click.stop="handlePin(item)">
|
||||
<PinOff class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>取消固定</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" @click.stop="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>删除</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button variant="ghost" size="icon" class="size-7" title="复制" @click.stop="handleCopy(item)">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="size-7" title="取消固定" @click.stop="handlePin(item)">
|
||||
<PinOff class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -8,14 +8,14 @@ import { Effect, EffectState } from '@tauri-apps/api/window'
|
||||
import { commands } from '@/lib/bindings'
|
||||
import {
|
||||
ClipboardList, Pin, PinOff, Trash2, Search, Image as ImageIcon,
|
||||
FileText, Files, Loader2,
|
||||
FileText, Files,
|
||||
} from '@lucide/vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
// ===== 与 Rust 端对应的数据结构(bindings 提供,camelCase) =====
|
||||
// kind 为 bindings 生成的 string,前端按字符串比较即可
|
||||
@@ -364,7 +364,6 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="popup-root flex flex-col h-screen w-screen" @keydown="onKeydown">
|
||||
<TooltipProvider>
|
||||
<!-- 搜索栏(与剪切板主页统一样式) -->
|
||||
<div class="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
||||
<div class="relative flex-1 max-w-sm">
|
||||
@@ -390,8 +389,18 @@ onUnmounted(() => {
|
||||
<!-- 列表 -->
|
||||
<ScrollArea class="popup-list flex-1 min-h-0">
|
||||
<div class="space-y-1.5 p-2">
|
||||
<div v-if="loading && !hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<Loader2 class="size-6 animate-spin" />
|
||||
<div v-if="loading && !hasItems" class="space-y-1.5">
|
||||
<div
|
||||
v-for="n in 8"
|
||||
:key="n"
|
||||
class="flex items-start gap-3 rounded-lg border px-3 py-2"
|
||||
>
|
||||
<Skeleton class="size-4 shrink-0 mt-0.5" />
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<Skeleton class="h-3.5 w-3/4" />
|
||||
<Skeleton class="h-2.5 w-1/4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<ClipboardList class="size-10 mb-2 opacity-40" />
|
||||
@@ -417,22 +426,12 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-item-actions shrink-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button class="popup-action-btn size-7" @click="togglePin(item, $event)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>固定</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button class="popup-action-btn size-7 hover:text-destructive" @click="deleteItem(item, $event)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>删除</TooltipContent>
|
||||
</Tooltip>
|
||||
<button class="popup-action-btn size-7" :title="item.pinned ? '取消固定' : '固定'" @click="togglePin(item, $event)">
|
||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button class="popup-action-btn size-7 hover:text-destructive" title="删除" @click="deleteItem(item, $event)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -472,7 +471,6 @@ onUnmounted(() => {
|
||||
<span><kbd>Enter</kbd> 粘贴</span>
|
||||
<span><kbd>Esc</kbd> 关闭</span>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useDownloaderStore, type DownloadTask, type TaskStatus, type CheckUrlRe
|
||||
import { useModuleTabs } from '@/lib/use-module-tabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { pendingNewDownload } from '@/lib/trayEvents'
|
||||
import { pendingNewDownload, pendingShowDownloadTasks } from '@/lib/trayEvents'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -536,6 +536,11 @@ onMounted(async () => {
|
||||
pendingNewDownload.value = false
|
||||
addDialogOpen.value = true
|
||||
}
|
||||
// 消费浏览器扩展新增下载标志位:直接显示任务列表页
|
||||
if (pendingShowDownloadTasks.value) {
|
||||
pendingShowDownloadTasks.value = false
|
||||
activeTab.value = 'tasks'
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -1102,8 +1102,9 @@ onUnmounted(() => {
|
||||
// 不 dispose store:SSE 订阅保持,确保切走监控模块后 OSD 仍有数据
|
||||
// store.subscribe() 内部有守卫(if unlistenFns.length return),重复 init 不会重复订阅
|
||||
// 不关闭 OSD 窗口:切走监控模块时保持 OSD 显示,由模块禁用或应用退出时统一清理
|
||||
// 释放 OSD 事件监听(App 启动或模块重新挂载时会重新注册)
|
||||
store.disposeOsd()
|
||||
// 不调用 store.disposeOsd():tray:toggle-osd 监听与 OSD 配置 watcher 由 App.vue 的
|
||||
// initOsd() 注册,属应用级常驻(与模块生命周期解耦);若在此释放,切走监控模块后
|
||||
// 托盘菜单的 OSD 开关会失效。OSD 事件监听仅在 App 卸载(应用退出)时统一释放。
|
||||
})
|
||||
|
||||
// 当 Kernel 从未就绪变成就绪时,主动拉一次快照填充 UI
|
||||
|
||||
@@ -328,9 +328,9 @@ const init = async () => {
|
||||
await store.waitForApi()
|
||||
store.refreshVersion()
|
||||
loadProxiesWithError()
|
||||
// 若自动切换已开启,恢复定时器
|
||||
// 若自动切换已开启,恢复定时器(静默,不弹通知、不立即执行)
|
||||
if (autoSwitchEnabled.value) {
|
||||
startAutoSwitch()
|
||||
startAutoSwitch(false, false)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -370,10 +370,10 @@ watch(running, async (val, old) => {
|
||||
await store.waitForApi()
|
||||
await store.refreshVersion()
|
||||
await loadProxiesWithError()
|
||||
// 自动切换若已开启,mihomo 启动/重启后恢复定时器
|
||||
// 自动切换若已开启,mihomo 启动/重启后恢复定时器(静默,不弹通知)
|
||||
// (handleStop 会停掉旧定时器,此处统一接管启动路径,避免开关显示开但功能静默失效)
|
||||
if (autoSwitchEnabled.value) {
|
||||
startAutoSwitch()
|
||||
startAutoSwitch(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -495,16 +495,21 @@ const quickSwitchNode = async (name: string) => {
|
||||
}
|
||||
|
||||
// ===== 自动切换节点 =====
|
||||
const startAutoSwitch = () => {
|
||||
const startAutoSwitch = (notify = true, immediate = true) => {
|
||||
stopAutoSwitch()
|
||||
if (!autoSwitchEnabled.value) return
|
||||
const ms = autoSwitchInterval.value * 60 * 1000
|
||||
autoSwitchTimer = setInterval(runAutoSwitch, ms)
|
||||
toast.success('自动切换已开启', {
|
||||
description: `每 ${autoSwitchInterval.value} 分钟测试并切换至最优节点`
|
||||
})
|
||||
// 立即执行一次
|
||||
runAutoSwitch()
|
||||
// 仅用户主动开启时提示;模块挂载/内核重启恢复定时器时静默,避免每次切换都弹通知
|
||||
if (notify) {
|
||||
toast.success('自动切换已开启', {
|
||||
description: `每 ${autoSwitchInterval.value} 分钟测试并切换至最优节点`
|
||||
})
|
||||
}
|
||||
// 立即执行一次(用户主动开启/调整时立即生效;进入模块恢复时跳过,避免每次进入都测速切换)
|
||||
if (immediate) {
|
||||
runAutoSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
const stopAutoSwitch = () => {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { Pin } from '@lucide/vue'
|
||||
|
||||
defineProps<{
|
||||
items: string[]
|
||||
favs?: string[]
|
||||
open?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
pick: [value: string]
|
||||
fav: [value: string]
|
||||
}>()
|
||||
|
||||
function pick(value: string) {
|
||||
emit('pick', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- mousedown.prevent 阻止点击下拉项时输入框失焦,使 focus 触发的历史面板保持展开 -->
|
||||
<div
|
||||
v-if="open && ((favs && favs.length) || items.length)"
|
||||
class="qp-fa-dropdown"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<!-- 钉住/常用(独立保存,显示在历史之上) -->
|
||||
<template v-if="favs && favs.length">
|
||||
<p class="qp-fa-dropdown-label">常用</p>
|
||||
<div
|
||||
v-for="f in favs"
|
||||
:key="'fav-' + f"
|
||||
class="qp-fa-dropdown-item"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 min-w-0 text-left text-xs truncate"
|
||||
:title="f"
|
||||
@click="pick(f)"
|
||||
>
|
||||
{{ f || '(空)' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="qp-fa-pin pinned"
|
||||
title="取消钉住"
|
||||
@click="emit('fav', f)"
|
||||
>
|
||||
<Pin class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 历史(最多 10 条,排除已钉住的) -->
|
||||
<p
|
||||
v-if="items.filter(x => !(favs ?? []).includes(x)).length"
|
||||
class="qp-fa-dropdown-label"
|
||||
>
|
||||
历史
|
||||
</p>
|
||||
<div
|
||||
v-for="it in items.filter(x => !(favs ?? []).includes(x))"
|
||||
:key="'his-' + it"
|
||||
class="qp-fa-dropdown-item"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 min-w-0 text-left text-xs truncate"
|
||||
:title="it"
|
||||
@click="pick(it)"
|
||||
>
|
||||
{{ it || '(空)' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="favs"
|
||||
type="button"
|
||||
class="qp-fa-pin"
|
||||
title="钉住为常用"
|
||||
@click="emit('fav', it)"
|
||||
>
|
||||
<Pin class="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.qp-fa-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
background: var(--popover, var(--card));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.qp-fa-dropdown-label {
|
||||
padding: 3px 8px 1px;
|
||||
font-size: 10px;
|
||||
color: var(--muted-foreground);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.qp-fa-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.qp-fa-dropdown-item:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.qp-fa-pin {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 2px;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
.qp-fa-pin:hover {
|
||||
opacity: 1;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.qp-fa-pin.pinned {
|
||||
color: var(--primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.qp-fa-dropdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.qp-fa-dropdown::-webkit-scrollbar-thumb {
|
||||
background: var(--muted-foreground);
|
||||
opacity: 0.3;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.qp-fa-dropdown::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -528,7 +528,15 @@ async function changeEngine(v: string) {
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary">文件</Badge>
|
||||
<span class="text-muted-foreground">索引指定目录,快速定位文件</span>
|
||||
<span class="text-muted-foreground">索引指定目录,快速定位文件,支持打开/显示/复制路径/删除,.lnk 按应用处理</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary">目录操作</Badge>
|
||||
<span class="text-muted-foreground">文件资源管理器批量处理:批量解压、正则批量重命名(实时预览)、筛选批量删除</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary">历史</Badge>
|
||||
<span class="text-muted-foreground">记录最近交互,空查询优先展示</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary">剪贴板</Badge>
|
||||
@@ -548,7 +556,7 @@ async function changeEngine(v: string) {
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary">系统</Badge>
|
||||
<span class="text-muted-foreground">锁屏、退出应用</span>
|
||||
<span class="text-muted-foreground">系统命令(注册表、CMD/PowerShell、任务管理器、控制面板、关机/重启/休眠)及锁屏、退出应用</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Badge variant="secondary">网页</Badge>
|
||||
|
||||
@@ -38,8 +38,17 @@ const state = reactive<TrayMenuState>({
|
||||
const loadingAction = ref<string | null>(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<string, unknown>) {
|
||||
// 点击菜单项即关闭下拉并取消焦点,避免下次打开残留
|
||||
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(() => {
|
||||
<label class="tray-node-label">节点</label>
|
||||
<Select
|
||||
:model-value="state.proxyCurrent ?? ''"
|
||||
v-model:open="nodeSelectOpen"
|
||||
:disabled="proxyLoading"
|
||||
@update:model-value="handleSelectNode"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user