截图模块初始化

This commit is contained in:
zhongluofeng
2026-07-31 18:31:13 +08:00
parent 66575c6166
commit 89e5b7bed5
7 changed files with 2082 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { availableMonitors } from '@tauri-apps/api/window'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { toast } from 'vue-sonner'
interface CaptureData {
pngBase64: string
width: number
height: number
}
export interface RecentCapture {
id: string
pngBase64: string
width: number
height: number
time: number
mode: string
}
export type CaptureMode = 'region' | 'window' | 'fullscreen'
const OVERLAY_LABEL = 'screenshot-overlay'
const EDITOR_LABEL = 'screenshot-editor'
export const useScreenshotStore = defineStore('screenshot', () => {
const capturing = ref(false)
const recent = ref<RecentCapture[]>([])
let exportUnlisten: UnlistenFn | null = null
/** 计算所有显示器的逻辑像素联合矩形(用于覆盖层窗口定位/尺寸) */
async function computeVirtualLogicalRect() {
const monitors = await availableMonitors()
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (const m of monitors) {
const s = m.scaleFactor || 1
const lx = m.position.x / s
const ly = m.position.y / s
const lw = m.size.width / s
const lh = m.size.height / s
if (lx < minX) minX = lx
if (ly < minY) minY = ly
if (lx + lw > maxX) maxX = lx + lw
if (ly + lh > maxY) maxY = ly + lh
}
if (!Number.isFinite(minX)) {
minX = 0
minY = 0
maxX = 800
maxY = 600
}
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
}
/** 启动截图:region=区域选择,window=窗口拾取,fullscreen=直接进编辑器 */
async function startCapture(mode: CaptureMode) {
if (capturing.value) return
capturing.value = true
try {
// 1. 捕获虚拟屏(覆盖层尚未创建 → 不会出现在截图中)
const data = await invoke<CaptureData>('screenshot_capture_fullscreen')
if (mode === 'fullscreen') {
// 全屏截图直接送入编辑器
await invoke('screenshot_set_editor_image', { pngBase64: data.pngBase64 })
// 清掉静态全屏缓存(编辑器用 cropped/全图,不再需要原始 BGRA
await invoke('screenshot_take_fullscreen').catch(() => {})
await openEditor()
return
}
// 2. 区域/窗口模式:创建覆盖层选区窗口
await openOverlay(mode)
} catch (e) {
console.error('[screenshot] 捕获失败', e)
toast.error('截图启动失败:' + (e as Error).message)
// 失败时清理静态缓存
await invoke('screenshot_take_fullscreen').catch(() => {})
} finally {
capturing.value = false
}
}
async function openOverlay(mode: CaptureMode) {
const existing = await WebviewWindow.getByLabel(OVERLAY_LABEL)
if (existing) await existing.close()
const rect = await computeVirtualLogicalRect()
const url = `index.html#screenshot-overlay?mode=${mode}`
new WebviewWindow(OVERLAY_LABEL, {
url,
title: '截图',
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
decorations: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
shadow: false,
focus: true,
visible: true,
})
}
async function openEditor() {
const existing = await WebviewWindow.getByLabel(EDITOR_LABEL)
if (existing) {
await existing.show()
await existing.setFocus()
return
}
new WebviewWindow(EDITOR_LABEL, {
url: 'index.html#screenshot-editor',
title: '截图编辑器',
width: 960,
height: 720,
minWidth: 640,
minHeight: 480,
decorations: false,
transparent: true,
alwaysOnTop: false,
skipTaskbar: false,
resizable: true,
shadow: true,
focus: true,
visible: true,
})
}
function addRecent(pngBase64: string, width: number, height: number, mode: string) {
recent.value.unshift({
id: crypto.randomUUID(),
pngBase64,
width,
height,
time: Date.now(),
mode,
})
if (recent.value.length > 12) recent.value.pop()
}
async function copyImage(pngBase64: string) {
await invoke('screenshot_copy_image', { pngBase64 })
toast.success('已复制到剪贴板')
}
async function saveImage(pngBase64: string) {
const { save } = await import('@tauri-apps/plugin-dialog')
const ts = new Date()
.toISOString()
.replace(/[:.]/g, '-')
.slice(0, 19)
const path = await save({
defaultPath: `screenshot_${ts}.png`,
filters: [{ name: 'PNG', extensions: ['png'] }],
})
if (!path) return
await invoke('screenshot_save_png', { pngBase64, path })
toast.success('已保存到文件')
}
/** 监听编辑器导出事件(主窗口记录历史 + 提示) */
async function initExportListener() {
if (exportUnlisten) return
exportUnlisten = await listen<{
pngBase64: string
width: number
height: number
}>('screenshot-exported', (e) => {
addRecent(e.payload.pngBase64, e.payload.width, e.payload.height, 'edited')
})
}
function destroyExportListener() {
if (exportUnlisten) {
exportUnlisten()
exportUnlisten = null
}
}
return {
capturing,
recent,
startCapture,
openEditor,
addRecent,
copyImage,
saveImage,
initExportListener,
destroyExportListener,
}
})