- 新增 documents.html 文档管理页面 - 文档列表展示(支持筛选和分页) - 文档上传功能(带元信息表单) - 文档详情查看(右侧滑出面板) - 文档删除功能 - 状态统计卡片(待处理/处理中/已索引/失败) - 新增 documents.css 和 documents.js - 纯静态页面实现,无需额外框架 - 与现有 index.html 保持一致的设计风格 - 修复列表滚动问题(覆盖 body overflow 设置) - 修复时间字段显示 NaN 问题(增加 Invalid Date 检查) - 在 index.html 侧边栏添加文档管理入口 - 归档项目文档到 devflow 和 openspec - devflow/projects/2026-06-25-doc-management-ui/ - openspec/changes/doc-management-ui/ - 更新 devflow/index.md
628 lines
16 KiB
Markdown
628 lines
16 KiB
Markdown
# 文档管理页面开发 - 设计文档
|
||
|
||
## 1. 架构设计
|
||
|
||
### 1.1 整体架构
|
||
```
|
||
documents.html (独立页面)
|
||
├── HTML 结构
|
||
│ ├── 顶部导航栏
|
||
│ ├── 状态统计区域
|
||
│ ├── 操作工具栏
|
||
│ ├── 文档列表区域
|
||
│ └── 详情面板(滑出式)
|
||
├── CSS 样式(复用 styles.css + 少量定制)
|
||
└── JavaScript 逻辑
|
||
├── API 调用层
|
||
├── 状态管理
|
||
├── UI 渲染
|
||
└── 事件处理
|
||
```
|
||
|
||
### 1.2 页面结构
|
||
```html
|
||
<body>
|
||
<div class="app-layout">
|
||
<!-- 左侧导航(可选,或仅顶部导航) -->
|
||
<aside class="sidebar-mini">
|
||
<a href="index.html">返回主页</a>
|
||
<a href="documents.html" class="active">文档管理</a>
|
||
</aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<main class="main-content">
|
||
<!-- 顶部导航栏 -->
|
||
<header class="page-header">
|
||
<h1>文档管理</h1>
|
||
<button id="uploadBtn">上传文档</button>
|
||
<button id="refreshBtn">刷新</button>
|
||
</header>
|
||
|
||
<!-- 状态统计卡片 -->
|
||
<section class="stats-cards">
|
||
<div class="stat-card" data-status="PENDING">
|
||
<span class="stat-label">待处理</span>
|
||
<span class="stat-value" id="statPending">0</span>
|
||
</div>
|
||
<div class="stat-card" data-status="PROCESSING">
|
||
<span class="stat-label">处理中</span>
|
||
<span class="stat-value" id="statProcessing">0</span>
|
||
</div>
|
||
<div class="stat-card" data-status="INDEXED">
|
||
<span class="stat-label">已索引</span>
|
||
<span class="stat-value" id="statIndexed">0</span>
|
||
</div>
|
||
<div class="stat-card" data-status="FAILED">
|
||
<span class="stat-label">失败</span>
|
||
<span class="stat-value" id="statFailed">0</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 操作工具栏 -->
|
||
<div class="toolbar">
|
||
<div class="filters">
|
||
<select id="statusFilter">
|
||
<option value="">全部状态</option>
|
||
<option value="PENDING">待处理</option>
|
||
<option value="PROCESSING">处理中</option>
|
||
<option value="INDEXED">已索引</option>
|
||
<option value="FAILED">失败</option>
|
||
</select>
|
||
<input type="text" id="faultSourceFilter" placeholder="按故障源筛选">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文档列表 -->
|
||
<div class="documents-table-container">
|
||
<table class="documents-table">
|
||
<thead>
|
||
<tr>
|
||
<th>文件名</th>
|
||
<th>类别</th>
|
||
<th>故障源</th>
|
||
<th>接口名称</th>
|
||
<th>版本</th>
|
||
<th>状态</th>
|
||
<th>分块数</th>
|
||
<th>上传时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="documentsTableBody">
|
||
<!-- 动态生成 -->
|
||
</tbody>
|
||
</table>
|
||
<div class="pagination" id="pagination">
|
||
<!-- 分页控件 -->
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 详情面板(右侧滑出) -->
|
||
<aside class="detail-panel" id="detailPanel">
|
||
<div class="panel-header">
|
||
<h2>文档详情</h2>
|
||
<button id="closePanelBtn">×</button>
|
||
</div>
|
||
<div class="panel-content" id="panelContent">
|
||
<!-- 动态生成 -->
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<!-- 上传对话框 -->
|
||
<div class="modal" id="uploadModal">
|
||
<div class="modal-content">
|
||
<h2>上传文档</h2>
|
||
<form id="uploadForm">
|
||
<div class="form-group">
|
||
<label>选择文件</label>
|
||
<input type="file" id="fileInput" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>文档类别</label>
|
||
<select id="faultCategory">
|
||
<option value="EXTERNAL_API">外部接口调用失败</option>
|
||
<option value="INTERNAL_ERROR">系统内部错误</option>
|
||
<option value="DATABASE">数据库问题</option>
|
||
<option value="CACHE">缓存问题</option>
|
||
<option value="NETWORK">网络问题</option>
|
||
<option value="THREAD">线程问题</option>
|
||
<option value="MEMORY">内存问题</option>
|
||
<option value="CONFIG">配置问题</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>故障源</label>
|
||
<input type="text" id="faultSource" placeholder="如:广东、order-service">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>接口名称</label>
|
||
<input type="text" id="apiName" placeholder="如:社保查询、订单服务API">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>版本</label>
|
||
<input type="text" id="version" value="v1.0">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>分块大小</label>
|
||
<input type="number" id="chunkSize" value="500">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>分块重叠</label>
|
||
<input type="number" id="chunkOverlap" value="50">
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="submit" id="submitUploadBtn">上传</button>
|
||
<button type="button" id="cancelUploadBtn">取消</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 删除确认对话框 -->
|
||
<div class="modal" id="deleteModal">
|
||
<div class="modal-content">
|
||
<h2>确认删除</h2>
|
||
<p id="deleteMessage"></p>
|
||
<p class="warning">此操作将删除 MySQL 和 Milvus 中的所有数据,不可恢复。</p>
|
||
<div class="modal-actions">
|
||
<button id="confirmDeleteBtn" class="danger">删除</button>
|
||
<button id="cancelDeleteBtn">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
```
|
||
|
||
## 2. API 交互设计
|
||
|
||
### 2.1 API 响应格式
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": { ... },
|
||
"timestamp": 1719283200000
|
||
}
|
||
```
|
||
|
||
### 2.2 API 调用封装
|
||
```javascript
|
||
class DocumentAPI {
|
||
constructor() {
|
||
this.baseUrl = '/api/documents';
|
||
}
|
||
|
||
async uploadDocument(formData) {
|
||
const response = await fetch(`${this.baseUrl}/upload`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
return this.handleResponse(response);
|
||
}
|
||
|
||
async getDocument(docId) {
|
||
const response = await fetch(`${this.baseUrl}/${docId}`);
|
||
return this.handleResponse(response);
|
||
}
|
||
|
||
async getDocumentsByStatus(status, page = 0, size = 20) {
|
||
const response = await fetch(
|
||
`${this.baseUrl}/status/${status}?page=${page}&size=${size}`
|
||
);
|
||
return this.handleResponse(response);
|
||
}
|
||
|
||
async getDocumentsByFaultSource(faultSource) {
|
||
const response = await fetch(
|
||
`${this.baseUrl}/faultSource/${encodeURIComponent(faultSource)}`
|
||
);
|
||
return this.handleResponse(response);
|
||
}
|
||
|
||
async deleteDocument(docId) {
|
||
const response = await fetch(`${this.baseUrl}/${docId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
return this.handleResponse(response);
|
||
}
|
||
|
||
async handleResponse(response) {
|
||
const result = await response.json();
|
||
if (result.code !== 200) {
|
||
throw new Error(result.message || '请求失败');
|
||
}
|
||
return result.data;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 状态管理
|
||
```javascript
|
||
class DocumentManager {
|
||
constructor() {
|
||
this.api = new DocumentAPI();
|
||
this.documents = [];
|
||
this.currentFilter = { status: '', faultSource: '' };
|
||
this.currentPage = 0;
|
||
this.pageSize = 20;
|
||
this.selectedDocId = null;
|
||
}
|
||
|
||
async loadDocuments() {
|
||
// 根据筛选条件加载文档
|
||
if (this.currentFilter.status) {
|
||
this.documents = await this.api.getDocumentsByStatus(
|
||
this.currentFilter.status,
|
||
this.currentPage,
|
||
this.pageSize
|
||
);
|
||
} else if (this.currentFilter.faultSource) {
|
||
this.documents = await this.api.getDocumentsByFaultSource(
|
||
this.currentFilter.faultSource
|
||
);
|
||
} else {
|
||
// 默认加载已索引的文档
|
||
this.documents = await this.api.getDocumentsByStatus(
|
||
'INDEXED',
|
||
this.currentPage,
|
||
this.pageSize
|
||
);
|
||
}
|
||
this.renderDocuments();
|
||
this.updateStats();
|
||
}
|
||
|
||
async updateStats() {
|
||
const statuses = ['PENDING', 'PROCESSING', 'INDEXED', 'FAILED'];
|
||
for (const status of statuses) {
|
||
const docs = await this.api.getDocumentsByStatus(status, 0, 999);
|
||
document.getElementById(`stat${status.charAt(0) + status.slice(1).toLowerCase()}`).textContent = docs.length;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 3. UI 组件设计
|
||
|
||
### 3.1 状态徽章
|
||
```javascript
|
||
function getStatusBadge(status) {
|
||
const badges = {
|
||
PENDING: { text: '待处理', color: '#757575' },
|
||
PROCESSING: { text: '处理中', color: '#1a73e8' },
|
||
INDEXED: { text: '已索引', color: '#34a853' },
|
||
FAILED: { text: '失败', color: '#ea4335' }
|
||
};
|
||
const badge = badges[status] || badges.PENDING;
|
||
return `<span class="status-badge" style="background: ${badge.color}">${badge.text}</span>`;
|
||
}
|
||
```
|
||
|
||
### 3.2 文档列表行
|
||
```javascript
|
||
function renderDocumentRow(doc) {
|
||
return `
|
||
<tr data-doc-id="${doc.docId}" class="document-row">
|
||
<td>${doc.fileName}</td>
|
||
<td>${doc.faultCategory}</td>
|
||
<td>${doc.faultSource || '-'}</td>
|
||
<td>${doc.apiName || '-'}</td>
|
||
<td>${doc.version}</td>
|
||
<td>${getStatusBadge(doc.status)}</td>
|
||
<td>${doc.chunkCount}</td>
|
||
<td>${formatDateTime(doc.createdAt)}</td>
|
||
<td>
|
||
<button class="btn-view" data-doc-id="${doc.docId}">查看</button>
|
||
<button class="btn-delete" data-doc-id="${doc.docId}">删除</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
```
|
||
|
||
### 3.3 详情面板
|
||
```javascript
|
||
function renderDetailPanel(doc) {
|
||
return `
|
||
<div class="detail-section">
|
||
<h3>基本信息</h3>
|
||
<div class="detail-item">
|
||
<label>文档ID:</label>
|
||
<span>${doc.docId}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>文件名:</label>
|
||
<span>${doc.fileName}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>文件大小:</label>
|
||
<span>${formatFileSize(doc.fileSize)}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>状态:</label>
|
||
${getStatusBadge(doc.status)}
|
||
</div>
|
||
</div>
|
||
<div class="detail-section">
|
||
<h3>分类信息</h3>
|
||
<div class="detail-item">
|
||
<label>文档类别:</label>
|
||
<span>${doc.faultCategory}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>故障源:</label>
|
||
<span>${doc.faultSource || '-'}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>接口名称:</label>
|
||
<span>${doc.apiName || '-'}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>版本:</label>
|
||
<span>${doc.version}</span>
|
||
</div>
|
||
</div>
|
||
<div class="detail-section">
|
||
<h3>索引信息</h3>
|
||
<div class="detail-item">
|
||
<label>分块数量:</label>
|
||
<span>${doc.chunkCount}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>索引时间:</label>
|
||
<span>${formatDateTime(doc.indexedAt)}</span>
|
||
</div>
|
||
${doc.status === 'FAILED' ? `
|
||
<div class="detail-item error">
|
||
<label>错误信息:</label>
|
||
<span>${doc.errorMessage}</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
<div class="detail-section">
|
||
<h3>时间信息</h3>
|
||
<div class="detail-item">
|
||
<label>创建时间:</label>
|
||
<span>${formatDateTime(doc.createdAt)}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
```
|
||
|
||
## 4. 样式设计
|
||
|
||
### 4.1 核心样式变量(复用 styles.css)
|
||
```css
|
||
/* 复用现有变量 */
|
||
--primary-color: #1a73e8;
|
||
--background: #ffffff;
|
||
--surface: #f1f3f4;
|
||
--border: #dadce0;
|
||
--text: #202124;
|
||
--text-secondary: #5f6368;
|
||
```
|
||
|
||
### 4.2 文档管理特定样式
|
||
```css
|
||
/* 状态统计卡片 */
|
||
.stats-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: #ffffff;
|
||
border: 1px solid #dadce0;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.stat-card:hover {
|
||
border-color: #1a73e8;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* 文档表格 */
|
||
.documents-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: #ffffff;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.documents-table th {
|
||
background: #f1f3f4;
|
||
padding: 12px;
|
||
text-align: left;
|
||
font-weight: 500;
|
||
border-bottom: 1px solid #dadce0;
|
||
}
|
||
|
||
.documents-table td {
|
||
padding: 12px;
|
||
border-bottom: 1px solid #f1f3f4;
|
||
}
|
||
|
||
.document-row:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
/* 状态徽章 */
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
color: #ffffff;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 详情面板 */
|
||
.detail-panel {
|
||
position: fixed;
|
||
top: 0;
|
||
right: -400px;
|
||
width: 400px;
|
||
height: 100vh;
|
||
background: #ffffff;
|
||
border-left: 1px solid #dadce0;
|
||
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
|
||
transition: right 0.3s ease;
|
||
overflow-y: auto;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.detail-panel.open {
|
||
right: 0;
|
||
}
|
||
```
|
||
|
||
## 5. 事件处理流程
|
||
|
||
### 5.1 上传文档流程
|
||
```
|
||
1. 用户点击"上传文档"按钮
|
||
↓
|
||
2. 显示上传表单对话框
|
||
↓
|
||
3. 用户选择文件并填写表单
|
||
↓
|
||
4. 点击"上传"按钮,触发表单提交
|
||
↓
|
||
5. 构建 FormData,调用 API
|
||
POST /api/documents/upload
|
||
↓
|
||
6. 显示加载状态(禁用按钮,显示加载图标)
|
||
↓
|
||
7. 成功:关闭对话框,刷新列表,高亮新文档
|
||
失败:显示错误信息,保持对话框打开
|
||
```
|
||
|
||
### 5.2 删除文档流程
|
||
```
|
||
1. 用户点击"删除"按钮
|
||
↓
|
||
2. 显示删除确认对话框
|
||
↓
|
||
3. 用户确认删除
|
||
↓
|
||
4. 调用 API
|
||
DELETE /api/documents/{docId}
|
||
↓
|
||
5. 成功:关闭对话框,刷新列表
|
||
失败:显示错误信息
|
||
```
|
||
|
||
### 5.3 筛选流程
|
||
```
|
||
1. 用户选择筛选条件
|
||
- 点击状态卡片
|
||
- 选择状态下拉框
|
||
- 输入故障源
|
||
↓
|
||
2. 更新 currentFilter
|
||
↓
|
||
3. 重置 currentPage = 0
|
||
↓
|
||
4. 调用 loadDocuments()
|
||
↓
|
||
5. 渲染新的文档列表
|
||
```
|
||
|
||
## 6. 错误处理
|
||
|
||
### 6.1 网络错误
|
||
```javascript
|
||
try {
|
||
const data = await api.uploadDocument(formData);
|
||
showSuccess('文档上传成功');
|
||
} catch (error) {
|
||
showError('上传失败: ' + error.message);
|
||
}
|
||
```
|
||
|
||
### 6.2 业务错误
|
||
```javascript
|
||
async handleResponse(response) {
|
||
const result = await response.json();
|
||
if (result.code !== 200) {
|
||
throw new Error(result.message || '请求失败');
|
||
}
|
||
return result.data;
|
||
}
|
||
```
|
||
|
||
### 6.3 用户提示
|
||
```javascript
|
||
function showError(message) {
|
||
// 显示顶部通知条
|
||
const notification = document.createElement('div');
|
||
notification.className = 'notification error';
|
||
notification.textContent = message;
|
||
document.body.appendChild(notification);
|
||
setTimeout(() => notification.remove(), 3000);
|
||
}
|
||
```
|
||
|
||
## 7. 性能优化
|
||
|
||
### 7.1 分页加载
|
||
- 每页 20 条记录
|
||
- 避免一次性加载所有文档
|
||
|
||
### 7.2 防抖处理
|
||
- 故障源输入框使用防抖(300ms)
|
||
- 避免频繁调用 API
|
||
|
||
### 7.3 缓存策略
|
||
- 状态统计数据缓存 5 秒
|
||
- 避免频繁刷新统计数据
|
||
|
||
## 8. 可访问性
|
||
|
||
- 按钮添加 aria-label
|
||
- 表格添加 caption
|
||
- 表单字段添加 label 关联
|
||
- 对话框添加 role="dialog" 和 aria-modal="true"
|
||
|
||
## 9. 浏览器兼容性
|
||
|
||
- 目标浏览器:Chrome 90+, Firefox 88+, Safari 14+
|
||
- 使用标准 Fetch API(无需 polyfill)
|
||
- 使用 ES6 语法(async/await, class, arrow function)
|
||
|
||
## 10. 测试场景
|
||
|
||
### 10.1 功能测试
|
||
- [ ] 上传文档(成功 / 失败)
|
||
- [ ] 查看文档列表
|
||
- [ ] 按状态筛选
|
||
- [ ] 按故障源筛选
|
||
- [ ] 查看文档详情
|
||
- [ ] 删除文档
|
||
- [ ] 刷新列表
|
||
- [ ] 分页切换
|
||
|
||
### 10.2 边界测试
|
||
- [ ] 空列表状态
|
||
- [ ] 大文件上传(接近 10MB)
|
||
- [ ] 网络超时
|
||
- [ ] 后端服务不可用
|
||
- [ ] 特殊字符文件名
|
||
- [ ] 中文故障源
|
||
|
||
### 10.3 用户体验测试
|
||
- [ ] 上传进度反馈
|
||
- [ ] 错误信息清晰
|
||
- [ ] 加载状态提示
|
||
- [ ] 删除二次确认
|
||
- [ ] 表单验证
|