镜像自地址
https://github.com/lhly/search-syntax-pro.git
已同步 2026-10-20 22:37:48 +08:00
- 新增全局快捷键支持(Ctrl+Shift+F/Command+Shift+F) - 添加快捷键命令类型定义(chrome-commands.d.ts) - 实现版本管理工具(version.ts) - 新增模板和快捷键相关类型定义 - 优化搜索表单组件支持外部控制高级选项 - 新增引擎选择器组件(EngineSelector) - 改进全局样式和按钮样式 - 在设置页面新增快捷键配置选项 - 添加测试页面和调试工具 - 更新文档包括发布说明和测试指南 本次提交包含多项功能新增和优化,主要围绕快捷键功能的实现和用户体验提升。
39 KiB
39 KiB
P0 - 核心体验提升功能
优先级: P0(最高) 目标: 降低学习成本 60%+,提升专业用户效率 50%+ 周期: 2-3 周 版本: v1.6.0
📋 功能清单
| 功能 | 用户价值 | 技术复杂度 | 工作量 | 负责人 |
|---|---|---|---|---|
| 搜索模板/预设 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 3-5天 | 待定 |
| 键盘快捷键系统 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 2-3天 | 待定 |
| 智能语法推荐引擎 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 5-7天 | 待定 |
1. 搜索模板/预设功能
📖 功能描述
提供预定义的搜索模板,让用户无需学习复杂语法即可快速执行常见搜索场景。
🎯 用户价值
- 降低学习门槛: 新用户无需了解语法即可使用高级功能
- 提升效率: 一键应用模板,减少重复配置
- 知识传播: 内置最佳实践,教育用户正确使用搜索语法
💡 核心设计
数据结构
// src/types/template.ts
/**
* 搜索模板接口
*/
export interface SearchTemplate {
/** 模板唯一标识 */
id: string;
/** 模板名称 */
name: string;
/** 模板描述 */
description: string;
/** 模板图标 (emoji) */
icon: string;
/** 模板分类 */
category: TemplateCategory;
/** 搜索参数 */
params: Partial<SearchParams>;
/** 标签列表(用于搜索和分类) */
tags: string[];
/** 是否为内置模板 */
isBuiltIn: boolean;
/** 创建时间 */
createdAt: number;
/** 使用次数 */
usageCount: number;
}
/**
* 模板分类
*/
export type TemplateCategory =
| 'academic' // 学术研究
| 'tech' // 技术开发
| 'news' // 新闻资讯
| 'social' // 社交媒体
| 'shopping' // 购物比价
| 'media' // 图片视频
| 'custom'; // 自定义
/**
* 模板分组
*/
export interface TemplateGroup {
category: TemplateCategory;
name: string;
icon: string;
templates: SearchTemplate[];
}
内置模板定义
// src/data/builtin-templates.ts
export const BUILTIN_TEMPLATES: SearchTemplate[] = [
// ========== 学术研究 ==========
{
id: 'academic_paper',
name: '学术论文搜索',
description: '搜索 PDF 格式的学术论文,限定近5年',
icon: '📚',
category: 'academic',
params: {
engine: 'google',
fileType: 'pdf',
dateRange: {
from: '2020-01-01',
to: new Date().toISOString().split('T')[0]
}
},
tags: ['学术', '论文', 'PDF', '科研'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
{
id: 'scholar_google',
name: 'Google Scholar 搜索',
description: '在 Google Scholar 中搜索学术文献',
icon: '🎓',
category: 'academic',
params: {
engine: 'google',
site: 'scholar.google.com',
keyword: ''
},
tags: ['学术', 'Google', '文献'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
// ========== 技术开发 ==========
{
id: 'github_code',
name: 'GitHub 代码搜索',
description: '搜索 GitHub 上的开源代码',
icon: '💻',
category: 'tech',
params: {
engine: 'github',
keyword: '',
language: 'TypeScript'
},
tags: ['代码', 'GitHub', '开源'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
{
id: 'stackoverflow_qa',
name: 'Stack Overflow 问答',
description: '在 Stack Overflow 搜索技术问题解答',
icon: '❓',
category: 'tech',
params: {
engine: 'stackoverflow',
keyword: '',
tags: ['javascript']
},
tags: ['问答', 'Stack Overflow', '编程'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
{
id: 'tech_blog',
name: '技术博客搜索',
description: '搜索主流技术博客平台的文章',
icon: '✍️',
category: 'tech',
params: {
engine: 'google',
keyword: '',
orKeywords: ['site:dev.to', 'site:medium.com', 'site:hashnode.com']
},
tags: ['博客', '教程', '技术文章'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
// ========== 新闻资讯 ==========
{
id: 'news_recent',
name: '最新新闻',
description: '搜索最近24小时的新闻',
icon: '📰',
category: 'news',
params: {
engine: 'google',
keyword: '',
dateRange: {
from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0],
to: new Date().toISOString().split('T')[0]
}
},
tags: ['新闻', '热点', '实时'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
// ========== 社交媒体 ==========
{
id: 'twitter_trending',
name: 'Twitter 热门话题',
description: '搜索 Twitter 上的热门讨论',
icon: '🐦',
category: 'social',
params: {
engine: 'twitter',
keyword: '',
minRetweets: 100,
minFaves: 500,
contentFilters: ['links', 'media']
},
tags: ['Twitter', '热点', '社交'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
{
id: 'reddit_discussion',
name: 'Reddit 讨论',
description: '搜索 Reddit 上的相关讨论',
icon: '🔴',
category: 'social',
params: {
engine: 'reddit',
keyword: '',
site: 'reddit.com'
},
tags: ['Reddit', '社区', '讨论'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
// ========== 图片视频 ==========
{
id: 'image_search',
name: '高清图片搜索',
description: '搜索高分辨率图片',
icon: '🖼️',
category: 'media',
params: {
engine: 'google',
keyword: '',
fileType: 'jpg',
exactMatch: 'high resolution'
},
tags: ['图片', '高清', '素材'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
},
// ========== 文档资料 ==========
{
id: 'official_docs',
name: '官方文档搜索',
description: '在官方文档网站中搜索',
icon: '📖',
category: 'tech',
params: {
engine: 'google',
keyword: '',
site: '', // 用户需要指定
inUrl: 'docs'
},
tags: ['文档', '官方', 'API'],
isBuiltIn: true,
createdAt: Date.now(),
usageCount: 0
}
];
模板管理服务
// src/services/template-manager.ts
import { BUILTIN_TEMPLATES } from '@/data/builtin-templates';
import { STORAGE_KEYS } from '@/types';
import type { SearchTemplate, TemplateCategory, SearchParams } from '@/types';
/**
* 模板管理器
*/
export class TemplateManager {
private templates: SearchTemplate[] = [];
constructor() {
this.loadTemplates();
}
/**
* 加载模板(内置 + 用户自定义)
*/
async loadTemplates(): Promise<void> {
// 加载内置模板
const builtIn = [...BUILTIN_TEMPLATES];
// 加载用户自定义模板
const custom = await this.loadCustomTemplates();
// 合并并按使用次数排序
this.templates = [...builtIn, ...custom].sort(
(a, b) => b.usageCount - a.usageCount
);
}
/**
* 加载用户自定义模板
*/
private async loadCustomTemplates(): Promise<SearchTemplate[]> {
const result = await chrome.storage.local.get(STORAGE_KEYS.TEMPLATES);
return result[STORAGE_KEYS.TEMPLATES] || [];
}
/**
* 获取所有模板
*/
getAllTemplates(): SearchTemplate[] {
return this.templates;
}
/**
* 按分类获取模板
*/
getTemplatesByCategory(category: TemplateCategory): SearchTemplate[] {
return this.templates.filter(t => t.category === category);
}
/**
* 搜索模板
*/
searchTemplates(query: string): SearchTemplate[] {
const lowerQuery = query.toLowerCase();
return this.templates.filter(t =>
t.name.toLowerCase().includes(lowerQuery) ||
t.description.toLowerCase().includes(lowerQuery) ||
t.tags.some(tag => tag.toLowerCase().includes(lowerQuery))
);
}
/**
* 应用模板
*/
async applyTemplate(
templateId: string,
currentParams: SearchParams
): Promise<SearchParams> {
const template = this.templates.find(t => t.id === templateId);
if (!template) {
throw new Error(`模板不存在: ${templateId}`);
}
// 增加使用次数
await this.incrementUsage(templateId);
// 合并参数(保留用户已输入的关键词)
return {
...template.params,
keyword: currentParams.keyword || template.params.keyword || '',
} as SearchParams;
}
/**
* 保存为自定义模板
*/
async saveAsTemplate(
name: string,
description: string,
params: SearchParams,
category: TemplateCategory = 'custom'
): Promise<SearchTemplate> {
const template: SearchTemplate = {
id: `custom_${Date.now()}`,
name,
description,
icon: this.getCategoryIcon(category),
category,
params,
tags: this.extractTags(params),
isBuiltIn: false,
createdAt: Date.now(),
usageCount: 0
};
// 保存到存储
const custom = await this.loadCustomTemplates();
custom.push(template);
await chrome.storage.local.set({
[STORAGE_KEYS.TEMPLATES]: custom
});
// 重新加载
await this.loadTemplates();
return template;
}
/**
* 删除自定义模板
*/
async deleteTemplate(templateId: string): Promise<void> {
const template = this.templates.find(t => t.id === templateId);
if (!template) {
throw new Error(`模板不存在: ${templateId}`);
}
if (template.isBuiltIn) {
throw new Error('无法删除内置模板');
}
// 从存储中删除
const custom = await this.loadCustomTemplates();
const filtered = custom.filter(t => t.id !== templateId);
await chrome.storage.local.set({
[STORAGE_KEYS.TEMPLATES]: filtered
});
// 重新加载
await this.loadTemplates();
}
/**
* 增加模板使用次数
*/
private async incrementUsage(templateId: string): Promise<void> {
const template = this.templates.find(t => t.id === templateId);
if (!template) return;
template.usageCount++;
// 如果是自定义模板,更新存储
if (!template.isBuiltIn) {
const custom = await this.loadCustomTemplates();
const index = custom.findIndex(t => t.id === templateId);
if (index !== -1) {
custom[index].usageCount = template.usageCount;
await chrome.storage.local.set({
[STORAGE_KEYS.TEMPLATES]: custom
});
}
}
}
/**
* 从参数中提取标签
*/
private extractTags(params: SearchParams): string[] {
const tags: string[] = [];
if (params.site) tags.push(`site:${params.site}`);
if (params.fileType) tags.push(params.fileType);
if (params.engine) tags.push(params.engine);
if (params.exactMatch) tags.push('精确匹配');
if (params.dateRange) tags.push('日期范围');
return tags;
}
/**
* 获取分类图标
*/
private getCategoryIcon(category: TemplateCategory): string {
const icons: Record<TemplateCategory, string> = {
academic: '📚',
tech: '💻',
news: '📰',
social: '🐦',
shopping: '🛒',
media: '🖼️',
custom: '⭐'
};
return icons[category];
}
}
// 导出单例
export const templateManager = new TemplateManager();
UI 组件设计
// src/components/TemplateSelector.tsx
import { useState, useEffect } from 'react';
import { templateManager } from '@/services/template-manager';
import type { SearchTemplate, TemplateCategory, SearchParams } from '@/types';
interface TemplateSelectorProps {
currentParams: SearchParams;
onApplyTemplate: (params: SearchParams) => void;
}
export function TemplateSelector({ currentParams, onApplyTemplate }: TemplateSelectorProps) {
const [templates, setTemplates] = useState<SearchTemplate[]>([]);
const [selectedCategory, setSelectedCategory] = useState<TemplateCategory | 'all'>('all');
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
loadTemplates();
}, []);
const loadTemplates = async () => {
await templateManager.loadTemplates();
setTemplates(templateManager.getAllTemplates());
};
const filteredTemplates = templates.filter(t => {
// 分类过滤
if (selectedCategory !== 'all' && t.category !== selectedCategory) {
return false;
}
// 搜索过滤
if (searchQuery) {
const query = searchQuery.toLowerCase();
return (
t.name.toLowerCase().includes(query) ||
t.description.toLowerCase().includes(query) ||
t.tags.some(tag => tag.toLowerCase().includes(query))
);
}
return true;
});
const handleApply = async (templateId: string) => {
const newParams = await templateManager.applyTemplate(templateId, currentParams);
onApplyTemplate(newParams);
};
return (
<div className="template-selector">
{/* 搜索框 */}
<input
type="text"
placeholder="搜索模板..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="input mb-3"
/>
{/* 分类标签 */}
<div className="flex gap-2 mb-4 overflow-x-auto">
<button
onClick={() => setSelectedCategory('all')}
className={`btn btn-sm ${selectedCategory === 'all' ? 'btn-primary' : 'btn-ghost'}`}
>
全部
</button>
<button
onClick={() => setSelectedCategory('academic')}
className={`btn btn-sm ${selectedCategory === 'academic' ? 'btn-primary' : 'btn-ghost'}`}
>
📚 学术
</button>
<button
onClick={() => setSelectedCategory('tech')}
className={`btn btn-sm ${selectedCategory === 'tech' ? 'btn-primary' : 'btn-ghost'}`}
>
💻 技术
</button>
<button
onClick={() => setSelectedCategory('news')}
className={`btn btn-sm ${selectedCategory === 'news' ? 'btn-primary' : 'btn-ghost'}`}
>
📰 新闻
</button>
<button
onClick={() => setSelectedCategory('social')}
className={`btn btn-sm ${selectedCategory === 'social' ? 'btn-primary' : 'btn-ghost'}`}
>
🐦 社交
</button>
</div>
{/* 模板列表 */}
<div className="grid grid-cols-1 gap-2 max-h-96 overflow-y-auto">
{filteredTemplates.map(template => (
<div
key={template.id}
className="p-3 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition"
onClick={() => handleApply(template.id)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xl">{template.icon}</span>
<h4 className="font-medium">{template.name}</h4>
{!template.isBuiltIn && (
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">
自定义
</span>
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{template.description}
</p>
<div className="flex gap-1 flex-wrap">
{template.tags.map(tag => (
<span
key={tag}
className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded"
>
{tag}
</span>
))}
</div>
</div>
<div className="text-xs text-gray-500">
使用 {template.usageCount} 次
</div>
</div>
</div>
))}
</div>
{filteredTemplates.length === 0 && (
<div className="text-center py-8 text-gray-500">
未找到匹配的模板
</div>
)}
</div>
);
}
📝 实施步骤
- 第1天: 数据结构设计和内置模板定义
- 第2天: TemplateManager 服务实现
- 第3天: TemplateSelector UI 组件开发
- 第4天: 集成到主界面,测试交互流程
- 第5天: 优化体验,添加动画和反馈
✅ 验收标准
- 提供至少 15 个内置模板
- 支持按分类和搜索筛选模板
- 模板应用后正确填充搜索参数
- 支持保存为自定义模板
- 模板使用次数统计准确
- UI 响应流畅,动画自然
2. 键盘快捷键系统
📖 功能描述
提供全局和局部键盘快捷键,让专业用户通过键盘完成所有操作,减少鼠标依赖。
🎯 用户价值
- 效率提升: 专业用户操作速度提升 50%+
- 无缝体验: 无需中断思考,全键盘操作流
- 可访问性: 提升键盘导航用户的体验
💡 核心设计
快捷键定义
// src/config/keyboard-shortcuts.ts
/**
* 快捷键定义
*/
export interface KeyboardShortcut {
/** 快捷键组合 (例: 'Ctrl+K', 'Alt+Shift+F') */
key: string;
/** 快捷键描述 */
description: string;
/** 执行的动作 */
action: string;
/** 作用域 ('global' | 'popup' | 'options') */
scope: ShortcutScope;
/** 是否可自定义 */
customizable: boolean;
}
export type ShortcutScope = 'global' | 'popup' | 'options';
/**
* 内置快捷键映射
*/
export const DEFAULT_SHORTCUTS: Record<string, KeyboardShortcut> = {
// ========== 全局快捷键 ==========
'open_popup': {
key: 'Ctrl+Shift+F',
description: '打开搜索面板',
action: 'OPEN_POPUP',
scope: 'global',
customizable: true
},
// ========== 弹窗内快捷键 ==========
'execute_search': {
key: 'Ctrl+Enter',
description: '执行搜索',
action: 'EXECUTE_SEARCH',
scope: 'popup',
customizable: true
},
'open_history': {
key: 'Ctrl+H',
description: '打开搜索历史',
action: 'OPEN_HISTORY',
scope: 'popup',
customizable: true
},
'open_templates': {
key: 'Ctrl+T',
description: '打开模板选择器',
action: 'OPEN_TEMPLATES',
scope: 'popup',
customizable: true
},
'copy_query': {
key: 'Ctrl+Shift+C',
description: '复制生成的查询',
action: 'COPY_QUERY',
scope: 'popup',
customizable: true
},
'toggle_advanced': {
key: 'Ctrl+A',
description: '切换高级选项',
action: 'TOGGLE_ADVANCED',
scope: 'popup',
customizable: true
},
'close_popup': {
key: 'Escape',
description: '关闭面板',
action: 'CLOSE_POPUP',
scope: 'popup',
customizable: false
},
// 快速切换搜索引擎
'switch_engine_1': {
key: 'Ctrl+1',
description: '切换到第1个搜索引擎',
action: 'SWITCH_ENGINE:0',
scope: 'popup',
customizable: true
},
'switch_engine_2': {
key: 'Ctrl+2',
description: '切换到第2个搜索引擎',
action: 'SWITCH_ENGINE:1',
scope: 'popup',
customizable: true
},
// ... 其他引擎切换快捷键
// 导航快捷键
'focus_keyword': {
key: 'Ctrl+K',
description: '聚焦关键词输入框',
action: 'FOCUS_KEYWORD',
scope: 'popup',
customizable: true
},
'next_field': {
key: 'Tab',
description: '下一个输入框',
action: 'NEXT_FIELD',
scope: 'popup',
customizable: false
},
'prev_field': {
key: 'Shift+Tab',
description: '上一个输入框',
action: 'PREV_FIELD',
scope: 'popup',
customizable: false
},
};
快捷键管理器
// src/services/shortcut-manager.ts
import { DEFAULT_SHORTCUTS } from '@/config/keyboard-shortcuts';
import type { KeyboardShortcut, ShortcutScope } from '@/config/keyboard-shortcuts';
/**
* 快捷键管理器
*/
export class ShortcutManager {
private shortcuts: Map<string, KeyboardShortcut> = new Map();
private handlers: Map<string, () => void> = new Map();
constructor() {
this.loadShortcuts();
this.setupListeners();
}
/**
* 加载快捷键配置
*/
private async loadShortcuts(): Promise<void> {
// 加载默认快捷键
Object.entries(DEFAULT_SHORTCUTS).forEach(([id, shortcut]) => {
this.shortcuts.set(id, shortcut);
});
// 加载用户自定义快捷键
const custom = await this.loadCustomShortcuts();
Object.entries(custom).forEach(([id, shortcut]) => {
if (shortcut.customizable) {
this.shortcuts.set(id, shortcut);
}
});
}
/**
* 加载用户自定义快捷键
*/
private async loadCustomShortcuts(): Promise<Record<string, KeyboardShortcut>> {
const result = await chrome.storage.local.get('custom_shortcuts');
return result.custom_shortcuts || {};
}
/**
* 设置监听器
*/
private setupListeners(): void {
document.addEventListener('keydown', this.handleKeyPress.bind(this));
}
/**
* 处理按键事件
*/
private handleKeyPress(event: KeyboardEvent): void {
const key = this.normalizeKey(event);
// 查找匹配的快捷键
for (const [id, shortcut] of this.shortcuts.entries()) {
if (shortcut.key === key && this.isInScope(shortcut.scope)) {
event.preventDefault();
event.stopPropagation();
// 执行注册的处理器
const handler = this.handlers.get(id);
if (handler) {
handler();
}
break;
}
}
}
/**
* 规范化按键组合
*/
private normalizeKey(event: KeyboardEvent): string {
const parts: string[] = [];
if (event.ctrlKey || event.metaKey) parts.push('Ctrl');
if (event.altKey) parts.push('Alt');
if (event.shiftKey) parts.push('Shift');
// 特殊键
if (['Escape', 'Enter', 'Tab', 'Space'].includes(event.key)) {
parts.push(event.key);
} else if (event.key.length === 1) {
// 字母或数字
parts.push(event.key.toUpperCase());
}
return parts.join('+');
}
/**
* 检查是否在快捷键作用域内
*/
private isInScope(scope: ShortcutScope): boolean {
// 实现逻辑:检查当前页面类型
const currentPage = window.location.pathname;
if (scope === 'global') return true;
if (scope === 'popup') return currentPage.includes('popup');
if (scope === 'options') return currentPage.includes('options');
return false;
}
/**
* 注册快捷键处理器
*/
register(shortcutId: string, handler: () => void): void {
this.handlers.set(shortcutId, handler);
}
/**
* 注销快捷键处理器
*/
unregister(shortcutId: string): void {
this.handlers.delete(shortcutId);
}
/**
* 获取所有快捷键
*/
getAllShortcuts(): KeyboardShortcut[] {
return Array.from(this.shortcuts.values());
}
/**
* 更新自定义快捷键
*/
async updateShortcut(shortcutId: string, newKey: string): Promise<void> {
const shortcut = this.shortcuts.get(shortcutId);
if (!shortcut || !shortcut.customizable) {
throw new Error('快捷键不可自定义');
}
// 检查冲突
const conflict = this.findConflict(newKey, shortcutId);
if (conflict) {
throw new Error(`快捷键冲突: ${conflict.description}`);
}
// 更新快捷键
shortcut.key = newKey;
this.shortcuts.set(shortcutId, shortcut);
// 保存到存储
const custom = await this.loadCustomShortcuts();
custom[shortcutId] = shortcut;
await chrome.storage.local.set({ custom_shortcuts: custom });
}
/**
* 查找快捷键冲突
*/
private findConflict(key: string, excludeId: string): KeyboardShortcut | null {
for (const [id, shortcut] of this.shortcuts.entries()) {
if (id !== excludeId && shortcut.key === key) {
return shortcut;
}
}
return null;
}
}
// 导出单例
export const shortcutManager = new ShortcutManager();
快捷键提示组件
// src/components/ShortcutHint.tsx
import { useState, useEffect } from 'react';
import { shortcutManager } from '@/services/shortcut-manager';
import type { KeyboardShortcut } from '@/config/keyboard-shortcuts';
export function ShortcutHint() {
const [visible, setVisible] = useState(false);
const [shortcuts, setShortcuts] = useState<KeyboardShortcut[]>([]);
useEffect(() => {
setShortcuts(shortcutManager.getAllShortcuts());
// 按 ? 键显示快捷键帮助
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) {
setVisible(!visible);
}
};
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
}, [visible]);
if (!visible) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl max-h-96 overflow-y-auto">
<h3 className="text-lg font-bold mb-4">键盘快捷键</h3>
<div className="space-y-4">
{/* 全局快捷键 */}
<div>
<h4 className="font-medium mb-2">全局快捷键</h4>
<table className="w-full text-sm">
<tbody>
{shortcuts.filter(s => s.scope === 'global').map(shortcut => (
<tr key={shortcut.action}>
<td className="py-1 pr-4">
<kbd className="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">
{shortcut.key}
</kbd>
</td>
<td className="py-1">{shortcut.description}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 弹窗快捷键 */}
<div>
<h4 className="font-medium mb-2">弹窗快捷键</h4>
<table className="w-full text-sm">
<tbody>
{shortcuts.filter(s => s.scope === 'popup').map(shortcut => (
<tr key={shortcut.action}>
<td className="py-1 pr-4">
<kbd className="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">
{shortcut.key}
</kbd>
</td>
<td className="py-1">{shortcut.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="mt-4 text-sm text-gray-500 text-center">
按 ? 键或 ESC 键关闭此窗口
</div>
</div>
</div>
);
}
📝 实施步骤
- 第1天: 快捷键定义和 ShortcutManager 实现
- 第2天: 集成到主界面,实现核心快捷键
- 第3天: 快捷键帮助 UI 和自定义设置
✅ 验收标准
- 支持至少 15 个常用快捷键
- 全局快捷键可在任意页面唤起扩展
- 快捷键冲突检测和提示
- 支持自定义快捷键配置
- 按 ? 键显示快捷键帮助
3. 智能语法推荐引擎
📖 功能描述
基于关键词分析和历史行为,自动推荐适合的搜索语法组合。
🎯 用户价值
- 智能辅助: AI 级别的搜索优化建议
- 学习效果: 通过推荐让用户了解最佳实践
- 精确度提升: 提高搜索结果相关性 30-40%
💡 核心设计
推荐引擎架构
// src/services/suggestion-engine.ts
import type { SearchParams, SearchHistory, SyntaxType } from '@/types';
/**
* 语法建议
*/
export interface SyntaxSuggestion {
/** 建议类型 */
type: 'auto' | 'context' | 'pattern' | 'history';
/** 建议的语法 */
syntax: SyntaxType;
/** 建议理由 */
reason: string;
/** 置信度 (0-1) */
confidence: number;
/** 预览效果 */
preview: string;
/** 应用后的参数 */
appliedParams: Partial<SearchParams>;
}
/**
* 智能推荐引擎
*/
export class SuggestionEngine {
/**
* 获取推荐建议
*/
getSuggestions(
keyword: string,
currentParams: SearchParams,
history: SearchHistory[]
): SyntaxSuggestion[] {
const suggestions: SyntaxSuggestion[] = [];
// 1. 基于关键词模式识别
suggestions.push(...this.analyzeKeywordPattern(keyword, currentParams));
// 2. 基于搜索历史推荐
suggestions.push(...this.analyzeHistory(keyword, history));
// 3. 基于上下文推荐
suggestions.push(...this.analyzeContext(currentParams));
// 按置信度排序并去重
return this.deduplicateAndSort(suggestions);
}
/**
* 关键词模式识别
*/
private analyzeKeywordPattern(
keyword: string,
currentParams: SearchParams
): SyntaxSuggestion[] {
const suggestions: SyntaxSuggestion[] = [];
const lowerKeyword = keyword.toLowerCase();
// 学术搜索意图
if (/论文|研究|学术|paper|research|journal/i.test(keyword)) {
if (!currentParams.fileType) {
suggestions.push({
type: 'pattern',
syntax: 'filetype',
reason: '检测到学术搜索意图,建议添加 PDF 过滤',
confidence: 0.85,
preview: 'filetype:pdf',
appliedParams: { fileType: 'pdf' }
});
}
if (!currentParams.dateRange) {
suggestions.push({
type: 'pattern',
syntax: 'date_range',
reason: '学术搜索建议限制近5年文献',
confidence: 0.75,
preview: '2020-01-01 至今',
appliedParams: {
dateRange: {
from: '2020-01-01',
to: new Date().toISOString().split('T')[0]
}
}
});
}
}
// 站内搜索意图
const sitePattern = /在(\w+)\s*搜索|search\s+in\s+(\w+)|site:(\S+)/i;
const siteMatch = keyword.match(sitePattern);
if (siteMatch && !currentParams.site) {
const siteName = siteMatch[1] || siteMatch[2] || siteMatch[3];
suggestions.push({
type: 'pattern',
syntax: 'site',
reason: '检测到站内搜索意图',
confidence: 0.9,
preview: `site:${siteName}.com`,
appliedParams: { site: `${siteName}.com` }
});
}
// 新闻时效性意图
if (/最新|今日|最近|latest|recent|today/i.test(keyword)) {
if (!currentParams.dateRange) {
suggestions.push({
type: 'pattern',
syntax: 'date_range',
reason: '检测到时效性需求,建议限制最近24小时',
confidence: 0.8,
preview: '最近24小时',
appliedParams: {
dateRange: {
from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0],
to: new Date().toISOString().split('T')[0]
}
}
});
}
}
// 精确匹配建议
if (keyword.length > 15 && !currentParams.exactMatch) {
suggestions.push({
type: 'pattern',
syntax: 'exact',
reason: '关键词较长,建议使用精确匹配提高准确度',
confidence: 0.7,
preview: `"${keyword}"`,
appliedParams: { exactMatch: keyword }
});
}
// 技术文档搜索
if (/文档|教程|api|docs|documentation|tutorial|guide/i.test(keyword)) {
if (!currentParams.inUrl) {
suggestions.push({
type: 'pattern',
syntax: 'inurl',
reason: '技术搜索建议限定文档URL',
confidence: 0.75,
preview: 'inurl:docs',
appliedParams: { inUrl: 'docs' }
});
}
}
return suggestions;
}
/**
* 基于历史记录推荐
*/
private analyzeHistory(
keyword: string,
history: SearchHistory[]
): SyntaxSuggestion[] {
const suggestions: SyntaxSuggestion[] = [];
// 查找相似的历史搜索
const similar = history.filter(h =>
this.calculateSimilarity(h.keyword, keyword) > 0.7
);
if (similar.length === 0) return suggestions;
// 统计常用语法组合
const syntaxUsage: Record<string, number> = {};
similar.forEach(h => {
if (h.syntax.site) syntaxUsage['site'] = (syntaxUsage['site'] || 0) + 1;
if (h.syntax.fileType) syntaxUsage['filetype'] = (syntaxUsage['filetype'] || 0) + 1;
if (h.syntax.exactMatch) syntaxUsage['exact'] = (syntaxUsage['exact'] || 0) + 1;
if (h.syntax.dateRange) syntaxUsage['date_range'] = (syntaxUsage['date_range'] || 0) + 1;
});
// 生成历史推荐
Object.entries(syntaxUsage).forEach(([syntax, count]) => {
const confidence = Math.min(count / similar.length, 0.9);
if (confidence > 0.5) {
suggestions.push({
type: 'history',
syntax: syntax as SyntaxType,
reason: `您在 ${count} 次类似搜索中使用了此语法`,
confidence,
preview: this.getPreview(syntax, similar[0]),
appliedParams: this.extractParams(syntax, similar[0])
});
}
});
return suggestions;
}
/**
* 基于上下文推荐
*/
private analyzeContext(currentParams: SearchParams): SyntaxSuggestion[] {
const suggestions: SyntaxSuggestion[] = [];
// 如果已设置 site,建议添加 inurl
if (currentParams.site && !currentParams.inUrl) {
suggestions.push({
type: 'context',
syntax: 'inurl',
reason: '已限定站点,可进一步限定 URL 路径',
confidence: 0.6,
preview: 'inurl:blog',
appliedParams: { inUrl: 'blog' }
});
}
// 如果设置了精确匹配,建议添加排除词
if (currentParams.exactMatch && !currentParams.excludeWords?.length) {
suggestions.push({
type: 'context',
syntax: 'exclude',
reason: '精确匹配可能结果过多,建议排除不相关词',
confidence: 0.5,
preview: '-广告 -推广',
appliedParams: { excludeWords: ['广告', '推广'] }
});
}
return suggestions;
}
/**
* 计算关键词相似度 (Jaccard 相似度)
*/
private calculateSimilarity(str1: string, str2: string): number {
const set1 = new Set(str1.toLowerCase().split(/\s+/));
const set2 = new Set(str2.toLowerCase().split(/\s+/));
const intersection = new Set([...set1].filter(x => set2.has(x)));
const union = new Set([...set1, ...set2]);
return intersection.size / union.size;
}
/**
* 去重并按置信度排序
*/
private deduplicateAndSort(suggestions: SyntaxSuggestion[]): SyntaxSuggestion[] {
const seen = new Set<string>();
const unique = suggestions.filter(s => {
const key = `${s.syntax}:${s.preview}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
return unique.sort((a, b) => b.confidence - a.confidence).slice(0, 5);
}
/**
* 获取预览文本
*/
private getPreview(syntax: string, history: SearchHistory): string {
switch (syntax) {
case 'site':
return `site:${history.syntax.site}`;
case 'filetype':
return `filetype:${history.syntax.fileType}`;
case 'exact':
return `"${history.syntax.exactMatch}"`;
case 'date_range':
return `${history.syntax.dateRange?.from} 至 ${history.syntax.dateRange?.to}`;
default:
return syntax;
}
}
/**
* 提取参数
*/
private extractParams(syntax: string, history: SearchHistory): Partial<SearchParams> {
switch (syntax) {
case 'site':
return { site: history.syntax.site };
case 'filetype':
return { fileType: history.syntax.fileType };
case 'exact':
return { exactMatch: history.syntax.exactMatch };
case 'date_range':
return { dateRange: history.syntax.dateRange };
default:
return {};
}
}
}
// 导出单例
export const suggestionEngine = new SuggestionEngine();
推荐建议 UI 组件
// src/components/SuggestionPanel.tsx
import { useState, useEffect } from 'react';
import { suggestionEngine } from '@/services/suggestion-engine';
import type { SyntaxSuggestion, SearchParams, SearchHistory } from '@/types';
interface SuggestionPanelProps {
keyword: string;
currentParams: SearchParams;
history: SearchHistory[];
onApplySuggestion: (params: Partial<SearchParams>) => void;
}
export function SuggestionPanel({
keyword,
currentParams,
history,
onApplySuggestion
}: SuggestionPanelProps) {
const [suggestions, setSuggestions] = useState<SyntaxSuggestion[]>([]);
useEffect(() => {
if (keyword.trim()) {
const newSuggestions = suggestionEngine.getSuggestions(
keyword,
currentParams,
history
);
setSuggestions(newSuggestions);
} else {
setSuggestions([]);
}
}, [keyword, currentParams, history]);
if (suggestions.length === 0) return null;
return (
<div className="suggestion-panel bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3 mb-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 dark:text-blue-400">💡</span>
<h4 className="font-medium text-sm text-blue-900 dark:text-blue-100">
智能推荐
</h4>
</div>
<div className="space-y-2">
{suggestions.map((suggestion, index) => (
<div
key={index}
className="bg-white dark:bg-gray-800 rounded p-2 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition"
onClick={() => onApplySuggestion(suggestion.appliedParams)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">
{suggestion.preview}
</code>
<span className="text-xs text-gray-500">
{Math.round(suggestion.confidence * 100)}% 匹配
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
{suggestion.reason}
</p>
</div>
<button className="text-xs text-blue-600 hover:text-blue-800">
应用
</button>
</div>
</div>
))}
</div>
</div>
);
}
📝 实施步骤
- 第1-2天: SuggestionEngine 核心逻辑实现
- 第3-4天: 模式识别规则完善
- 第5天: SuggestionPanel UI 组件开发
- 第6-7天: 集成测试和规则优化
✅ 验收标准
- 至少支持 8 种模式识别规则
- 历史推荐准确率 > 70%
- 推荐建议置信度计算合理
- 推荐建议 UI 清晰易懂
- 一键应用推荐参数
📊 P0 功能整体验收
性能指标
- 模板加载时间 < 200ms
- 快捷键响应时间 < 50ms
- 推荐建议生成时间 < 100ms
用户体验指标
- 新用户完成首次搜索时间 < 30秒
- 模板使用率 > 40%
- 快捷键使用率 > 20%(专业用户)
- 推荐建议采纳率 > 30%
质量指标
- 单元测试覆盖率 > 80%
- E2E 测试通过率 100%
- 无 P0/P1 级别 Bug
📚 相关文档
下一步: 完成 P0 功能后,继续阅读 P1 功能扩展文档