feat(doc-management): 添加文档管理前端页面
- 新增 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
This commit is contained in:
@@ -0,0 +1,740 @@
|
||||
/* 文档管理页面样式 */
|
||||
|
||||
/* 覆盖 body 的 overflow 设置 */
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 左侧迷你导航 */
|
||||
.sidebar-mini {
|
||||
width: 200px;
|
||||
background: #e8f0fe;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.sidebar-mini-header {
|
||||
padding: 20px 16px;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.sidebar-mini-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.sidebar-mini-nav {
|
||||
padding: 16px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
color: #202124;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: #d2e3fc;
|
||||
}
|
||||
|
||||
.nav-link svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-link span {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 页面头部 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary, .btn-secondary, .btn-danger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #1a73e8;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #1557b0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #ffffff;
|
||||
color: #202124;
|
||||
border: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ea4335;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #d33426;
|
||||
}
|
||||
|
||||
.btn-primary svg, .btn-secondary svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-primary:disabled, .btn-secondary:disabled, .btn-danger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 状态统计卡片 */
|
||||
.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: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: #1a73e8;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.stat-icon.pending {
|
||||
background: #f1f3f4;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.stat-icon.processing {
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.stat-icon.indexed {
|
||||
background: #e6f4ea;
|
||||
color: #34a853;
|
||||
}
|
||||
|
||||
.stat-icon.failed {
|
||||
background: #fce8e6;
|
||||
color: #ea4335;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #5f6368;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-select, .filter-input {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: #ffffff;
|
||||
color: #202124;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-select:focus, .filter-input:focus {
|
||||
border-color: #1a73e8;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* 文档表格 */
|
||||
.documents-table-container {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #dadce0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.documents-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.documents-table thead {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.documents-table th {
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #5f6368;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.documents-table td {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
color: #202124;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.documents-table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.documents-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 状态徽章 */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: #757575;
|
||||
}
|
||||
|
||||
.status-badge.processing {
|
||||
background: #1a73e8;
|
||||
}
|
||||
|
||||
.status-badge.indexed {
|
||||
background: #34a853;
|
||||
}
|
||||
|
||||
.status-badge.failed {
|
||||
background: #ea4335;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-view, .btn-delete {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
background: #d2e3fc;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #fce8e6;
|
||||
color: #ea4335;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #f6c1bc;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 详情面板 */
|
||||
.detail-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -450px;
|
||||
width: 450px;
|
||||
height: 100vh;
|
||||
background: #ffffff;
|
||||
border-left: 1px solid #dadce0;
|
||||
box-shadow: -2px 0 16px rgba(0,0,0,0.1);
|
||||
transition: right 0.3s ease;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.detail-panel.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
background: #ffffff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 50%;
|
||||
font-size: 24px;
|
||||
color: #5f6368;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background: #f1f3f4;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
flex: 0 0 120px;
|
||||
font-size: 13px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.detail-item span {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #202124;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-item.error span {
|
||||
color: #ea4335;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #f1f3f4;
|
||||
border-top-color: #1a73e8;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 对话框 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.modal-small {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ea4335;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .file-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dadce0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #202124;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus, .file-input:focus {
|
||||
border-color: #1a73e8;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: #fef7e0;
|
||||
border: 1px solid #f9ab00;
|
||||
border-radius: 8px;
|
||||
color: #b06000;
|
||||
font-size: 13px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.warning-message svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.btn-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spinner-small {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
/* 通知 */
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
||||
animation: slideIn 0.3s ease;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: #34a853;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: #ea4335;
|
||||
}
|
||||
|
||||
.notification svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-mini {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.sidebar-mini-header h2,
|
||||
.nav-link span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>文档管理 - 智能OnCall助手</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="documents.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- 左侧导航 -->
|
||||
<aside class="sidebar-mini">
|
||||
<div class="sidebar-mini-header">
|
||||
<h2>文档管理</h2>
|
||||
</div>
|
||||
<nav class="sidebar-mini-nav">
|
||||
<a href="index.html" class="nav-link">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12L5 10M5 10L12 3L19 10M5 10V20C5 20.5523 5.44772 21 6 21H9M19 10L21 12M19 10V20C19 20.5523 18.5523 21 18 21H15M9 21C9.55228 21 10 20.5523 10 20V16C10 15.4477 10.4477 15 11 15H13C13.5523 15 14 15.4477 14 16V20C14 20.5523 14.4477 21 15 21M9 21H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>返回主页</span>
|
||||
</a>
|
||||
<a href="documents.html" class="nav-link active">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 12H15M9 16H15M17 21H7C5.89543 21 5 20.1046 5 19V5C5 3.89543 5.89543 3 7 3H12.5858C12.851 3 13.1054 3.10536 13.2929 3.29289L18.7071 8.70711C18.8946 8.89464 19 9.149 19 9.41421V19C19 20.1046 18.1046 21 17 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>文档管理</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="main-content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="page-header">
|
||||
<h1>文档管理</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn-primary" id="uploadBtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15V3M12 3L7 8M12 3L17 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 17L2 19C2 20.1046 2.89543 21 4 21L20 21C21.1046 21 22 20.1046 22 19V17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>上传文档</span>
|
||||
</button>
|
||||
<button class="btn-secondary" id="refreshBtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4V9H4.58152M19.9381 11C19.446 7.05369 16.0796 4 12 4C8.64262 4 5.76829 6.06817 4.58152 9M4.58152 9H9M20 20V15H19.4185M19.4185 15C18.2317 17.9318 15.3574 20 12 20C7.92038 20 4.55399 16.9463 4.06189 13M19.4185 15H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 状态统计卡片 -->
|
||||
<section class="stats-cards">
|
||||
<div class="stat-card" data-status="PENDING">
|
||||
<div class="stat-icon pending">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V12L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">待处理</span>
|
||||
<span class="stat-value" id="statPending">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" data-status="PROCESSING">
|
||||
<div class="stat-icon processing">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2V6M12 18V22M22 12H18M6 12H2M19.07 4.93L16.24 7.76M7.76 16.24L4.93 19.07M19.07 19.07L16.24 16.24M7.76 7.76L4.93 4.93" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">处理中</span>
|
||||
<span class="stat-value" id="statProcessing">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" data-status="INDEXED">
|
||||
<div class="stat-icon indexed">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">已索引</span>
|
||||
<span class="stat-value" id="statIndexed">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" data-status="FAILED">
|
||||
<div class="stat-icon failed">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V12M12 16H12.01M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-label">失败</span>
|
||||
<span class="stat-value" id="statFailed">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="filters">
|
||||
<select id="statusFilter" class="filter-select">
|
||||
<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" class="filter-input" placeholder="按故障源筛选">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文档列表 -->
|
||||
<div class="documents-table-container">
|
||||
<div class="empty-state" id="emptyState" style="display: none;">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 12H15M9 16H15M17 21H7C5.89543 21 5 20.1046 5 19V5C5 3.89543 5.89543 3 7 3H12.5858C12.851 3 13.1054 3.10536 13.2929 3.29289L18.7071 8.70711C18.8946 8.89464 19 9.149 19 9.41421V19C19 20.1046 18.1046 21 17 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<p>暂无文档</p>
|
||||
<button class="btn-primary" onclick="document.getElementById('uploadBtn').click()">上传第一个文档</button>
|
||||
</div>
|
||||
<table class="documents-table" id="documentsTable">
|
||||
<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>
|
||||
</main>
|
||||
|
||||
<!-- 详情面板(右侧滑出) -->
|
||||
<aside class="detail-panel" id="detailPanel">
|
||||
<div class="panel-header">
|
||||
<h2>文档详情</h2>
|
||||
<button class="btn-close" id="closePanelBtn">×</button>
|
||||
</div>
|
||||
<div class="panel-content" id="panelContent">
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<div class="modal" id="uploadModal">
|
||||
<div class="modal-backdrop" id="uploadModalBackdrop"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>上传文档</h2>
|
||||
<button class="btn-close" id="closeUploadModalBtn">×</button>
|
||||
</div>
|
||||
<form id="uploadForm">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="fileInput">选择文件 <span class="required">*</span></label>
|
||||
<input type="file" id="fileInput" class="file-input" required>
|
||||
<small class="form-hint">支持的文件类型:PDF、Word、Markdown 等,最大 10MB</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="faultCategory">文档类别</label>
|
||||
<select id="faultCategory" class="form-select">
|
||||
<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 for="faultSource">故障源</label>
|
||||
<input type="text" id="faultSource" class="form-input" placeholder="如:广东、order-service">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="apiName">接口名称</label>
|
||||
<input type="text" id="apiName" class="form-input" placeholder="如:社保查询、订单服务API">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version">版本</label>
|
||||
<input type="text" id="version" class="form-input" value="v1.0">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="chunkSize">分块大小</label>
|
||||
<input type="number" id="chunkSize" class="form-input" value="500" min="100" max="2000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="chunkOverlap">分块重叠</label>
|
||||
<input type="number" id="chunkOverlap" class="form-input" value="50" min="0" max="500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn-secondary" id="cancelUploadBtn">取消</button>
|
||||
<button type="submit" class="btn-primary" id="submitUploadBtn">
|
||||
<span class="btn-text">上传</span>
|
||||
<span class="btn-loading" style="display: none;">
|
||||
<span class="spinner-small"></span>
|
||||
<span>上传中...</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<div class="modal" id="deleteModal">
|
||||
<div class="modal-backdrop" id="deleteModalBackdrop"></div>
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h2>确认删除</h2>
|
||||
<button class="btn-close" id="closeDeleteModalBtn">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="deleteMessage"></p>
|
||||
<p class="warning-message">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8V12M12 16H12.01M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
此操作将删除 MySQL 和 Milvus 中的所有数据,不可恢复。
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" id="cancelDeleteBtn">取消</button>
|
||||
<button class="btn-danger" id="confirmDeleteBtn">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知容器 -->
|
||||
<div class="notification-container" id="notificationContainer"></div>
|
||||
|
||||
<script src="documents.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,565 @@
|
||||
// 文档管理应用
|
||||
class DocumentManagementApp {
|
||||
constructor() {
|
||||
this.api = new DocumentAPI();
|
||||
this.documents = [];
|
||||
this.currentFilter = { status: '', faultSource: '' };
|
||||
this.currentPage = 0;
|
||||
this.pageSize = 20;
|
||||
this.selectedDocId = null;
|
||||
this.deleteTargetDocId = null;
|
||||
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
this.loadInitialData();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
// 按钮
|
||||
this.uploadBtn = document.getElementById('uploadBtn');
|
||||
this.refreshBtn = document.getElementById('refreshBtn');
|
||||
|
||||
// 状态卡片
|
||||
this.statCards = document.querySelectorAll('.stat-card');
|
||||
|
||||
// 筛选器
|
||||
this.statusFilter = document.getElementById('statusFilter');
|
||||
this.faultSourceFilter = document.getElementById('faultSourceFilter');
|
||||
|
||||
// 表格
|
||||
this.documentsTableBody = document.getElementById('documentsTableBody');
|
||||
this.documentsTable = document.getElementById('documentsTable');
|
||||
this.emptyState = document.getElementById('emptyState');
|
||||
|
||||
// 详情面板
|
||||
this.detailPanel = document.getElementById('detailPanel');
|
||||
this.panelContent = document.getElementById('panelContent');
|
||||
this.closePanelBtn = document.getElementById('closePanelBtn');
|
||||
|
||||
// 上传对话框
|
||||
this.uploadModal = document.getElementById('uploadModal');
|
||||
this.uploadForm = document.getElementById('uploadForm');
|
||||
this.fileInput = document.getElementById('fileInput');
|
||||
this.submitUploadBtn = document.getElementById('submitUploadBtn');
|
||||
this.cancelUploadBtn = document.getElementById('cancelUploadBtn');
|
||||
this.closeUploadModalBtn = document.getElementById('closeUploadModalBtn');
|
||||
this.uploadModalBackdrop = document.getElementById('uploadModalBackdrop');
|
||||
|
||||
// 删除对话框
|
||||
this.deleteModal = document.getElementById('deleteModal');
|
||||
this.deleteMessage = document.getElementById('deleteMessage');
|
||||
this.confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||
this.cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
|
||||
this.closeDeleteModalBtn = document.getElementById('closeDeleteModalBtn');
|
||||
this.deleteModalBackdrop = document.getElementById('deleteModalBackdrop');
|
||||
|
||||
// 通知容器
|
||||
this.notificationContainer = document.getElementById('notificationContainer');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 上传按钮
|
||||
this.uploadBtn.addEventListener('click', () => this.showUploadModal());
|
||||
|
||||
// 刷新按钮
|
||||
this.refreshBtn.addEventListener('click', () => this.refreshList());
|
||||
|
||||
// 状态卡片点击
|
||||
this.statCards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const status = card.dataset.status;
|
||||
this.applyStatusFilter(status);
|
||||
});
|
||||
});
|
||||
|
||||
// 筛选器
|
||||
this.statusFilter.addEventListener('change', () => {
|
||||
this.currentFilter.status = this.statusFilter.value;
|
||||
this.currentPage = 0;
|
||||
this.loadDocuments();
|
||||
});
|
||||
|
||||
// 故障源筛选(防抖)
|
||||
let faultSourceTimeout;
|
||||
this.faultSourceFilter.addEventListener('input', () => {
|
||||
clearTimeout(faultSourceTimeout);
|
||||
faultSourceTimeout = setTimeout(() => {
|
||||
this.currentFilter.faultSource = this.faultSourceFilter.value.trim();
|
||||
this.currentPage = 0;
|
||||
this.loadDocuments();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 详情面板关闭
|
||||
this.closePanelBtn.addEventListener('click', () => this.closeDetailPanel());
|
||||
|
||||
// 上传对话框
|
||||
this.uploadForm.addEventListener('submit', (e) => this.handleUpload(e));
|
||||
this.cancelUploadBtn.addEventListener('click', () => this.hideUploadModal());
|
||||
this.closeUploadModalBtn.addEventListener('click', () => this.hideUploadModal());
|
||||
this.uploadModalBackdrop.addEventListener('click', () => this.hideUploadModal());
|
||||
|
||||
// 删除对话框
|
||||
this.confirmDeleteBtn.addEventListener('click', () => this.handleDelete());
|
||||
this.cancelDeleteBtn.addEventListener('click', () => this.hideDeleteModal());
|
||||
this.closeDeleteModalBtn.addEventListener('click', () => this.hideDeleteModal());
|
||||
this.deleteModalBackdrop.addEventListener('click', () => this.hideDeleteModal());
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadDocuments(),
|
||||
this.updateStats()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('初始化数据加载失败:', error);
|
||||
this.showError('数据加载失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDocuments() {
|
||||
try {
|
||||
// 根据筛选条件加载文档
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error('加载文档失败:', error);
|
||||
this.showError('加载文档失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
renderDocuments() {
|
||||
if (!this.documents || this.documents.length === 0) {
|
||||
this.documentsTable.style.display = 'none';
|
||||
this.emptyState.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
this.documentsTable.style.display = 'table';
|
||||
this.emptyState.style.display = 'none';
|
||||
|
||||
this.documentsTableBody.innerHTML = this.documents.map(doc =>
|
||||
this.renderDocumentRow(doc)
|
||||
).join('');
|
||||
|
||||
// 绑定操作按钮事件
|
||||
this.documentsTableBody.querySelectorAll('.btn-view').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const docId = btn.dataset.docId;
|
||||
this.showDocumentDetail(docId);
|
||||
});
|
||||
});
|
||||
|
||||
this.documentsTableBody.querySelectorAll('.btn-delete').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const docId = btn.dataset.docId;
|
||||
const fileName = btn.dataset.fileName;
|
||||
this.showDeleteConfirm(docId, fileName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderDocumentRow(doc) {
|
||||
return `
|
||||
<tr data-doc-id="${doc.docId}">
|
||||
<td title="${doc.fileName}">${this.truncateText(doc.fileName, 30)}</td>
|
||||
<td>${this.getFaultCategoryLabel(doc.faultCategory)}</td>
|
||||
<td>${doc.faultSource || '-'}</td>
|
||||
<td>${doc.apiName || '-'}</td>
|
||||
<td>${doc.version}</td>
|
||||
<td>${this.getStatusBadge(doc.status)}</td>
|
||||
<td>${doc.chunkCount}</td>
|
||||
<td>${this.formatDateTime(doc.createdAt)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-view" data-doc-id="${doc.docId}">查看</button>
|
||||
<button class="btn-delete" data-doc-id="${doc.docId}" data-file-name="${doc.fileName}">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
getStatusBadge(status) {
|
||||
const badges = {
|
||||
PENDING: { text: '待处理', className: 'pending' },
|
||||
PROCESSING: { text: '处理中', className: 'processing' },
|
||||
INDEXED: { text: '已索引', className: 'indexed' },
|
||||
FAILED: { text: '失败', className: 'failed' }
|
||||
};
|
||||
const badge = badges[status] || badges.PENDING;
|
||||
return `<span class="status-badge ${badge.className}">${badge.text}</span>`;
|
||||
}
|
||||
|
||||
getFaultCategoryLabel(category) {
|
||||
const labels = {
|
||||
EXTERNAL_API: '外部接口',
|
||||
INTERNAL_ERROR: '内部错误',
|
||||
DATABASE: '数据库',
|
||||
CACHE: '缓存',
|
||||
NETWORK: '网络',
|
||||
THREAD: '线程',
|
||||
MEMORY: '内存',
|
||||
CONFIG: '配置'
|
||||
};
|
||||
return labels[category] || category;
|
||||
}
|
||||
|
||||
async updateStats() {
|
||||
try {
|
||||
const statuses = ['PENDING', 'PROCESSING', 'INDEXED', 'FAILED'];
|
||||
const results = await Promise.all(
|
||||
statuses.map(status =>
|
||||
this.api.getDocumentsByStatus(status, 0, 999)
|
||||
)
|
||||
);
|
||||
|
||||
statuses.forEach((status, index) => {
|
||||
const count = results[index] ? results[index].length : 0;
|
||||
const elementId = `stat${status.charAt(0) + status.slice(1).toLowerCase()}`;
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = count;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新统计失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
applyStatusFilter(status) {
|
||||
this.statusFilter.value = status;
|
||||
this.currentFilter.status = status;
|
||||
this.currentFilter.faultSource = '';
|
||||
this.faultSourceFilter.value = '';
|
||||
this.currentPage = 0;
|
||||
this.loadDocuments();
|
||||
}
|
||||
|
||||
async refreshList() {
|
||||
this.refreshBtn.disabled = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
this.loadDocuments(),
|
||||
this.updateStats()
|
||||
]);
|
||||
this.showSuccess('刷新成功');
|
||||
} catch (error) {
|
||||
this.showError('刷新失败: ' + error.message);
|
||||
} finally {
|
||||
this.refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传对话框
|
||||
showUploadModal() {
|
||||
this.uploadModal.classList.add('show');
|
||||
this.uploadForm.reset();
|
||||
}
|
||||
|
||||
hideUploadModal() {
|
||||
this.uploadModal.classList.remove('show');
|
||||
}
|
||||
|
||||
async handleUpload(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const file = this.fileInput.files[0];
|
||||
if (!file) {
|
||||
this.showError('请选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小(10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
this.showError('文件大小不能超过 10MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 FormData
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('faultCategory', document.getElementById('faultCategory').value);
|
||||
formData.append('faultSource', document.getElementById('faultSource').value);
|
||||
formData.append('apiName', document.getElementById('apiName').value);
|
||||
formData.append('version', document.getElementById('version').value);
|
||||
formData.append('chunkSize', document.getElementById('chunkSize').value);
|
||||
formData.append('chunkOverlap', document.getElementById('chunkOverlap').value);
|
||||
|
||||
// 显示加载状态
|
||||
this.submitUploadBtn.disabled = true;
|
||||
this.submitUploadBtn.querySelector('.btn-text').style.display = 'none';
|
||||
this.submitUploadBtn.querySelector('.btn-loading').style.display = 'flex';
|
||||
|
||||
try {
|
||||
const docId = await this.api.uploadDocument(formData);
|
||||
this.showSuccess('文档上传成功');
|
||||
this.hideUploadModal();
|
||||
await this.refreshList();
|
||||
} catch (error) {
|
||||
this.showError('上传失败: ' + error.message);
|
||||
} finally {
|
||||
this.submitUploadBtn.disabled = false;
|
||||
this.submitUploadBtn.querySelector('.btn-text').style.display = 'inline';
|
||||
this.submitUploadBtn.querySelector('.btn-loading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除对话框
|
||||
showDeleteConfirm(docId, fileName) {
|
||||
this.deleteTargetDocId = docId;
|
||||
this.deleteMessage.textContent = `确定删除文档 "${fileName}" 吗?`;
|
||||
this.deleteModal.classList.add('show');
|
||||
}
|
||||
|
||||
hideDeleteModal() {
|
||||
this.deleteModal.classList.remove('show');
|
||||
this.deleteTargetDocId = null;
|
||||
}
|
||||
|
||||
async handleDelete() {
|
||||
if (!this.deleteTargetDocId) return;
|
||||
|
||||
this.confirmDeleteBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await this.api.deleteDocument(this.deleteTargetDocId);
|
||||
this.showSuccess('文档删除成功');
|
||||
this.hideDeleteModal();
|
||||
await this.refreshList();
|
||||
} catch (error) {
|
||||
this.showError('删除失败: ' + error.message);
|
||||
} finally {
|
||||
this.confirmDeleteBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 详情面板
|
||||
async showDocumentDetail(docId) {
|
||||
this.selectedDocId = docId;
|
||||
this.detailPanel.classList.add('open');
|
||||
|
||||
// 显示加载状态
|
||||
this.panelContent.innerHTML = `
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const doc = await this.api.getDocument(docId);
|
||||
this.renderDetailPanel(doc);
|
||||
} catch (error) {
|
||||
this.panelContent.innerHTML = `
|
||||
<div class="loading-state">
|
||||
<p style="color: #ea4335;">加载失败: ${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderDetailPanel(doc) {
|
||||
this.panelContent.innerHTML = `
|
||||
<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>${this.formatFileSize(doc.fileSize)}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>状态:</label>
|
||||
${this.getStatusBadge(doc.status)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h3>分类信息</h3>
|
||||
<div class="detail-item">
|
||||
<label>文档类别:</label>
|
||||
<span>${this.getFaultCategoryLabel(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>${this.formatDateTime(doc.indexedAt) || '-'}</span>
|
||||
</div>
|
||||
${doc.status === 'FAILED' && doc.errorMessage ? `
|
||||
<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>${this.formatDateTime(doc.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
closeDetailPanel() {
|
||||
this.detailPanel.classList.remove('open');
|
||||
this.selectedDocId = null;
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
formatDateTime(dateTime) {
|
||||
if (!dateTime) return '-';
|
||||
const date = new Date(dateTime);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
truncateText(text, maxLength) {
|
||||
if (!text) return '-';
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
// 通知
|
||||
showSuccess(message) {
|
||||
this.showNotification(message, 'success');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.showNotification(message, 'error');
|
||||
}
|
||||
|
||||
showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
|
||||
const icon = type === 'success'
|
||||
? '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
||||
: '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 8V12M12 16H12.01M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
notification.innerHTML = `${icon}<span>${message}</span>`;
|
||||
this.notificationContainer.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code !== 200) {
|
||||
throw new Error(result.message || '请求失败');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new DocumentManagementApp();
|
||||
});
|
||||
@@ -26,7 +26,14 @@
|
||||
</svg>
|
||||
<span>新建对话</span>
|
||||
</button>
|
||||
|
||||
|
||||
<a href="documents.html" class="sidebar-btn" style="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 12px; color: #202124; text-decoration: none; transition: background 0.3s ease; margin-top: 8px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 20px; height: 20px; flex-shrink: 0;">
|
||||
<path d="M9 12H15M9 16H15M17 21H7C5.89543 21 5 20.1046 5 19V5C5 3.89543 5.89543 3 7 3H12.5858C12.851 3 13.1054 3.10536 13.2929 3.29289L18.7071 8.70711C18.8946 8.89464 19 9.149 19 9.41421V19C19 20.1046 18.1046 21 17 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span style="font-size: 14px; font-weight: 500;">文档管理</span>
|
||||
</a>
|
||||
|
||||
<div class="chat-history-section">
|
||||
<div class="history-header">
|
||||
<span>近期对话</span>
|
||||
|
||||
Reference in New Issue
Block a user