From fdfd17657c982208f2e5b04c35e5ffac9bfa7eca Mon Sep 17 00:00:00 2001 From: CNWei Date: Fri, 6 Nov 2026 12:30:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(search):=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=BC=95=E6=93=8E=E6=94=AF=E6=8C=81=E5=9B=BE=E6=A0=87=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=EF=BC=88png=20/=20svg=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - settings.search.engines[] 新增 icon:空=品牌图标/通用地球,'/' 开头走托管目录,或 http(s) 链接 - 后端校验图标地址(空 / 托管路径 / http(s),≤500 字符) - 后台每张引擎卡加「图标地址」输入;前后台都用组件库 EngineIcon 的 src( 渲染,SVG 静态不执行脚本) - @stash/ui 0.3.4 --- HANDOFF.md | 4 ++- src/db/settings.rs | 32 ++++++++++++++++++++ stash-web/src/AppNext.tsx | 13 +++++--- stash-web/src/components/admin/AdminNext.tsx | 15 ++++++++- stash-web/src/settings.ts | 7 +++++ 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index e798485..044d2c7 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -117,7 +117,7 @@ stash-ui/src/components/ SearchField / Input / EngineIcon 等 "search": { "default": "internal", "engines": [ - { "id": "google", "name": "Google", "url": "https://www.google.com/search?q={query}", "enabled": true }, + { "id": "google", "name": "Google", "url": "https://www.google.com/search?q={query}", "icon": "", "enabled": true }, { "id": "bing", "name": "Bing", "url": "https://www.bing.com/search?q={query}", "enabled": true }, { "id": "baidu", "name": "百度", "url": "https://www.baidu.com/s?wd={query}", "enabled": true }, { "id": "sogou", "name": "搜狗", "url": "https://www.sogou.com/web?query={query}", "enabled": true }, @@ -134,6 +134,8 @@ stash-ui/src/components/ SearchField / Input / EngineIcon 等 - 外部引擎 URL 必须以 `http(s)://` 开头并包含 `{query}`,否则后端 400。 - 引擎 id 只能用小写字母 / 数字 / `-` / `_`(1–30 位,`internal` 保留);内置五个用品牌图标, 后台新加的引擎 id 自动生成为 `custom-xxx`、图标用通用地球(组件库 EngineIcon 自动回落)。 +- 每个引擎可带 `icon`(png / svg 地址,`/` 开头的托管路径或 http(s) 链接):给了就用 `` 渲染自己 + 的图标,留空回落到品牌图标 / 地球;SVG 走 img 是静态的,不执行脚本。 - `background.light/dark` 为空时用内置 `stash-web/src/assets/site-bg.jpg` / `site-bg-dark.jpg`;否则填托管目录 `public/` 路径或完整 URL。 - 旧 settings JSON 缺失字段会自动回落默认值;多余旧字段在下次保存时被丢弃。 diff --git a/src/db/settings.rs b/src/db/settings.rs index c92af3c..c74f5df 100644 --- a/src/db/settings.rs +++ b/src/db/settings.rs @@ -162,6 +162,8 @@ pub struct SearchEngineSettings { pub name: String, /// 搜索地址模板,用 `{query}` 表示关键词 pub url: String, + /// 自定义图标地址(png / svg);空表示用内置品牌图标或通用地球 + pub icon: String, pub enabled: bool, } @@ -182,30 +184,35 @@ impl Default for SearchSettings { id: "google".into(), name: "Google".into(), url: "https://www.google.com/search?q={query}".into(), + icon: String::new(), enabled: true, }, SearchEngineSettings { id: "bing".into(), name: "Bing".into(), url: "https://www.bing.com/search?q={query}".into(), + icon: String::new(), enabled: true, }, SearchEngineSettings { id: "baidu".into(), name: "百度".into(), url: "https://www.baidu.com/s?wd={query}".into(), + icon: String::new(), enabled: true, }, SearchEngineSettings { id: "sogou".into(), name: "搜狗".into(), url: "https://www.sogou.com/web?query={query}".into(), + icon: String::new(), enabled: true, }, SearchEngineSettings { id: "360".into(), name: "360".into(), url: "https://www.so.com/s?q={query}".into(), + icon: String::new(), enabled: true, }, ], @@ -469,6 +476,19 @@ pub fn validate(settings: &Settings) -> Result<(), StashError> { "搜索地址需以 http(s):// 开头并包含 {query}".into(), )); } + let icon = engine.icon.trim(); + if icon.chars().count() > 500 { + return Err(StashError::InvalidDirectory("引擎图标地址最多 500 个字符".into())); + } + if !icon.is_empty() + && !(icon.starts_with("http://") + || icon.starts_with("https://") + || icon.starts_with('/')) + { + return Err(StashError::InvalidDirectory( + "引擎图标需为空、/ 开头的托管路径或 http(s):// 链接".into(), + )); + } } let default = settings.search.default.trim(); if default != "internal" { @@ -637,6 +657,7 @@ mod tests { id: "my-engine_1".into(), name: "自定义".into(), url: "https://example.com/search?q={query}".into(), + icon: "/favicon.svg".into(), enabled: true, }); assert!(save(&mut conn, settings, 0).is_ok(), "合法自定义 id 应能保存"); @@ -647,10 +668,21 @@ mod tests { id: bad_id.into(), name: "坏".into(), url: "https://example.com/?q={query}".into(), + icon: String::new(), enabled: true, }); assert!(save(&mut conn, bad, 0).is_err(), "{bad_id} 应被拒绝"); } + + let mut bad_icon = Settings::default(); + bad_icon.search.engines.push(SearchEngineSettings { + id: "custom-x".into(), + name: "坏图标".into(), + url: "https://example.com/?q={query}".into(), + icon: "javascript:alert(1)".into(), + enabled: true, + }); + assert!(save(&mut conn, bad_icon, 0).is_err(), "非法图标地址应被拒绝"); } #[test] diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx index a963e3a..aab45c2 100644 --- a/stash-web/src/AppNext.tsx +++ b/stash-web/src/AppNext.tsx @@ -69,7 +69,7 @@ type Route = | { kind: 'item'; key: string; itemId: string } | { kind: 'api' } -const INTERNAL_SEARCH = { id: 'internal', name: '站内搜索', url: '' } as const +const INTERNAL_SEARCH = { id: 'internal', name: '站内搜索', url: '', icon: '' } as const const SEARCH_ENGINE_KEY = 'stash-search-engine' @@ -362,7 +362,12 @@ export default function AppNext() { INTERNAL_SEARCH, ...site.search.engines .filter((engine) => engine.enabled) - .map((engine) => ({ id: engine.id, name: engine.name, url: engine.url })), + .map((engine) => ({ + id: engine.id, + name: engine.name, + url: engine.url, + icon: engine.icon ?? '', + })), ] const searchEngine = searchEngines.find((engine) => engine.id === searchEngineId) ?? @@ -930,7 +935,7 @@ export default function AppNext() { } onClick={() => setEngineOpen((open) => !open)} > - + {searchEngine.name} @@ -977,7 +982,7 @@ export default function AppNext() { className={`engine-option${engine.id === searchEngine.id ? ' is-active' : ''}`} onClick={() => chooseEngine(engine.id)} > - + {engine.name} ))} diff --git a/stash-web/src/components/admin/AdminNext.tsx b/stash-web/src/components/admin/AdminNext.tsx index 3a22717..29b2353 100644 --- a/stash-web/src/components/admin/AdminNext.tsx +++ b/stash-web/src/components/admin/AdminNext.tsx @@ -2102,6 +2102,7 @@ function SearchPanel({ id: `custom-${Date.now().toString(36)}`, name: '新搜索引擎', url: '', + icon: '', enabled: true, }, ], @@ -2147,7 +2148,7 @@ function SearchPanel({ key={engine.id} >
- + {engine.name || engine.id}
+ + updateEngine(engine.id, { icon: event.target.value })} + /> +
))} diff --git a/stash-web/src/settings.ts b/stash-web/src/settings.ts index 869a1dc..f38544a 100644 --- a/stash-web/src/settings.ts +++ b/stash-web/src/settings.ts @@ -84,6 +84,8 @@ export type SearchEngineSettings = { name: string /** 搜索地址模板,用 {query} 表示关键词 */ url: string + /** 自定义图标地址(png / svg);空表示用品牌图标或通用地球 */ + icon: string enabled: boolean } @@ -99,30 +101,35 @@ export const DEFAULT_SEARCH_ENGINES: SearchEngineSettings[] = [ id: 'google', name: 'Google', url: 'https://www.google.com/search?q={query}', + icon: '', enabled: true, }, { id: 'bing', name: 'Bing', url: 'https://www.bing.com/search?q={query}', + icon: '', enabled: true, }, { id: 'baidu', name: '百度', url: 'https://www.baidu.com/s?wd={query}', + icon: '', enabled: true, }, { id: 'sogou', name: '搜狗', url: 'https://www.sogou.com/web?query={query}', + icon: '', enabled: true, }, { id: '360', name: '360', url: 'https://www.so.com/s?q={query}', + icon: '', enabled: true, }, ]