From ed34bdba098fb67bdd9f91188a71f5c74f11d986 Mon Sep 17 00:00:00 2001 From: guopenghui Date: Mon, 18 May 2026 13:31:37 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(setting):=20=E7=BD=91=E9=A1=B5?= =?UTF-8?q?=E5=BF=AB=E5=BC=80=E6=94=AF=E6=8C=81=E6=99=AE=E9=80=9A=E7=BD=91?= =?UTF-8?q?=E7=AB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal-plugins/setting/src/env.d.ts | 34 ++- .../WebSearchSetting/WebSearchSetting.vue | 49 +++- .../WebSearchEditor/WebSearchEditor.vue | 156 +++++++++-- src/main/api/renderer/systemCommands.ts | 18 ++ src/main/api/renderer/webSearch.ts | 264 ++++++++++++------ src/main/core/startupDataMigrations.ts | 44 +++ tests/main/startupDataMigrations.test.ts | 89 ++++++ tests/main/systemCommandsWebSearch.test.ts | 125 +++++++++ tests/main/webSearch.test.ts | 219 +++++++++++++++ 9 files changed, 883 insertions(+), 115 deletions(-) create mode 100644 tests/main/startupDataMigrations.test.ts create mode 100644 tests/main/systemCommandsWebSearch.test.ts create mode 100644 tests/main/webSearch.test.ts diff --git a/internal-plugins/setting/src/env.d.ts b/internal-plugins/setting/src/env.d.ts index 14152b3a..6598c20a 100644 --- a/internal-plugins/setting/src/env.d.ts +++ b/internal-plugins/setting/src/env.d.ts @@ -404,9 +404,37 @@ declare global { // 网页快开 webSearch: { - getAll: () => Promise<{ success: boolean; data?: any[]; error?: string }> - add: (engine: any) => Promise<{ success: boolean; error?: string }> - update: (engine: any) => Promise<{ success: boolean; error?: string }> + getAll: () => Promise<{ + success: boolean + data?: Array<{ + id: string + name: string + url: string + icon: string + enabled: boolean + type: 'search' | 'webpage' + keyword?: string + }> + error?: string + }> + add: (engine: { + id: string + name: string + url: string + icon: string + enabled: boolean + type: 'search' | 'webpage' + keyword?: string + }) => Promise<{ success: boolean; error?: string }> + update: (engine: { + id: string + name: string + url: string + icon: string + enabled: boolean + type: 'search' | 'webpage' + keyword?: string + }) => Promise<{ success: boolean; error?: string }> delete: (id: string) => Promise<{ success: boolean; error?: string }> fetchFavicon: ( url: string diff --git a/internal-plugins/setting/src/views/WebSearchSetting/WebSearchSetting.vue b/internal-plugins/setting/src/views/WebSearchSetting/WebSearchSetting.vue index d850a4a7..10e0c451 100644 --- a/internal-plugins/setting/src/views/WebSearchSetting/WebSearchSetting.vue +++ b/internal-plugins/setting/src/views/WebSearchSetting/WebSearchSetting.vue @@ -16,6 +16,8 @@ interface WebSearchEngine { url: string icon: string enabled: boolean + type: 'search' | 'webpage' + keyword?: string } // 搜索引擎列表 @@ -89,6 +91,10 @@ async function handleSave(engine: WebSearchEngine): Promise { error('请填写名称和 URL') return } + if (engine.type === 'webpage' && !engine.keyword?.trim()) { + error('请填写匹配关键字') + return + } try { const engineData = { @@ -96,7 +102,9 @@ async function handleSave(engine: WebSearchEngine): Promise { name: engine.name, url: engine.url, icon: engine.icon || '', - enabled: engine.enabled !== undefined ? engine.enabled : true + enabled: engine.enabled !== undefined ? engine.enabled : true, + type: engine.type || 'search', + keyword: engine.keyword || '' } if (editingEngine.value) { @@ -161,7 +169,7 @@ onMounted(() => {
- +
@@ -182,9 +190,15 @@ onMounted(() => {

{{ engine.name }}

{{ engine.url }}
+
+ 匹配关键字:{{ engine.keyword }} +
+ {{ + engine.type === 'webpage' ? '网页' : '搜索引擎' + }}
@@ -387,6 +401,8 @@ onMounted(() => { .toggle-sm { transform: scale(0.8); transform-origin: center; + display: flex; + align-items: center; } .engine-url { @@ -396,13 +412,40 @@ onMounted(() => { line-height: 1.4; } +.engine-type-badge { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + height: 22px; + padding: 0 6px; + border-radius: 4px; + font-size: 11px; + line-height: 1; + color: var(--primary-color); + background: var(--primary-light-bg); +} + +.engine-keyword { + margin-top: 4px; + font-size: 12px; + color: var(--text-secondary); +} + .engine-actions { display: flex; + align-items: center; gap: 8px; margin-left: 16px; flex-shrink: 0; } +.engine-actions .icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; +} + /* 图标按钮颜色样式 */ .edit-btn { color: var(--primary-color); diff --git a/internal-plugins/setting/src/views/WebSearchSetting/components/WebSearchEditor/WebSearchEditor.vue b/internal-plugins/setting/src/views/WebSearchSetting/components/WebSearchEditor/WebSearchEditor.vue index 938b99c8..4b3a8725 100644 --- a/internal-plugins/setting/src/views/WebSearchSetting/components/WebSearchEditor/WebSearchEditor.vue +++ b/internal-plugins/setting/src/views/WebSearchSetting/components/WebSearchEditor/WebSearchEditor.vue @@ -8,6 +8,8 @@ interface WebSearchEngine { url: string icon: string enabled: boolean + type: 'search' | 'webpage' + keyword?: string } interface Props { @@ -23,6 +25,16 @@ const emit = defineEmits<{ const { error } = useToast() const isEditing = computed(() => props.editingEngine !== null) +const isWebpage = computed(() => formData.value.type === 'webpage') +const urlLabel = computed(() => (isWebpage.value ? '网页 URL *' : 'URL 模板 *')) +const urlPlaceholder = computed(() => + isWebpage.value ? '例如:https://www.example.com' : '例如:https://www.google.com/search?q={q}' +) +const urlHint = computed(() => + isWebpage.value + ? '支持 http/https,未填写协议时会自动补充 https://' + : '使用 {q} 作为搜索关键词的占位符;未填写协议时会自动补充 https://' +) const isFetchingIcon = ref(false) const formData = ref({ @@ -30,7 +42,9 @@ const formData = ref({ name: '', url: '', icon: '', - enabled: true + enabled: true, + type: 'webpage', + keyword: '' }) // 监听 editingEngine 变化 @@ -38,14 +52,20 @@ watch( () => props.editingEngine, (newEngine) => { if (newEngine) { - formData.value = { ...newEngine } + formData.value = { + ...newEngine, + type: newEngine.type || 'search', + keyword: newEngine.keyword || '' + } } else { formData.value = { id: '', name: '', url: '', icon: '', - enabled: true + enabled: true, + type: 'webpage', + keyword: '' } } }, @@ -77,39 +97,109 @@ async function handleFetchFavicon(): Promise { function handleSave(): void { if (!formData.value.name || !formData.value.url) { - error('请填写名称和 URL 模板') + error('请填写名称和 URL') return } - if (!formData.value.url.includes('{q}')) { - error('URL 模板必须包含 {q} 占位符') - return + + const nextData = { + ...formData.value, + name: formData.value.name.trim(), + url: formData.value.url.trim(), + keyword: formData.value.keyword?.trim() || '' + } + + if (nextData.type === 'webpage') { + if (!nextData.keyword) { + error('请填写匹配关键字') + return + } + if (nextData.url.includes('{q}')) { + error('网页 URL 不能包含 {q} 占位符') + return + } + nextData.url = ensureUrlProtocol(nextData.url) + if (!isHttpUrl(nextData.url)) { + error('网页 URL 必须是有效的 http/https 地址') + return + } + } else { + if (!nextData.url.includes('{q}')) { + error('URL 模板必须包含 {q} 占位符') + return + } + nextData.url = ensureUrlProtocol(nextData.url) + if (!isHttpUrl(nextData.url.replace('{q}', 'test'))) { + error('URL 模板必须是有效的 http/https 地址') + return + } + nextData.keyword = '' + } + + emit('save', nextData) +} + +function ensureUrlProtocol(url: string): string { + if (/^https?:\/\//i.test(url)) { + return url + } + return `https://${url}` +} + +function isHttpUrl(url: string): boolean { + try { + const parsed = new URL(url) + return parsed.protocol === 'http:' || parsed.protocol === 'https:' + } catch { + return false } - emit('save', { ...formData.value }) }