This commit is contained in:
zhongluofeng
2026-07-30 09:09:29 +08:00
parent 74902c4cec
commit f452322aad
32 changed files with 4586 additions and 115 deletions
+8
View File
@@ -391,6 +391,14 @@ export const useAppStore = defineStore('app', () => {
} catch (e) {
// 非 Tauri 环境下忽略
}
// 切换效果后修复 snap-layout 子窗口背景(clearEffects 触发重绘会导致白色背景显现)
try {
const { invoke } = await import('@tauri-apps/api/core')
invoke('fix_snap_background').catch(() => {})
} catch {
/* 非 Tauri 环境忽略 */
}
}
// 系统主题变化时,若当前为"跟随系统"模式,同步更新 DOM 和窗口效果。
+293
View File
@@ -0,0 +1,293 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { createLogger } from '@/lib/logger'
const logger = createLogger('clipboard')
// ===== 与 Rust 端对应的数据结构(camelCase =====
export type ClipboardKind = 'text' | 'image' | 'files'
export interface ClipboardItem {
id: number
kind: ClipboardKind
preview: string
size: number
pinned: boolean
pinnedOrder: number | null
createdAt: number
}
/** 历史分页结果(与 Rust 端 HistoryPage 对应) */
export interface HistoryPage {
items: ClipboardItem[]
total: number
}
export interface ClipboardItemDetail extends ClipboardItem {
content: string | null
imageBase64: string | null
}
export interface ClipboardSettings {
enabled: boolean
maxItems: number
maxImageKb: number
recordText: boolean
recordImage: boolean
recordFiles: boolean
dedup: boolean
shortcut: string
}
export interface ClipboardStatus {
running: boolean
count: number
}
const DEFAULT_SETTINGS: ClipboardSettings = {
enabled: true,
maxItems: 500,
maxImageKb: 5120,
recordText: true,
recordImage: true,
recordFiles: true,
dedup: true,
shortcut: 'Alt+V',
}
export const useClipboardStore = defineStore('clipboard', () => {
const history = ref<ClipboardItem[]>([])
const historyTotal = ref(0)
const pinned = ref<ClipboardItem[]>([])
const settings = ref<ClipboardSettings>({ ...DEFAULT_SETTINGS })
const status = ref<ClipboardStatus>({ running: false, count: 0 })
const loading = ref(false)
// 事件监听(应用级单例,只注册一次)
let changedUnlisten: UnlistenFn | null = null
let debounceTimer: ReturnType<typeof setTimeout> | null = null
/** 初始化:加载状态/设置,注册事件监听 */
const init = async () => {
try {
const [s, st] = await Promise.all([
invoke<ClipboardSettings>('clipboard_get_settings'),
invoke<ClipboardStatus>('clipboard_status'),
])
settings.value = { ...DEFAULT_SETTINGS, ...s }
status.value = st
} catch (e) {
logger.error('初始化失败: ' + e)
}
if (!changedUnlisten) {
changedUnlisten = await listen('clipboard-changed', () => {
// 防抖:短时间内多次复制只刷新一次
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
refreshHistory()
refreshStatus()
}, 250)
})
}
}
const dispose = () => {
if (changedUnlisten) {
changedUnlisten()
changedUnlisten = null
}
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
}
// ===== 查询 =====
/** 拉取指定页的历史数据。pageSize 默认 50。 */
const fetchHistoryPage = async (opts: {
kind?: string
page?: number
pageSize?: number
} = {}) => {
const kind = opts.kind ?? 'all'
const pageSize = opts.pageSize ?? 50
const page = Math.max(1, opts.page ?? 1)
const offset = (page - 1) * pageSize
try {
const res = await invoke<HistoryPage>('clipboard_get_history', {
limit: pageSize,
offset,
kind,
})
history.value = res.items
historyTotal.value = res.total
} catch (e) {
logger.error('获取历史失败: ' + e)
}
return history.value
}
/** 兼容旧调用:拉取第一页 */
const refreshHistory = async (kind: string = 'all') => fetchHistoryPage({ kind, page: 1 })
const refreshPinned = async () => {
try {
pinned.value = await invoke<ClipboardItem[]>('clipboard_get_pinned')
} catch (e) {
logger.error('获取固定条目失败: ' + e)
}
return pinned.value
}
/** 搜索(分页)。pageSize 默认 50。 */
const searchPage = async (query: string, page: number = 1, pageSize: number = 50) => {
if (!query.trim()) {
return fetchHistoryPage({ page, pageSize })
}
try {
const res = await invoke<HistoryPage>('clipboard_search', {
query,
limit: pageSize,
offset: (page - 1) * pageSize,
})
history.value = res.items
historyTotal.value = res.total
} catch (e) {
logger.error('搜索失败: ' + e)
}
return history.value
}
/** 兼容旧调用:搜索第一页 */
const search = async (query: string) => searchPage(query, 1)
const getItem = async (id: number) => {
try {
return await invoke<ClipboardItemDetail | null>('clipboard_get_item', { id })
} catch (e) {
logger.error('获取详情失败: ' + e)
return null
}
}
const refreshStatus = async () => {
try {
status.value = await invoke<ClipboardStatus>('clipboard_status')
} catch (e) {
logger.error('获取状态失败: ' + e)
}
}
// ===== 操作 =====
const setPinned = async (id: number, pinned: boolean) => {
try {
await invoke('clipboard_set_pinned', { id, pinned })
// 固定/取消后刷新两个列表
await Promise.all([refreshHistory(), refreshPinned()])
} catch (e) {
logger.error('切换固定失败: ' + e)
}
}
const remove = async (id: number) => {
try {
await invoke('clipboard_delete', { id })
history.value = history.value.filter((i) => i.id !== id)
pinned.value = pinned.value.filter((i) => i.id !== id)
status.value.count = Math.max(0, status.value.count - 1)
} catch (e) {
logger.error('删除失败: ' + e)
}
}
const clear = async () => {
try {
await invoke('clipboard_clear')
history.value = []
await refreshStatus()
} catch (e) {
logger.error('清空失败: ' + e)
}
}
const copyBack = async (id: number) => {
await invoke('clipboard_copy_back', { id })
// copy_back 会触发 suppress,不会产生 clipboard-changed 事件
}
const saveSettings = async (s: ClipboardSettings) => {
try {
await invoke('clipboard_save_settings', { settings: s })
settings.value = { ...s }
await refreshStatus()
} catch (e) {
logger.error('保存设置失败: ' + e)
throw e
}
}
const start = async () => {
await invoke('clipboard_start')
await refreshStatus()
}
const stop = async () => {
await invoke('clipboard_stop')
await refreshStatus()
}
// ===== 快捷弹窗 =====
const showPopup = async () => {
await invoke('clipboard_show_popup')
}
const hidePopup = async () => {
await invoke('clipboard_hide_popup')
}
/// 隐藏弹窗并模拟 Ctrl+V 粘贴到原窗口
const pasteToTarget = async () => {
await invoke('clipboard_paste_to_target')
}
const registerShortcut = async (shortcut: string) => {
await invoke('clipboard_register_shortcut', { shortcut })
}
const unregisterShortcut = async () => {
await invoke('clipboard_unregister_shortcut')
}
return {
history,
historyTotal,
pinned,
settings,
status,
loading,
init,
dispose,
fetchHistoryPage,
refreshHistory,
refreshPinned,
searchPage,
search,
getItem,
refreshStatus,
setPinned,
remove,
clear,
copyBack,
saveSettings,
start,
stop,
showPopup,
hidePopup,
pasteToTarget,
registerShortcut,
unregisterShortcut,
}
})
+37 -2
View File
@@ -42,6 +42,25 @@ export interface DownloaderSettings {
extensionPort: number
extensionSecret: string
deleteFilesOnRemove: boolean
checkDuplicate: boolean
}
/** 重复类型 */
export type DuplicateKind = 'none' | 'url' | 'filename' | 'fileExists'
/** check_url 返回的结果 */
export interface CheckUrlResult {
ok: boolean
error: string | null
filename: string | null
totalSize: number | null
supportsResume: boolean
duplicate: DuplicateKind
existing: {
id: string
filename: string
status: TaskStatus
} | null
}
export interface DownloaderStatus {
@@ -115,18 +134,33 @@ export const useDownloaderStore = defineStore('downloader', () => {
url: string,
filename?: string,
dir?: string,
headers?: Record<string, string>
headers?: Record<string, string>,
autoRename = false
): Promise<string> => {
const id = await invoke<string>('downloader_add_task', {
url,
filename: filename || null,
dir: dir || null,
headers: headers || null
headers: headers || null,
autoRename
})
await refreshTasks()
return id
}
/** 检查 URL 重复性并探测文件信息 */
const checkUrl = async (
url: string,
dir?: string,
headers?: Record<string, string>
): Promise<CheckUrlResult> => {
return await invoke<CheckUrlResult>('downloader_check_url', {
url,
dir: dir || null,
headers: headers || null
})
}
const pauseTask = async (id: string) => {
await invoke('downloader_pause_task', { id })
await refreshTasks()
@@ -223,6 +257,7 @@ export const useDownloaderStore = defineStore('downloader', () => {
// tasks
refreshTasks,
addTask,
checkUrl,
pauseTask,
resumeTask,
removeTask,