From d64d047a367d78441f6f94fc939a70015430a15f Mon Sep 17 00:00:00 2001 From: Zain Date: Sat, 8 Nov 2025 11:49:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(history):=20=E6=B7=BB=E5=8A=A0=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=AE=B0=E5=BD=95=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现历史记录管理页面,包含以下功能: - 添加历史记录搜索和过滤功能 - 支持快速搜索和编辑后搜索 - 添加历史记录展开/收起功能 - 添加从选项页面恢复历史记录的功能 --- src/components/HistoryManager.tsx | 260 ++++++++++++++++++++++++++++++ src/components/SearchHistory.tsx | 44 +++-- src/i18n/translations.ts | 46 +++++- src/options/App.tsx | 136 ++++++++++++---- src/popup/App.tsx | 12 ++ 5 files changed, 455 insertions(+), 43 deletions(-) create mode 100644 src/components/HistoryManager.tsx diff --git a/src/components/HistoryManager.tsx b/src/components/HistoryManager.tsx new file mode 100644 index 0000000..f54f567 --- /dev/null +++ b/src/components/HistoryManager.tsx @@ -0,0 +1,260 @@ +import { useState } from 'react' +import type { SearchHistory, UserSettings } from '@/types' +import { formatDistanceToNow } from 'date-fns' +import { zhCN, enUS } from 'date-fns/locale' +import { useTranslation } from '@/i18n' +import { SearchAdapterFactory } from '@/services/adapters' +import { DEFAULT_SETTINGS } from '@/types' + +interface HistoryManagerProps { + history: SearchHistory[] + onClear: () => void +} + +export function HistoryManager({ history, onClear }: HistoryManagerProps) { + const { t, language } = useTranslation() + const locale = language === 'en-US' ? enUS : zhCN + const [searchFilter, setSearchFilter] = useState('') + + // 获取搜索引擎显示名称 + const getEngineName = (engine: string) => { + return t(`common.searchEngines.${engine}`, undefined, engine) + } + + // 格式化时间戳 + const formatTime = (timestamp: number) => { + try { + return formatDistanceToNow(new Date(timestamp), { + addSuffix: true, + locale + }) + } catch { + return t('searchHistory.unknownTime') + } + } + + // 获取搜索语法的标签 + const getSyntaxBadges = (item: SearchHistory) => { + const badges = [] + + if (item.syntax.site) { + badges.push({ + key: 'site', + label: `site: ${item.syntax.site}`, + className: 'badge-primary' + }) + } + + if (item.syntax.fileType) { + badges.push({ + key: 'filetype', + label: `filetype: ${item.syntax.fileType}`, + className: 'badge-secondary' + }) + } + + if (item.syntax.exactMatch) { + badges.push({ + key: 'exact', + label: `"${item.syntax.exactMatch}"`, + className: 'badge-primary' + }) + } + + return badges + } + + // 快速搜索功能 - 直接执行搜索 + const handleQuickSearch = async (item: SearchHistory) => { + try { + // 1. 构建搜索URL + const adapter = SearchAdapterFactory.getAdapter(item.engine) + const searchUrl = adapter.buildQuery({ + keyword: item.keyword, + engine: item.engine, + site: item.syntax.site, + fileType: item.syntax.fileType, + exactMatch: item.syntax.exactMatch + }) + + // 2. 获取用户设置 + const { user_settings } = await chrome.storage.local.get('user_settings') + const settings: UserSettings = user_settings || DEFAULT_SETTINGS + + // 3. 打开搜索 + if (settings.autoOpenInNewTab) { + await chrome.tabs.create({ url: searchUrl }) + } else { + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }) + if (tabs[0]?.id) { + await chrome.tabs.update(tabs[0].id, { url: searchUrl }) + } else { + // 回退:如果无法获取当前标签页,创建新标签页 + await chrome.tabs.create({ url: searchUrl }) + } + } + } catch (error) { + console.error('执行搜索失败:', error) + } + } + + // 编辑搜索功能 - 保存到临时存储并提示打开popup + const handleEditSearch = async (item: SearchHistory, event: React.MouseEvent) => { + event.stopPropagation() + + try { + // 保存到临时存储 + await chrome.storage.local.set({ + pending_restore_history: item + }) + + // 显示通知(简单的alert,后续可以优化为toast通知) + alert(t('historyManager.editPrepared')) + } catch (error) { + console.error('保存编辑状态失败:', error) + } + } + + // 过滤历史记录 + const filteredHistory = searchFilter + ? history.filter(item => + item.keyword.toLowerCase().includes(searchFilter.toLowerCase()) || + item.engine.toLowerCase().includes(searchFilter.toLowerCase()) || + item.syntax.site?.toLowerCase().includes(searchFilter.toLowerCase()) || + item.syntax.fileType?.toLowerCase().includes(searchFilter.toLowerCase()) + ) + : history + + return ( +
+ {/* 顶部操作栏 */} +
+
+ setSearchFilter(e.target.value)} + className="input w-full" + /> +
+ +
+ + {/* 统计信息 */} +
+ {searchFilter ? ( +

+ {t('historyManager.filterResults', { + count: filteredHistory.length, + total: history.length + })} +

+ ) : ( +

{t('historyManager.totalRecords', { count: history.length })}

+ )} +
+ + {/* 历史记录列表 */} + {filteredHistory.length > 0 ? ( +
+ {filteredHistory.map((item) => ( +
+
+
handleQuickSearch(item)} + title={t('historyManager.quickSearchTooltip')} + > +
+

+ {item.keyword || t('searchHistory.emptyKeyword')} +

+ + {getEngineName(item.engine)} + +
+ +

+ {formatTime(item.timestamp)} +

+ + {/* 搜索语法标签 */} + {getSyntaxBadges(item).length > 0 && ( +
+ {getSyntaxBadges(item).map((badge) => ( + + {badge.label} + + ))} +
+ )} + + {/* 生成的查询 */} +
+ + {item.generatedQuery} + +
+
+ + {/* 操作按钮 */} +
+ + +
+
+
+ ))} +
+ ) : ( +
+ + + +

+ {searchFilter + ? t('historyManager.noFilterResults') + : t('searchHistory.emptyState') + } +

+
+ )} + + {/* 使用提示 */} +
+

+ 💡 {t('historyManager.usageTips')} +

+
    +
  • {t('historyManager.tip1')}
  • +
  • {t('historyManager.tip2')}
  • +
+
+
+ ) +} diff --git a/src/components/SearchHistory.tsx b/src/components/SearchHistory.tsx index 7006225..cd3a501 100644 --- a/src/components/SearchHistory.tsx +++ b/src/components/SearchHistory.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import type { SearchHistory } from '@/types' import { formatDistanceToNow } from 'date-fns' import { zhCN, enUS } from 'date-fns/locale' @@ -12,6 +13,7 @@ interface SearchHistoryProps { export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProps) { const { t, language } = useTranslation() const locale = language === 'en-US' ? enUS : zhCN + const [showAll, setShowAll] = useState(false) // 获取搜索引擎显示名称 const getEngineName = (engine: string) => { @@ -61,6 +63,10 @@ export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProp return badges } + // 计算要显示的记录数 + const displayCount = showAll ? history.length : 10 + const hasMore = history.length > 10 + return (
@@ -77,7 +83,7 @@ export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProp
- {history.slice(0, 10).map((item) => ( + {history.slice(0, displayCount).map((item) => (
{t('searchHistory.emptyState')}

)} - - {history.length > 10 && ( -
-

- {t('searchHistory.summary', { count: history.length })} -

-
- )}
+ + {/* 展开/收起按钮 - 当历史记录超过10条时显示 */} + {hasMore && ( +
+
+

+ {showAll + ? t('searchHistory.showingAll', { count: history.length }) + : t('searchHistory.showingRecent', { showing: 10, total: history.length }) + } +

+ + {!showAll && ( +

+ {t('searchHistory.viewAllHint')} +

+ )} +
+
+ )}
) } diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index fddeb81..0eb50a0 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -49,11 +49,19 @@ const translations: Record> = { 'searchHistory.emptyState': '暂无搜索历史', 'searchHistory.summary': '显示最近10条记录,共{count}条', 'searchHistory.unknownTime': '未知时间', + 'searchHistory.showingAll': '已展开全部 {count} 条记录', + 'searchHistory.showingRecent': '显示最近 {showing} 条,共 {total} 条', + 'searchHistory.expandAll': '展开全部 {count} 条记录 ↓', + 'searchHistory.collapse': '收起历史记录 ↑', + 'searchHistory.viewAllHint': '或前往设置页面查看完整历史记录', 'options.header.title': '搜索语法大师 - 设置', 'options.header.subtitle': '设置和配置', + 'options.tabs.settings': '设置', + 'options.tabs.history': '历史记录', 'options.sections.basic': '基本设置', 'options.sections.features': '功能设置', 'options.sections.data': '数据管理', + 'options.sections.historyManagement': '历史记录管理', 'options.fields.defaultEngine.label': '默认搜索引擎', 'options.fields.language.label': '界面语言', 'options.fields.theme.label': '主题设置', @@ -81,6 +89,7 @@ const translations: Record> = { 'options.confirm.reset': '确定要重置所有设置吗?\n\n这将恢复默认配置,但会保留您的搜索历史记录。\n操作将立即生效并自动保存。', 'options.confirm.clearAll': '确定要清除所有数据吗?这将删除搜索历史、设置等所有信息,且无法恢复。', 'options.confirm.clearAllSecond': '请再次确认:此操作不可撤销!', + 'options.confirm.clearHistory': '确定要清除所有历史记录吗?此操作无法恢复。', 'options.alert.importSuccess': '数据导入成功!页面将重新加载。', 'options.alert.importFailure': '导入失败: {error}', 'options.alert.importInvalid': '无效的备份文件格式', @@ -89,7 +98,19 @@ const translations: Record> = { 'options.footer.title': '关于', 'options.footer.version': '版本: {version}', 'options.footer.author': '作者: {author}', - 'options.footer.homepage': '项目主页' + 'options.footer.homepage': '项目主页', + 'historyManager.searchPlaceholder': '搜索历史记录...', + 'historyManager.totalRecords': '共 {count} 条历史记录', + 'historyManager.filterResults': '找到 {count} 条记录(共 {total} 条)', + 'historyManager.quickSearch': '立即搜索', + 'historyManager.editSearch': '编辑后搜索', + 'historyManager.quickSearchTooltip': '直接执行此搜索', + 'historyManager.editSearchTooltip': 'Shift+点击可编辑参数后搜索', + 'historyManager.editPrepared': '搜索参数已准备好,请点击浏览器工具栏的扩展图标查看和编辑', + 'historyManager.noFilterResults': '未找到匹配的历史记录', + 'historyManager.usageTips': '使用提示', + 'historyManager.tip1': '点击历史记录可直接执行搜索', + 'historyManager.tip2': '点击"编辑后搜索"按钮可在弹窗中修改参数后搜索' }, 'en-US': { 'common.languages.zh-CN': 'Simplified Chinese', @@ -139,11 +160,19 @@ const translations: Record> = { 'searchHistory.emptyState': 'No search history yet', 'searchHistory.summary': 'Showing the latest 10 records, {count} total', 'searchHistory.unknownTime': 'Unknown time', + 'searchHistory.showingAll': 'Showing all {count} records', + 'searchHistory.showingRecent': 'Showing latest {showing} of {total} records', + 'searchHistory.expandAll': 'Show all {count} records ↓', + 'searchHistory.collapse': 'Collapse history ↑', + 'searchHistory.viewAllHint': 'Or visit settings page to view complete history', 'options.header.title': 'SearchSyntax Pro - Settings', 'options.header.subtitle': 'Settings & configuration', + 'options.tabs.settings': 'Settings', + 'options.tabs.history': 'History', 'options.sections.basic': 'Basic settings', 'options.sections.features': 'Feature settings', 'options.sections.data': 'Data management', + 'options.sections.historyManagement': 'History Management', 'options.fields.defaultEngine.label': 'Default search engine', 'options.fields.language.label': 'Interface language', 'options.fields.theme.label': 'Theme', @@ -171,6 +200,7 @@ const translations: Record> = { 'options.confirm.reset': 'Reset all settings?\n\nThis will restore default configuration but keep your search history.\nChanges will take effect immediately and be saved automatically.', 'options.confirm.clearAll': 'Are you sure you want to clear all data? This will delete history, settings, and cannot be undone.', 'options.confirm.clearAllSecond': 'Please confirm again: this action cannot be undone!', + 'options.confirm.clearHistory': 'Are you sure you want to clear all history? This action cannot be undone.', 'options.alert.importSuccess': 'Data imported successfully! The page will reload.', 'options.alert.importFailure': 'Import failed: {error}', 'options.alert.importInvalid': 'Invalid backup file format', @@ -179,7 +209,19 @@ const translations: Record> = { 'options.footer.title': 'About', 'options.footer.version': 'Version: {version}', 'options.footer.author': 'Author: {author}', - 'options.footer.homepage': 'Project home' + 'options.footer.homepage': 'Project home', + 'historyManager.searchPlaceholder': 'Search history...', + 'historyManager.totalRecords': 'Total {count} records', + 'historyManager.filterResults': 'Found {count} records (of {total} total)', + 'historyManager.quickSearch': 'Quick Search', + 'historyManager.editSearch': 'Edit & Search', + 'historyManager.quickSearchTooltip': 'Execute this search directly', + 'historyManager.editSearchTooltip': 'Shift+click to edit parameters before searching', + 'historyManager.editPrepared': 'Search parameters prepared. Please click the extension icon in the browser toolbar to view and edit.', + 'historyManager.noFilterResults': 'No matching history found', + 'historyManager.usageTips': 'Usage Tips', + 'historyManager.tip1': 'Click on a history item to execute the search directly', + 'historyManager.tip2': 'Click "Edit & Search" button to modify parameters in popup before searching' } } diff --git a/src/options/App.tsx b/src/options/App.tsx index c096a20..62dbe1c 100644 --- a/src/options/App.tsx +++ b/src/options/App.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react' import { ThemeProvider } from '@/hooks/useTheme' import { useStorage } from '@/hooks/useStorage' import { Logo } from '@/components/Logo' -import type { UserSettings, Language } from '@/types' +import { HistoryManager } from '@/components/HistoryManager' +import type { UserSettings, Language, SearchHistory } from '@/types' import { DEFAULT_SETTINGS } from '@/types' import { TranslationProvider, useTranslation, translate } from '@/i18n' @@ -12,17 +13,34 @@ function App() { const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [message, setMessage] = useState<{ type: 'success' | 'error'; key: string } | null>(null) + const [history, setHistory] = useState([]) + const [activeTab, setActiveTab] = useState<'settings' | 'history'>('settings') - // 从存储中加载设置 + // 从存储中加载设置和历史记录 const { data: storedSettings, save: saveSettings } = useStorage('user_settings', DEFAULT_SETTINGS) + const { data: storedHistory } = useStorage('search_history') useEffect(() => { setSettings(storedSettings || DEFAULT_SETTINGS) setLoading(false) }, [storedSettings]) + useEffect(() => { + if (storedHistory) { + setHistory(storedHistory) + } + }, [storedHistory]) + const language = settings?.language ?? 'zh-CN' + // 清除历史记录 + const handleClearHistory = () => { + if (confirm(translate(language, 'options.confirm.clearHistory'))) { + setHistory([]) + chrome.storage.local.remove('search_history') + } + } + // 保存设置 const handleSaveSettings = async () => { if (!settings) return @@ -161,11 +179,15 @@ function App() { updateSettings={updateSettings} message={message} saving={saving} + history={history} + activeTab={activeTab} + onTabChange={setActiveTab} onReset={handleResetSettings} onSave={handleSaveSettings} onExport={handleExportData} onImport={handleImportData} onClearAll={handleClearAllData} + onClearHistory={handleClearHistory} /> @@ -179,11 +201,15 @@ interface OptionsContentProps { updateSettings: (updater: (prev: UserSettings) => UserSettings) => void message: { type: 'success' | 'error'; key: string } | null saving: boolean + history: SearchHistory[] + activeTab: 'settings' | 'history' + onTabChange: (tab: 'settings' | 'history') => void onReset: () => void onSave: () => void onExport: () => void onImport: () => void onClearAll: () => void + onClearHistory: () => void } function OptionsContent({ @@ -192,11 +218,15 @@ function OptionsContent({ updateSettings, message, saving, + history, + activeTab, + onTabChange, onReset, onSave, onExport, onImport, - onClearAll + onClearAll, + onClearHistory }: OptionsContentProps) { const { t } = useTranslation() @@ -220,13 +250,39 @@ function OptionsContent({
{/* 头部 */}
-
- -
-

- {t('options.header.title')} -

-

{t('options.header.subtitle')}

+
+
+ +
+

+ {t('options.header.title')} +

+

{t('options.header.subtitle')}

+
+
+ + {/* Tab 切换 */} +
+ +
@@ -234,14 +290,26 @@ function OptionsContent({ {/* 消息提示 */} {message && (
{t(message.key)}
)} + {/* Tab 内容 */} + {activeTab === 'history' ? ( +
+

+ {t('options.sections.historyManagement')} +

+ +
+ ) : ( + <> + {/* 原有的设置内容 */} + {/* 基本设置 */}

@@ -460,26 +528,30 @@ function OptionsContent({

- {/* 关于信息 */} -
-

- {t('options.footer.title')} -

-
-

{t('options.footer.version', { version: '1.0.0' })}

-

{t('options.footer.author', { author: '冷火凉烟' })}

-

- - {t('options.footer.homepage')} - -

-
-
+ {/* 关于信息 - 仅在设置页面显示 */} + {activeTab === 'settings' && ( + + )} + + )} ) } diff --git a/src/popup/App.tsx b/src/popup/App.tsx index e9d9f5b..af0abbd 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -45,6 +45,18 @@ function App() { } }, [storedHistory]) + // 监听从 Options 页面传递的待恢复历史记录 + useEffect(() => { + chrome.storage.local.get('pending_restore_history', (data) => { + if (data.pending_restore_history) { + const historyItem = data.pending_restore_history as SearchHistoryType + restoreFromHistory(historyItem) + // 清除临时状态 + chrome.storage.local.remove('pending_restore_history') + } + }) + }, []) + // 生成搜索查询 const generateQuery = (params: SearchParams) => { try {