import { ref } from 'vue' const STORAGE_KEY = 'pub_search_history' const MAX_ENTRIES = 50 export interface HistoryEntry { id: string query: string expanded_query: string result_count: number | null timestamp: string } function loadAll(): HistoryEntry[] { try { const raw = localStorage.getItem(STORAGE_KEY) return raw ? JSON.parse(raw) : [] } catch { return [] } } function saveAll(entries: HistoryEntry[]) { localStorage.setItem(STORAGE_KEY, JSON.stringify(entries)) } /** 把 #N 引用替换为 expanded_query(加括号保护优先级) */ export function resolveQuery(query: string, entries: HistoryEntry[]): string { return query.replace(/#(\d+)/g, (_m, num) => { const found = entries.find(e => e.id === `#${num}`) return found ? `(${found.expanded_query})` : _m }) } /** 递归展开所有 #N 引用为纯查询,带循环引用检测 */ export function expandQuery(query: string, entries: HistoryEntry[]): string { const seen = new Set() let prev = '' let current = query for (let i = 0; i < 10; i++) { if (current === prev) break const refs = current.match(/#(\d+)/g) if (refs) { const uniqueRefs = [...new Set(refs)] for (const ref of uniqueRefs) { if (seen.has(ref)) return prev // 循环引用 → 返回上次安全结果 seen.add(ref) } } prev = current current = resolveQuery(current, entries) } return current } /** 重新计算所有条目的 expanded_query,在删除/淘汰后保证一致性 */ function recomputeExpanded(entries: HistoryEntry[]) { const ids = new Set(entries.map(e => e.id)) for (const entry of entries) { entry.expanded_query = expandQuery(entry.query, entries) .replace(/#(\d+)/g, (m) => ids.has(m) ? m : '(deleted)') } } export function useSearchHistory() { const entries = ref(loadAll()) function getAll(): HistoryEntry[] { return [...entries.value] } function add(query: string, resultCount?: number | null): HistoryEntry { const all = loadAll() const nextNum = all.length > 0 ? Math.max(...all.map(e => parseInt(e.id.slice(1), 10))) + 1 : 1 const id = `#${nextNum}` const expanded = expandQuery(query, all) const entry: HistoryEntry = { id, query, expanded_query: expanded, result_count: resultCount ?? null, timestamp: new Date().toISOString(), } if (all.length >= MAX_ENTRIES) { all.sort((a, b) => a.timestamp.localeCompare(b.timestamp)) all.shift() recomputeExpanded(all) // 清除悬空 #N 引用 } all.push(entry) saveAll(all) entries.value = [...all] return entry } function remove(id: string) { const all = loadAll().filter(e => e.id !== id) recomputeExpanded(all) // 重新展开,清除悬空 #N 引用 saveAll(all) entries.value = all } function clear() { localStorage.removeItem(STORAGE_KEY) entries.value = [] } function download() { const all = loadAll() /** 转义 TSV 特殊字符:制表符/换行/回车 → 空格 */ const escapeTsv = (s: string) => s.replace(/[\t\n\r]/g, ' ') const lines = all.map(e => `${e.id}\t${e.result_count ?? ''}\t${e.timestamp}\t${escapeTsv(e.query)}` ) const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `pubmed-search-history-${new Date().toISOString().slice(0, 10)}.tsv` a.click() // 延迟回收 blob URL,确保浏览器已开始下载 setTimeout(() => URL.revokeObjectURL(url), 1000) } return { entries, getAll, add, remove, clear, download, } }