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({