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([]) const historyTotal = ref(0) const pinned = ref([]) const settings = ref({ ...DEFAULT_SETTINGS }) const status = ref({ running: false, count: 0 }) const loading = ref(false) // 事件监听(应用级单例,只注册一次) let changedUnlisten: UnlistenFn | null = null let debounceTimer: ReturnType | null = null /** 初始化:加载状态/设置,注册事件监听 */ const init = async () => { try { const [s, st] = await Promise.all([ invoke('clipboard_get_settings'), invoke('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('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('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('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('clipboard_get_item', { id }) } catch (e) { logger.error('获取详情失败: ' + e) return null } } const refreshStatus = async () => { try { status.value = await invoke('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, } })