search-syntax-pro/src/options/App.tsx
Zain 22f8b16839 feat(历史记录): 添加popup显示限制和设置页确认逻辑
添加popup页面最多显示50条记录的限制,并在设置页面添加历史记录超出限制时的确认逻辑
当历史记录超过限制时,显示提示信息并引导用户前往设置页面
2025-11-08 15:53:02 +08:00

599 行
19 KiB
TypeScript

此文件含有不可见的 Unicode 字符

此文件含有人类无法区分的不可见的 Unicode 字符,但可以由计算机进行不同的处理。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import { useState, useEffect } from 'react'
import { ThemeProvider } from '@/hooks/useTheme'
import { useStorage } from '@/hooks/useStorage'
import { Logo } from '@/components/Logo'
import { HistoryManager } from '@/components/HistoryManager'
import type { UserSettings, Language, SearchHistory } from '@/types'
import { DEFAULT_SETTINGS } from '@/types'
import { TranslationProvider, useTranslation, translate } from '@/i18n'
// 设置页面组件
function App() {
const [settings, setSettings] = useState<UserSettings | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; key: string; params?: Record<string, 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: storedHistory } = useStorage<SearchHistory[]>('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
setSaving(true)
setMessage(null)
try {
// 检查是否需要清理历史记录
const currentHistoryData = await chrome.storage.local.get('search_history')
const currentHistory: SearchHistory[] = currentHistoryData.search_history || []
const willDelete = currentHistory.length - settings.historyLimit
// 如果当前记录超过新限制,需要用户确认
if (willDelete > 0) {
const confirmed = confirm(
translate(language, 'options.confirm.trimHistory', {
current: currentHistory.length.toString(),
limit: settings.historyLimit.toString(),
willDelete: willDelete.toString()
})
)
if (!confirmed) {
// 用户取消,不保存设置
setSaving(false)
setMessage({ type: 'error', key: 'options.messages.saveCancelled' })
setTimeout(() => setMessage(null), 3000)
return
}
// 用户确认,立即清理超出的记录(保留最新的N条)
const trimmedHistory = currentHistory.slice(0, settings.historyLimit)
await chrome.storage.local.set({ search_history: trimmedHistory })
// 更新本地状态
setHistory(trimmedHistory)
}
// 保存设置
const success = await saveSettings(settings)
if (success) {
if (willDelete > 0) {
setMessage({
type: 'success',
key: 'options.messages.saveSuccessWithTrim',
params: { removed: willDelete.toString() }
})
} else {
setMessage({ type: 'success', key: 'options.messages.saveSuccess' })
}
} else {
setMessage({ type: 'error', key: 'options.messages.saveFailure' })
}
} catch (error) {
console.error('保存设置失败:', error)
setMessage({ type: 'error', key: 'options.messages.saveFailure' })
} finally {
setSaving(false)
}
// 3秒后清除消息
setTimeout(() => setMessage(null), 3000)
}
// 重置设置
const handleResetSettings = async () => {
if (confirm(translate(language, 'options.confirm.reset'))) {
const defaultSettings = { ...DEFAULT_SETTINGS }
setSettings(defaultSettings)
// 自动保存重置后的设置
setSaving(true)
setMessage(null)
try {
const success = await saveSettings(defaultSettings)
if (success) {
setMessage({ type: 'success', key: 'options.messages.resetSuccess' })
} else {
setMessage({ type: 'error', key: 'options.messages.resetFailure' })
}
} catch (error) {
setMessage({ type: 'error', key: 'options.messages.resetFailure' })
} finally {
setSaving(false)
}
// 3秒后清除消息
setTimeout(() => setMessage(null), 3000)
}
}
const handleExportData = () => {
chrome.storage.local.get(null, (data) => {
const exportData = {
version: '1.0.0',
timestamp: Date.now(),
data: data
}
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 = `ssp-backup-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
})
}
const handleImportData = () => {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json'
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
try {
const importData = JSON.parse(event.target?.result as string)
// 验证数据格式
if (!importData.version || !importData.data) {
throw new Error(translate(language, 'options.alert.importInvalid'))
}
// 导入数据
chrome.storage.local.clear(() => {
chrome.storage.local.set(importData.data, () => {
alert(translate(language, 'options.alert.importSuccess'))
window.location.reload()
})
})
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : translate(language, 'options.alert.unknownError')
alert(translate(language, 'options.alert.importFailure', { error: errorMessage }))
}
}
reader.readAsText(file)
}
input.click()
}
const handleClearAllData = () => {
if (confirm(translate(language, 'options.confirm.clearAll'))) {
if (confirm(translate(language, 'options.confirm.clearAllSecond'))) {
chrome.storage.local.clear(() => {
alert(translate(language, 'options.alert.clearSuccess'))
window.location.reload()
})
}
}
}
const updateSettings = (updater: (prev: UserSettings) => UserSettings) => {
setSettings((prev) => (prev ? updater(prev) : prev))
}
return (
<ThemeProvider>
<TranslationProvider language={language}>
<OptionsContent
loading={loading}
settings={settings}
updateSettings={updateSettings}
message={message}
saving={saving}
history={history}
activeTab={activeTab}
onTabChange={setActiveTab}
onReset={handleResetSettings}
onSave={handleSaveSettings}
onExport={handleExportData}
onImport={handleImportData}
onClearAll={handleClearAllData}
onClearHistory={handleClearHistory}
/>
</TranslationProvider>
</ThemeProvider>
)
}
export default App
interface OptionsContentProps {
loading: boolean
settings: UserSettings | null
updateSettings: (updater: (prev: UserSettings) => UserSettings) => void
message: { type: 'success' | 'error'; key: string; params?: Record<string, 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({
loading,
settings,
updateSettings,
message,
saving,
history,
activeTab,
onTabChange,
onReset,
onSave,
onExport,
onImport,
onClearAll,
onClearHistory
}: OptionsContentProps) {
const { t } = useTranslation()
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
)
}
if (!settings) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-red-600">{t('options.status.loadError')}</div>
</div>
)
}
return (
<div className="space-y-6">
{/* 头部 */}
<header className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Logo size={40} color="#3B82F6" />
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{t('options.header.title')}
</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>
</header>
{/* 消息提示 */}
{message && (
<div className={`p-4 rounded-lg ${
message.type === 'success'
? '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'
}`}>
{t(message.key, message.params)}
</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">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
{t('options.sections.basic')}
</h2>
<div className="space-y-4">
{/* 默认搜索引擎 */}
<div>
<label htmlFor="defaultEngine" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('options.fields.defaultEngine.label')}
</label>
<select
id="defaultEngine"
value={settings.defaultEngine}
onChange={(e) =>
updateSettings((prev) => ({
...prev,
defaultEngine: e.target.value as UserSettings['defaultEngine']
}))
}
className="input"
>
{(['baidu', 'google', 'bing'] as const).map((engine) => (
<option key={engine} value={engine}>
{t(`common.searchEngines.${engine}`)}
</option>
))}
</select>
</div>
{/* 界面语言 */}
<div>
<label htmlFor="language" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('options.fields.language.label')}
</label>
<select
id="language"
value={settings.language}
onChange={(e) =>
updateSettings((prev) => ({
...prev,
language: e.target.value as UserSettings['language']
}))
}
className="input"
>
{(['zh-CN', 'en-US'] as Language[]).map((lang) => (
<option key={lang} value={lang}>
{t(`common.languages.${lang}`)}
</option>
))}
</select>
</div>
{/* 主题设置 */}
<div>
<label htmlFor="theme" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('options.fields.theme.label')}
</label>
<select
id="theme"
value={settings.theme}
onChange={(e) =>
updateSettings((prev) => ({
...prev,
theme: e.target.value as UserSettings['theme']
}))
}
className="input"
>
<option value="auto">{t('options.fields.theme.auto')}</option>
<option value="light">{t('options.fields.theme.light')}</option>
<option value="dark">{t('options.fields.theme.dark')}</option>
</select>
</div>
</div>
</section>
{/* 功能设置 */}
<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.features')}
</h2>
<div className="space-y-4">
{/* 启用搜索历史 */}
<div className="flex items-center justify-between">
<div>
<label htmlFor="enableHistory" className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t('options.fields.enableHistory.label')}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{t('options.fields.enableHistory.description')}
</p>
</div>
<input
id="enableHistory"
type="checkbox"
checked={settings.enableHistory}
onChange={(e) =>
updateSettings((prev) => ({
...prev,
enableHistory: e.target.checked
}))
}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
</div>
{/* 自动在新标签页打开 */}
<div className="flex items-center justify-between">
<div>
<label htmlFor="autoOpenInNewTab" className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t('options.fields.autoOpen.label')}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{t('options.fields.autoOpen.description')}
</p>
</div>
<input
id="autoOpenInNewTab"
type="checkbox"
checked={settings.autoOpenInNewTab}
onChange={(e) =>
updateSettings((prev) => ({
...prev,
autoOpenInNewTab: e.target.checked
}))
}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
</div>
{/* 历史记录限制 */}
<div>
<label htmlFor="historyLimit" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('options.fields.historyLimit.label')}
</label>
<input
id="historyLimit"
type="number"
min="10"
max="5000"
value={settings.historyLimit}
onChange={(e) => {
const parsed = parseInt(e.target.value)
updateSettings((prev) => ({
...prev,
historyLimit: Number.isNaN(parsed) ? prev.historyLimit : parsed
}))
}}
className="input"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('options.fields.historyLimit.description')}
</p>
</div>
</div>
</section>
{/* 数据管理 */}
<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.data')}
</h2>
<div className="flex flex-col gap-5">
<button
onClick={onExport}
className="btn btn-secondary"
>
{t('options.actions.export')}
</button>
<button
onClick={onImport}
className="btn btn-secondary"
>
{t('options.actions.import')}
</button>
<button
onClick={onClearAll}
className="btn btn-danger"
>
{t('options.actions.clearAll')}
</button>
</div>
</section>
{/* 操作按钮 */}
<section className="flex justify-between items-center">
<button
onClick={onReset}
className="btn btn-ghost"
>
{t('options.actions.reset')}
</button>
<div className="space-x-3">
<button
onClick={() => window.close()}
className="btn btn-ghost"
>
{t('options.actions.cancel')}
</button>
<button
onClick={onSave}
disabled={saving}
className="btn btn-primary"
>
{saving ? t('options.actions.saving') : t('options.actions.save')}
</button>
</div>
</section>
{/* 关于信息 - 仅在设置页面显示 */}
{activeTab === 'settings' && (
<footer 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.footer.title')}
</h2>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<p>{t('options.footer.version', { version: '1.0.0' })}</p>
<p>{t('options.footer.author', { author: '冷火凉烟' })}</p>
<p>
<a
href="https://github.com/lhly/search-syntax-pro"
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 hover:text-primary-700"
>
{t('options.footer.homepage')}
</a>
</p>
</div>
</footer>
)}
</>
)}
</div>
)
}