性能优化

This commit is contained in:
zhongluofeng
2026-08-06 10:33:16 +08:00
parent c7578a2e6b
commit e66c53e66d
105 changed files with 7273 additions and 5002 deletions
+86 -26
View File
@@ -1,15 +1,20 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
import { availableMonitors } from '@tauri-apps/api/window'
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
import { toast } from 'vue-sonner'
import { EVENTS, WINDOWS } from '@/lib/constants'
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts
import { commands } from '@/lib/bindings'
export interface RecentCapture {
id: string
pngBase64: string
/** 缩略图 data URLJPEG~256px 宽,常驻内存的只有它,完整图已落盘缓存) */
thumb: string
/** 完整 PNG 缓存文件路径(按需通过 screenshot_load_cache 加载) */
filePath: string
width: number
height: number
time: number
@@ -47,7 +52,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
let exportUnlisten: UnlistenFn | null = null
let shortcutUnlisten: UnlistenFn | null = null
/** 常驻截图覆盖层窗口(启动时创建,之后每次截图复用,避免重复 WebView 初始化) */
const OVERLAY_LABEL = 'screenshot-overlay'
const OVERLAY_LABEL = WINDOWS.screenshotOverlay
let overlayWin: WebviewWindow | null = null
let overlayReadyResolve: (() => void) | null = null
let overlayReadyPromise: Promise<void> | null = null
@@ -122,17 +127,17 @@ export const useScreenshotStore = defineStore('screenshot', () => {
}
}
// 捕获虚拟屏(覆盖层隐藏 → 不会出现在截图中),仅存原始像素,不做 PNG 编码
await invoke('screenshot_capture_fullscreen')
await commands.screenshotCaptureFullscreen()
// 用物理像素把覆盖层对齐到虚拟屏(多显示器/混合 DPI 下保证底图 1:1 与坐标一致)
const rect = await computeVirtualPhysicalRect()
await overlayWin?.setPosition(new PhysicalPosition(rect.x, rect.y))
await overlayWin?.setSize(new PhysicalSize(rect.width, rect.height))
await waitOverlayReady()
await emit('screenshot-begin')
await emit(EVENTS.screenshotBegin)
} catch (e) {
console.error('[screenshot] 捕获失败', e)
toast.error('截图启动失败:' + (e as Error).message)
await invoke('screenshot_clear_fullscreen').catch(() => {})
await commands.screenshotClearFullscreen().catch(() => {})
} finally {
capturing.value = false
}
@@ -188,7 +193,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
if (readyListenerInit) return
readyListenerInit = true
resetOverlayReady()
await listen('screenshot-overlay-ready', () => {
await listen(EVENTS.screenshotOverlayReady, () => {
overlayReadyResolve?.()
})
await ensureOverlay()
@@ -214,25 +219,71 @@ export const useScreenshotStore = defineStore('screenshot', () => {
}
// ===== 历史 / 导出 =====
function addRecent(pngBase64: string, width: number, height: number, mode: string) {
recent.value.unshift({
id: crypto.randomUUID(),
pngBase64,
width,
height,
time: Date.now(),
mode,
/** 由完整 PNG base64 生成缩略图 data URLcanvas 缩放至 ~256px 宽,JPEG 压缩) */
function makeThumb(pngBase64: string, width: number, height: number): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
try {
const targetW = 256
const scale = targetW / width
const targetH = Math.max(1, Math.round(height * scale))
const canvas = document.createElement('canvas')
canvas.width = targetW
canvas.height = targetH
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('无法创建画布上下文'))
return
}
ctx.drawImage(img, 0, 0, targetW, targetH)
resolve(canvas.toDataURL('image/jpeg', 0.7))
} catch (e) {
reject(e)
}
}
img.onerror = () => reject(new Error('缩略图解码失败'))
img.src = `data:image/png;base64,${pngBase64}`
})
const limit = Math.max(1, settings.value.historyLimit)
if (recent.value.length > limit) recent.value.length = limit
}
function clearHistory() {
/** 追加历史项:删除超出上限的最旧缓存文件 */
function addRecent(thumb: string, filePath: string, width: number, height: number, mode: string) {
recent.value.unshift({ id: crypto.randomUUID(), thumb, filePath, width, height, time: Date.now(), mode })
const limit = Math.max(1, settings.value.historyLimit)
if (recent.value.length > limit) {
const removed = recent.value.splice(limit)
for (const item of removed) {
void commands.screenshotDeleteCache(item.filePath)
}
}
}
/** 移除单个历史项(同时删除缓存文件) */
async function removeRecent(item: RecentCapture) {
const idx = recent.value.findIndex((r) => r.id === item.id)
if (idx >= 0) recent.value.splice(idx, 1)
try {
await commands.screenshotDeleteCache(item.filePath)
} catch (e) {
console.error('[screenshot] 删除缓存失败', e)
}
}
async function clearHistory() {
for (const item of recent.value) {
void commands.screenshotDeleteCache(item.filePath)
}
recent.value = []
}
/** 从历史缓存加载完整 PNG base64(一次性,不常驻) */
async function loadFullImage(item: RecentCapture): Promise<string> {
return await commands.screenshotLoadCache(item.filePath)
}
async function copyImage(pngBase64: string) {
await invoke('screenshot_copy_image', { pngBase64 })
await commands.screenshotCopyImage(pngBase64)
toast.success('已复制到剪贴板')
}
@@ -247,14 +298,21 @@ export const useScreenshotStore = defineStore('screenshot', () => {
filters: [{ name: 'PNG', extensions: ['png'] }],
})
if (!path) return
await invoke('screenshot_save_png', { pngBase64, path })
await commands.screenshotSavePng(pngBase64, path)
toast.success('已保存到文件')
}
/** 覆盖层/编辑器导出处理:记录历史 + 按设置自动保存 */
/** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */
async function handleExport(payload: { pngBase64: string; width: number; height: number }) {
const { pngBase64, width, height } = payload
addRecent(pngBase64, width, height, 'capture')
try {
// 完整图落盘缓存目录,历史只保留缩略图,避免完整 base64 常驻内存
const filePath = await commands.screenshotSaveCache(pngBase64)
const thumb = await makeThumb(pngBase64, width, height)
addRecent(thumb, filePath, width, height, 'capture')
} catch (e) {
console.error('[screenshot] 历史缓存失败', e)
}
// 自动保存到指定目录
if (settings.value.autoSave && settings.value.saveDir) {
const ts = new Date()
@@ -263,7 +321,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
.slice(0, 19)
const path = `${settings.value.saveDir.replace(/\\$/, '')}\\screenshot_${ts}.png`
try {
await invoke('screenshot_save_png', { pngBase64, path })
await commands.screenshotSavePng(pngBase64, path)
} catch (e) {
console.error('[screenshot] 自动保存失败', e)
}
@@ -286,7 +344,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
/** 监听 Rust 侧 emit 的 'screenshot-shortcut' 事件(快捷键按下时触发) */
async function initShortcutListener() {
if (shortcutUnlisten) return
shortcutUnlisten = await listen('screenshot-shortcut', () => {
shortcutUnlisten = await listen(EVENTS.screenshotShortcut, () => {
void startCapture()
})
}
@@ -294,7 +352,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
/** 应用启动时按已保存的快捷键注册全局热键(支持自定义,默认 Ctrl+Alt+A */
async function initShortcutRegistration() {
try {
await invoke('screenshot_register_shortcut', { shortcut: settings.value.shortcut })
await commands.screenshotRegisterShortcut(settings.value.shortcut)
} catch (e) {
console.error('[screenshot] 快捷键注册失败', e)
}
@@ -305,7 +363,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
const next = shortcut.trim()
setSettings({ shortcut: next })
try {
await invoke('screenshot_register_shortcut', { shortcut: next })
await commands.screenshotRegisterShortcut(next)
return true
} catch (e) {
console.error('[screenshot] 快捷键注册失败', e)
@@ -339,6 +397,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
copyImage,
saveImage,
handleExport,
removeRecent,
loadFullImage,
loadSettings,
setSettings,
setShortcut,