优化,贴图

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
+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>