服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
310 lines
8.6 KiB
TypeScript
310 lines
8.6 KiB
TypeScript
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<AdvancedSearchMode>("quick");
|
|
const exactMatch = ref(false);
|
|
const stringMatch = ref<AdvancedStringMatch>("text");
|
|
const query = ref("");
|
|
const scopePath = ref("");
|
|
const hits = ref<AdvancedSearchItem[]>([]);
|
|
const nextCursor = ref(-1);
|
|
const searching = ref(false);
|
|
const stale = ref(false);
|
|
const error = ref("");
|
|
const indexStatus = ref<AdvancedSearchIndexStatus>({
|
|
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<AdvancedSearchItem[] | null> {
|
|
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,
|
|
};
|
|
});
|