af912662de
v-combobox 清空时 search 为 null 使 trim() 抛错;422 响应剥离不可序列化的 ctx 避免 500。 Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
2.5 KiB
TypeScript
91 lines
2.5 KiB
TypeScript
import { ref, type Ref } from 'vue'
|
|
import { http } from '@/api'
|
|
|
|
const LEGACY_HISTORY = 'nexus_servers_search_history'
|
|
const LEGACY_LAST = 'nexus_servers_search_last'
|
|
|
|
export interface ServerSearchHistoryResponse {
|
|
history: string[]
|
|
last: string | null
|
|
}
|
|
|
|
function readLegacyLocal(): { history: string[]; last: string } | null {
|
|
try {
|
|
const raw = localStorage.getItem(LEGACY_HISTORY)
|
|
const last = localStorage.getItem(LEGACY_LAST) || ''
|
|
if (!raw) return last.trim() ? { history: [], last } : null
|
|
const parsed = JSON.parse(raw) as unknown
|
|
if (!Array.isArray(parsed)) return last.trim() ? { history: [], last } : null
|
|
const history = parsed.filter((x): x is string => typeof x === 'string' && x.trim().length > 0)
|
|
if (!history.length && !last.trim()) return null
|
|
return { history, last }
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function clearLegacyLocal() {
|
|
try {
|
|
localStorage.removeItem(LEGACY_HISTORY)
|
|
localStorage.removeItem(LEGACY_LAST)
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/** Servers page search history — persisted per admin in MySQL. */
|
|
export function useServerSearchHistory() {
|
|
const items = ref<string[]>([])
|
|
const bootstrapped = ref(false)
|
|
|
|
async function load(): Promise<string> {
|
|
const res = await http.get<ServerSearchHistoryResponse>('/servers/search-history')
|
|
items.value = res.history || []
|
|
return res.last || ''
|
|
}
|
|
|
|
async function migrateLegacyIfEmpty(): Promise<boolean> {
|
|
const legacy = readLegacyLocal()
|
|
if (!legacy) return false
|
|
if (items.value.length > 0) {
|
|
clearLegacyLocal()
|
|
return false
|
|
}
|
|
await http.put<ServerSearchHistoryResponse>('/servers/search-history', {
|
|
history: legacy.history,
|
|
last: legacy.last || legacy.history[0] || '',
|
|
})
|
|
clearLegacyLocal()
|
|
return true
|
|
}
|
|
|
|
async function bootstrap(searchRef: Ref<string>) {
|
|
if (bootstrapped.value) return
|
|
try {
|
|
let last = await load()
|
|
if (!items.value.length && (await migrateLegacyIfEmpty())) {
|
|
last = await load()
|
|
}
|
|
if (last) searchRef.value = last
|
|
} catch {
|
|
// offline / auth — keep empty search
|
|
} finally {
|
|
bootstrapped.value = true
|
|
}
|
|
}
|
|
|
|
async function record(query: string | null | undefined) {
|
|
const normalized = String(query ?? '').trim()
|
|
try {
|
|
const res = await http.put<ServerSearchHistoryResponse>('/servers/search-history', {
|
|
query: normalized,
|
|
})
|
|
items.value = res.history || []
|
|
} catch {
|
|
// non-blocking
|
|
}
|
|
}
|
|
|
|
return { items, bootstrapped, load, bootstrap, record }
|
|
}
|