截图模块优化调整
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import {
|
||||
ClipboardList, Copy, Pin, PinOff, Trash2, Search, Image as ImageIcon,
|
||||
FileText, Files, Settings as SettingsIcon, Loader2, Eraser, Check, Keyboard,
|
||||
FileText, Files, Settings as SettingsIcon, Loader2, Eraser, Keyboard,
|
||||
} from '@lucide/vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useClipboardStore, type ClipboardItem, type ClipboardKind, type ClipboardItemDetail } from '@/stores/clipboardStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
const store = useClipboardStore()
|
||||
|
||||
const activeTab = ref('history')
|
||||
const tabsStore = useModuleTabsStore()
|
||||
const tabsListRef = useModuleTabs(activeTab, [
|
||||
{ value: 'history', label: '历史' },
|
||||
{ value: 'pinned', label: '固定' },
|
||||
@@ -114,6 +116,9 @@ const handleSaveSettings = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 注册保存处理函数到标签栏 store(TitleBar 保存按钮调用)
|
||||
tabsStore.registerSave(handleSaveSettings)
|
||||
|
||||
const handleShortcutChange = async () => {
|
||||
// 快捷键变化立即保存并注册(不等点击"保存设置")
|
||||
try {
|
||||
@@ -124,6 +129,86 @@ const handleShortcutChange = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 快捷键录入器 =====
|
||||
const recording = ref(false)
|
||||
const recorderRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
/** 把存储的快捷键字符串格式化为展示形式:alt+v → Alt + V */
|
||||
function displayShortcut(s: string): string {
|
||||
if (!s) return ''
|
||||
return s
|
||||
.split('+')
|
||||
.map(p => {
|
||||
const t = p.trim()
|
||||
if (!t) return ''
|
||||
if (t.length === 1) return t.toUpperCase()
|
||||
return t.charAt(0).toUpperCase() + t.slice(1)
|
||||
})
|
||||
.join(' + ')
|
||||
}
|
||||
|
||||
/** 把键盘事件转为 Tauri 快捷键字符串(小写,+ 分隔) */
|
||||
function eventToShortcut(e: KeyboardEvent): string | null {
|
||||
const mods: string[] = []
|
||||
if (e.ctrlKey) mods.push('ctrl')
|
||||
if (e.altKey) mods.push('alt')
|
||||
if (e.shiftKey) mods.push('shift')
|
||||
if (e.metaKey) mods.push('super')
|
||||
let main = ''
|
||||
const code = e.code || ''
|
||||
if (/^Key[A-Z]$/.test(code)) main = code.slice(3).toLowerCase()
|
||||
else if (/^Digit[0-9]$/.test(code)) main = code.slice(5)
|
||||
else if (/^F([1-9]|1[0-2])$/.test(code)) main = code.toLowerCase()
|
||||
else if (code === 'Space') main = 'space'
|
||||
else if (code === 'PrintScreen') main = 'printscreen'
|
||||
else if (code.startsWith('Numpad')) main = code.slice(6).toLowerCase()
|
||||
else return null
|
||||
// 必须至少一个修饰键(功能键 F1-F12 / PrintScreen 例外)
|
||||
const isFunctionKey = /^f([1-9]|1[0-2])$/.test(main) || main === 'printscreen'
|
||||
if (mods.length === 0 && !isFunctionKey) return null
|
||||
return [...mods, main].join('+')
|
||||
}
|
||||
|
||||
function onRecordKey(e: KeyboardEvent) {
|
||||
if (!recording.value) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Escape') {
|
||||
recording.value = false
|
||||
return
|
||||
}
|
||||
// 仅修饰键按下时不结束(等待主键)
|
||||
if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return
|
||||
const combo = eventToShortcut(e)
|
||||
if (!combo) {
|
||||
toast.warning('不支持的按键组合,请使用字母/数字/功能键 + 修饰键')
|
||||
return
|
||||
}
|
||||
recording.value = false
|
||||
void commitShortcut(combo)
|
||||
}
|
||||
|
||||
async function commitShortcut(combo: string) {
|
||||
form.value.shortcut = combo
|
||||
await handleShortcutChange()
|
||||
}
|
||||
|
||||
async function startRecord() {
|
||||
recording.value = true
|
||||
await nextTick()
|
||||
recorderRef.value?.focus()
|
||||
}
|
||||
|
||||
watch(recording, (on) => {
|
||||
if (on) window.addEventListener('keydown', onRecordKey, true)
|
||||
else window.removeEventListener('keydown', onRecordKey, true)
|
||||
})
|
||||
|
||||
async function clearShortcut() {
|
||||
form.value.shortcut = ''
|
||||
await handleShortcutChange()
|
||||
}
|
||||
|
||||
const handleTestPopup = async () => {
|
||||
try {
|
||||
await store.showPopup()
|
||||
@@ -204,6 +289,7 @@ onMounted(async () => {
|
||||
|
||||
onUnmounted(() => {
|
||||
store.dispose()
|
||||
window.removeEventListener('keydown', onRecordKey, true)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -245,33 +331,39 @@ onUnmounted(() => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 分页(置于列表上方,靠左显示) -->
|
||||
<div v-if="totalPages > 1" class="flex items-center justify-start pb-2 shrink-0">
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:page="currentPage"
|
||||
:total="store.historyTotal"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
@update:page="gotoPage"
|
||||
class="justify-start"
|
||||
>
|
||||
<PaginationContent v-slot="{ items }" class="gap-1">
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<PaginationItem
|
||||
v-if="item.type === 'page'"
|
||||
:value="item.value"
|
||||
:is-active="item.value === page"
|
||||
size="icon"
|
||||
class="size-7 text-xs"
|
||||
>
|
||||
{{ item.value }}
|
||||
</PaginationItem>
|
||||
<PaginationEllipsis v-else class="size-7" />
|
||||
</template>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
<!-- 分页 + 快捷键提示(置于列表上方,分页靠左,快捷弹窗提示靠右) -->
|
||||
<div class="flex items-center justify-between pb-2 shrink-0">
|
||||
<template v-if="totalPages > 1">
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:page="currentPage"
|
||||
:total="store.historyTotal"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:sibling-count="1"
|
||||
show-edges
|
||||
@update:page="gotoPage"
|
||||
class="justify-start"
|
||||
>
|
||||
<PaginationContent v-slot="{ items }" class="gap-1">
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<PaginationItem
|
||||
v-if="item.type === 'page'"
|
||||
:value="item.value"
|
||||
:is-active="item.value === page"
|
||||
size="icon"
|
||||
class="size-7 text-xs"
|
||||
>
|
||||
{{ item.value }}
|
||||
</PaginationItem>
|
||||
<PaginationEllipsis v-else class="size-7" />
|
||||
</template>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</template>
|
||||
<div v-else />
|
||||
<span class="text-xs text-muted-foreground shrink-0">
|
||||
快捷弹窗:<span v-if="form.shortcut" class="font-medium text-foreground">{{ displayShortcut(form.shortcut) }}</span><span v-else>未设置</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ScrollArea class="flex-1 min-h-0">
|
||||
@@ -356,11 +448,10 @@ onUnmounted(() => {
|
||||
<!-- 设置 -->
|
||||
<TabsContent value="settings" class="flex-1 min-h-0 overflow-y-auto mt-4 tab-animate">
|
||||
<div class="max-w-xl space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-base font-medium flex items-center gap-2">
|
||||
<SettingsIcon class="h-4 w-4" />基本设置
|
||||
</h3>
|
||||
<Button @click="handleSaveSettings"><Check class="h-4 w-4 mr-1" />保存设置</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
@@ -434,12 +525,29 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<Button variant="outline" size="sm" @click="handleTestPopup">测试弹窗</Button>
|
||||
</div>
|
||||
<Input
|
||||
v-model="form.shortcut"
|
||||
placeholder="如 Alt+V / Ctrl+Shift+V"
|
||||
@change="handleShortcutChange"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">格式:修饰键+主键,如 Ctrl+C、Alt+V、Shift+F1。修改后自动生效。</p>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<div
|
||||
ref="recorderRef"
|
||||
class="hotkey-recorder"
|
||||
:class="{ recording }"
|
||||
tabindex="0"
|
||||
@click="startRecord"
|
||||
>
|
||||
<template v-if="recording">按下快捷键…(Esc 取消)</template>
|
||||
<template v-else-if="form.shortcut">
|
||||
{{ displayShortcut(form.shortcut) }}
|
||||
</template>
|
||||
<template v-else>未设置(点击录入)</template>
|
||||
</div>
|
||||
<Button
|
||||
v-if="form.shortcut"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 text-muted-foreground"
|
||||
@click="clearShortcut"
|
||||
>清除</Button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">点击方框录入快捷键,需至少一个修饰键 + 字母/数字/功能键。默认 Alt+V</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
import { useDownloaderStore, type DownloadTask, type TaskStatus, type CheckUrlResult } from '@/stores/downloaderStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { pendingNewDownload } from '@/lib/trayEvents'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -42,6 +43,7 @@ const logger = createLogger('downloader')
|
||||
|
||||
// ===== 主 Tab 状态 =====
|
||||
const activeTab = ref('tasks')
|
||||
const tabsStore = useModuleTabsStore()
|
||||
const tabsListRef = useModuleTabs(activeTab, [
|
||||
{ value: 'tasks', label: '下载任务' },
|
||||
{ value: 'settings', label: '设置' },
|
||||
@@ -483,6 +485,9 @@ const handleSaveSettings = async (): Promise<boolean> => {
|
||||
}
|
||||
}
|
||||
|
||||
// 注册保存处理函数到标签栏 store(TitleBar 保存按钮调用)
|
||||
tabsStore.registerSave(handleSaveSettings)
|
||||
|
||||
// Dialog 保存:成功后关闭弹窗
|
||||
const handleDialogSave = async () => {
|
||||
const ok = await handleSaveSettings()
|
||||
@@ -963,14 +968,6 @@ const toggleSortOrder = () => {
|
||||
<TabsContent value="settings" class="flex-1 mt-4 min-h-0 tab-animate">
|
||||
<ScrollArea class="h-full pr-3">
|
||||
<div class="flex flex-col gap-4 pb-4 max-w-2xl">
|
||||
<!-- 顶部保存按钮 -->
|
||||
<div class="flex justify-end">
|
||||
<Button @click="handleSaveSettings">
|
||||
<Check class="h-4 w-4" />
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 下载设置 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { appDataDir } from '@tauri-apps/api/path'
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { useProxyStore, type ProxyNode } from '@/stores/proxyStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -71,6 +72,7 @@ const onConfirmOpenChange = (open: boolean) => {
|
||||
|
||||
const activeTab = ref('overview')
|
||||
// 浮动标签切换器:注册到 TitleBar,滚动遮挡时自动显示
|
||||
const tabsStore = useModuleTabsStore()
|
||||
const tabsListRef = useModuleTabs(activeTab, [
|
||||
{ value: 'overview', label: '概览' },
|
||||
{ value: 'proxies', label: '节点' },
|
||||
@@ -856,10 +858,8 @@ const syncLocalSettings = () => {
|
||||
|
||||
watch(() => store.settings, syncLocalSettings, { immediate: true })
|
||||
|
||||
const savingSettings = ref(false)
|
||||
const saveSettingsForm = async () => {
|
||||
if (!store.settings) return
|
||||
savingSettings.value = true
|
||||
try {
|
||||
await store.saveSettings({
|
||||
...store.settings,
|
||||
@@ -868,10 +868,11 @@ const saveSettingsForm = async () => {
|
||||
toast.success('设置已保存')
|
||||
} catch (e) {
|
||||
toast.error('保存失败', { description: String(e) })
|
||||
} finally {
|
||||
savingSettings.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 注册保存处理函数到标签栏 store(TitleBar 保存按钮调用)
|
||||
tabsStore.registerSave(saveSettingsForm)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1513,16 +1514,10 @@ const saveSettingsForm = async () => {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<!-- 保存设置(顶部醒目位置) -->
|
||||
<div class="flex items-center gap-3 rounded-md border border-primary/30 bg-primary/5 p-3">
|
||||
<Button size="sm" :disabled="savingSettings" @click="saveSettingsForm">
|
||||
<Loader2 v-if="savingSettings" key="saving" class="size-3.5 animate-spin" />
|
||||
<Check v-else key="saved-icon" class="size-3.5" />保存设置
|
||||
</Button>
|
||||
<p class="text-xs text-muted-foreground flex-1">
|
||||
修改端口/接口/密钥/模式后需重启 mihomo 生效。DNS、规则等高级配置请直接编辑订阅文件。
|
||||
</p>
|
||||
</div>
|
||||
<!-- 说明文字(保存按钮已移至顶部标签栏) -->
|
||||
<p class="text-xs text-muted-foreground rounded-md border border-primary/30 bg-primary/5 p-3">
|
||||
修改端口/接口/密钥/模式后需重启 mihomo 生效。DNS、规则等高级配置请直接编辑订阅文件。
|
||||
</p>
|
||||
<Separator />
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
|
||||
@@ -160,24 +160,22 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full overflow-hidden">
|
||||
<Tabs v-model="activeTab" class="h-full flex flex-col">
|
||||
<div ref="tabsListRef" class="px-4 pt-3 pb-2 shrink-0">
|
||||
<TabsList>
|
||||
<TabsTrigger value="settings">设置</TabsTrigger>
|
||||
<TabsTrigger value="history">
|
||||
历史
|
||||
<span v-if="store.recent.length" class="ml-1 text-xs text-muted-foreground">
|
||||
({{ store.recent.length }})
|
||||
</span>
|
||||
<div class="h-full p-6 overflow-hidden flex flex-col">
|
||||
<Tabs v-model="activeTab" class="flex-1 min-h-0 flex flex-col">
|
||||
<div ref="tabsListRef" class="shrink-0">
|
||||
<TabsList class="grid w-full max-w-md grid-cols-2 !bg-transparent !p-0 !shadow-none">
|
||||
<TabsTrigger value="settings" class="gap-1.5"><Settings class="size-3.5" />设置</TabsTrigger>
|
||||
<TabsTrigger value="history" class="gap-1.5">
|
||||
<ImageIcon class="size-3.5" />历史
|
||||
<span v-if="store.recent.length" class="text-xs text-muted-foreground">({{ store.recent.length }})</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<!-- 截图设置 -->
|
||||
<TabsContent value="settings" class="flex-1 min-h-0 mt-0">
|
||||
<TabsContent value="settings" class="flex-1 min-h-0 mt-4">
|
||||
<ScrollArea class="h-full">
|
||||
<div class="p-4 pt-0 space-y-4 max-w-3xl mx-auto">
|
||||
<div class="space-y-4 max-w-3xl">
|
||||
<!-- 快捷键 -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -230,7 +228,8 @@ onUnmounted(() => {
|
||||
<div class="mt-3 pt-3 border-t text-sm text-muted-foreground space-y-1.5">
|
||||
<p>· 进入截图后 <span class="text-foreground">移动鼠标</span> 自动识别窗口,<span class="text-foreground">点击</span> 选中窗口</p>
|
||||
<p>· <span class="text-foreground">长按拖动</span> 自由选择区域,选区可拖动 / 缩放手柄调整大小</p>
|
||||
<p>· 选区右下方编辑栏可标注(矩形 / 椭圆 / 箭头 / 序号 / 画笔 / 文字 / 马赛克 / 高亮)</p>
|
||||
<p>· 编辑栏支持矩形 / 椭圆 / 箭头 / 序号 / 画笔 / 文字 / 马赛克 / 高亮,颜色与粗细可随时调整</p>
|
||||
<p>· 点击标注可选中移动,文字标注点击内容可重新编辑</p>
|
||||
<p>
|
||||
· <kbd class="px-1 py-0.5 text-xs rounded bg-muted border">Enter</kbd> / 双击复制并完成,
|
||||
<kbd class="px-1 py-0.5 text-xs rounded bg-muted border">Esc</kbd> 逐级取消
|
||||
@@ -329,7 +328,7 @@ onUnmounted(() => {
|
||||
<!-- 历史记录 -->
|
||||
<TabsContent value="history" class="flex-1 min-h-0 mt-0">
|
||||
<ScrollArea class="h-full">
|
||||
<div class="p-4 pt-0">
|
||||
<div>
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="store.recent.length === 0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ export const moduleConfig: ModuleConfig = {
|
||||
id: 'screenshot',
|
||||
name: '截图',
|
||||
icon: 'screenshot',
|
||||
description: '区域截图、窗口截图与图片编辑',
|
||||
description: '截图、标注与图片编辑',
|
||||
category: 'media',
|
||||
defaultEnabled: true,
|
||||
loader: () => import('./ScreenshotModule.vue'),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Effect, EffectState } from '@tauri-apps/api/window'
|
||||
import {
|
||||
Globe, Power, PowerOff, RefreshCw, Check, Monitor, Download,
|
||||
Settings, LogOut, Camera,
|
||||
Settings, LogOut,
|
||||
} from '@lucide/vue'
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
@@ -526,10 +526,6 @@ onUnmounted(() => {
|
||||
<Download class="h-4 w-4" />
|
||||
<span>新建下载</span>
|
||||
</button>
|
||||
<button class="tray-item" @click="handleAction('screenshot_region')">
|
||||
<Camera class="h-4 w-4" />
|
||||
<span>区域截图</span>
|
||||
</button>
|
||||
|
||||
<div class="tray-separator"></div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user