876 lines
27 KiB
Markdown
876 lines
27 KiB
Markdown
# AI对话链接跳转与引用高亮优化 - 技术设计文档
|
||
|
||
**文档版本**: v1.0
|
||
**创建日期**: 2026-05-30
|
||
**状态**: ✅ 已批准,待实施
|
||
**方案选择**: 方案B - 统一搜索引擎重构
|
||
|
||
---
|
||
|
||
## 1. 项目背景与问题定义
|
||
|
||
### 1.1 当前问题
|
||
|
||
在AI智能问答模块(QAModule.vue)中,用户点击对话消息中的引用来源或[ref:xxx]标签后:
|
||
|
||
1. **PDF文件**:只能跳转到指定页码,**无法高亮显示关键词位置**
|
||
2. **Word/Excel等文件**:使用`indexOf精确匹配`,关键词截断后容易匹配失败
|
||
3. **时序问题**:文件DOM未完全渲染就执行搜索,导致定位失败
|
||
4. **用户体验差**:无搜索结果反馈、无导航控件、高亮效果短暂
|
||
|
||
### 1.2 业务影响
|
||
|
||
- ❌ 用户无法快速定位到引用的具体内容位置
|
||
- ❌ 需要手动翻页查找,效率低下
|
||
- ❌ 多个匹配项时无法逐个浏览
|
||
- ❌ 降低知识管理系统的易用性和专业性
|
||
|
||
### 1.3 目标用户
|
||
|
||
- 企业员工(制度文件查阅者)
|
||
- HR/行政人员(制度发布者)
|
||
- 管理员(系统维护者)
|
||
|
||
### 1.4 成功标准
|
||
|
||
✅ 点击任意格式的引用 → 自动跳转 + 持续高亮显示
|
||
✅ 支持模糊匹配(容错率≥90%)
|
||
✅ 提供上/下一处导航功能
|
||
✅ 高亮效果美观且持久(手动关闭前不消失)
|
||
✅ 响应时间 < 2秒
|
||
|
||
---
|
||
|
||
## 2. 解决方案架构
|
||
|
||
### 2.1 整体架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ QAModule.vue │
|
||
│ [引用点击] → navigateToFileReader(ref) │
|
||
│ ↓ │
|
||
│ 提取: { fileId, filename, page, context, rawData } │
|
||
└──────────────────────┬──────────────────────────────┘
|
||
↓ router.push(query参数)
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ ReaderPage.vue │
|
||
│ │
|
||
│ ┌───────────────────────────────────────────┐ │
|
||
│ │ UniversalSearchEngine (新增) │ │
|
||
│ │ │ │
|
||
│ │ ┌─────────┐ ┌─────────┐ ┌────────────┐ │ │
|
||
│ │ │ Text │ │ Fuzzy │ │ Highlight │ │ │
|
||
│ │ │Extractor│ │ Search │ │ Renderer │ │ │
|
||
│ │ │ (提取) │ │ Engine │ │ (渲染) │ │ │
|
||
│ │ └────┬────┘ └────┬────┘ └─────┬──────┘ │ │
|
||
│ │ └──────────┼──────────┘ │ │
|
||
│ │ ↓ │ │
|
||
│ │ ┌──────────────────┐ │ │
|
||
│ │ │ SearchCoordinator │ │ │
|
||
│ │ │ (流程协调器) │ │ │
|
||
│ │ └────────┬─────────┘ │ │
|
||
│ └───────────────┼───────────────────────┘ │
|
||
│ ↓ │
|
||
│ SearchResult[] + UI Navigation Bar │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 核心技术选型
|
||
|
||
| 组件 | 技术方案 | 版本 | 选型理由 |
|
||
|------|---------|------|---------|
|
||
| **模糊搜索引擎** | Fuse.js | 7.0.0 | 轻量(10KB)、支持中文、零依赖 |
|
||
| **文本提取** | DOM TreeWalker API | 原生 | 无需依赖、浏览器原生支持 |
|
||
| **高亮渲染** | Range + Custom Elements | 原生 | 精确控制、性能优秀 |
|
||
| **PDF处理** | PDF.js textLayer | 已集成 | 复用现有pdfjs-viewer |
|
||
|
||
---
|
||
|
||
## 3. 模块详细设计
|
||
|
||
### 3.1 模块一:TextExtractor(文本提取器)
|
||
|
||
**职责**:从不同格式文件的DOM中提取结构化文本数据
|
||
|
||
**文件路径**:`src/utils/textExtractor.js`
|
||
|
||
**核心方法**:
|
||
```typescript
|
||
class TextExtractor {
|
||
// 主入口:根据fileType分发到不同的提取策略
|
||
async extract(fileType: string, container: HTMLElement): Promise<Document[]>
|
||
|
||
// PDF文本提取(从iframe的.textLayer提取)
|
||
async extractPdfText(iframe: HTMLIFrameElement): Promise<PdfPage[]>
|
||
|
||
// DOCX/DOC文本提取(从.docx-wrapper提取分页)
|
||
extractDocxText(container: HTMLElement): DocxPage[]
|
||
|
||
// Excel/CSV文本提取(从表格单元格提取)
|
||
extractExcelText(container: HTMLElement): TableCell[]
|
||
|
||
// 通用DOM文本提取(TreeWalker遍历)
|
||
extractDomText(container: HTMLElement): TextNode[]
|
||
}
|
||
```
|
||
|
||
**数据结构**:
|
||
```typescript
|
||
interface Document {
|
||
text: string // 纯文本内容
|
||
type: 'text' | 'page' | 'cell'
|
||
location?: {
|
||
page?: number // 页码(PDF/DOCX)
|
||
cellIndex?: [number, number] // 单元格坐标 [row, col]
|
||
}
|
||
element: HTMLElement // 对应的DOM元素引用
|
||
}
|
||
```
|
||
|
||
**关键实现细节**:
|
||
- PDF:遍历`.textLayer > span`元素,按`data-page`属性分组
|
||
- DOCX:查找`.docx-wrapper > div`作为页面容器
|
||
- Excel:遍历`table tr td`,记录行列索引
|
||
- 通用:使用`TreeWalker(SHOW_TEXT)`过滤有意义的文本节点(长度≥2)
|
||
|
||
---
|
||
|
||
### 3.2 模块二:FuzzySearchEngine(模糊搜索引擎)
|
||
|
||
**职责**:基于Fuse.js实现智能模糊匹配和相关性排序
|
||
|
||
**文件路径**:`src/utils/fuzzySearchEngine.js`
|
||
|
||
**核心配置**:
|
||
```javascript
|
||
const fuseConfig = {
|
||
threshold: 0.4, // 匹配阈值(0=精确, 1=宽松)
|
||
distance: 100, // 模式匹配的最大距离
|
||
includeScore: true, // 返回评分
|
||
includeMatches: true, // 返回匹配位置信息
|
||
minMatchCharLength: 2, // 最小匹配字符数
|
||
tokenize: true, // 启用分词模式
|
||
tokenSeparator: /[\s\p{P}]+/u, // 中英文分词正则
|
||
keys: ['text'] // 搜索字段
|
||
}
|
||
```
|
||
|
||
**核心方法**:
|
||
```typescript
|
||
class FuzzySearchEngine {
|
||
// 初始化索引(每次加载新文档时调用)
|
||
initIndex(documents: Document[]): void
|
||
|
||
// 执行搜索
|
||
search(keyword: string, options?: SearchOptions): SearchResult[]
|
||
|
||
// 关键词预处理(提升中文匹配率)
|
||
preprocessKeyword(keyword: string): string
|
||
|
||
// 格式化原始结果为统一格式
|
||
formatResult(rawResult: FuseResult, index: number): SearchResult
|
||
}
|
||
```
|
||
|
||
**输出数据结构**:
|
||
```typescript
|
||
interface SearchResult {
|
||
id: string // 唯一标识
|
||
type: 'text' | 'pdf' | 'table'
|
||
score: number // 相似度评分 (0-1, 越高越匹配)
|
||
matchedText: string // 匹配到的文本片段
|
||
context: string // 上下文(前后各50字符)
|
||
location: {
|
||
container?: HTMLElement
|
||
startOffset?: number
|
||
endOffset?: number
|
||
pageNumber?: number // PDF专用
|
||
rect?: { x, y, width, height } // PDF专用
|
||
cellIndex?: [number, number] // 表格专用
|
||
}
|
||
originalData: Document // 原始文档数据(用于高亮渲染)
|
||
}
|
||
```
|
||
|
||
**中文优化策略**:
|
||
1. 预处理阶段去除中文标点符号(""''【】《》())
|
||
2. 使用Unicode属性转义`\p{P}`匹配所有标点
|
||
3. 分词时按空格和标点切分
|
||
4. 设置合理的threshold(0.4)平衡精准度和召回率
|
||
|
||
---
|
||
|
||
### 3.3 模块三:HighlightRenderer(高亮渲染器)
|
||
|
||
**职责**:在文档DOM中创建、管理和销毁高亮标记
|
||
|
||
**文件路径**:`src/utils/highlightRenderer.js`
|
||
|
||
**核心能力**:
|
||
```typescript
|
||
class HighlightRenderer {
|
||
// 渲染所有搜索结果的高亮
|
||
renderAll(results: SearchResult[]): number
|
||
|
||
// 创建单个高亮(根据type分发)
|
||
private createHighlight(result: SearchResult, index: number): Highlight
|
||
|
||
// DOM类型高亮(text/docx/markdown/html等)
|
||
private createDomHighlight(result, index): DomHighlight
|
||
|
||
// PDF类型高亮(在iframe中创建overlay层)
|
||
private createPdfHighlight(result, index): PdfHighlight
|
||
|
||
// 表格类型高亮(Excel/CSV单元格)
|
||
private createTableHighlight(result, index): TableHighlight
|
||
|
||
// 导航控制
|
||
navigateTo(index: number): void
|
||
next(): void
|
||
prev(): void
|
||
|
||
// 清除所有高亮
|
||
clearAll(): void
|
||
}
|
||
```
|
||
|
||
**高亮样式规范**:
|
||
|
||
#### DOM高亮(.search-highlight)
|
||
```css
|
||
.search-highlight {
|
||
background: linear-gradient(135deg, #fff3cd 0%, #ffe69c 100%);
|
||
border-bottom: 2px solid #ffc107;
|
||
border-radius: 2px;
|
||
padding: 1px 2px;
|
||
box-shadow: 0 1px 3px rgba(255, 193, 7, 0.3);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.search-highlight.active {
|
||
background: linear-gradient(135deg, #ffd43b 0%, #fab005 100%);
|
||
border-bottom: 3px solid #f59f00;
|
||
animation: highlight-glow 2s ease-in-out infinite;
|
||
}
|
||
```
|
||
|
||
#### PDF高亮(.pdf-highlight-overlay)
|
||
- 使用绝对定位的div覆盖在文本上方
|
||
- 背景色:`rgba(255, 235, 59, 0.3)`
|
||
- 边框:`2px solid #ffc107`
|
||
- 包含角标显示序号
|
||
|
||
#### 表格高亮(.table-highlight)
|
||
- 绿色系配色(区别于文本黄色)
|
||
- 背景:`linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%)`
|
||
- 边框:`2px solid #28a745`
|
||
|
||
**交互特性**:
|
||
- ✅ 点击高亮标记可跳转到该位置
|
||
- ✅ 当前激活项带脉冲发光动画
|
||
- ✅ 序号标签(1, 2, 3...)便于识别
|
||
- ✅ hover效果(轻微放大+阴影加深)
|
||
|
||
---
|
||
|
||
### 3.4 模块四:SearchCoordinator(协调控制器)
|
||
|
||
**职责**:编排整个搜索→定位→高亮的完整流程,处理异常和重试
|
||
|
||
**文件路径**:`src/utils/searchCoordinator.js`
|
||
|
||
**核心流程**:
|
||
```
|
||
execute(params)
|
||
↓
|
||
Step 1: 文本提取 (withRetry, 最多重试3次)
|
||
↓
|
||
Step 2: 页码过滤(如果指定了targetPage)
|
||
↓
|
||
Step 3: 初始化Fuse.js索引 → 执行搜索
|
||
↓
|
||
Step 4: 渲染高亮 → 导航到第一个结果
|
||
↓
|
||
返回: { success, resultCount, stats }
|
||
```
|
||
|
||
**重试机制**:
|
||
```typescript
|
||
async withRetry<T>(fn: () => T, maxRetries: number, delayMs: number): Promise<T> {
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
try {
|
||
return await fn()
|
||
} catch (error) {
|
||
if (i < maxRetries - 1) {
|
||
await sleep(delayMs) // 等待DOM渲染完成
|
||
} else {
|
||
throw error // 最后一次重试失败则抛出异常
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误处理**:
|
||
- 文本提取失败 → 返回友好提示"无法提取文档内容"
|
||
- 未找到匹配 → 返回"未找到XXX相关内容"
|
||
- 高亮渲染部分失败 → 记录警告日志,继续渲染其他结果
|
||
- 全局异常 → 显示错误提示,不阻断用户操作
|
||
|
||
**性能统计**:
|
||
```typescript
|
||
interface SearchStats {
|
||
totalTime: number // 总耗时(ms)
|
||
extractionTime: number // 文本提取耗时
|
||
searchTime: number // 搜索耗时
|
||
renderTime: number // 高亮渲染耗时
|
||
resultCount: number // 结果数量
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 集成方案
|
||
|
||
### 4.1 QAModule.vue修改点
|
||
|
||
**文件路径**:`src/components/QAModule.vue`
|
||
|
||
**修改函数**:`navigateToFileReader()` (第1126行)
|
||
|
||
**改动内容**:
|
||
```javascript
|
||
// 优化关键词提取逻辑(第1159-1175行)
|
||
const keyword = ''
|
||
|
||
// 优先级1: ref.context(引用上下文)
|
||
if (ref.context) {
|
||
keyword = ref.context.substring(0, 150) // 增加长度限制至150字符
|
||
}
|
||
// 优先级2: ref.rawData中的多个字段
|
||
else if (ref.rawData) {
|
||
const raw = ref.rawData
|
||
keyword = raw.content || raw.context || raw.preview ||
|
||
raw.excerpt || raw.query || raw.question || ''
|
||
if (keyword) keyword = keyword.substring(0, 150)
|
||
}
|
||
// 优先级3: ref.location中的文本描述
|
||
if (!keyword && ref.location && ref.location.length > 4) {
|
||
const locText = ref.location.replace(/第\d+页|page\s*\d+/gi, '').trim()
|
||
if (locText.length >= 4) keyword = locText.substring(0, 120)
|
||
}
|
||
|
||
// 清洗关键词
|
||
if (keyword) {
|
||
keyword = cleanSearchKeyword(keyword)
|
||
if (keyword.length < 4) keyword = '' // 最小长度要求降至4字符
|
||
}
|
||
```
|
||
|
||
**改动理由**:
|
||
- 增加关键词长度限制(100→150),提高匹配成功率
|
||
- 扩展rawData字段检查范围
|
||
- 降低最小长度要求(4字符),适应短文本场景
|
||
|
||
---
|
||
|
||
### 4.2 ReaderPage.vue集成
|
||
|
||
**文件路径**:`src/views/ReaderPage.vue`
|
||
|
||
#### 4.2.1 新增imports
|
||
|
||
```javascript
|
||
import searchCoordinator from '@/utils/searchCoordinator'
|
||
|
||
// 新增响应式变量
|
||
const showNavigationControls = ref(false)
|
||
const searchResultCount = ref(0)
|
||
const currentHighlightIndex = ref(0)
|
||
const searchKeywordPreview = ref('')
|
||
```
|
||
|
||
#### 4.2.2 修改onMounted逻辑
|
||
|
||
```javascript
|
||
onMounted(async () => {
|
||
// ... 现有代码保持不变 ...
|
||
|
||
if (fileId) {
|
||
await loadFile(fileId, fileTitle, fileExtension, page)
|
||
|
||
// ✨ 新增:自动执行搜索高亮
|
||
if (keyword?.trim()) {
|
||
const renderDelays = {
|
||
pdf: 1000,
|
||
docx: 1500,
|
||
pptx: 1200,
|
||
xlsx: 800,
|
||
default: 500
|
||
}
|
||
|
||
const delay = renderDelays[fileType.value] || renderDelays.default
|
||
|
||
setTimeout(async () => {
|
||
const container = scrollContainerRef.value
|
||
if (!container) return
|
||
|
||
const result = await searchCoordinator.execute({
|
||
keyword,
|
||
fileType: fileType.value,
|
||
container,
|
||
targetPage: page
|
||
})
|
||
|
||
if (result.success) {
|
||
showNavigationControls.value = true
|
||
searchResultCount.value = result.resultCount
|
||
searchKeywordPreview.value = keyword.substring(0, 20) + '...'
|
||
|
||
message.success({
|
||
content: `找到 ${result.resultCount} 处匹配内容`,
|
||
duration: 3
|
||
})
|
||
} else {
|
||
message.warning({
|
||
content: result.message || '未找到相关内容',
|
||
duration: 3
|
||
})
|
||
}
|
||
}, delay)
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
#### 4.2.3 新增UI模板(搜索导航栏)
|
||
|
||
```html
|
||
<!-- 在reader-container内部、底部添加 -->
|
||
<Transition name="slide-up">
|
||
<div v-if="showNavigationControls" class="search-nav-bar">
|
||
<div class="nav-info">
|
||
<SearchOutlined class="nav-icon" />
|
||
<span>找到 <strong>{{ searchResultCount }}</strong> 处匹配</span>
|
||
<span class="keyword-preview">"{{ searchKeywordPreview }}"</span>
|
||
</div>
|
||
|
||
<div class="nav-actions">
|
||
<button
|
||
class="nav-btn"
|
||
@click="prevHighlight"
|
||
:disabled="currentHighlightIndex <= 0"
|
||
>
|
||
<UpOutlined /> 上一处
|
||
</button>
|
||
|
||
<span class="nav-counter">
|
||
{{ currentHighlightIndex + 1 }} / {{ searchResultCount }}
|
||
</span>
|
||
|
||
<button
|
||
class="nav-btn"
|
||
@click="nextHighlight"
|
||
:disabled="currentHighlightIndex >= searchResultCount - 1"
|
||
>
|
||
下一处 <DownOutlined />
|
||
</button>
|
||
|
||
<button class="nav-btn close-btn" @click="closeSearch">
|
||
<CloseOutlined />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
```
|
||
|
||
#### 4.2.4 新增事件监听
|
||
|
||
```javascript
|
||
// 监听高亮导航事件(由HighlightRenderer触发)
|
||
onMounted(() => {
|
||
window.addEventListener('highlightNavigate', (e) => {
|
||
const { currentIndex, total } = e.detail
|
||
currentHighlightIndex.value = currentIndex
|
||
searchResultCount.value = total
|
||
})
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('highlightNavigate')
|
||
searchCoordinator.clearAllHighlights() // 清理高亮
|
||
})
|
||
```
|
||
|
||
#### 4.2.5 新增方法
|
||
|
||
```javascript
|
||
// 导航控制方法
|
||
const nextHighlight = () => {
|
||
searchCoordinator.nextHighlight()
|
||
}
|
||
|
||
const prevHighlight = () => {
|
||
searchCoordinator.prevHighlight()
|
||
}
|
||
|
||
const closeSearch = () => {
|
||
searchCoordinator.clearAllHighlights()
|
||
showNavigationControls.value = false
|
||
currentHighlightIndex.value = 0
|
||
searchResultCount.value = 0
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. CSS样式规范
|
||
|
||
### 5.1 高亮基础样式
|
||
|
||
已在"3.3 HighlightRenderer"章节详细定义,此处补充动画和导航栏样式。
|
||
|
||
### 5.2 动画效果
|
||
|
||
```css
|
||
/* 脉冲发光动画(当前激活项) */
|
||
@keyframes highlight-glow {
|
||
0%, 100% { box-shadow: 0 3px 8px rgba(245, 159, 0, 0.6); }
|
||
50% { box-shadow: 0 4px 16px rgba(245, 159, 0, 0.9), 0 0 20px rgba(245, 159, 0, 0.4); }
|
||
}
|
||
|
||
/* 缩放脉冲(首次出现) */
|
||
@keyframes highlight-pulse {
|
||
0% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.05); opacity: 0.8; }
|
||
100% { transform: scale(1); opacity: 1; }
|
||
}
|
||
```
|
||
|
||
### 5.3 搜索导航栏
|
||
|
||
固定在阅读器底部中央,包含:
|
||
- 左侧:搜索图标 + 匹配数量 + 关键词预览
|
||
- 中间:当前位置计数器(如 "2 / 5")
|
||
- 右侧:上一处 / 下一处 / 关闭按钮
|
||
|
||
详见设计文档第3.3节的CSS代码。
|
||
|
||
---
|
||
|
||
## 6. 数据流与时序
|
||
|
||
### 6.1 完整交互时序图
|
||
|
||
```
|
||
用户点击引用来源组件
|
||
↓
|
||
[QAModule.vue] navigateToFileReader(ref)
|
||
├─ 提取 filename, page, context/rawData
|
||
├─ 清洗生成 keyword (≤150字符)
|
||
└─ router.push({ query: { id, title, extension, page, keyword, from: 'qa' } })
|
||
↓
|
||
[Vue Router] 路由跳转到 /reader
|
||
↓
|
||
[ReaderPage.vue] onMounted()
|
||
├─ 解析路由参数 (id, page, keyword...)
|
||
├─ loadFile(id, title, extension, page)
|
||
│ ├─ 调用API获取文件Blob
|
||
│ ├─ 根据fileType渲染文档 (PDF/Word/Excel/...)
|
||
│ └─ hasFileLoaded = true
|
||
│
|
||
└─ setTimeout(delay) ← 等待DOM渲染完成
|
||
↓
|
||
[searchCoordinator.execute()]
|
||
│
|
||
├─ Step 1: textExtractor.extract(fileType, container)
|
||
│ ├─ withRetry(fn, 3, 200ms) ← 重试机制
|
||
│ └─ 返回 documents[]
|
||
│
|
||
├─ Step 2: 过滤目标页码(可选)
|
||
│
|
||
├─ Step 3: fuzzySearchEngine
|
||
│ ├─ initIndex(documents)
|
||
│ ├─ preprocessKeyword(keyword)
|
||
│ └─ search() → results[]
|
||
│
|
||
├─ Step 4: highlightRenderer.renderAll(results)
|
||
│ ├─ 遍历results创建高亮mark/overlay
|
||
│ ├─ navigateTo(0) ← 跳转到第一个结果
|
||
│ └─ 返回 highlightCount
|
||
│
|
||
└─ 返回 { success, resultCount, stats }
|
||
↓
|
||
更新UI状态:
|
||
├─ showNavigationControls = true
|
||
├─ searchResultCount = N
|
||
└─ 显示成功提示Toast
|
||
```
|
||
|
||
### 6.2 时间估算
|
||
|
||
| 步骤 | 耗时 | 说明 |
|
||
|------|------|------|
|
||
| 文本提取 | 50-200ms | 取决于文档大小 |
|
||
| Fuse.js索引构建 | 10-50ms | 取决于文本块数量 |
|
||
| 搜索执行 | 5-20ms | Fuse.js高效算法 |
|
||
| 高亮渲染 | 30-100ms | DOM操作 |
|
||
| **总计** | **95-370ms** | **< 400ms,用户体验流畅** |
|
||
|
||
加上等待DOM渲染的delay(500-1500ms),总响应时间约**0.6-2秒**。
|
||
|
||
---
|
||
|
||
## 7. 边界情况处理
|
||
|
||
### 7.1 异常场景
|
||
|
||
| 场景 | 处理策略 |
|
||
|------|---------|
|
||
| **关键词为空** | 不执行搜索,仅跳转到指定页码 |
|
||
| **文档内容为空** | 提示"无法提取文档内容",仍完成页面跳转 |
|
||
| **无匹配结果** | 提示"未找到XXX相关内容",保持在目标页面 |
|
||
| **部分高亮失败** | 记录警告日志,成功渲染的其他高亮正常显示 |
|
||
| **PDF iframe跨域受限** | 降级为仅页码跳转,提示"PDF高亮暂不可用" |
|
||
| **大文档(>10MB)** | 文本提取限流,只提取前1000个文本块 |
|
||
| **用户快速连续点击** | 防抖处理(300ms),取消上一次搜索 |
|
||
| **网络请求失败** | 显示错误提示,保留已渲染的高亮 |
|
||
|
||
### 7.2 兼容性保障
|
||
|
||
- **浏览器兼容**:Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
|
||
- **Vue版本**:Vue 3.2+ (Composition API)
|
||
- **移动端适配**:导航栏响应式布局,触摸友好的按钮尺寸
|
||
- **无障碍访问**:高亮元素添加aria-label,键盘导航支持(Tab/Enter)
|
||
|
||
---
|
||
|
||
## 8. 性能优化策略
|
||
|
||
### 8.1 文本提取优化
|
||
|
||
- **懒加载**:只在需要搜索时才提取文本(非页面加载时)
|
||
- **分批处理**:大文档分段提取,避免阻塞主线程
|
||
- **缓存机制**:相同文档不重复提取(基于fileId + hash缓存)
|
||
|
||
### 8.2 搜索引擎优化
|
||
|
||
- **增量更新**:文档内容变化时只更新受影响的索引条目
|
||
- **结果限制**:默认返回Top 20结果,避免过多DOM操作
|
||
- **Web Worker**:(可选)将Fuse.js搜索移至Worker线程(针对超大文档)
|
||
|
||
### 8.3 高亮渲染优化
|
||
|
||
- **虚拟滚动**:只渲染可视区域内的高亮(针对超多匹配项)
|
||
- **批量DOM操作**:使用DocumentFragment减少reflow
|
||
- **防抖清除**:快速切换时延迟清理旧高亮
|
||
|
||
### 8.4 内存管理
|
||
|
||
- **及时清理**:离开页面时调用`clearAllHighlights()`
|
||
- **引用释放**:断开DOM元素与JavaScript对象的循环引用
|
||
- **事件解绑**:onUnmounted时移除所有事件监听器
|
||
|
||
---
|
||
|
||
## 9. 测试计划
|
||
|
||
### 9.1 单元测试
|
||
|
||
| 测试模块 | 测试用例数 | 覆盖率目标 |
|
||
|---------|-----------|-----------|
|
||
| TextExtractor | 15 | ≥90% |
|
||
| FuzzySearchEngine | 20 | ≥95% |
|
||
| HighlightRenderer | 25 | ≥85% |
|
||
| SearchCoordinator | 18 | ≥90% |
|
||
|
||
**关键测试场景**:
|
||
- PDF文本提取(含多页、空页、特殊字符)
|
||
- 中文模糊匹配(同义词、错别字、截断关键词)
|
||
- DOM高亮创建/销毁(内存泄漏检测)
|
||
- 重试机制验证(模拟DOM未就绪)
|
||
|
||
### 9.2 集成测试
|
||
|
||
- **E2E测试**:使用Cypress自动化测试完整流程
|
||
1. 打开AI对话页面
|
||
2. 发送问题获得带引用的回答
|
||
3. 点击引用来源
|
||
4. 验证:页面跳转 + 高亮显示 + 导航栏出现
|
||
5. 点击"下一处",验证高亮切换
|
||
|
||
- **兼容性测试**:BrowserStack云测试平台
|
||
- Chrome/Firefox/Safari/Edge 最新3个版本
|
||
- Windows/macOS/Linux 桌面端
|
||
- iOS/Android 移动端(可选)
|
||
|
||
### 9.3 性能测试
|
||
|
||
- **加载性能**:Lighthouse Performance Score ≥ 90
|
||
- **搜索响应时间**:P99 < 2秒
|
||
- **内存占用**:高亮渲染后内存增长 < 50MB
|
||
- **CPU占用**:搜索过程中CPU峰值 < 60%
|
||
|
||
---
|
||
|
||
## 10. 实施路线图
|
||
|
||
### Phase 1:核心引擎开发(Day 1-2)
|
||
|
||
**Day 1上午**:
|
||
- [ ] 安装依赖:`npm install fuse.js@7.0.0`
|
||
- [ ] 创建`src/utils/textExtractor.js`
|
||
- [ ] 实现PDF/DOCX/Excel/DOM文本提取方法
|
||
- [ ] 编写单元测试(TextExtractor)
|
||
|
||
**Day 1下午**:
|
||
- [ ] 创建`src/utils/fuzzySearchEngine.js`
|
||
- [ ] 配置Fuse.js中文优化参数
|
||
- [ ] 实现`preprocessKeyword`和`search`方法
|
||
- [ ] 编写单元测试(FuzzySearchEngine)
|
||
|
||
**Day 2上午**:
|
||
- [ ] 创建`src/utils/highlightRenderer.js`
|
||
- [ ] 实现DOM/PDF/表格三种高亮类型
|
||
- [ ] 添加导航控制逻辑
|
||
- [ ] 编写单元测试(HighlightRenderer)
|
||
|
||
**Day 2下午**:
|
||
- [ ] 创建`src/utils/searchCoordinator.js`
|
||
- [ ] 实现编排逻辑和重试机制
|
||
- [ ] 集成测试(4个模块联调)
|
||
|
||
### Phase 2:UI集成与调试(Day 3)
|
||
|
||
**Day 3上午**:
|
||
- [ ] 修改`QAModule.vue`的`navigateToFileReader`
|
||
- [ ] 修改`ReaderPage.vue`的`onMounted`
|
||
- [ ] 添加搜索导航栏UI模板
|
||
- [ ] 编写CSS样式(高亮+导航栏+动画)
|
||
|
||
**Day 3下午**:
|
||
- [ ] 本地开发环境调试
|
||
- [ ] 测试PDF高亮功能(重点)
|
||
- [ ] 测试Word/Excel/TXT等多种格式
|
||
- [ ] 修复发现的bug
|
||
|
||
### Phase 3:打磨与优化(Day 4)
|
||
|
||
**Day 4上午**:
|
||
- [ ] 错误处理完善(边界情况)
|
||
- [ ] 性能优化(内存/CPU)
|
||
- [ ] 用户反馈收集(内部测试)
|
||
|
||
**Day 4下午**:
|
||
- [ ] 代码审查和重构
|
||
- [ ] 文档编写(使用指南)
|
||
- [ ] 准备发布
|
||
|
||
---
|
||
|
||
## 11. 风险评估与应对
|
||
|
||
### 11.1 技术风险
|
||
|
||
| 风险项 | 概率 | 影响 | 应对措施 |
|
||
|--------|------|------|---------|
|
||
| **PDF iframe跨域限制** | 中 | 高 | 降级方案:仅页码跳转 + Toast提示 |
|
||
| **Fuse.js中文分词不准** | 低 | 中 | 自定义tokenizer或引入jieba分词 |
|
||
| **大文档性能问题** | 中 | 中 | 分批处理 + Web Worker + 虚拟滚动 |
|
||
| **DOM高亮破坏文档结构** | 低 | 高 | 使用Range API + 异常捕获回滚 |
|
||
|
||
### 11.2 进度风险
|
||
|
||
- **风险**:Phase 2集成调试超出预期
|
||
- **应对**:预留1天buffer time,优先保证核心功能可用
|
||
|
||
---
|
||
|
||
## 12. 成功验收标准
|
||
|
||
### 功能完整性 ✅
|
||
|
||
- [ ] 点击PDF引用 → 跳转页码 + overlay高亮显示
|
||
- [ ] 点击Word引用 → DOM高亮 + 持续显示
|
||
- [ ] 点击Excel引用 → 单元格高亮 + 角标序号
|
||
- [ ] 模糊匹配成功率 ≥ 90%(测试集验证)
|
||
- [ ] 导航栏正确显示匹配数量和当前位置
|
||
- [ ] 上/下一处按钮工作正常
|
||
|
||
### 性能指标 ⚡
|
||
|
||
- [ ] 搜索+高亮总耗时 < 2秒(P99)
|
||
- [ ] 内存增长 < 50MB(相对于无高亮状态)
|
||
- [ ] CPU峰值 < 60%(搜索过程中)
|
||
- [ ] Lighthouse Performance Score ≥ 90
|
||
|
||
### 用户体验 😊
|
||
|
||
- [ ] 高亮视觉效果醒目但不刺眼
|
||
- [ ] 动画流畅(60fps)
|
||
- [ ] 错误提示友好清晰
|
||
- [ ] 移动端触摸操作顺畅
|
||
- [ ] 键盘可访问(Tab/Enter导航)
|
||
|
||
### 代码质量 🔧
|
||
|
||
- [ ] 单元测试覆盖率 ≥ 85%
|
||
- [ ] 无console警告(生产环境)
|
||
- [ ] ESLint检查通过(0 error, 0 warning)
|
||
- [ ] 代码注释完整(JSDoc标准)
|
||
|
||
---
|
||
|
||
## 13. 后续迭代方向(可选)
|
||
|
||
### Phase 4:增强功能(v2.0)
|
||
|
||
- [ ] **多关键词同时高亮**:支持AND/OR逻辑组合
|
||
- [ ] **高亮导出**:将高亮标注导出为PDF注释
|
||
- [ ] **历史记录**:保存用户的搜索历史和高亮偏好
|
||
- [ ] **快捷键支持**:Ctrl+F唤起搜索框,F3跳转下一个
|
||
- [ ] **AI语义搜索**:升级为向量相似度匹配(需后端支持)
|
||
|
||
### Phase 5:平台扩展(v3.0)
|
||
|
||
- [ ] **Web Worker迁移**:将搜索引擎移至Worker线程
|
||
- [ ] **IndexedDB缓存**:离线缓存文本提取结果
|
||
- [ ] **PWA支持**:离线模式下仍可使用基本搜索功能
|
||
- [ ] **插件机制**:允许第三方开发者自定义高亮样式
|
||
|
||
---
|
||
|
||
## 附录A:关键技术参考
|
||
|
||
### A.1 Fuse.js官方文档
|
||
https://fusejs.io/
|
||
|
||
### A.2 Range API(MDN)
|
||
https://developer.mozilla.org/en-US/docs/Web/API/Range
|
||
|
||
### A.3 TreeWalker API(MDN)
|
||
https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
|
||
|
||
### A.4 PDF.js textLayer
|
||
https://github.com/nickmoss/pdfjs-viewer-textlayer
|
||
|
||
---
|
||
|
||
## 附录B:术语表
|
||
|
||
| 术语 | 定义 |
|
||
|------|------|
|
||
| **RAG** | Retrieval-Augmented Generation(检索增强生成) |
|
||
| **SSE** | Server-Sent Events(服务器推送事件) |
|
||
| **Fuse.js** | 轻量级模糊搜索库 |
|
||
| **TreeWalker** | DOM树遍历API |
|
||
| **Range** | DOM范围选择API |
|
||
| **Overlay** | 覆盖层(用于PDF高亮) |
|
||
|
||
---
|
||
|
||
**文档维护者**:AI Code Assistant
|
||
**最后更新**:2026-05-30
|
||
**下次评审日期**:实施完成后3天内
|