feat(search): 按参考稿重做搜索框并支持后台配置
- 顶部悬浮引擎标签 + 下拉选项,使用 stash-ui 新增的 EngineIcon
- 站内搜索保持现有本地过滤与回车打开首个匹配项
- 外部引擎使用配置的 {query} 模板新标签页搜索
- settings 新增 search.default / search.engines,后台新增『搜索框』配置默认项、名称、地址与启停
- 默认默认搜索项为站内搜索,地址校验必须含 {query}
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Card,
|
||||
Caret,
|
||||
Empty,
|
||||
EngineIcon,
|
||||
FormField,
|
||||
Icon,
|
||||
ICON_NAMES,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
ToggleRow,
|
||||
Toaster,
|
||||
toast,
|
||||
type EngineIconName,
|
||||
type IconName,
|
||||
type MenuItem,
|
||||
} from '@stash/ui'
|
||||
@@ -36,11 +38,13 @@ import { LoginDialog } from '../auth/LoginDialog'
|
||||
import { GroupMark as GroupGlyph } from '../common/GroupMark'
|
||||
import { isDarkScheme, readThemeOverride, writeThemeOverride } from '../../theme'
|
||||
import {
|
||||
DEFAULT_SEARCH_ENGINES,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
TONE_OPTIONS,
|
||||
type ProfileSettings,
|
||||
type BackgroundSettings,
|
||||
type ProfileSettings,
|
||||
type SearchSettings,
|
||||
type SettingsDocument,
|
||||
type SettingsTone,
|
||||
type SiteSettings,
|
||||
@@ -81,6 +85,7 @@ type TabKey =
|
||||
| 'home-cards'
|
||||
| 'home-header'
|
||||
| 'home-background'
|
||||
| 'home-search'
|
||||
| 'profile'
|
||||
| 'system'
|
||||
|
||||
@@ -94,6 +99,7 @@ const ADMIN_NAV: MenuItem[] = [
|
||||
children: [
|
||||
{ key: 'home-header', label: '页头' },
|
||||
{ key: 'home-background', label: '背景管理' },
|
||||
{ key: 'home-search', label: '搜索框' },
|
||||
{ key: 'home-sidebar', label: '侧边栏' },
|
||||
{ key: 'home-cards', label: '内容卡片' },
|
||||
],
|
||||
@@ -391,6 +397,13 @@ export function AdminNext() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === 'home-search' && (
|
||||
<SearchPanel
|
||||
value={draft.search}
|
||||
onChange={(search) => setDraft({ ...draft, search })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === 'profile' && (
|
||||
<ProfilePanel
|
||||
value={draft.profile}
|
||||
@@ -402,6 +415,7 @@ export function AdminNext() {
|
||||
|
||||
{(tab === 'home-header' ||
|
||||
tab === 'home-background' ||
|
||||
tab === 'home-search' ||
|
||||
tab === 'profile') && (
|
||||
<Space gap="sm" justify="end" className="admin-actions">
|
||||
<Button
|
||||
@@ -2051,6 +2065,104 @@ function HeaderPanel({
|
||||
)
|
||||
}
|
||||
|
||||
function SearchPanel({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: SearchSettings
|
||||
onChange: (search: SearchSettings) => void
|
||||
}) {
|
||||
const updateEngine = (
|
||||
id: string,
|
||||
patch: Partial<SearchSettings['engines'][number]>,
|
||||
) => {
|
||||
const engines = value.engines.map((engine) =>
|
||||
engine.id === id ? { ...engine, ...patch } : engine,
|
||||
)
|
||||
const nextDefault =
|
||||
patch.enabled === false && value.default === id ? 'internal' : value.default
|
||||
onChange({ ...value, engines, default: nextDefault })
|
||||
}
|
||||
|
||||
const defaultOptions = [
|
||||
{ value: 'internal', label: '站内搜索' },
|
||||
...value.engines
|
||||
.filter((engine) => engine.enabled)
|
||||
.map((engine) => ({ value: engine.id, label: engine.name })),
|
||||
]
|
||||
|
||||
return (
|
||||
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
||||
<Title level={4}>搜索框</Title>
|
||||
<Text size="sm" tone="muted">
|
||||
说明:站内搜索始终可用;外部搜索引擎可以改名称、地址和启停,地址里的 {'{query}'}{' '}
|
||||
会替换成关键词。默认项是刷新后搜索框选中的引擎。
|
||||
</Text>
|
||||
|
||||
<FormField label="默认搜索项">
|
||||
<Select
|
||||
options={defaultOptions}
|
||||
value={value.default}
|
||||
onChange={(next) => onChange({ ...value, default: next })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="search-engine-list">
|
||||
{value.engines.map((engine) => (
|
||||
<div
|
||||
className={`search-engine-row${engine.enabled ? '' : ' is-disabled'}`}
|
||||
key={engine.id}
|
||||
>
|
||||
<div className="search-engine-head">
|
||||
<EngineIcon name={engine.id as EngineIconName} size={18} />
|
||||
<Text strong>{engine.name || engine.id}</Text>
|
||||
<Switch
|
||||
checked={engine.enabled}
|
||||
label={`启用 ${engine.name || engine.id}`}
|
||||
onChange={(enabled) => updateEngine(engine.id, { enabled })}
|
||||
/>
|
||||
</div>
|
||||
<div className="search-engine-fields">
|
||||
<FormField label="名称" htmlFor={`search-name-${engine.id}`}>
|
||||
<Input
|
||||
id={`search-name-${engine.id}`}
|
||||
value={engine.name}
|
||||
onChange={(event) => updateEngine(engine.id, { name: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="搜索地址"
|
||||
htmlFor={`search-url-${engine.id}`}
|
||||
hint="必须包含 {query}"
|
||||
>
|
||||
<Input
|
||||
id={`search-url-${engine.id}`}
|
||||
value={engine.url}
|
||||
onChange={(event) => updateEngine(engine.id, { url: event.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
default: 'internal',
|
||||
engines: DEFAULT_SEARCH_ENGINES.map((engine) => ({ ...engine })),
|
||||
})
|
||||
}
|
||||
>
|
||||
恢复默认引擎
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
function BackgroundPanel({
|
||||
value,
|
||||
onChange,
|
||||
|
||||
Reference in New Issue
Block a user