镜像自地址
https://github.com/lhly/search-syntax-pro.git
已同步 2026-10-21 22:45:41 +08:00
feat(history): 添加历史记录管理功能
实现历史记录管理页面,包含以下功能: - 添加历史记录搜索和过滤功能 - 支持快速搜索和编辑后搜索 - 添加历史记录展开/收起功能 - 添加从选项页面恢复历史记录的功能
这个提交包含在:
父节点
f32329ce19
当前提交
d64d047a36
260
src/components/HistoryManager.tsx
普通文件
260
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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 顶部操作栏 */}
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('historyManager.searchPlaceholder')}
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
className="input w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClear}
|
||||||
|
className="btn btn-danger"
|
||||||
|
disabled={history.length === 0}
|
||||||
|
>
|
||||||
|
{t('searchHistory.clearButton')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{searchFilter ? (
|
||||||
|
<p>
|
||||||
|
{t('historyManager.filterResults', {
|
||||||
|
count: filteredHistory.length,
|
||||||
|
total: history.length
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>{t('historyManager.totalRecords', { count: history.length })}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 历史记录列表 */}
|
||||||
|
{filteredHistory.length > 0 ? (
|
||||||
|
<div className="space-y-3 max-h-[600px] overflow-y-auto">
|
||||||
|
{filteredHistory.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-500 dark:hover:border-primary-500 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div
|
||||||
|
className="flex-1 cursor-pointer"
|
||||||
|
onClick={() => handleQuickSearch(item)}
|
||||||
|
title={t('historyManager.quickSearchTooltip')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<p className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{item.keyword || t('searchHistory.emptyKeyword')}
|
||||||
|
</p>
|
||||||
|
<span className="text-xs px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">
|
||||||
|
{getEngineName(item.engine)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
{formatTime(item.timestamp)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 搜索语法标签 */}
|
||||||
|
{getSyntaxBadges(item).length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mb-2">
|
||||||
|
{getSyntaxBadges(item).map((badge) => (
|
||||||
|
<span
|
||||||
|
key={badge.key}
|
||||||
|
className={`badge ${badge.className} text-xs`}
|
||||||
|
>
|
||||||
|
{badge.label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 生成的查询 */}
|
||||||
|
<div className="p-2 bg-gray-50 dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-600">
|
||||||
|
<code className="text-xs text-gray-600 dark:text-gray-400 break-all">
|
||||||
|
{item.generatedQuery}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickSearch(item)}
|
||||||
|
className="btn btn-sm btn-primary whitespace-nowrap"
|
||||||
|
title={t('historyManager.quickSearchTooltip')}
|
||||||
|
>
|
||||||
|
🚀 {t('historyManager.quickSearch')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleEditSearch(item, e)}
|
||||||
|
className="btn btn-sm btn-ghost whitespace-nowrap"
|
||||||
|
title={t('historyManager.editSearchTooltip')}
|
||||||
|
>
|
||||||
|
📝 {t('historyManager.editSearch')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
|
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-base">
|
||||||
|
{searchFilter
|
||||||
|
? t('historyManager.noFilterResults')
|
||||||
|
: t('searchHistory.emptyState')
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用提示 */}
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300 mb-2">
|
||||||
|
💡 {t('historyManager.usageTips')}
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-blue-600 dark:text-blue-400 space-y-1 list-disc list-inside">
|
||||||
|
<li>{t('historyManager.tip1')}</li>
|
||||||
|
<li>{t('historyManager.tip2')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import type { SearchHistory } from '@/types'
|
import type { SearchHistory } from '@/types'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import { zhCN, enUS } from 'date-fns/locale'
|
import { zhCN, enUS } from 'date-fns/locale'
|
||||||
@ -12,6 +13,7 @@ interface SearchHistoryProps {
|
|||||||
export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProps) {
|
export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProps) {
|
||||||
const { t, language } = useTranslation()
|
const { t, language } = useTranslation()
|
||||||
const locale = language === 'en-US' ? enUS : zhCN
|
const locale = language === 'en-US' ? enUS : zhCN
|
||||||
|
const [showAll, setShowAll] = useState(false)
|
||||||
|
|
||||||
// 获取搜索引擎显示名称
|
// 获取搜索引擎显示名称
|
||||||
const getEngineName = (engine: string) => {
|
const getEngineName = (engine: string) => {
|
||||||
@ -61,6 +63,10 @@ export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProp
|
|||||||
return badges
|
return badges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算要显示的记录数
|
||||||
|
const displayCount = showAll ? history.length : 10
|
||||||
|
const hasMore = history.length > 10
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card p-4">
|
<div className="card p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@ -77,7 +83,7 @@ export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||||
{history.slice(0, 10).map((item) => (
|
{history.slice(0, displayCount).map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer transition-colors"
|
className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer transition-colors"
|
||||||
@ -125,15 +131,35 @@ export function SearchHistory({ history, onRestore, onClear }: SearchHistoryProp
|
|||||||
<p className="text-sm">{t('searchHistory.emptyState')}</p>
|
<p className="text-sm">{t('searchHistory.emptyState')}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{history.length > 10 && (
|
|
||||||
<div className="text-center py-2">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{t('searchHistory.summary', { count: history.length })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 展开/收起按钮 - 当历史记录超过10条时显示 */}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-blue-700 dark:text-blue-300 text-center mb-2">
|
||||||
|
{showAll
|
||||||
|
? t('searchHistory.showingAll', { count: history.length })
|
||||||
|
: t('searchHistory.showingRecent', { showing: 10, total: history.length })
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(!showAll)}
|
||||||
|
className="w-full btn btn-sm btn-primary"
|
||||||
|
>
|
||||||
|
{showAll
|
||||||
|
? t('searchHistory.collapse')
|
||||||
|
: t('searchHistory.expandAll', { count: history.length })
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{!showAll && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2">
|
||||||
|
{t('searchHistory.viewAllHint')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,11 +49,19 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'searchHistory.emptyState': '暂无搜索历史',
|
'searchHistory.emptyState': '暂无搜索历史',
|
||||||
'searchHistory.summary': '显示最近10条记录,共{count}条',
|
'searchHistory.summary': '显示最近10条记录,共{count}条',
|
||||||
'searchHistory.unknownTime': '未知时间',
|
'searchHistory.unknownTime': '未知时间',
|
||||||
|
'searchHistory.showingAll': '已展开全部 {count} 条记录',
|
||||||
|
'searchHistory.showingRecent': '显示最近 {showing} 条,共 {total} 条',
|
||||||
|
'searchHistory.expandAll': '展开全部 {count} 条记录 ↓',
|
||||||
|
'searchHistory.collapse': '收起历史记录 ↑',
|
||||||
|
'searchHistory.viewAllHint': '或前往设置页面查看完整历史记录',
|
||||||
'options.header.title': '搜索语法大师 - 设置',
|
'options.header.title': '搜索语法大师 - 设置',
|
||||||
'options.header.subtitle': '设置和配置',
|
'options.header.subtitle': '设置和配置',
|
||||||
|
'options.tabs.settings': '设置',
|
||||||
|
'options.tabs.history': '历史记录',
|
||||||
'options.sections.basic': '基本设置',
|
'options.sections.basic': '基本设置',
|
||||||
'options.sections.features': '功能设置',
|
'options.sections.features': '功能设置',
|
||||||
'options.sections.data': '数据管理',
|
'options.sections.data': '数据管理',
|
||||||
|
'options.sections.historyManagement': '历史记录管理',
|
||||||
'options.fields.defaultEngine.label': '默认搜索引擎',
|
'options.fields.defaultEngine.label': '默认搜索引擎',
|
||||||
'options.fields.language.label': '界面语言',
|
'options.fields.language.label': '界面语言',
|
||||||
'options.fields.theme.label': '主题设置',
|
'options.fields.theme.label': '主题设置',
|
||||||
@ -81,6 +89,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'options.confirm.reset': '确定要重置所有设置吗?\n\n这将恢复默认配置,但会保留您的搜索历史记录。\n操作将立即生效并自动保存。',
|
'options.confirm.reset': '确定要重置所有设置吗?\n\n这将恢复默认配置,但会保留您的搜索历史记录。\n操作将立即生效并自动保存。',
|
||||||
'options.confirm.clearAll': '确定要清除所有数据吗?这将删除搜索历史、设置等所有信息,且无法恢复。',
|
'options.confirm.clearAll': '确定要清除所有数据吗?这将删除搜索历史、设置等所有信息,且无法恢复。',
|
||||||
'options.confirm.clearAllSecond': '请再次确认:此操作不可撤销!',
|
'options.confirm.clearAllSecond': '请再次确认:此操作不可撤销!',
|
||||||
|
'options.confirm.clearHistory': '确定要清除所有历史记录吗?此操作无法恢复。',
|
||||||
'options.alert.importSuccess': '数据导入成功!页面将重新加载。',
|
'options.alert.importSuccess': '数据导入成功!页面将重新加载。',
|
||||||
'options.alert.importFailure': '导入失败: {error}',
|
'options.alert.importFailure': '导入失败: {error}',
|
||||||
'options.alert.importInvalid': '无效的备份文件格式',
|
'options.alert.importInvalid': '无效的备份文件格式',
|
||||||
@ -89,7 +98,19 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'options.footer.title': '关于',
|
'options.footer.title': '关于',
|
||||||
'options.footer.version': '版本: {version}',
|
'options.footer.version': '版本: {version}',
|
||||||
'options.footer.author': '作者: {author}',
|
'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': {
|
'en-US': {
|
||||||
'common.languages.zh-CN': 'Simplified Chinese',
|
'common.languages.zh-CN': 'Simplified Chinese',
|
||||||
@ -139,11 +160,19 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'searchHistory.emptyState': 'No search history yet',
|
'searchHistory.emptyState': 'No search history yet',
|
||||||
'searchHistory.summary': 'Showing the latest 10 records, {count} total',
|
'searchHistory.summary': 'Showing the latest 10 records, {count} total',
|
||||||
'searchHistory.unknownTime': 'Unknown time',
|
'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.title': 'SearchSyntax Pro - Settings',
|
||||||
'options.header.subtitle': 'Settings & configuration',
|
'options.header.subtitle': 'Settings & configuration',
|
||||||
|
'options.tabs.settings': 'Settings',
|
||||||
|
'options.tabs.history': 'History',
|
||||||
'options.sections.basic': 'Basic settings',
|
'options.sections.basic': 'Basic settings',
|
||||||
'options.sections.features': 'Feature settings',
|
'options.sections.features': 'Feature settings',
|
||||||
'options.sections.data': 'Data management',
|
'options.sections.data': 'Data management',
|
||||||
|
'options.sections.historyManagement': 'History Management',
|
||||||
'options.fields.defaultEngine.label': 'Default search engine',
|
'options.fields.defaultEngine.label': 'Default search engine',
|
||||||
'options.fields.language.label': 'Interface language',
|
'options.fields.language.label': 'Interface language',
|
||||||
'options.fields.theme.label': 'Theme',
|
'options.fields.theme.label': 'Theme',
|
||||||
@ -171,6 +200,7 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'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.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.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.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.importSuccess': 'Data imported successfully! The page will reload.',
|
||||||
'options.alert.importFailure': 'Import failed: {error}',
|
'options.alert.importFailure': 'Import failed: {error}',
|
||||||
'options.alert.importInvalid': 'Invalid backup file format',
|
'options.alert.importInvalid': 'Invalid backup file format',
|
||||||
@ -179,7 +209,19 @@ const translations: Record<Language, Record<string, string>> = {
|
|||||||
'options.footer.title': 'About',
|
'options.footer.title': 'About',
|
||||||
'options.footer.version': 'Version: {version}',
|
'options.footer.version': 'Version: {version}',
|
||||||
'options.footer.author': 'Author: {author}',
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useState, useEffect } from 'react'
|
|||||||
import { ThemeProvider } from '@/hooks/useTheme'
|
import { ThemeProvider } from '@/hooks/useTheme'
|
||||||
import { useStorage } from '@/hooks/useStorage'
|
import { useStorage } from '@/hooks/useStorage'
|
||||||
import { Logo } from '@/components/Logo'
|
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 { DEFAULT_SETTINGS } from '@/types'
|
||||||
import { TranslationProvider, useTranslation, translate } from '@/i18n'
|
import { TranslationProvider, useTranslation, translate } from '@/i18n'
|
||||||
|
|
||||||
@ -12,17 +13,34 @@ function App() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; key: string } | null>(null)
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; key: string } | null>(null)
|
||||||
|
const [history, setHistory] = useState<SearchHistory[]>([])
|
||||||
|
const [activeTab, setActiveTab] = useState<'settings' | 'history'>('settings')
|
||||||
|
|
||||||
// 从存储中加载设置
|
// 从存储中加载设置和历史记录
|
||||||
const { data: storedSettings, save: saveSettings } = useStorage<UserSettings>('user_settings', DEFAULT_SETTINGS)
|
const { data: storedSettings, save: saveSettings } = useStorage<UserSettings>('user_settings', DEFAULT_SETTINGS)
|
||||||
|
const { data: storedHistory } = useStorage<SearchHistory[]>('search_history')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSettings(storedSettings || DEFAULT_SETTINGS)
|
setSettings(storedSettings || DEFAULT_SETTINGS)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}, [storedSettings])
|
}, [storedSettings])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (storedHistory) {
|
||||||
|
setHistory(storedHistory)
|
||||||
|
}
|
||||||
|
}, [storedHistory])
|
||||||
|
|
||||||
const language = settings?.language ?? 'zh-CN'
|
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 () => {
|
const handleSaveSettings = async () => {
|
||||||
if (!settings) return
|
if (!settings) return
|
||||||
@ -161,11 +179,15 @@ function App() {
|
|||||||
updateSettings={updateSettings}
|
updateSettings={updateSettings}
|
||||||
message={message}
|
message={message}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
|
history={history}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
onReset={handleResetSettings}
|
onReset={handleResetSettings}
|
||||||
onSave={handleSaveSettings}
|
onSave={handleSaveSettings}
|
||||||
onExport={handleExportData}
|
onExport={handleExportData}
|
||||||
onImport={handleImportData}
|
onImport={handleImportData}
|
||||||
onClearAll={handleClearAllData}
|
onClearAll={handleClearAllData}
|
||||||
|
onClearHistory={handleClearHistory}
|
||||||
/>
|
/>
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
@ -179,11 +201,15 @@ interface OptionsContentProps {
|
|||||||
updateSettings: (updater: (prev: UserSettings) => UserSettings) => void
|
updateSettings: (updater: (prev: UserSettings) => UserSettings) => void
|
||||||
message: { type: 'success' | 'error'; key: string } | null
|
message: { type: 'success' | 'error'; key: string } | null
|
||||||
saving: boolean
|
saving: boolean
|
||||||
|
history: SearchHistory[]
|
||||||
|
activeTab: 'settings' | 'history'
|
||||||
|
onTabChange: (tab: 'settings' | 'history') => void
|
||||||
onReset: () => void
|
onReset: () => void
|
||||||
onSave: () => void
|
onSave: () => void
|
||||||
onExport: () => void
|
onExport: () => void
|
||||||
onImport: () => void
|
onImport: () => void
|
||||||
onClearAll: () => void
|
onClearAll: () => void
|
||||||
|
onClearHistory: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function OptionsContent({
|
function OptionsContent({
|
||||||
@ -192,11 +218,15 @@ function OptionsContent({
|
|||||||
updateSettings,
|
updateSettings,
|
||||||
message,
|
message,
|
||||||
saving,
|
saving,
|
||||||
|
history,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
onReset,
|
onReset,
|
||||||
onSave,
|
onSave,
|
||||||
onExport,
|
onExport,
|
||||||
onImport,
|
onImport,
|
||||||
onClearAll
|
onClearAll,
|
||||||
|
onClearHistory
|
||||||
}: OptionsContentProps) {
|
}: OptionsContentProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -220,13 +250,39 @@ function OptionsContent({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<header className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
<header className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center justify-between">
|
||||||
<Logo size={40} color="#3B82F6" />
|
<div className="flex items-center space-x-4">
|
||||||
<div>
|
<Logo size={40} color="#3B82F6" />
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
<div>
|
||||||
{t('options.header.title')}
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
</h1>
|
{t('options.header.title')}
|
||||||
<p className="text-gray-600 dark:text-gray-400">{t('options.header.subtitle')}</p>
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">{t('options.header.subtitle')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab 切换 */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onTabChange('settings')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
activeTab === 'settings'
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
⚙️ {t('options.tabs.settings')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onTabChange('history')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
activeTab === 'history'
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📜 {t('options.tabs.history')} {history.length > 0 && `(${history.length})`}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -234,14 +290,26 @@ function OptionsContent({
|
|||||||
{/* 消息提示 */}
|
{/* 消息提示 */}
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`p-4 rounded-lg ${
|
<div className={`p-4 rounded-lg ${
|
||||||
message.type === 'success'
|
message.type === 'success'
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||||
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||||
}`}>
|
}`}>
|
||||||
{t(message.key)}
|
{t(message.key)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Tab 内容 */}
|
||||||
|
{activeTab === 'history' ? (
|
||||||
|
<section className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||||
|
{t('options.sections.historyManagement')}
|
||||||
|
</h2>
|
||||||
|
<HistoryManager history={history} onClear={onClearHistory} />
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* 原有的设置内容 */}
|
||||||
|
|
||||||
{/* 基本设置 */}
|
{/* 基本设置 */}
|
||||||
<section className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
<section className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||||
@ -460,26 +528,30 @@ function OptionsContent({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 关于信息 */}
|
{/* 关于信息 - 仅在设置页面显示 */}
|
||||||
<footer className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
{activeTab === 'settings' && (
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
<footer className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||||
{t('options.footer.title')}
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||||
</h2>
|
{t('options.footer.title')}
|
||||||
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
</h2>
|
||||||
<p>{t('options.footer.version', { version: '1.0.0' })}</p>
|
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<p>{t('options.footer.author', { author: '冷火凉烟' })}</p>
|
<p>{t('options.footer.version', { version: '1.0.0' })}</p>
|
||||||
<p>
|
<p>{t('options.footer.author', { author: '冷火凉烟' })}</p>
|
||||||
<a
|
<p>
|
||||||
href="https://github.com/lhly/search-syntax-pro"
|
<a
|
||||||
target="_blank"
|
href="https://github.com/lhly/search-syntax-pro"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="text-primary-600 hover:text-primary-700"
|
rel="noopener noreferrer"
|
||||||
>
|
className="text-primary-600 hover:text-primary-700"
|
||||||
{t('options.footer.homepage')}
|
>
|
||||||
</a>
|
{t('options.footer.homepage')}
|
||||||
</p>
|
</a>
|
||||||
</div>
|
</p>
|
||||||
</footer>
|
</div>
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,18 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [storedHistory])
|
}, [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) => {
|
const generateQuery = (params: SearchParams) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户