优化,贴图

This commit is contained in:
zhongluofeng
2026-08-07 18:09:13 +08:00
parent e09b0567d6
commit d09599fa95
40 changed files with 1627 additions and 493 deletions
+60 -29
View File
@@ -16,6 +16,7 @@ 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'
@@ -308,7 +309,7 @@ onUnmounted(() => {
<TabsContent value="history" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
<div class="flex items-center gap-2 mb-3 shrink-0">
<div class="relative flex-1 max-w-sm">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input v-model="searchQuery" placeholder="搜索历史..." class="pl-8" />
</div>
<div class="flex items-center gap-1">
@@ -327,7 +328,7 @@ onUnmounted(() => {
{{ store.historyTotal }}
</Badge>
<Button variant="outline" size="sm" @click="clearOpen = true" :disabled="!historyList.length">
<Eraser class="h-4 w-4 mr-1" />清空
<Eraser class="size-4 mr-1" />清空
</Button>
</div>
@@ -372,7 +373,7 @@ onUnmounted(() => {
v-if="!historyList.length"
class="flex flex-col items-center justify-center text-muted-foreground py-12"
>
<ClipboardList class="h-12 w-12 mb-3 opacity-40" />
<ClipboardList class="size-12 mb-3 opacity-40" />
<p class="text-sm">暂无历史记录复制内容后将自动收录</p>
</div>
<Card
@@ -381,9 +382,9 @@ onUnmounted(() => {
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="h-4 w-4 text-muted-foreground shrink-0" />
<component :is="kindIcon(item.kind)" class="size-4 text-muted-foreground shrink-0" />
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
<p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
<span>{{ formatTime(item.createdAt) }}</span>
@@ -391,15 +392,30 @@ onUnmounted(() => {
</div>
</div>
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)">
<Copy class="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-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="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
<Trash2 class="h-3.5 w-3.5" />
</Button>
<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>
</div>
</CardContent>
</Card>
@@ -414,15 +430,15 @@ onUnmounted(() => {
v-if="!pinnedList.length"
class="flex flex-col items-center justify-center h-full text-muted-foreground"
>
<Pin class="h-12 w-12 mb-3 opacity-40" />
<Pin class="size-12 mb-3 opacity-40" />
<p class="text-sm">暂无固定条目</p>
<p class="text-xs mt-1">鼠标悬停历史条目点击图钉按钮即可固定</p>
</div>
<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="h-4 w-4 text-primary shrink-0" />
<component :is="kindIcon(item.kind)" class="size-4 text-primary shrink-0" />
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
<p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
<span>{{ formatTime(item.createdAt) }}</span>
@@ -430,15 +446,30 @@ onUnmounted(() => {
</div>
</div>
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)">
<Copy class="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7" title="取消固定" @click.stop="handlePin(item)">
<PinOff class="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
<Trash2 class="h-3.5 w-3.5" />
</Button>
<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>
</div>
</CardContent>
</Card>
@@ -450,7 +481,7 @@ onUnmounted(() => {
<div class="max-w-xl space-y-4">
<div class="flex items-center">
<h3 class="text-base font-medium flex items-center gap-2">
<SettingsIcon class="h-4 w-4" />基本设置
<SettingsIcon class="size-4" />基本设置
</h3>
</div>
@@ -519,7 +550,7 @@ onUnmounted(() => {
<div class="flex items-center justify-between">
<div>
<Label class="flex items-center gap-2">
<Keyboard class="h-4 w-4" />快捷弹窗快捷键
<Keyboard class="size-4" />快捷弹窗快捷键
</Label>
<p class="text-xs text-muted-foreground mt-1">全局快捷键触发鼠标位置历史弹窗留空禁用</p>
</div>
@@ -563,7 +594,7 @@ onUnmounted(() => {
</DialogHeader>
<div class="flex-1 min-h-0 overflow-auto">
<div v-if="detailLoading" class="flex items-center justify-center py-12">
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
<Loader2 class="size-6 animate-spin text-muted-foreground" />
</div>
<template v-else-if="detail">
<img
+24 -11
View File
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input'
import {
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
} from '@/components/ui/pagination'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
// ===== 与 Rust 端对应的数据结构(bindings 提供,camelCase =====
// kind 为 bindings 生成的 string,前端按字符串比较即可
@@ -363,10 +364,11 @@ 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">
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
ref="searchInputRef"
v-model="searchQuery"
@@ -389,10 +391,10 @@ 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="h-6 w-6 animate-spin" />
<Loader2 class="size-6 animate-spin" />
</div>
<div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
<ClipboardList class="h-10 w-10 mb-2 opacity-40" />
<ClipboardList class="size-10 mb-2 opacity-40" />
<p class="text-sm">
{{ searchQuery ? '无匹配结果' : '暂无历史记录' }}
</p>
@@ -406,21 +408,31 @@ onUnmounted(() => {
@mouseenter="onItemHover(idx, item)"
@mouseleave="onItemLeave"
>
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<component :is="kindIcon(item.kind)" class="size-4 text-muted-foreground shrink-0 mt-0.5" />
<div class="flex-1 min-w-0">
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
<p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
<span class="popup-item-kind px-1.5 py-0 text-[10px] border rounded-sm" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span>
<span>{{ formatTime(item.createdAt) }}</span>
</div>
</div>
<div class="popup-item-actions shrink-0">
<button class="popup-action-btn h-7 w-7" title="固定" @click="togglePin(item, $event)">
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
</button>
<button class="popup-action-btn h-7 w-7 hover:text-destructive" title="删除" @click="deleteItem(item, $event)">
<Trash2 class="h-3.5 w-3.5" />
</button>
<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>
</div>
</div>
</div>
@@ -460,6 +472,7 @@ onUnmounted(() => {
<span><kbd>Enter</kbd> 粘贴</span>
<span><kbd>Esc</kbd> 关闭</span>
</div>
</TooltipProvider>
</div>
</template>
+87 -102
View File
@@ -616,15 +616,15 @@ const toggleSortOrder = () => {
<div ref="tabsListRef">
<TabsList class="grid w-full grid-cols-3 max-w-md !bg-transparent !p-0 !shadow-none">
<TabsTrigger value="tasks" class="gap-1.5">
<ListChecks class="h-4 w-4" />
<ListChecks class="size-4" />
下载任务
</TabsTrigger>
<TabsTrigger value="settings" class="gap-1.5">
<SettingsIcon class="h-4 w-4" />
<SettingsIcon class="size-4" />
设置
</TabsTrigger>
<TabsTrigger value="extension" class="gap-1.5">
<Puzzle class="h-4 w-4" />
<Puzzle class="size-4" />
浏览器扩展
</TabsTrigger>
</TabsList>
@@ -650,19 +650,19 @@ const toggleSortOrder = () => {
<Separator orientation="vertical" class="h-5" />
<div v-if="running" class="flex items-center gap-2 text-xs">
<Badge variant="secondary" class="gap-1">
<Download class="h-3 w-3" />
<Download class="size-3" />
下载中 {{ statusCounts.active }}
</Badge>
<Badge variant="secondary" class="gap-1">
<Clock class="h-3 w-3" />
<Clock class="size-3" />
等待 {{ statusCounts.queued }}
</Badge>
<Badge v-if="statusCounts.paused > 0" variant="secondary" class="gap-1">
<Pause class="h-3 w-3" />
<Pause class="size-3" />
已暂停 {{ statusCounts.paused }}
</Badge>
<Badge variant="secondary" class="gap-1">
<Check class="h-3 w-3" />
<Check class="size-3" />
已完成 {{ statusCounts.complete }}
</Badge>
</div>
@@ -673,7 +673,7 @@ const toggleSortOrder = () => {
variant="ghost"
@click="store.refreshTasks()"
>
<RefreshCw class="h-4 w-4" />
<RefreshCw class="size-4" />
</Button>
</div>
</div>
@@ -683,20 +683,15 @@ const toggleSortOrder = () => {
<!-- 工具栏筛选 / 排序 / 搜索 -->
<div class="shrink-0 flex items-center gap-2 flex-wrap">
<!-- 新建下载按钮 -->
<Tooltip>
<TooltipTrigger as-child>
<Button
size="sm"
class="h-8 gap-1.5"
:disabled="addingTask"
@click="addDialogOpen = true"
>
<Plus class="h-4 w-4" />
新建下载
</Button>
</TooltipTrigger>
<TooltipContent>添加下载链接</TooltipContent>
</Tooltip>
<Button
size="sm"
class="h-8 gap-1.5"
:disabled="addingTask"
@click="addDialogOpen = true"
>
<Plus class="size-4" />
新建下载
</Button>
<!-- 状态筛选 -->
<Select v-model="statusFilter">
@@ -736,11 +731,11 @@ const toggleSortOrder = () => {
<Button
size="sm"
variant="outline"
class="h-8 w-8 p-0 bg-transparent"
class="size-8 p-0 bg-transparent"
@click="toggleSortOrder"
>
<ArrowUp v-if="sortOrder === 'asc'" class="h-4 w-4" />
<ArrowDown v-else class="h-4 w-4" />
<ArrowUp v-if="sortOrder === 'asc'" class="size-4" />
<ArrowDown v-else class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ sortOrder === 'asc' ? '升序(点击切换降序)' : '降序(点击切换升序)' }}</TooltipContent>
@@ -748,7 +743,7 @@ const toggleSortOrder = () => {
<!-- 搜索框 -->
<div class="relative flex-1 min-w-[180px] max-w-xs">
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Search class="absolute left-2 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
v-model="searchText"
placeholder="搜索文件名..."
@@ -756,20 +751,15 @@ const toggleSortOrder = () => {
/>
</div>
<Tooltip>
<TooltipTrigger as-child>
<Button
size="sm"
variant="outline"
class="h-8 gap-1.5 bg-transparent"
@click="downloadSettingsOpen = true"
>
<SettingsIcon class="h-4 w-4" />
下载设置
</Button>
</TooltipTrigger>
<TooltipContent>下载设置</TooltipContent>
</Tooltip>
<Button
size="sm"
variant="outline"
class="h-8 gap-1.5 bg-transparent"
@click="downloadSettingsOpen = true"
>
<SettingsIcon class="size-4" />
下载设置
</Button>
<span class="text-xs text-muted-foreground ml-auto">
{{ sortedTasks.length }} 个任务
@@ -779,10 +769,10 @@ const toggleSortOrder = () => {
<!-- 任务列表 -->
<ScrollArea class="flex-1 min-h-0">
<div v-if="pagedTasks.length === 0" key="empty" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10">
<CheckCircle2 class="h-16 w-16 opacity-30" />
<CheckCircle2 class="size-16 opacity-30" />
<p>暂无符合条件的任务</p>
<Button size="sm" class="mt-2" @click="addDialogOpen = true">
<Plus class="h-4 w-4" />
<Plus class="size-4" />
新建下载
</Button>
</div>
@@ -792,19 +782,14 @@ const toggleSortOrder = () => {
<div class="flex items-start justify-between gap-3 mb-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<Tooltip>
<TooltipTrigger as-child>
<span class="font-medium truncate cursor-default">
{{ getFileName(task) }}
</span>
</TooltipTrigger>
<TooltipContent>{{ getFileName(task) }}</TooltipContent>
</Tooltip>
<span class="font-medium truncate cursor-default">
{{ getFileName(task) }}
</span>
<Badge
:variant="getTaskStatusBadge(task).variant"
class="shrink-0"
>
<component :is="getTaskStatusBadge(task).icon" class="h-3 w-3" />
<component :is="getTaskStatusBadge(task).icon" class="size-3" />
{{ getTaskStatusBadge(task).text }}
</Badge>
</div>
@@ -813,7 +798,7 @@ const toggleSortOrder = () => {
{{ formatSize(task.completedSize) }} / {{ formatSize(task.totalSize) }}
</span>
<span v-if="task.status === 'active'" class="flex items-center gap-1 text-green-600 dark:text-green-400">
<Download class="h-3 w-3" />
<Download class="size-3" />
{{ formatSpeed(task.speed) }}
</span>
<Badge v-if="task.status === 'active' && task.segments.length > 1" variant="outline" class="gap-1 py-0 px-1.5 text-[10px]">
@@ -826,7 +811,7 @@ const toggleSortOrder = () => {
{{ truncatePath(task.dir) }}
</span>
</TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ task.dir }}</TooltipContent>
<TooltipContent class="max-w-[480px] break-words">{{ task.dir }}</TooltipContent>
</Tooltip>
<span v-if="task.error" class="text-destructive">
{{ task.error }}
@@ -839,10 +824,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8"
class="size-8"
@click="handlePause(task.id)"
>
<Pause class="h-4 w-4" />
<Pause class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ task.status === 'queued' ? '取消排队' : '暂停' }}</TooltipContent>
@@ -852,10 +837,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8"
class="size-8"
@click="handleResume(task.id)"
>
<Play class="h-4 w-4" />
<Play class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>继续</TooltipContent>
@@ -865,10 +850,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8"
class="size-8"
@click="handleShowDetail(task)"
>
<Info class="h-4 w-4" />
<Info class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>详细信息</TooltipContent>
@@ -878,10 +863,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8"
class="size-8"
@click="handleOpenDir(task)"
>
<FolderOpen class="h-4 w-4" />
<FolderOpen class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>打开目录</TooltipContent>
@@ -891,10 +876,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8 text-destructive hover:text-destructive"
class="size-8 text-destructive hover:text-destructive"
@click="handleRemove(task)"
>
<Trash2 class="h-4 w-4" />
<Trash2 class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>删除</TooltipContent>
@@ -919,12 +904,12 @@ const toggleSortOrder = () => {
<Button
size="sm"
variant="outline"
class="h-8 w-8 p-0"
class="size-8 p-0"
:disabled="currentPage <= 1"
@click="currentPage = 1"
>
<ChevronLeft class="h-4 w-4" />
<ChevronLeft class="h-4 w-4 -ml-2" />
<ChevronLeft class="size-4" />
<ChevronLeft class="size-4 -ml-2" />
</Button>
</TooltipTrigger>
<TooltipContent>第一页</TooltipContent>
@@ -934,11 +919,11 @@ const toggleSortOrder = () => {
<Button
size="sm"
variant="outline"
class="h-8 w-8 p-0"
class="size-8 p-0"
:disabled="currentPage <= 1"
@click="currentPage--"
>
<ChevronLeft class="h-4 w-4" />
<ChevronLeft class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>上一页</TooltipContent>
@@ -951,11 +936,11 @@ const toggleSortOrder = () => {
<Button
size="sm"
variant="outline"
class="h-8 w-8 p-0"
class="size-8 p-0"
:disabled="currentPage >= totalPages"
@click="currentPage++"
>
<ChevronRight class="h-4 w-4" />
<ChevronRight class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>下一页</TooltipContent>
@@ -965,12 +950,12 @@ const toggleSortOrder = () => {
<Button
size="sm"
variant="outline"
class="h-8 w-8 p-0"
class="size-8 p-0"
:disabled="currentPage >= totalPages"
@click="currentPage = totalPages"
>
<ChevronRight class="h-4 w-4" />
<ChevronRight class="h-4 w-4 -ml-2" />
<ChevronRight class="size-4" />
<ChevronRight class="size-4 -ml-2" />
</Button>
</TooltipTrigger>
<TooltipContent>最后一页</TooltipContent>
@@ -988,7 +973,7 @@ const toggleSortOrder = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<FolderOpen class="h-4 w-4 text-primary" />
<FolderOpen class="size-4 text-primary" />
下载设置
</CardTitle>
</CardHeader>
@@ -1004,7 +989,7 @@ const toggleSortOrder = () => {
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="handleSelectSettingsDir">
<FolderOpen class="h-4 w-4" />
<FolderOpen class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>选择目录</TooltipContent>
@@ -1079,7 +1064,7 @@ const toggleSortOrder = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Zap class="h-4 w-4 text-primary" />
<Zap class="size-4 text-primary" />
扩展 API
</CardTitle>
</CardHeader>
@@ -1121,7 +1106,7 @@ const toggleSortOrder = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<Puzzle class="h-4 w-4 text-primary" />
<Puzzle class="size-4 text-primary" />
Thing Extension
</CardTitle>
</CardHeader>
@@ -1142,7 +1127,7 @@ const toggleSortOrder = () => {
{{ store.extensionInfo.url }}
</span>
</TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ store.extensionInfo.url }}</TooltipContent>
<TooltipContent class="max-w-[480px] break-words">{{ store.extensionInfo.url }}</TooltipContent>
</Tooltip>
</div>
<Tooltip>
@@ -1150,10 +1135,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8 shrink-0"
class="size-8 shrink-0"
@click="handleCopy(store.extensionInfo.url, 'API 地址')"
>
<Copy class="h-4 w-4" />
<Copy class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>复制 API 地址</TooltipContent>
@@ -1173,11 +1158,11 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8"
class="size-8"
@click="showSecret = !showSecret"
>
<Eye v-if="!showSecret" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
<Eye v-if="!showSecret" class="size-4" />
<EyeOff v-else class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ showSecret ? '隐藏' : '显示' }}</TooltipContent>
@@ -1187,10 +1172,10 @@ const toggleSortOrder = () => {
<Button
size="icon"
variant="ghost"
class="h-8 w-8"
class="size-8"
@click="handleCopy(store.extensionInfo.secret, '密钥')"
>
<Copy class="h-4 w-4" />
<Copy class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>复制密钥</TooltipContent>
@@ -1205,7 +1190,7 @@ const toggleSortOrder = () => {
<Label class="text-xs text-muted-foreground">安装扩展</Label>
<div class="flex flex-wrap items-center gap-2">
<Button variant="outline" @click="handleInstallExtensionOnline">
<ExternalLink class="h-4 w-4" />
<ExternalLink class="size-4" />
在线安装
</Button>
</div>
@@ -1235,7 +1220,7 @@ const toggleSortOrder = () => {
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<SettingsIcon class="h-4 w-4 text-primary" />
<SettingsIcon class="size-4 text-primary" />
下载设置
</DialogTitle>
<DialogDescription class="text-xs">
@@ -1256,7 +1241,7 @@ const toggleSortOrder = () => {
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="handleSelectSettingsDir">
<FolderOpen class="h-4 w-4" />
<FolderOpen class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>选择目录</TooltipContent>
@@ -1328,7 +1313,7 @@ const toggleSortOrder = () => {
<Button variant="outline">取消</Button>
</DialogClose>
<Button @click="handleDialogSave">
<Check class="h-4 w-4" />
<Check class="size-4" />
保存
</Button>
</DialogFooter>
@@ -1340,7 +1325,7 @@ const toggleSortOrder = () => {
<DialogContent class="max-w-lg">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Link2 class="h-4 w-4 text-primary" />
<Link2 class="size-4 text-primary" />
新建下载
</DialogTitle>
<DialogDescription class="text-xs">
@@ -1371,7 +1356,7 @@ const toggleSortOrder = () => {
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="handleSelectDir">
<FolderOpen class="h-4 w-4" />
<FolderOpen class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>选择目录</TooltipContent>
@@ -1389,8 +1374,8 @@ const toggleSortOrder = () => {
<Button variant="outline">取消</Button>
</DialogClose>
<Button :disabled="addingTask || !addUriText.trim()" @click="handleAddDownload">
<Loader2 v-if="addingTask" class="h-4 w-4 animate-spin" />
<Plus v-else class="h-4 w-4" />
<Loader2 v-if="addingTask" class="size-4 animate-spin" />
<Plus v-else class="size-4" />
添加下载
</Button>
</DialogFooter>
@@ -1402,7 +1387,7 @@ const toggleSortOrder = () => {
<AlertDialogContent class="max-w-md">
<AlertDialogHeader>
<AlertDialogTitle class="flex items-center gap-2">
<AlertCircle class="h-4 w-4 text-amber-500" />
<AlertCircle class="size-4 text-amber-500" />
下载重复提醒
</AlertDialogTitle>
<AlertDialogDescription class="text-xs">
@@ -1423,7 +1408,7 @@ const toggleSortOrder = () => {
<AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel>
<Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button>
<AlertDialogAction @click="onDuplicateConfirm">
<Download class="h-4 w-4" />
<Download class="size-4" />
仍然下载
</AlertDialogAction>
</AlertDialogFooter>
@@ -1469,7 +1454,7 @@ const toggleSortOrder = () => {
<DialogContent class="max-w-xl max-h-[80vh]">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Info class="h-4 w-4 text-primary" />
<Info class="size-4 text-primary" />
任务详情
</DialogTitle>
<DialogDescription class="text-xs">
@@ -1487,7 +1472,7 @@ const toggleSortOrder = () => {
</div>
<Tooltip>
<TooltipTrigger as-child>
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')">
<Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')">
<Copy class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
@@ -1501,7 +1486,7 @@ const toggleSortOrder = () => {
<div class="flex items-center justify-between">
<span class="text-xs text-muted-foreground">状态</span>
<Badge :variant="getTaskStatusBadge(detailTask).variant" class="gap-1">
<component :is="getTaskStatusBadge(detailTask).icon" class="h-3 w-3" />
<component :is="getTaskStatusBadge(detailTask).icon" class="size-3" />
{{ getTaskStatusBadge(detailTask).text }}
</Badge>
</div>
@@ -1514,7 +1499,7 @@ const toggleSortOrder = () => {
</div>
<Tooltip>
<TooltipTrigger as-child>
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')">
<Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')">
<Copy class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
@@ -1532,13 +1517,13 @@ const toggleSortOrder = () => {
<TooltipTrigger as-child>
<span class="font-mono text-xs break-all cursor-default">{{ detailTask.dir }}</span>
</TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ detailTask.dir }}</TooltipContent>
<TooltipContent class="max-w-[480px] break-words">{{ detailTask.dir }}</TooltipContent>
</Tooltip>
<span class="font-mono text-xs text-muted-foreground break-all">{{ detailTask.filename }}</span>
</div>
<Tooltip>
<TooltipTrigger as-child>
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')">
<Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')">
<Copy class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
@@ -1643,7 +1628,7 @@ const toggleSortOrder = () => {
<Button variant="outline">关闭</Button>
</DialogClose>
<Button v-if="detailTask?.dir" variant="outline" @click="handleOpenDir(detailTask)">
<FolderOpen class="h-4 w-4" />
<FolderOpen class="size-4" />
打开目录
</Button>
</DialogFooter>
+62 -16
View File
@@ -1145,7 +1145,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
<CardTitle class="flex items-center justify-between text-sm">
<span class="flex items-center gap-1.5"><Cpu class="size-4 text-primary" />CPU</span>
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="cpuModel ?? ''">{{ cpuModel ?? '--' }}</span>
<Tooltip>
<TooltipTrigger as-child>
<span class="text-xs text-muted-foreground font-normal truncate ml-2">{{ cpuModel ?? '--' }}</span>
</TooltipTrigger>
<TooltipContent>{{ cpuModel ?? '' }}</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
@@ -1178,7 +1183,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
<CardTitle class="flex items-center justify-between text-sm">
<span class="flex items-center gap-1.5"><Gauge class="size-4 text-primary" />GPU</span>
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="gpuModel ?? ''">{{ gpuModel ?? '--' }}</span>
<Tooltip>
<TooltipTrigger as-child>
<span class="text-xs text-muted-foreground font-normal truncate ml-2">{{ gpuModel ?? '--' }}</span>
</TooltipTrigger>
<TooltipContent>{{ gpuModel ?? '' }}</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
@@ -1211,9 +1221,14 @@ watch(() => store.status?.ready, (ready, prev) => {
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
<CardTitle class="flex items-center justify-between text-sm">
<span class="flex items-center gap-1.5"><MemoryStick class="size-4 text-primary" />内存</span>
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="memModuleModels.join(', ')">
{{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }}
</span>
<Tooltip>
<TooltipTrigger as-child>
<span class="text-xs text-muted-foreground font-normal truncate ml-2">
{{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }}
</span>
</TooltipTrigger>
<TooltipContent>{{ memModuleModels.join(', ') }}</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
@@ -1275,7 +1290,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<div v-if="storageDrives.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
<div v-for="drive in storageDrives" :key="drive.name" class="border rounded-md p-2 space-y-1">
<div class="flex items-center justify-between gap-2">
<span class="text-xs font-medium truncate" :title="drive.name">{{ drive.name }}</span>
<Tooltip>
<TooltipTrigger as-child>
<span class="text-xs font-medium truncate">{{ drive.name }}</span>
</TooltipTrigger>
<TooltipContent>{{ drive.name }}</TooltipContent>
</Tooltip>
<span :class="['text-xs font-mono tabular-nums shrink-0', tempColor(drive.temp)]">{{ fmt(drive.temp, 0) }}°C</span>
</div>
<div>
@@ -1330,14 +1350,24 @@ watch(() => store.status?.ready, (ready, prev) => {
<Play class="size-3" />启动
</Button>
<!-- 提权按钮标志未启用时显示设置标志 + 以管理员权限重启 Thing -->
<Button v-if="!store.elevateOnLaunch" size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" title="以管理员权限重启 Thing(弹 UAC,ThingHK 子进程继承权限,后续启动自动提权,崩溃自动重启)" @click="handleElevateSelf">
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
<ShieldCheck v-else class="size-3" />提权
</Button>
<Tooltip v-if="!store.elevateOnLaunch">
<TooltipTrigger as-child>
<Button size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" @click="handleElevateSelf">
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
<ShieldCheck v-else class="size-3" />提权
</Button>
</TooltipTrigger>
<TooltipContent class="max-w-[480px] break-words">以管理员权限重启 Thing UACThingHK 子进程继承权限后续启动自动提权崩溃自动重启</TooltipContent>
</Tooltip>
<!-- 取消提权按钮标志已启用时显示清除标志下次启动不触发 UAC -->
<Button v-else size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" title="取消提权,下次启动将以普通权限运行(不影响当前会话)" @click="handleCancelElevation">
<ShieldOff class="size-3" />取消提权
</Button>
<Tooltip v-else>
<TooltipTrigger as-child>
<Button size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" @click="handleCancelElevation">
<ShieldOff class="size-3" />取消提权
</Button>
</TooltipTrigger>
<TooltipContent class="max-w-[480px] break-words">取消提权下次启动将以普通权限运行不影响当前会话</TooltipContent>
</Tooltip>
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleRefresh">
<RefreshCw class="size-3" />刷新
</Button>
@@ -1420,7 +1450,12 @@ watch(() => store.status?.ready, (ready, prev) => {
<div class="text-[11px] uppercase tracking-wide text-muted-foreground/70 mb-1">{{ typeLabel(typeGroup.type) }}</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-0.5 text-xs">
<div v-for="s in typeGroup.items" :key="s.id" class="flex justify-between items-center py-0.5">
<span class="text-muted-foreground truncate pr-2" :title="s.name">{{ s.name }}</span>
<Tooltip>
<TooltipTrigger as-child>
<span class="text-muted-foreground truncate pr-2">{{ s.name }}</span>
</TooltipTrigger>
<TooltipContent>{{ s.name }}</TooltipContent>
</Tooltip>
<span class="font-mono tabular-nums shrink-0" :class="{ 'text-muted-foreground/50': s.value == null }">
{{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }}
<span class="text-muted-foreground ml-0.5">{{ s.unit }}</span>
@@ -2074,7 +2109,18 @@ watch(() => store.status?.ready, (ready, prev) => {
</Button>
</div>
<div class="text-xs text-muted-foreground">
Kernel ProcessManager 统一管理应用启动时自动拉起崩溃自动重启
Kernel ProcessManager 统一管理崩溃自动重启
</div>
<!-- 应用启动时自动启动 toggle参照代理模块 -->
<div class="flex items-center justify-between rounded-md border p-3">
<div>
<p class="text-sm">应用启动时自动启动监控内核</p>
<p class="text-xs text-muted-foreground">软件启动时自动运行 ThingHK 内核</p>
</div>
<Switch
:model-value="store.autoStart"
@update:model-value="(v: boolean) => store.setAutoStart(v)"
/>
</div>
</CardContent>
</Card>
@@ -2108,7 +2154,7 @@ watch(() => store.status?.ready, (ready, prev) => {
>{{ pathDisplay || pathShort }}</span>
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
</TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ store.kernelInfo.path ?? '' }}</TooltipContent>
<TooltipContent class="max-w-[480px] break-words">{{ store.kernelInfo.path ?? '' }}</TooltipContent>
</Tooltip>
<template v-if="store.kernelInfo.exists">
<Tooltip>
+5 -2
View File
@@ -30,9 +30,12 @@ export const moduleConfig: ModuleConfig = {
},
lifecycle: {
onEnable: async () => {
// 启用模块时拉起 Kernel 并开始 SSE 订阅
// 若用户在监控设置中开启了"自动启动",则随模块启用而运行 Kernel
try {
await invoke('monitor_start')
const autoStart = await invoke<boolean>('monitor_get_auto_start')
if (autoStart) {
await invoke('monitor_start')
}
} catch {
/* 忽略:可能 Kernel 未安装 */
}
+14 -6
View File
@@ -345,14 +345,26 @@ onMounted(() => {
// 窗口/标签页不可见时暂停状态轮询,恢复可见后下个 tick 自动继续
if (document.hidden) return
await store.refreshStatus()
// 同步系统代理真实状态(注册表可能被外部改动,3s 周期足够感知)
await store.refreshSystemProxy()
}, 3000)
// 页面重新可见时立即刷新一次系统代理状态(切回标签页/从托盘返回主窗口)
document.addEventListener('visibilitychange', onVisibilityChange)
})
onUnmounted(() => {
if (statusTimer) clearInterval(statusTimer)
document.removeEventListener('visibilitychange', onVisibilityChange)
stopAutoSwitch()
})
/** 页面可见性变化时刷新系统代理状态(低成本感知外部修改) */
async function onVisibilityChange() {
if (!document.hidden) {
await store.refreshSystemProxy()
}
}
watch(running, async (val, old) => {
if (val && !old) {
await store.waitForApi()
@@ -978,13 +990,9 @@ tabsStore.registerSave(saveSettingsForm)
<div class="flex items-center gap-1.5 min-w-0">
<Tooltip>
<TooltipTrigger as-child>
<span
v-if="store.kernel?.exists"
class="font-mono text-xs text-right truncate cursor-default"
>{{ pathDisplay || pathShort }}</span>
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
<span class="font-mono text-xs text-right truncate cursor-default">{{ pathDisplay || pathShort }}</span>
</TooltipTrigger>
<TooltipContent class="max-w-[400px] break-all">{{ store.kernel?.path ?? '' }}</TooltipContent>
<TooltipContent class="max-w-[480px] break-words">{{ store.kernel?.path ?? '' }}</TooltipContent>
</Tooltip>
<template v-if="store.kernel?.exists">
<Tooltip>
+4
View File
@@ -51,6 +51,10 @@ export const moduleConfig: ModuleConfig = {
const s = await commands.proxyGetSettings()
if (s.autoStart) {
await commands.proxyStart()
// 若同时开启了"启动时自动开启系统代理",则一并开启系统代理
if (s.autoSystemProxy) {
await commands.proxySetSystemProxy()
}
}
} catch {
/* 忽略:可能内核未安装 */
+17 -9
View File
@@ -6,6 +6,7 @@ import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'
import { commands } from '@/lib/bindings'
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History, FolderOpen, Ruler, Trash2 } from '@lucide/vue'
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
@@ -429,9 +430,10 @@ onUnmounted(() => {
<template>
<div class="qp-root flex flex-col h-screen w-screen" @keydown="onKeydown">
<TooltipProvider>
<!-- 搜索输入 -->
<div class="qp-input-wrap">
<Search class="h-4 w-4 text-muted-foreground shrink-0" />
<Search class="size-4 text-muted-foreground shrink-0" />
<input
ref="inputRef"
v-model="query"
@@ -446,16 +448,16 @@ onUnmounted(() => {
<!-- 结果区 -->
<div class="qp-results">
<div v-if="loading && !hasResults()" class="qp-empty">
<Loader2 class="h-6 w-6 animate-spin mb-2" />
<Loader2 class="size-6 animate-spin mb-2" />
<p class="text-sm">搜索中</p>
</div>
<div v-else-if="!hasResults() && query.trim()" class="qp-empty">
<Search class="h-10 w-10 mb-3 opacity-40" />
<Search class="size-10 mb-3 opacity-40" />
<p class="text-sm">无匹配结果</p>
<p class="text-xs mt-1 opacity-60"> Enter 在搜索引擎中查找</p>
</div>
<div v-else-if="!hasResults()" class="qp-empty">
<Command class="h-10 w-10 mb-3 opacity-40" />
<Command class="size-10 mb-3 opacity-40" />
<p class="text-sm">输入关键词开始搜索</p>
<p class="text-xs mt-1 opacity-60">命令 · 计算 · 系统 · 网页</p>
</div>
@@ -477,7 +479,7 @@ onUnmounted(() => {
<component
v-else
:is="groupIcon(item.group)"
class="h-4 w-4 text-muted-foreground shrink-0"
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'">
@@ -522,7 +524,7 @@ onUnmounted(() => {
<component
v-else
:is="groupIcon(item.group)"
class="h-4 w-4 text-muted-foreground shrink-0"
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'">
@@ -552,7 +554,7 @@ onUnmounted(() => {
<component
v-else
:is="groupIcon(item.group)"
class="h-4 w-4 text-muted-foreground shrink-0"
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'">
@@ -605,10 +607,15 @@ onUnmounted(() => {
<div v-if="pendingDelete" class="qp-confirm-mask" @click.self="cancelDelete">
<div class="qp-confirm-box" role="alertdialog" aria-modal="true">
<div class="qp-confirm-head">
<Trash2 class="h-4 w-4 shrink-0 text-destructive" />
<Trash2 class="size-4 shrink-0 text-destructive" />
<p class="qp-confirm-title">确定删除</p>
</div>
<p class="qp-confirm-name truncate" :title="pendingDelete.name">{{ pendingDelete.name }}</p>
<Tooltip>
<TooltipTrigger as-child>
<p class="qp-confirm-name truncate">{{ pendingDelete.name }}</p>
</TooltipTrigger>
<TooltipContent>{{ pendingDelete.name }}</TooltipContent>
</Tooltip>
<p class="qp-confirm-tip">将移动到回收站{{ pendingDelete.isDir ? '(含所有子内容)' : '' }}删除后可在回收站中还原</p>
<div class="flex items-center justify-end gap-2 mt-4">
<button class="qp-confirm-btn" @click="cancelDelete">取消</button>
@@ -616,6 +623,7 @@ onUnmounted(() => {
</div>
</div>
</div>
</TooltipProvider>
</div>
</template>
+13 -13
View File
@@ -276,11 +276,11 @@ async function changeEngine(v: string) {
<!-- 标题与测试 -->
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold flex items-center gap-2">
<Command class="h-5 w-5 text-primary" />
<Command class="size-5 text-primary" />
快速面板
</h2>
<Button size="sm" @click="testPopup">
<Zap class="h-4 w-4 mr-1" />
<Zap class="size-4 mr-1" />
测试唤起
</Button>
</div>
@@ -289,7 +289,7 @@ async function changeEngine(v: string) {
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<Keyboard class="h-4 w-4" />
<Keyboard class="size-4" />
唤起快捷键
</CardTitle>
</CardHeader>
@@ -350,7 +350,7 @@ async function changeEngine(v: string) {
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<Globe class="h-4 w-4" />
<Globe class="size-4" />
默认搜索引擎
</CardTitle>
</CardHeader>
@@ -376,7 +376,7 @@ async function changeEngine(v: string) {
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<FolderTree class="h-4 w-4" />
<FolderTree class="size-4" />
文件索引
</CardTitle>
</CardHeader>
@@ -406,14 +406,14 @@ async function changeEngine(v: string) {
class="flex items-center gap-2 p-2 rounded-md bg-muted/40"
>
<FolderTree class="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span class="text-xs font-mono truncate flex-1" :title="dir">{{ dir }}</span>
<span class="text-xs font-mono truncate flex-1">{{ dir }}</span>
<Button
variant="ghost"
size="icon"
class="h-6 w-6 shrink-0 hover:text-destructive"
class="size-6 shrink-0 hover:text-destructive"
@click="removeDir(idx)"
>
<X class="h-3 w-3" />
<X class="size-3" />
</Button>
</div>
<div v-if="!form.indexDirs.length" class="text-xs text-muted-foreground py-2">
@@ -435,7 +435,7 @@ async function changeEngine(v: string) {
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<Terminal class="h-4 w-4" />
<Terminal class="size-4" />
自定义命令
</CardTitle>
</CardHeader>
@@ -452,11 +452,11 @@ async function changeEngine(v: string) {
<p class="text-xs font-medium truncate">{{ cmd.title }}</p>
<p class="text-xs text-muted-foreground font-mono truncate">{{ cmd.command }} {{ cmd.args.join(' ') }}</p>
</div>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="editCustomCommand(idx)">
<Pencil class="h-3 w-3" />
<Button variant="ghost" size="icon" class="size-6 shrink-0" @click="editCustomCommand(idx)">
<Pencil class="size-3" />
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0 hover:text-destructive" @click="removeCustomCommand(idx)">
<X class="h-3 w-3" />
<Button variant="ghost" size="icon" class="size-6 shrink-0 hover:text-destructive" @click="removeCustomCommand(idx)">
<X class="size-3" />
</Button>
</div>
<div v-if="!form.customCommands.length" class="text-xs text-muted-foreground py-2">
+102 -75
View File
@@ -13,6 +13,7 @@ import {
Eraser, Undo2, Redo2, Trash2, Copy, Save, X, Image as ImageIcon,
} from '@lucide/vue'
import { Button } from '@/components/ui/button'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
// ===== 标注数据结构 =====
type ToolType = 'rect' | 'arrow' | 'pen' | 'text' | 'mosaic' | 'highlight'
@@ -451,41 +452,48 @@ onUnmounted(() => {
<template>
<div class="h-screen w-screen flex flex-col bg-zinc-950 text-zinc-100 overflow-hidden">
<TooltipProvider>
<!-- 标题栏 -->
<div
class="h-9 flex items-center justify-between px-3 bg-zinc-900 border-b border-zinc-800 shrink-0 select-none"
data-tauri-drag-region
>
<span class="text-sm font-medium">截图编辑器</span>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:text-white hover:bg-zinc-800"
title="关闭"
@click="closeWindow"
@mousedown.stop
>
<X class="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:text-white hover:bg-zinc-800"
@click="closeWindow"
@mousedown.stop
>
<X class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>关闭</TooltipContent>
</Tooltip>
</div>
<!-- 工具栏 -->
<div class="flex items-center gap-3 px-3 py-2 bg-zinc-900 border-b border-zinc-800 shrink-0 flex-wrap">
<!-- 工具组 -->
<div class="flex items-center gap-1">
<Button
v-for="tool in TOOLS"
:key="tool.value"
variant="ghost"
size="icon-sm"
:class="currentTool === tool.value
? 'bg-zinc-700 text-white hover:bg-zinc-700 hover:text-white'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'"
:title="tool.label"
@click="currentTool = tool.value"
>
<component :is="tool.icon" class="h-4 w-4" />
</Button>
<Tooltip v-for="tool in TOOLS" :key="tool.value">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
:class="currentTool === tool.value
? 'bg-zinc-700 text-white hover:bg-zinc-700 hover:text-white'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'"
@click="currentTool = tool.value"
>
<component :is="tool.icon" class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ tool.label }}</TooltipContent>
</Tooltip>
</div>
<div class="h-6 w-px bg-zinc-700" />
@@ -493,15 +501,17 @@ onUnmounted(() => {
<!-- 颜色 -->
<div class="flex items-center gap-1.5">
<span class="text-xs text-zinc-500">颜色</span>
<button
v-for="c in COLORS"
:key="c"
class="size-6 rounded-full border border-zinc-500 transition-transform hover:scale-110"
:class="currentColor === c ? 'ring-2 ring-blue-400 ring-offset-1 ring-offset-zinc-900 scale-110' : ''"
:style="{ backgroundColor: c }"
:title="c"
@click="currentColor = c"
/>
<Tooltip v-for="c in COLORS" :key="c">
<TooltipTrigger as-child>
<button
class="size-6 rounded-full border border-zinc-500 transition-transform hover:scale-110"
:class="currentColor === c ? 'ring-2 ring-blue-400 ring-offset-1 ring-offset-zinc-900 scale-110' : ''"
:style="{ backgroundColor: c }"
@click="currentColor = c"
/>
</TooltipTrigger>
<TooltipContent>{{ c }}</TooltipContent>
</Tooltip>
</div>
<div class="h-6 w-px bg-zinc-700" />
@@ -558,46 +568,62 @@ onUnmounted(() => {
<!-- 历史 / 清空 -->
<div class="flex items-center gap-1">
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canUndo"
title="橡皮(撤销最近标注)"
@click="undo"
>
<Eraser class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canUndo"
title="撤销"
@click="undo"
>
<Undo2 class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canRedo"
title="重做"
@click="redo"
>
<Redo2 class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-red-900 hover:text-white"
:disabled="!canUndo"
title="清空"
@click="clearAll"
>
<Trash2 class="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canUndo"
@click="undo"
>
<Eraser class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>橡皮撤销最近标注</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canUndo"
@click="undo"
>
<Undo2 class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
:disabled="!canRedo"
@click="redo"
>
<Redo2 class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>重做</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon-sm"
class="text-zinc-400 hover:bg-red-900 hover:text-white"
:disabled="!canUndo"
@click="clearAll"
>
<Trash2 class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>清空</TooltipContent>
</Tooltip>
</div>
</div>
@@ -608,7 +634,7 @@ onUnmounted(() => {
v-if="loadError"
class="flex flex-col items-center justify-center h-full gap-3 text-zinc-400"
>
<ImageIcon class="h-12 w-12 opacity-40" />
<ImageIcon class="size-12 opacity-40" />
<p>未找到待编辑的截图</p>
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="closeWindow">
关闭
@@ -655,13 +681,14 @@ onUnmounted(() => {
取消
</Button>
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="saveToFile">
<Save class="h-4 w-4" />
<Save class="size-4" />
保存到文件
</Button>
<Button variant="ghost" class="bg-blue-600 text-white hover:bg-blue-500" @click="copyToClipboard">
<Copy class="h-4 w-4" />
<Copy class="size-4" />
复制到剪贴板
</Button>
</div>
</TooltipProvider>
</div>
</template>
+173 -57
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onUnmounted, watch, nextTick } from 'vue'
import {
Keyboard, Settings, FolderOpen, Camera, Copy, Save, Trash2, Timer,
Keyboard, Settings, FolderOpen, Camera, Copy, Save, Trash2, Timer, StickyNote,
Image as ImageIcon, Loader2,
} from '@lucide/vue'
import { toast } from 'vue-sonner'
@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
const store = useScreenshotStore()
@@ -23,7 +24,7 @@ const tabsListRef = useModuleTabs(activeTab, [
{ value: 'history', label: '历史' },
])
const HISTORY_LIMITS = [6, 12, 24, 48]
const HISTORY_LIMITS = [24, 48, 96]
const DELAY_OPTIONS = [0, 1, 2, 3, 5]
function formatTime(t: number): string {
@@ -69,14 +70,45 @@ async function handleSave(item: RecentCapture) {
}
}
async function handlePin(item: RecentCapture) {
const idx = store.recent.findIndex((r) => r.id === item.id)
await store.togglePin(idx >= 0 ? idx : 0)
}
function handleDelete(item: RecentCapture) {
void store.removeRecent(item)
toast.success('已从历史移除')
}
// ===== 快捷键录入器 =====
const recording = ref(false)
const recorderRef = ref<HTMLDivElement | null>(null)
// ===== 历史保留数量:预设 + 自定义输入 =====
const customLimit = ref('')
function onLimitPreset(n: number) {
store.setSettings({ historyLimit: n })
}
function onCustomLimit() {
const v = parseInt(customLimit.value, 10)
if (!Number.isFinite(v) || v < 1) {
toast.warning('请输入大于 0 的数字')
return
}
store.setSettings({ historyLimit: Math.min(999, Math.max(1, Math.floor(v))) })
}
// 当前值不在预设中时,把自定义输入框预填为当前值
watch(
() => store.settings.historyLimit,
(v) => {
if (!HISTORY_LIMITS.includes(v)) customLimit.value = String(v)
},
{ immediate: true },
)
// ===== 快捷键录入器(截图 / 贴图共用) =====
const recordingField = ref<'capture' | 'pin' | null>(null)
const captureRecorderRef = ref<HTMLDivElement | null>(null)
const pinRecorderRef = ref<HTMLDivElement | null>(null)
/** 把存储的快捷键字符串格式化为展示形式:ctrl+alt+a → Ctrl + Alt + A */
function displayShortcut(s: string): string {
@@ -119,11 +151,11 @@ function eventToShortcut(e: KeyboardEvent): string | null {
}
function onRecordKey(e: KeyboardEvent) {
if (!recording.value) return
if (!recordingField.value) return
e.preventDefault()
e.stopPropagation()
if (e.key === 'Escape') {
recording.value = false
recordingField.value = null
return
}
// 仅修饰键按下时不结束(等待主键)
@@ -133,24 +165,27 @@ function onRecordKey(e: KeyboardEvent) {
toast.warning('不支持的按键组合,请使用字母/数字/功能键 + 修饰键')
return
}
recording.value = false
void commitShortcut(combo)
const field = recordingField.value
recordingField.value = null
void commitShortcut(field, combo)
}
async function commitShortcut(combo: string) {
const ok = await store.setShortcut(combo)
async function commitShortcut(field: 'capture' | 'pin', combo: string) {
const ok = field === 'capture'
? await store.setShortcut(combo)
: await store.setPinShortcut(combo)
if (ok) toast.success(`快捷键已更新为 ${displayShortcut(combo)}`)
else toast.error('快捷键注册失败,可能被其他程序占用')
}
async function startRecord() {
recording.value = true
async function startRecord(field: 'capture' | 'pin') {
recordingField.value = field
await nextTick()
recorderRef.value?.focus()
;(field === 'capture' ? captureRecorderRef : pinRecorderRef).value?.focus()
}
watch(recording, (on) => {
if (on) window.addEventListener('keydown', onRecordKey, true)
watch(recordingField, (field) => {
if (field) window.addEventListener('keydown', onRecordKey, true)
else window.removeEventListener('keydown', onRecordKey, true)
})
@@ -159,6 +194,11 @@ async function clearShortcut() {
toast.success('已禁用截图快捷键')
}
async function clearPinShortcut() {
await store.setPinShortcut('')
toast.success('已禁用贴图快捷键')
}
// 导出监听(screenshot-exported)由应用级注册(main.ts/App.vue),随应用生命周期管理;
// 模块卸载不得销毁该单例监听,否则离开截图模块后全局快捷键截图将不记录历史/不自动保存。
@@ -188,7 +228,7 @@ onUnmounted(() => {
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<Keyboard class="h-4 w-4 text-primary" />
<Keyboard class="size-4 text-primary" />
截图快捷键
</CardTitle>
</CardHeader>
@@ -198,13 +238,13 @@ onUnmounted(() => {
<div class="flex items-center gap-2 flex-wrap">
<!-- 快捷键录入器 -->
<div
ref="recorderRef"
ref="captureRecorderRef"
class="hotkey-recorder"
:class="{ recording }"
:class="{ recording: recordingField === 'capture' }"
tabindex="0"
@click="startRecord"
@click="startRecord('capture')"
>
<template v-if="recording">按下快捷键Esc 取消</template>
<template v-if="recordingField === 'capture'">按下快捷键Esc 取消</template>
<template v-else-if="store.settings.shortcut">
{{ displayShortcut(store.settings.shortcut) }}
</template>
@@ -219,7 +259,7 @@ onUnmounted(() => {
>清除</Button>
</div>
<p class="text-sm text-muted-foreground">
点击方框录入自定义快捷键留空则禁用默认 Ctrl + Alt + A
点击方框录入自定义快捷键留空则禁用默认 Ctrl + 1
</p>
</div>
<Button
@@ -227,8 +267,8 @@ onUnmounted(() => {
:disabled="store.capturing"
@click="handleCapture"
>
<Loader2 v-if="store.capturing" class="h-4 w-4 animate-spin" />
<Camera v-else class="h-4 w-4" />
<Loader2 v-if="store.capturing" class="size-4 animate-spin" />
<Camera v-else class="size-4" />
立即截图
</Button>
</div>
@@ -246,11 +286,52 @@ onUnmounted(() => {
</CardContent>
</Card>
<!-- 贴图快捷键 -->
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<StickyNote class="size-4 text-primary" />
贴图快捷键
</CardTitle>
</CardHeader>
<CardContent>
<div class="flex items-center justify-between gap-4 py-1">
<div class="space-y-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<div
ref="pinRecorderRef"
class="hotkey-recorder"
:class="{ recording: recordingField === 'pin' }"
tabindex="0"
@click="startRecord('pin')"
>
<template v-if="recordingField === 'pin'">按下快捷键Esc 取消</template>
<template v-else-if="store.settings.pinShortcut">
{{ displayShortcut(store.settings.pinShortcut) }}
</template>
<template v-else>未设置点击录入</template>
</div>
<Button
v-if="store.settings.pinShortcut"
variant="ghost"
size="sm"
class="h-8 text-muted-foreground"
@click="clearPinShortcut"
>清除</Button>
</div>
<p class="text-sm text-muted-foreground">
把最近截图以原分辨率贴到屏幕显示在原框选位置再次按下可关闭贴图默认 Alt + 1
</p>
</div>
</div>
</CardContent>
</Card>
<!-- 截图选项 -->
<Card>
<CardHeader>
<CardTitle class="text-sm flex items-center gap-2">
<Settings class="h-4 w-4 text-primary" />
<Settings class="size-4 text-primary" />
截图选项
</CardTitle>
</CardHeader>
@@ -259,7 +340,7 @@ onUnmounted(() => {
<div class="flex items-center justify-between gap-3 py-2">
<div class="space-y-1">
<Label class="flex items-center gap-2 text-base font-medium">
<Timer class="h-4 w-4" />
<Timer class="size-4" />
延时截图
</Label>
<p class="text-sm text-muted-foreground">按下快捷键后倒计时再截图便于切到目标画面</p>
@@ -302,7 +383,7 @@ onUnmounted(() => {
</p>
</div>
<Button variant="outline" size="sm" class="shrink-0" @click="chooseSaveDir">
<FolderOpen class="h-4 w-4" />
<FolderOpen class="size-4" />
选择目录
</Button>
</div>
@@ -311,9 +392,9 @@ onUnmounted(() => {
<div class="flex items-center justify-between gap-3 py-2 border-t">
<div class="space-y-1">
<Label class="text-base font-medium">历史保留数量</Label>
<p class="text-sm text-muted-foreground">超过上限的旧截图将自动移除</p>
<p class="text-sm text-muted-foreground">截图完成后自动持久化保存到应用目录,超过上限的旧截图将自动移除</p>
</div>
<div class="flex gap-1 shrink-0">
<div class="flex items-center gap-1.5 shrink-0">
<Button
v-for="n in HISTORY_LIMITS"
:key="n"
@@ -323,8 +404,19 @@ onUnmounted(() => {
:class="store.settings.historyLimit === n
? 'bg-primary/10 text-primary hover:bg-primary/10'
: 'text-muted-foreground'"
@click="store.setSettings({ historyLimit: n })"
@click="onLimitPreset(n)"
>{{ n }}</Button>
<input
v-model="customLimit"
type="number"
min="1"
max="999"
class="h-8 w-16 rounded-md border border-input bg-transparent px-2 text-sm text-center outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
placeholder="自定义"
title="输入自定义保留数量"
@keydown.enter="onCustomLimit"
@blur="onCustomLimit"
>
</div>
</div>
</CardContent>
@@ -342,9 +434,9 @@ onUnmounted(() => {
v-if="store.recent.length === 0"
class="flex flex-col items-center justify-center h-64 text-muted-foreground gap-3"
>
<ImageIcon class="h-12 w-12 opacity-40" />
<ImageIcon class="size-12 opacity-40" />
<p>暂无截图历史</p>
<p class="text-xs">按 Ctrl + Alt + A 截图并导出后会显示在这里</p>
<p class="text-xs">按 Ctrl + 1 截图并导出后会显示在这里</p>
</div>
<!-- 历史网格 -->
@@ -373,31 +465,55 @@ onUnmounted(() => {
<div
class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Button
variant="secondary"
size="icon-sm"
title="复制到剪贴板"
@click.stop="handleCopy(item)"
>
<Copy class="h-4 w-4" />
</Button>
<Button
variant="secondary"
size="icon-sm"
title="保存到文件"
@click.stop="handleSave(item)"
>
<Save class="h-4 w-4" />
</Button>
<Button
variant="secondary"
size="icon-sm"
class="text-destructive hover:text-destructive"
title="从历史移除"
@click.stop="handleDelete(item)"
>
<Trash2 class="h-4 w-4" />
</Button>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="secondary"
size="icon-sm"
@click.stop="handlePin(item)"
>
<StickyNote class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>贴到屏幕</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="secondary"
size="icon-sm"
@click.stop="handleCopy(item)"
>
<Copy class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>复制到剪贴板</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="secondary"
size="icon-sm"
@click.stop="handleSave(item)"
>
<Save class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>保存到文件</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="secondary"
size="icon-sm"
class="text-destructive hover:text-destructive"
@click.stop="handleDelete(item)"
>
<Trash2 class="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>从历史移除</TooltipContent>
</Tooltip>
</div>
</div>
</div>
+129 -69
View File
@@ -11,6 +11,7 @@ import {
} from '@lucide/vue'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Slider } from '@/components/ui/slider'
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import {
TOOLS, COLORS, BLOCK_SIZES, ALPHAS, HANDLES, HANDLE_HIT, DRAG_THRESHOLD,
type Phase, type ToolType, type Annotation, type DrawableAnnotation,
@@ -1779,7 +1780,16 @@ async function finish() {
}
}
if (!out) return
await emit(EVENTS.screenshotExported, { pngBase64: out.b64, width: out.w, height: out.h })
// 附带框选区域的屏幕逻辑坐标(DIP):窗口逻辑位置 + 选区图像坐标。
// 截图图像按虚拟屏 DIP 捕获,坐标按 DIP 传递,贴图窗口按 DIP 布局即可 1:1 还原
const spPos = selPhys.value
await emit(EVENTS.screenshotExported, {
pngBase64: out.b64,
width: out.w,
height: out.h,
posX: Math.round(winOuterX / dpr + spPos.x),
posY: Math.round(winOuterY / dpr + spPos.y),
})
} catch (e) {
console.error('[screenshot] 完成失败', e)
} finally {
@@ -1801,7 +1811,14 @@ async function doSave() {
})
if (!path) return
await commands.screenshotSavePng(out.b64, path)
await emit(EVENTS.screenshotExported, { pngBase64: out.b64, width: out.w, height: out.h })
const spPos = selPhys.value
await emit(EVENTS.screenshotExported, {
pngBase64: out.b64,
width: out.w,
height: out.h,
posX: Math.round(winOuterX / dpr + spPos.x),
posY: Math.round(winOuterY / dpr + spPos.y),
})
await win.hide().catch(() => {})
} catch (e) {
console.error('[screenshot] 保存失败', e)
@@ -1985,6 +2002,7 @@ onUnmounted(() => {
@mousemove="onMouseMove"
@mouseup="onMouseUp"
>
<TooltipProvider>
<!-- 冻结的屏幕底图 -->
<img
v-if="imgSrc"
@@ -2094,31 +2112,37 @@ onUnmounted(() => {
<div v-if="showToolbar" ref="toolbarRef" class="toolbar" :style="toolbarPos" @mousedown.stop>
<!-- 标注工具 -->
<div class="tb-group">
<button
v-for="t in TOOLS"
:key="t.value"
class="tb-icon"
:class="{ active: phase === 'editing' && currentTool === t.value }"
:title="t.label"
@click="onSelectTool(t.value)"
>
<component :is="t.icon" class="tb-svg" />
</button>
<Tooltip v-for="t in TOOLS" :key="t.value">
<TooltipTrigger as-child>
<button
class="tb-icon"
:class="{ active: phase === 'editing' && currentTool === t.value }"
@click="onSelectTool(t.value)"
>
<component :is="t.icon" class="tb-svg" />
</button>
</TooltipTrigger>
<TooltipContent>{{ t.label }}</TooltipContent>
</Tooltip>
</div>
<div class="tb-sep" />
<!-- 颜色色卡显示当前颜色点击打开选色板 -->
<Popover v-model:open="colorPickerOpen">
<PopoverTrigger as-child>
<button
class="color-badge"
:class="{ disabled: !colorEnabled }"
:style="{ '--swatch-color': effectiveColor }"
:disabled="!colorEnabled"
title="颜色"
/>
</PopoverTrigger>
<Tooltip>
<TooltipTrigger as-child>
<PopoverTrigger as-child>
<button
class="color-badge"
:class="{ disabled: !colorEnabled }"
:style="{ '--swatch-color': effectiveColor }"
:disabled="!colorEnabled"
/>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>颜色</TooltipContent>
</Tooltip>
<PopoverContent class="w-auto p-3" align="start">
<div class="color-popover">
<input
@@ -2128,15 +2152,17 @@ onUnmounted(() => {
@input="onCustomColorPick(($event.target as HTMLInputElement).value)"
/>
<div class="color-swatches">
<button
v-for="c in COLORS"
:key="c"
class="color-swatch-mini"
:class="{ active: effectiveColor === c }"
:style="{ backgroundColor: c }"
:title="c"
@click="effectiveColor = c"
/>
<Tooltip v-for="c in COLORS" :key="c">
<TooltipTrigger as-child>
<button
class="color-swatch-mini"
:class="{ active: effectiveColor === c }"
:style="{ backgroundColor: c }"
@click="effectiveColor = c"
/>
</TooltipTrigger>
<TooltipContent>{{ c }}</TooltipContent>
</Tooltip>
</div>
</div>
</PopoverContent>
@@ -2144,14 +2170,18 @@ onUnmounted(() => {
<!-- 粗细 badge点击打开 Slider -->
<Popover v-model:open="lineWidthPickerOpen">
<PopoverTrigger as-child>
<button
class="width-badge"
:class="{ disabled: !lineWidthEnabled }"
:disabled="!lineWidthEnabled"
title="粗细"
>{{ effectiveLineWidth }}</button>
</PopoverTrigger>
<Tooltip>
<TooltipTrigger as-child>
<PopoverTrigger as-child>
<button
class="width-badge"
:class="{ disabled: !lineWidthEnabled }"
:disabled="!lineWidthEnabled"
>{{ effectiveLineWidth }}</button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>粗细</TooltipContent>
</Tooltip>
<PopoverContent class="w-auto p-3" align="start">
<div class="width-popover">
<div class="width-popover-header">
@@ -2174,43 +2204,62 @@ onUnmounted(() => {
<template v-if="currentTool === 'mosaic'">
<div class="tb-sep" />
<div class="tb-group">
<button
v-for="b in BLOCK_SIZES"
:key="b"
class="tb-num"
:class="{ active: blockSize === b }"
title="马赛克块大小"
@click="blockSize = b"
>{{ b }}</button>
<Tooltip v-for="b in BLOCK_SIZES" :key="b">
<TooltipTrigger as-child>
<button
class="tb-num"
:class="{ active: blockSize === b }"
@click="blockSize = b"
>{{ b }}</button>
</TooltipTrigger>
<TooltipContent>马赛克块大小</TooltipContent>
</Tooltip>
</div>
</template>
<template v-if="currentTool === 'highlight'">
<div class="tb-sep" />
<div class="tb-group">
<button
v-for="a in ALPHAS"
:key="a"
class="tb-num"
:class="{ active: highlightAlpha === a }"
title="高亮透明度"
@click="highlightAlpha = a"
>{{ Math.round(a * 100) }}%</button>
<Tooltip v-for="a in ALPHAS" :key="a">
<TooltipTrigger as-child>
<button
class="tb-num"
:class="{ active: highlightAlpha === a }"
@click="highlightAlpha = a"
>{{ Math.round(a * 100) }}%</button>
</TooltipTrigger>
<TooltipContent>高亮透明度</TooltipContent>
</Tooltip>
</div>
</template>
<div class="tb-sep" />
<div class="tb-group">
<button class="tb-icon" :disabled="!canUndo" title="撤销" @click="undo">
<Undo2 class="tb-svg" />
</button>
<button class="tb-icon" :disabled="!canRedo" title="重做" @click="redo">
<Redo2 class="tb-svg" />
</button>
<button class="tb-icon" :disabled="!canUndo" title="清空标注" @click="clearAnnotations">
<Eraser class="tb-svg" />
</button>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" :disabled="!canUndo" @click="undo">
<Undo2 class="tb-svg" />
</button>
</TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" :disabled="!canRedo" @click="redo">
<Redo2 class="tb-svg" />
</button>
</TooltipTrigger>
<TooltipContent>重做</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" :disabled="!canUndo" @click="clearAnnotations">
<Eraser class="tb-svg" />
</button>
</TooltipTrigger>
<TooltipContent>清空标注</TooltipContent>
</Tooltip>
</div>
<div class="tb-sep" />
@@ -2221,12 +2270,22 @@ onUnmounted(() => {
<!-- 复制 / 保存(置末) -->
<div class="tb-group">
<button class="tb-icon" title="保存到文件" @click="doSave">
<Save class="tb-svg" />
</button>
<button class="tb-icon primary" title="复制到剪贴板 (Enter)" @click="finish">
<Copy class="tb-svg" />
</button>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon" @click="doSave">
<Save class="tb-svg" />
</button>
</TooltipTrigger>
<TooltipContent>保存到文件</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<button class="tb-icon primary" @click="finish">
<Copy class="tb-svg" />
</button>
</TooltipTrigger>
<TooltipContent>复制到剪贴板 (Enter)</TooltipContent>
</Tooltip>
</div>
</div>
@@ -2245,6 +2304,7 @@ onUnmounted(() => {
<div class="mag-hint">{{ hint.split(' · ').join('\n') }}</div>
</div>
</div>
</TooltipProvider>
</div>
</template>
+347
View File
@@ -0,0 +1,347 @@
<script setup lang="ts">
// 贴图窗口:无边框、透明、置顶。
// 图片以原分辨率显示,窗口定位到截图时的框选位置(原框选位置)。
// 右下角展开图标,hover 展开 上一张 / 下一张 / 关闭。
import { ref, onMounted, onUnmounted } from 'vue'
import { getCurrentWindow, currentMonitor } from '@tauri-apps/api/window'
import { LogicalPosition, LogicalSize } from '@tauri-apps/api/dpi'
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
import { ChevronLeft, ChevronRight, X, Maximize2, Loader2 } from '@lucide/vue'
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts
import { commands } from '@/lib/bindings'
/** 图片四周留白(CSS 像素 = 逻辑像素 DIP,与截图图像分辨率单位一致),用于呈现蓝色光晕 */
const GLOW_PAD = 18
const win = getCurrentWindow()
const imgSrc = ref('')
const imgCssW = ref(0)
const imgCssH = ref(0)
const loading = ref(false)
const errorMsg = ref('')
/** 历史列表(localStorage 与主窗口共享,最新在前) */
const items = ref<{ filePath: string; width: number; height: number; posX?: number; posY?: number }[]>([])
const cur = ref(0)
let showUnlisten: UnlistenFn | null = null
/** 同步主界面主题(独立窗口无 pinia,从 localStorage 读取应用设置) */
function applyTheme() {
const root = document.documentElement
let theme = 'system'
try {
const raw = localStorage.getItem(STORAGE_KEYS.appSettings)
if (raw) {
const s = JSON.parse(raw)
theme = s.theme ?? 'system'
}
} catch { /* 忽略 */ }
let isDark: boolean
if (theme === 'system') {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
} else {
isDark = theme === 'dark'
}
root.classList.toggle('dark', isDark)
}
/** 从 localStorage 读取主窗口持久化的截图历史(图片文件在应用数据目录) */
function readHistory() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.screenshotHistory)
if (!raw) return []
const parsed = JSON.parse(raw) as {
version?: number
items?: { filePath: string; width: number; height: number; posX?: number; posY?: number }[]
}
if (!Array.isArray(parsed.items)) return []
return parsed.items
} catch {
return []
}
}
/**
* 窗口排版到目标位置(全部使用逻辑坐标/DIP):
* 截图图像的分辨率是逻辑像素(BMP 按虚拟屏 DIP 捕获),CSS 像素与 DIP 一致,
* 故图片以 item.width/height 的 CSS 尺寸显示即 1:1 还原;窗口尺寸 = 图片 + 2×留白,
* 窗口位置 = 原框选位置 - 留白,使图片精确覆盖在截图时的区域上(跨 DPI 显示器也无需换算)。
*/
async function layoutTo(item: { width: number; height: number; posX?: number; posY?: number }) {
const pad = GLOW_PAD
let x = item.posX !== undefined ? item.posX - pad : 0
let y = item.posY !== undefined ? item.posY - pad : 0
// 无定位信息(如编辑器导出)时放到当前鼠标位置附近(鼠标坐标为物理像素,需换算为 DIP)
if (item.posX === undefined || item.posY === undefined) {
try {
const [mx, my] = await commands.screenshotCursorPos()
const mon = await currentMonitor()
const s = mon?.scaleFactor || 1
x = mx / s - pad
y = my / s - pad
} catch { /* 忽略,沿用 0,0 */ }
}
// 粗略限制在鼠标所在显示器内(避免贴图完全出现在屏幕外,至少保留 40px 可见)
try {
const mon = await currentMonitor()
if (mon) {
const s = mon.scaleFactor || 1
const mlx = mon.position.x / s
const mly = mon.position.y / s
const mlw = mon.size.width / s
const mlh = mon.size.height / s
x = Math.min(Math.max(x, mlx), mlx + mlw - 40)
y = Math.min(Math.max(y, mly), mly + mlh - 40)
}
} catch { /* 忽略 */ }
// Windows 上非 resizable 窗口 setSize 会失效,须先临时开启再恢复:
// 布局结束后恢复为不可缩放,禁用边缘拖动修改大小(仅保留左键自由拖动)
await win.setPosition(new LogicalPosition(Math.round(x), Math.round(y)))
await win.setResizable(true)
await win.setSize(new LogicalSize(item.width + pad * 2, item.height + pad * 2))
await win.setResizable(false)
}
/** 加载并显示第 index 张截图(index 缺省/越界时按最近一张) */
async function loadImage(index: number) {
const list = items.value
if (!list.length) {
errorMsg.value = '暂无历史截图'
loading.value = false
await win.show().catch(() => {})
return
}
const idx = Math.max(0, Math.min(index, list.length - 1))
const item = list[idx]
cur.value = idx
loading.value = true
errorMsg.value = ''
try {
const b64 = await commands.screenshotLoadCache(item.filePath)
const dataUrl = `data:image/png;base64,${b64}`
// 预解码:等待图片就绪后再排版显示,避免闪烁
await new Promise<void>((resolve, reject) => {
const img = new Image()
img.onload = () => resolve()
img.onerror = () => reject(new Error('图片解码失败'))
img.src = dataUrl
})
// CSS 尺寸 = 原分辨率(逻辑像素/DIP),与 CSS 像素一致,1:1 还原截图时覆盖的区域
await layoutTo(item)
imgCssW.value = item.width
imgCssH.value = item.height
imgSrc.value = dataUrl
await win.show().catch(() => {})
loading.value = false
} catch (e) {
console.error('[screenshot] 贴图加载失败', e)
errorMsg.value = '加载图片失败:' + (e as Error).message
loading.value = false
await win.show().catch(() => {})
}
}
function prev() {
if (cur.value >= items.value.length - 1) return
void loadImage(cur.value + 1)
}
function next() {
if (cur.value <= 0) return
void loadImage(cur.value - 1)
}
function closePin() {
void win.hide().catch(() => {})
}
/** 左键按下时启动原生窗口拖动(与 OSD 窗口同款方案:
* 透明 + focus:false 窗口上 data-tauri-drag-region 不生效,需显式 startDragging */
function onImageMouseDown(e: MouseEvent) {
if (e.button !== 0) return
e.preventDefault()
void win.startDragging().catch(() => {})
}
/** 响应主窗口 'screenshot-pin-show':读取要贴的历史索引并加载显示 */
function openPin() {
items.value = readHistory()
let index = 0
try {
index = Number(localStorage.getItem(STORAGE_KEYS.screenshotPinIndex) ?? 0) || 0
} catch { /* 忽略 */ }
void loadImage(index)
}
onMounted(async () => {
applyTheme()
// 先注册 show 监听再通知 store 就绪,避免首轮事件丢失(与覆盖层同模式)
showUnlisten = await listen(EVENTS.screenshotPinShow, () => {
openPin()
})
await emit(EVENTS.screenshotPinReady)
})
onUnmounted(() => {
showUnlisten?.()
})
</script>
<template>
<div class="pin-root">
<!-- 图片区左键按住自由拖动显式 startDragging蓝色光晕阴影 -->
<div
class="pin-image"
:style="{ width: imgCssW + 'px', height: imgCssH + 'px' }"
@mousedown="onImageMouseDown"
>
<img
v-if="imgSrc"
:src="imgSrc"
:style="{ width: imgCssW + 'px', height: imgCssH + 'px' }"
draggable="false"
alt="贴图"
>
<div v-else-if="errorMsg" class="pin-tip">{{ errorMsg }}</div>
<div v-else-if="loading" class="pin-tip"><Loader2 class="size-5 animate-spin" /></div>
</div>
<!-- 右下角控制展开图标hover 展开 上一张 / 下一张 / 关闭 -->
<div class="pin-controls">
<div class="pin-actions">
<button
class="pin-btn"
title="上一张"
:disabled="cur >= items.length - 1"
@click="prev"
>
<ChevronLeft class="size-4" />
</button>
<button
class="pin-btn"
title="下一张"
:disabled="cur <= 0"
@click="next"
>
<ChevronRight class="size-4" />
</button>
<button class="pin-btn pin-btn-danger" title="关闭" @click="closePin">
<X class="size-4" />
</button>
</div>
<button class="pin-expand" title="更多操作">
<Maximize2 class="size-4" />
</button>
</div>
</div>
</template>
<style scoped>
.pin-root {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 18px; /* GLOW_PAD,与脚本常量一致;留白大于光晕扩散范围,避免光晕照出窗口外边框 */
background: transparent;
overflow: hidden;
}
.pin-image {
position: relative;
overflow: hidden;
border-radius: 0; /* 直角 */
cursor: grab;
/* 散发蓝色光晕的阴影:无实线边框,且扩散范围控制在留白内(光晕小、柔和) */
box-shadow:
0 0 6px rgba(59, 130, 246, 0.55),
0 0 14px rgba(59, 130, 246, 0.3);
}
.pin-image:active {
cursor: grabbing;
}
.pin-image img {
display: block;
user-select: none;
-webkit-user-drag: none;
}
.pin-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 100%;
padding: 0 16px;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
text-align: center;
background: rgba(15, 23, 42, 0.85);
}
/* 右下角控制:独立于图片拖动区(兄弟节点),点击不触发拖动 */
.pin-controls {
position: absolute;
right: 26px;
bottom: 26px;
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
}
.pin-expand,
.pin-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 8px;
cursor: pointer;
color: rgba(255, 255, 255, 0.92);
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(4px);
transition: background-color 0.15s ease;
}
.pin-btn:hover {
background: rgba(30, 58, 138, 0.85);
}
.pin-btn-danger:hover {
background: rgba(185, 28, 28, 0.85);
}
.pin-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pin-actions {
display: flex;
align-items: center;
gap: 6px;
opacity: 0;
visibility: hidden;
transform: translateX(8px);
transition:
opacity 0.15s ease,
transform 0.15s ease,
visibility 0.15s;
}
/* hover 展开三个图标按钮 */
.pin-controls:hover .pin-actions,
.pin-controls:focus-within .pin-actions {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
</style>
+11 -9
View File
@@ -29,6 +29,8 @@ onMounted(() => {
searchStore.registerAction('settings', 4, () => appStore.setEffect('mica'))
searchStore.registerAction('settings', 5, () => appStore.setEffect('acrylic'))
searchStore.registerAction('settings', 6, () => appStore.toggleAutoStart())
// 主动刷新所有进程状态,确保内核 badge 显示当前真实状态(而非过期缓存)
processStore.refreshAll().catch(() => { /* 忽略:后端可能未就绪 */ })
})
const themes: Array<{ id: Theme; name: string; color: string; icon: typeof Sun }> = [
@@ -132,7 +134,7 @@ const onDragEnd = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="h-5 w-5 text-primary" />
<Sparkles class="size-5 text-primary" />
常规设置
</CardTitle>
</CardHeader>
@@ -153,7 +155,7 @@ const onDragEnd = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Layers class="h-5 w-5 text-primary" />
<Layers class="size-5 text-primary" />
主题切换
</CardTitle>
</CardHeader>
@@ -175,7 +177,7 @@ const onDragEnd = () => {
>
<component
:is="theme.icon"
class="h-8 w-8 transition-colors duration-300"
class="size-8 transition-colors duration-300"
:class="theme.id === 'dark' || (theme.id === 'system' && systemDark) ? 'text-white' : 'text-gray-800'"
/>
</div>
@@ -198,7 +200,7 @@ const onDragEnd = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="h-5 w-5 text-primary" />
<Sparkles class="size-5 text-primary" />
效果切换
</CardTitle>
</CardHeader>
@@ -244,7 +246,7 @@ const onDragEnd = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Package class="h-5 w-5 text-primary" />
<Package class="size-5 text-primary" />
模块管理
</CardTitle>
</CardHeader>
@@ -271,7 +273,7 @@ const onDragEnd = () => {
<div
class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground/40 hover:text-muted-foreground transition-colors"
>
<GripVertical class="h-4 w-4 no-native-drag" />
<GripVertical class="size-4 no-native-drag" />
</div>
</TooltipTrigger>
<TooltipContent>拖拽排序</TooltipContent>
@@ -280,7 +282,7 @@ const onDragEnd = () => {
class="w-9 h-9 rounded-lg flex items-center justify-center"
:class="module.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'"
>
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" />
<component :is="getModuleIcon(module.icon)" class="size-5" />
</div>
<div>
<div class="font-medium text-sm flex items-center gap-2">
@@ -314,7 +316,7 @@ const onDragEnd = () => {
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<LogOut class="h-5 w-5 text-destructive" />
<LogOut class="size-5 text-destructive" />
退出程序
</CardTitle>
</CardHeader>
@@ -324,7 +326,7 @@ const onDragEnd = () => {
class="text-destructive border-destructive/20 hover:bg-destructive/10"
@click="quitApp"
>
<LogOut class="h-4 w-4 mr-2" />
<LogOut class="size-4 mr-2" />
彻底退出
</Button>
</CardContent>
+11 -11
View File
@@ -433,7 +433,7 @@ onUnmounted(() => {
<div class="tray-menu-scroll flex-1 min-h-0 overflow-y-auto">
<div class="tray-section">
<div class="tray-section-header">
<Globe class="h-4 w-4 text-primary" />
<Globe class="size-4 text-primary" />
<span class="tray-section-title">代理</span>
<span v-if="state.proxyRunning" class="tray-status-dot running"></span>
<span v-else class="tray-status-dot stopped"></span>
@@ -441,12 +441,12 @@ onUnmounted(() => {
<div class="tray-section-body">
<button class="tray-item" :disabled="proxyLoading" @click="handleProxyToggle">
<component :is="proxyButtonIcon" class="h-4 w-4" />
<component :is="proxyButtonIcon" class="size-4" />
<span>{{ proxyButtonLabel }}</span>
</button>
<button v-if="state.proxyRunning" class="tray-item" :disabled="proxyLoading" @click="handleSystemProxyToggle">
<Power class="h-4 w-4" />
<Power class="size-4" />
<span>系统代理</span>
<span class="tray-toggle-indicator" :class="{ active: state.systemProxy }"></span>
</button>
@@ -457,7 +457,7 @@ onUnmounted(() => {
:disabled="proxyLoading"
@click="handleAction('proxy_refresh')"
>
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': refreshing }" />
<RefreshCw class="size-4" :class="{ 'animate-spin': refreshing }" />
<span>刷新节点列表</span>
</button>
@@ -482,7 +482,7 @@ onUnmounted(() => {
:value="node.name"
class="tray-select-option"
>
<Check v-if="node.isCurrent" class="h-3 w-3 text-primary flex-shrink-0" />
<Check v-if="node.isCurrent" class="size-3 text-primary flex-shrink-0" />
<span class="tray-select-badge-option">{{ abbreviateNodeName(node.name) }}</span>
<span class="tray-select-delay" :class="delayClass(node.delay)">
{{ delayText(node.delay) }}
@@ -498,7 +498,7 @@ onUnmounted(() => {
<div class="tray-section">
<div class="tray-section-header">
<Monitor class="h-4 w-4 text-primary" />
<Monitor class="size-4 text-primary" />
<span class="tray-section-title">监控</span>
<span v-if="state.monitorRunning" class="tray-status-dot running"></span>
<span v-else class="tray-status-dot stopped"></span>
@@ -506,7 +506,7 @@ onUnmounted(() => {
<div class="tray-section-body">
<button class="tray-item" @click="handleAction('osd_toggle')">
<Monitor class="h-4 w-4" />
<Monitor class="size-4" />
<span>OSD 显示</span>
<span class="tray-toggle-indicator" :class="{ active: osdVisible }"></span>
</button>
@@ -515,7 +515,7 @@ onUnmounted(() => {
:disabled="!state.monitorRunning"
@click="handleAction('kernel_restart')"
>
<RefreshCw class="h-4 w-4" />
<RefreshCw class="size-4" />
<span>重启 Kernel</span>
</button>
</div>
@@ -524,18 +524,18 @@ onUnmounted(() => {
<div class="tray-separator"></div>
<button class="tray-item" @click="handleAction('download_new')">
<Download class="h-4 w-4" />
<Download class="size-4" />
<span>新建下载</span>
</button>
<div class="tray-separator"></div>
<button class="tray-item" @click="handleAction('settings')">
<Settings class="h-4 w-4" />
<Settings class="size-4" />
<span>常规设置</span>
</button>
<button class="tray-item tray-quit" @click="handleAction('quit')">
<LogOut class="h-4 w-4" />
<LogOut class="size-4" />
<span>退出</span>
</button>
</div>