Files
SuperBizAgent-java/openspec/changes/archive/2026-07-05-doc-management-ui/design.md
T

16 KiB
Raw Blame History

文档管理页面开发 - 设计文档

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">&times;</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 用户体验测试

  • 上传进度反馈
  • 错误信息清晰
  • 加载状态提示
  • 删除二次确认
  • 表单验证