import { computed, ref, watch } from "vue"; import { defineStore } from "pinia"; import { Events } from "@wailsio/runtime"; import { ArchiveService } from "../../bindings/pvfine/services"; import type { AdvancedSearchDetail, AdvancedSearchHit, AdvancedSearchIndexStatus, } from "../../bindings/pvfine/services/models"; import { useArchiveStore } from "./archive"; export type AdvancedSearchMode = "binary" | "string" | "quick"; export type AdvancedStringMatch = "text" | "regex"; export interface AdvancedSearchItem { key: string; name: string; path: string; size: number; dataType: number; fileIndex: number; details: AdvancedSearchDetail[]; } /** 高级搜索状态:弹窗隐藏时仍保留当前归档的查询与结果。 */ export const useAdvancedSearchStore = defineStore("advancedSearch", () => { const archive = useArchiveStore(); const visible = ref(false); const mode = ref("quick"); const exactMatch = ref(false); const stringMatch = ref("text"); const query = ref(""); const scopePath = ref(""); const hits = ref([]); const nextCursor = ref(-1); const searching = ref(false); const stale = ref(false); const error = ref(""); const indexStatus = ref({ state: "idle", stage: "", done: 0, total: 0, error: "", }); let requestId = 0; let retryAfterIndex = false; const regexEnabled = computed(() => mode.value === "string" && stringMatch.value === "regex"); const hasResults = computed(() => hits.value.length > 0); function eventData(event: any): any { return event?.data ?? event; } function readIndexStatus(data: any): AdvancedSearchIndexStatus { return { state: String(data?.state ?? "idle"), stage: String(data?.stage ?? ""), done: Number(data?.done ?? 0), total: Number(data?.total ?? 0), error: String(data?.error ?? ""), }; } function errorMessage(value: any): string { return String(value?.message ?? value ?? "高级搜索失败"); } function toItem(hit: AdvancedSearchHit): AdvancedSearchItem { return { key: `${hit.fileIndex}:${hit.path}:${hit.name ?? ""}:${(hit.details ?? []).filter((detail) => detail?.kind === "id").map((detail) => detail?.value).join(",")}`, name: hit.name ?? "", path: hit.path, size: hit.size, dataType: hit.dataType, fileIndex: hit.fileIndex, details: (hit.details ?? []).filter( (detail): detail is AdvancedSearchDetail => !!detail ), }; } function open() { visible.value = true; } function close() { visible.value = false; } function clear() { requestId++; query.value = ""; hits.value = []; nextCursor.value = -1; searching.value = false; stale.value = false; error.value = ""; retryAfterIndex = false; } function resetForArchive() { requestId++; visible.value = false; mode.value = "quick"; exactMatch.value = false; stringMatch.value = "text"; scopePath.value = ""; clear(); indexStatus.value = readIndexStatus(null); } async function search() { if (searching.value) return; const request = ++requestId; error.value = ""; stale.value = false; hits.value = []; nextCursor.value = -1; retryAfterIndex = false; if (!query.value.trim()) { searching.value = false; return; } if (!archive.open) { error.value = "请先打开归档文件"; return; } if (regexEnabled.value) { try { // 这是前端即时反馈;最终语法仍由 Go RE2 后端校验。 new RegExp(query.value); } catch (value: any) { error.value = `正则表达式无效: ${value?.message ?? value}`; return; } } searching.value = true; try { const result = await ArchiveService.AdvancedSearch( mode.value === "quick" && exactMatch.value ? "quick-exact" : mode.value, query.value, scopePath.value, regexEnabled.value, 0, 200 ); if (request !== requestId) return; const page = (result?.hits ?? []) .filter((hit): hit is AdvancedSearchHit => !!hit) .map(toItem); hits.value = page; nextCursor.value = result?.nextCursor ?? -1; } catch (value) { if (request !== requestId) return; const message = errorMessage(value); error.value = message; retryAfterIndex = message.includes("索引正在构建"); } finally { if (request === requestId) searching.value = false; } } async function cancel() { requestId++; retryAfterIndex = false; try { await ArchiveService.CancelAdvancedSearch(); nextCursor.value = -1; indexStatus.value = readIndexStatus(null); stale.value = hits.value.length > 0; } catch (value) { error.value = errorMessage(value); } finally { searching.value = false; } } async function loadMore() { if (searching.value || stale.value || nextCursor.value < 0 || !query.value.trim()) return; const request = requestId; searching.value = true; error.value = ""; try { const result = await ArchiveService.AdvancedSearch( mode.value === "quick" && exactMatch.value ? "quick-exact" : mode.value, query.value, scopePath.value, regexEnabled.value, nextCursor.value, 200 ); if (request !== requestId) return; const page = (result?.hits ?? []) .filter((hit): hit is AdvancedSearchHit => !!hit) .map(toItem); hits.value.push(...page); nextCursor.value = result?.nextCursor ?? -1; } catch (value) { if (request === requestId) error.value = errorMessage(value); } finally { if (request === requestId) searching.value = false; } } /** 加载剩余分页,调用方在成功后再一次性使用完整结果。 */ async function loadAll(): Promise { if (searching.value || !query.value.trim()) return [...hits.value]; if (stale.value) return null; const request = requestId; searching.value = true; error.value = ""; try { let cursor = nextCursor.value; while (cursor >= 0) { const result = await ArchiveService.AdvancedSearch( mode.value === "quick" && exactMatch.value ? "quick-exact" : mode.value, query.value, scopePath.value, regexEnabled.value, cursor, 200 ); if (request !== requestId || stale.value) return null; const page = (result?.hits ?? []) .filter((hit): hit is AdvancedSearchHit => !!hit) .map(toItem); hits.value.push(...page); const next = result?.nextCursor ?? -1; nextCursor.value = next; if (next === cursor) break; cursor = next; } return [...hits.value]; } catch (value) { if (request === requestId) error.value = errorMessage(value); throw value; } finally { if (request === requestId) searching.value = false; } } function markStale() { requestId++; searching.value = false; nextCursor.value = -1; indexStatus.value = readIndexStatus(null); if (query.value.trim() || hits.value.length > 0) stale.value = true; } watch([mode, query, scopePath, stringMatch, exactMatch], () => { requestId++; searching.value = false; retryAfterIndex = false; nextCursor.value = -1; if (hits.value.length > 0) stale.value = true; }, { flush: "sync" }); Events.On("archive:index-ready", () => { if (mode.value === "quick" && retryAfterIndex && visible.value) { retryAfterIndex = false; void search(); } }); Events.On("archive:opened", () => resetForArchive()); Events.On("archive:closed", () => resetForArchive()); Events.On("archive:advanced-search-stale", () => markStale()); Events.On("archive:advanced-index-progress", (event: any) => { indexStatus.value = readIndexStatus(eventData(event)); }); Events.On("archive:advanced-index-ready", (event: any) => { indexStatus.value = readIndexStatus(eventData(event)); if (retryAfterIndex && visible.value) { retryAfterIndex = false; void search(); } }); Events.On("archive:advanced-index-error", (event: any) => { indexStatus.value = readIndexStatus(eventData(event)); }); return { visible, mode, stringMatch, exactMatch, query, scopePath, hits, nextCursor, searching, stale, error, indexStatus, regexEnabled, hasResults, open, close, clear, search, cancel, loadMore, loadAll, }; });