优化,贴图
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user