diff --git a/public/_headers b/public/_headers index bf23c92..05e8e81 100644 --- a/public/_headers +++ b/public/_headers @@ -1,2 +1,2 @@ /* - Strict-Transport-Security: max-age=31536000; includeSubDomains; preload \ No newline at end of file + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload diff --git a/src/components/SearchHeader.vue b/src/components/SearchHeader.vue index a345430..0b27960 100644 --- a/src/components/SearchHeader.vue +++ b/src/components/SearchHeader.vue @@ -307,6 +307,23 @@
+ +
+
+
+ +
+
+

+ 本站已更换新域名 searchgal.top,请更新书签! +

+

+ 💡 如需迁移搜索记录,可在设置中导出历史后,到新站导入即可。 +

+
+
+
+
diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index 654f000..cc69454 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -454,6 +454,81 @@
+ + +
+
+
+ +
+
+

搜索历史

+

+ 共 {{ historyStore.historyCount }} 条记录 +

+
+
+ +
+ +
+ + + + +
+ + + +
+ + {{ importMessage }} +
+
+ + +
+ +

导出为 JSON 格式,可用于备份或迁移到其他设备。导入时会自动去重。

+
+
+
@@ -527,11 +602,152 @@ import { X, Plus, Volume2, + Download, + Upload, + History, + FileJson, + AlertCircle, + CheckCircle2, } from 'lucide-vue-next' import { useSettingsStore, DEFAULT_API_CONFIG } from '@/stores/settings' +import { useHistoryStore } from '@/stores/history' +import type { SearchHistory } from '@/utils/persistence' import apiData from '@/data/api.json' const settingsStore = useSettingsStore() +const historyStore = useHistoryStore() + +// 导入导出状态 +const importStatus = ref<'idle' | 'success' | 'error'>('idle') +const importMessage = ref('') +const fileInputRef = ref(null) + +// 导出搜索历史为 JSON +function exportHistory() { + playTap() + + const history = historyStore.searchHistory + if (history.length === 0) { + importStatus.value = 'error' + importMessage.value = '暂无搜索历史可导出' + setTimeout(() => { + importStatus.value = 'idle' + }, 3000) + return + } + + const exportData = { + version: '1.0', + exportedAt: new Date().toISOString(), + source: 'SearchGal', + count: history.length, + history: history, + } + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `searchgal-history-${new Date().toISOString().slice(0, 10)}.json` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + + playCelebration() + importStatus.value = 'success' + importMessage.value = `已导出 ${history.length} 条搜索记录` + setTimeout(() => { + importStatus.value = 'idle' + }, 3000) +} + +// 触发文件选择 +function triggerImport() { + playTap() + fileInputRef.value?.click() +} + +// 处理导入文件 +function handleImportFile(event: Event) { + const input = event.target as HTMLInputElement + const file = input.files?.[0] + + if (!file) {return} + + // 重置 input 以便可以选择相同文件 + input.value = '' + + if (!file.name.endsWith('.json')) { + importStatus.value = 'error' + importMessage.value = '请选择 .json 格式的文件' + setTimeout(() => { + importStatus.value = 'idle' + }, 3000) + return + } + + const reader = new FileReader() + reader.onload = (e) => { + try { + const content = e.target?.result as string + const data = JSON.parse(content) + + // 验证数据格式 + if (!data.history || !Array.isArray(data.history)) { + throw new Error('无效的文件格式') + } + + // 验证每条记录 + const validHistory: SearchHistory[] = [] + for (const item of data.history) { + if ( + typeof item.query === 'string' && + (item.mode === 'game' || item.mode === 'patch') && + typeof item.timestamp === 'number' && + typeof item.resultCount === 'number' + ) { + validHistory.push({ + query: item.query, + mode: item.mode, + timestamp: item.timestamp, + resultCount: item.resultCount, + }) + } + } + + if (validHistory.length === 0) { + throw new Error('文件中没有有效的搜索记录') + } + + // 使用 store 的 importHistory 方法(自动去重、排序、保存) + const importedCount = historyStore.importHistory(validHistory) + + playCelebration() + importStatus.value = 'success' + importMessage.value = `成功导入 ${importedCount} 条新记录(共 ${validHistory.length} 条)` + setTimeout(() => { + importStatus.value = 'idle' + }, 3000) + } catch (error) { + importStatus.value = 'error' + importMessage.value = error instanceof Error ? error.message : '导入失败' + setTimeout(() => { + importStatus.value = 'idle' + }, 3000) + } + } + + reader.onerror = () => { + importStatus.value = 'error' + importMessage.value = '读取文件失败' + setTimeout(() => { + importStatus.value = 'idle' + }, 3000) + } + + reader.readAsText(file) +} const props = defineProps<{ isOpen: boolean diff --git a/src/stores/history.ts b/src/stores/history.ts index 96e4722..44a4ec1 100644 --- a/src/stores/history.ts +++ b/src/stores/history.ts @@ -118,6 +118,29 @@ export const useHistoryStore = defineStore('history', () => { ) } + // 导入历史记录(合并去重) + function importHistory(items: SearchHistory[]): number { + let importedCount = 0 + + for (const item of items) { + const exists = searchHistory.value.some( + h => h.query === item.query && h.mode === item.mode, + ) + if (!exists) { + searchHistory.value.push(item) + importedCount++ + } + } + + // 按时间排序(最新在前) + searchHistory.value.sort((a, b) => b.timestamp - a.timestamp) + + // 保存到 localStorage + saveHistory() + + return importedCount + } + return { // 状态 searchHistory, @@ -137,6 +160,7 @@ export const useHistoryStore = defineStore('history', () => { clearHistoryByMode, getHistoryStats, searchInHistory, + importHistory, } }) diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..fb3e9fe --- /dev/null +++ b/vercel.json @@ -0,0 +1,26 @@ +{ + "redirects": [ + { + "source": "/:path*", + "has": [ + { + "type": "host", + "value": "www.searchgal.top" + } + ], + "destination": "https://searchgal.top/:path*", + "permanent": true + }, + { + "source": "/:path*", + "has": [ + { + "type": "host", + "value": "sg.saop.cc" + } + ], + "destination": "https://searchgal.top/:path*", + "permanent": true + } + ] +}