- 新增 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
16 KiB
16 KiB
文档管理页面开发 - 设计文档
1. 架构设计
1.1 整体架构
documents.html (独立页面)
├── HTML 结构
│ ├── 顶部导航栏
│ ├── 状态统计区域
│ ├── 操作工具栏
│ ├── 文档列表区域
│ └── 详情面板(滑出式)
├── CSS 样式(复用 styles.css + 少量定制)
└── JavaScript 逻辑
├── API 调用层
├── 状态管理
├── UI 渲染
└── 事件处理
1.2 页面结构
<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 响应格式
{
"code": 200,
"message": "success",
"data": { ... },
"timestamp": 1719283200000
}
2.2 API 调用封装
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 状态管理
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 状态徽章
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 文档列表行
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 详情面板
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)
/* 复用现有变量 */
--primary-color: #1a73e8;
--background: #ffffff;
--surface: #f1f3f4;
--border: #dadce0;
--text: #202124;
--text-secondary: #5f6368;
4.2 文档管理特定样式
/* 状态统计卡片 */
.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 网络错误
try {
const data = await api.uploadDocument(formData);
showSuccess('文档上传成功');
} catch (error) {
showError('上传失败: ' + error.message);
}
6.2 业务错误
async handleResponse(response) {
const result = await response.json();
if (result.code !== 200) {
throw new Error(result.message || '请求失败');
}
return result.data;
}
6.3 用户提示
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 用户体验测试
- 上传进度反馈
- 错误信息清晰
- 加载状态提示
- 删除二次确认
- 表单验证