Summary: - Implement PKB Dashboard and Workspace pages based on V3 prototype - Add single-layer header with integrated Tab navigation - Implement 3 work modes: Full Text, Deep Read, Batch Processing - Integrate Ant Design X Chat component for AI conversations - Create BatchModeComplete with template selection and document processing - Add compact work mode selector with dropdown design Backend: - Migrate PKB controllers and services to /modules/pkb structure - Register v2 API routes at /api/v2/pkb/knowledge - Maintain dual API routes for backward compatibility Technical details: - Use Zustand for state management - Handle SSE streaming responses for AI chat - Support document selection for Deep Read mode - Implement batch processing with progress tracking Known issues: - Batch processing API integration pending - Knowledge assets page navigation needs optimization Status: Frontend functional, pending refinement
577 lines
36 KiB
HTML
577 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>AI 临床医生与医院知识库 Dashboard V5</title>
|
||
|
||
<!-- 1. 引入 Tailwind CSS -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
||
<!-- 2. 配置 Import Map -->
|
||
<script type="importmap">
|
||
{
|
||
"imports": {
|
||
"react": "https://esm.sh/react@18.2.0",
|
||
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
||
"lucide-react": "https://esm.sh/lucide-react@0.344.0"
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- 3. 引入 Babel -->
|
||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||
|
||
<style>
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
::-webkit-scrollbar-thumb {
|
||
background: #cbd5e1;
|
||
border-radius: 3px;
|
||
}
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #94a3b8;
|
||
}
|
||
.animate-in {
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: scale(0.98); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
/* 增加一个微弱的呼吸动画给新建按钮 */
|
||
@keyframes pulse-soft {
|
||
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.2); }
|
||
50% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
||
}
|
||
.hover-pulse:hover {
|
||
animation: pulse-soft 2s infinite;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-50 text-slate-800 font-sans antialiased overflow-hidden">
|
||
|
||
<div id="root"></div>
|
||
|
||
<!-- 4. React 代码逻辑 -->
|
||
<script type="text/babel" data-type="module">
|
||
import React, { useState } from 'react';
|
||
import { createRoot } from 'react-dom/client';
|
||
import {
|
||
Plus, BookOpen, Microscope, FileText, User, X, CheckCircle2,
|
||
Loader2, ChevronRight, Upload, Database, BrainCircuit, MessageSquare,
|
||
MoreHorizontal, GraduationCap, Pill, Stethoscope, Wrench, BarChart3,
|
||
Eraser, ArrowRight, Sparkles, Trash2
|
||
} from 'lucide-react';
|
||
|
||
// --- MOCK DATA: Platform Context ---
|
||
const PLATFORM_MODULES = [
|
||
{ name: '智能统计分析', icon: BarChart3, active: false },
|
||
{ name: '智能数据清洗', icon: Eraser, active: false },
|
||
{ name: '智能文献', icon: BookOpen, active: false },
|
||
{ name: 'AI 问答', icon: MessageSquare, active: false },
|
||
{ name: 'AI 知识库', icon: Database, active: true },
|
||
];
|
||
|
||
// --- MOCK DATA: Knowledge Base Types (Reverted to V3 Clinical Terms) ---
|
||
const KB_TYPES = [
|
||
{
|
||
id: 'GUIDELINE',
|
||
name: '临床指南',
|
||
icon: BookOpen,
|
||
color: 'text-blue-600',
|
||
bg: 'bg-blue-100',
|
||
desc: '存储诊疗规范、专家共识。支持精确检索与引用跳转。',
|
||
tags: ['RAG', '精准溯源']
|
||
},
|
||
{
|
||
id: 'RESEARCH',
|
||
name: '科研文献',
|
||
icon: Microscope,
|
||
color: 'text-purple-600',
|
||
bg: 'bg-purple-100',
|
||
desc: '存储同主题文献。支持全文深度阅读、横向对比总结。',
|
||
tags: ['Long Context', '深度分析']
|
||
},
|
||
{
|
||
id: 'CASE_REPORT',
|
||
name: '典型病例',
|
||
icon: Stethoscope,
|
||
color: 'text-emerald-600',
|
||
bg: 'bg-emerald-100',
|
||
desc: '存储疑难病历。支持相似病例检索、临床决策辅助。',
|
||
tags: ['Multimodal', '时序分析']
|
||
},
|
||
{
|
||
id: 'DRUG_SAFETY',
|
||
name: '药品安全',
|
||
icon: Pill,
|
||
color: 'text-rose-600',
|
||
bg: 'bg-rose-100',
|
||
desc: '存储药品说明书。支持配伍禁忌、不良反应查询。',
|
||
tags: ['RAG', '结构化提取']
|
||
},
|
||
{
|
||
id: 'EXAM',
|
||
name: '职称考试',
|
||
icon: GraduationCap,
|
||
color: 'text-orange-600',
|
||
bg: 'bg-orange-100',
|
||
desc: '存储题库与解析。支持考点生成、模拟练习。',
|
||
tags: ['Hybrid', '题库模式']
|
||
},
|
||
{
|
||
id: 'CUSTOM',
|
||
name: '自定义',
|
||
icon: Wrench,
|
||
color: 'text-slate-600',
|
||
bg: 'bg-slate-200',
|
||
desc: '自由配置 AI 引擎参数,混合管理多种文档。',
|
||
tags: ['Advanced', '自由配置']
|
||
},
|
||
];
|
||
|
||
// --- MOCK DATA: Existing Assets (Default 3 items) ---
|
||
const MOCK_KBS = [
|
||
{
|
||
id: 1,
|
||
name: "心内科 2024 临床指南合集",
|
||
type: "GUIDELINE",
|
||
docCount: 12,
|
||
lastActive: "10分钟前",
|
||
snippet: "包含高血压、心衰、房颤等核心指南",
|
||
status: "ready"
|
||
},
|
||
{
|
||
id: 2,
|
||
name: "GLP-1 药物心脏获益研究",
|
||
type: "RESEARCH",
|
||
docCount: 5,
|
||
lastActive: "昨天",
|
||
snippet: "NEJM, Lancet 等顶级期刊文献综述",
|
||
status: "ready"
|
||
},
|
||
{
|
||
id: 3,
|
||
name: "2023 年度科室疑难病例归档",
|
||
type: "CASE_REPORT",
|
||
docCount: 28,
|
||
lastActive: "3天前",
|
||
snippet: "包含影像学报告与病程记录",
|
||
status: "processing"
|
||
}
|
||
];
|
||
|
||
function DashboardV5() {
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [createStep, setCreateStep] = useState(1);
|
||
const [selectedTypeId, setSelectedTypeId] = useState(null);
|
||
|
||
// Create Form State
|
||
const [formData, setFormData] = useState({ name: '', department: 'Cardiology' });
|
||
const [files, setFiles] = useState([]);
|
||
|
||
// Simulation: File Upload Pipeline
|
||
const simulateUpload = () => {
|
||
const newFile = {
|
||
id: Math.random().toString(),
|
||
name: `New_Clinical_Protocol_v${files.length + 1}.pdf`,
|
||
size: "3.5 MB",
|
||
status: 'uploading',
|
||
progress: 0
|
||
};
|
||
setFiles(prev => [...prev, newFile]);
|
||
|
||
let progress = 0;
|
||
const interval = setInterval(() => {
|
||
progress += 2;
|
||
setFiles(prev => prev.map(f => {
|
||
if (f.id !== newFile.id) return f;
|
||
let status = 'uploading';
|
||
if (progress > 15) status = 'analyzing_layout';
|
||
if (progress > 50) status = 'extracting_table';
|
||
if (progress > 85) status = 'indexing';
|
||
if (progress >= 100) status = 'ready';
|
||
return { ...f, progress: Math.min(progress, 100), status };
|
||
}));
|
||
if (progress >= 100) clearInterval(interval);
|
||
}, 100);
|
||
};
|
||
|
||
const getKbTypeConfig = (id) => KB_TYPES.find(t => t.id === id) || KB_TYPES[0];
|
||
|
||
const handleCreateOpen = () => {
|
||
setCreateStep(1);
|
||
setSelectedTypeId(null);
|
||
setFormData({ name: '', department: 'Cardiology' });
|
||
setFiles([]);
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const getStatusText = (status) => {
|
||
switch (status) {
|
||
case 'uploading': return '上传中...';
|
||
case 'analyzing_layout': return 'MinerU 版面分析...';
|
||
case 'extracting_table': return '结构化表格提取...';
|
||
case 'indexing': return '构建向量索引...';
|
||
case 'ready': return '就绪';
|
||
default: return '';
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col h-screen bg-gray-50 font-sans text-slate-800">
|
||
|
||
{/* ---------------- 1. PLATFORM GLOBAL NAVIGATION ---------------- */}
|
||
<header className="bg-white border-b border-gray-200 h-16 flex items-center px-6 justify-between flex-shrink-0 z-30 shadow-sm relative">
|
||
<div className="flex items-center space-x-8">
|
||
{/* Logo Area */}
|
||
<div className="flex items-center space-x-2 mr-2 cursor-pointer hover:opacity-80 transition-opacity">
|
||
<div className="w-8 h-8 bg-gradient-to-br from-indigo-600 to-blue-700 rounded-lg flex items-center justify-center text-white shadow-md">
|
||
<BrainCircuit className="w-5 h-5" />
|
||
</div>
|
||
<span className="text-lg font-bold text-slate-800 tracking-tight">AI 科研平台</span>
|
||
</div>
|
||
|
||
{/* Module Nav */}
|
||
<nav className="hidden lg:flex space-x-1">
|
||
{PLATFORM_MODULES.map((module) => {
|
||
const Icon = module.icon;
|
||
return (
|
||
<button
|
||
key={module.name}
|
||
className={`flex items-center space-x-2 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||
module.active
|
||
? 'bg-blue-50 text-blue-700 ring-1 ring-blue-100'
|
||
: 'text-slate-500 hover:text-slate-800 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
<Icon className={`w-4 h-4 ${module.active ? 'text-blue-600' : 'text-slate-400'}`} />
|
||
<span>{module.name}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
</div>
|
||
|
||
{/* User Profile */}
|
||
<div className="flex items-center space-x-4">
|
||
<div className="hidden md:flex flex-col items-end mr-1">
|
||
<span className="text-sm font-semibold text-slate-700">Dr. Li</span>
|
||
<span className="text-xs text-slate-500">心内科 | 主治医师</span>
|
||
</div>
|
||
<div className="w-9 h-9 bg-indigo-100 rounded-full flex items-center justify-center text-indigo-700 font-bold border-2 border-white shadow-sm cursor-pointer hover:ring-2 hover:ring-indigo-200 transition-all">
|
||
DL
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* ---------------- 2. MAIN CONTENT (1+3 GRID) ---------------- */}
|
||
<main className="flex-1 overflow-y-auto p-6 md:p-10 w-full max-w-[1600px] mx-auto">
|
||
|
||
{/* Layout Strategy: 4 Columns
|
||
1st Column: Create New (Highlighted)
|
||
2nd-4th Columns: Existing KBs
|
||
*/}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
|
||
{/* --- CARD 1: THE "HERO" CREATE CARD --- */}
|
||
<button
|
||
onClick={handleCreateOpen}
|
||
className="group relative bg-gradient-to-br from-blue-50 to-indigo-50 border-2 border-dashed border-blue-300 rounded-xl p-4 flex flex-col items-center justify-center hover:shadow-lg hover:border-blue-400 hover:from-blue-100 hover:to-indigo-100 transition-all h-[240px] overflow-hidden hover-pulse"
|
||
>
|
||
{/* Main Action */}
|
||
<div className="z-10 flex flex-col items-center mb-5 mt-2">
|
||
<div className="w-14 h-14 bg-blue-600 rounded-full flex items-center justify-center mb-3 shadow-md group-hover:scale-110 transition-transform duration-300">
|
||
<Plus className="w-8 h-8 text-white" />
|
||
</div>
|
||
<span className="font-bold text-lg text-blue-800 group-hover:text-blue-900 tracking-tight">创建知识库</span>
|
||
</div>
|
||
|
||
{/* Micro-Showcase: Capabilities (Added Text Labels) */}
|
||
<div className="z-10 w-full px-1">
|
||
<div className="flex justify-between items-center px-1 mb-2 opacity-70">
|
||
<span className="text-[10px] text-blue-800 font-bold uppercase tracking-wider mx-auto">支持 5 种专业类型</span>
|
||
</div>
|
||
<div className="flex justify-center gap-4">
|
||
{KB_TYPES.slice(0, 5).map(type => {
|
||
const TypeIcon = type.icon;
|
||
return (
|
||
<div key={type.id} className="flex flex-col items-center group/icon" title={type.name}>
|
||
<div className={`w-8 h-8 rounded-lg bg-white flex items-center justify-center mb-1 shadow-sm border border-blue-100 group-hover/icon:border-blue-300 transition-all`}>
|
||
<TypeIcon className={`w-4 h-4 ${type.color}`} />
|
||
</div>
|
||
{/* Added Text Label Here */}
|
||
<span className="text-[10px] text-slate-500 font-medium scale-90 whitespace-nowrap group-hover/icon:text-blue-600 transition-colors">
|
||
{type.name.substring(0, 4)}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
{/* --- CARDS 2-4: EXISTING ASSETS --- */}
|
||
{MOCK_KBS.map(kb => {
|
||
const style = getKbTypeConfig(kb.type);
|
||
const TypeIcon = style.icon;
|
||
|
||
return (
|
||
<div key={kb.id} className="bg-white rounded-xl border border-gray-200 p-5 hover:shadow-lg hover:border-blue-200/50 transition-all flex flex-col h-[240px] group relative">
|
||
{/* Card Header & Body */}
|
||
<div className="flex-1">
|
||
<div className="flex justify-between items-start mb-4">
|
||
<div className={`p-2.5 rounded-lg ${style.bg} ${style.color}`}>
|
||
<TypeIcon className="w-6 h-6" />
|
||
</div>
|
||
<div className="flex items-center space-x-1">
|
||
{kb.status === 'processing' && <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />}
|
||
<button className="text-gray-300 hover:text-gray-600 p-1 rounded hover:bg-gray-100">
|
||
<MoreHorizontal className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 className="font-bold text-lg text-slate-800 mb-2 line-clamp-1 group-hover:text-blue-700 transition-colors">{kb.name}</h3>
|
||
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className={`text-xs font-bold px-2 py-0.5 rounded border border-transparent ${style.bg} ${style.color.replace('text-', 'text-opacity-90 ')}`}>
|
||
{style.name}库
|
||
</span>
|
||
</div>
|
||
|
||
<p className="text-xs text-slate-400 mb-3 line-clamp-2 h-8 leading-relaxed">{kb.snippet}</p>
|
||
</div>
|
||
|
||
{/* Card Footer */}
|
||
<div className="pt-4 mt-2 border-t border-gray-100">
|
||
<div className="flex items-center justify-between text-xs text-slate-400 mb-3">
|
||
<span className="flex items-center"><FileText className="w-3 h-3 mr-1"/> {kb.docCount} 份文档</span>
|
||
<span>{kb.lastActive}</span>
|
||
</div>
|
||
<button className="w-full bg-slate-800 hover:bg-blue-600 text-white text-sm font-medium py-2.5 rounded-lg flex items-center justify-center transition-all shadow-sm transform active:scale-[0.98]">
|
||
<MessageSquare className="w-4 h-4 mr-2" />
|
||
进入工作台
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</main>
|
||
|
||
{/* ---------------- 3. THE "WIZARD" MODAL (Updated Terminology) ---------------- */}
|
||
{isModalOpen && (
|
||
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 animate-in">
|
||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl flex flex-col max-h-[90vh] overflow-hidden">
|
||
|
||
{/* Modal Header */}
|
||
<div className="px-8 py-6 border-b border-gray-100 flex justify-between items-center bg-white z-10">
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-slate-800 flex items-center">
|
||
{createStep === 1 && <Sparkles className="w-6 h-6 mr-3 text-blue-600" />}
|
||
{createStep === 1 ? '选择知识库类型' : createStep === 2 ? '基础信息配置' : '上传知识资产'}
|
||
</h2>
|
||
<p className="text-sm text-slate-500 mt-1.5 ml-0.5">
|
||
{createStep === 1 ? '不同的业务场景将配置不同的 AI 策略,请根据您的实际需求选择。' :
|
||
createStep === 2 ? '完善信息以便 AI 更好地扮演专家角色。' : '支持 PDF 批量上传,系统将自动进行 MinerU 深度解析。'}
|
||
</p>
|
||
</div>
|
||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-slate-700 bg-gray-50 hover:bg-gray-100 p-2 rounded-full transition-colors">
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Modal Content */}
|
||
<div className="flex-1 overflow-y-auto bg-slate-50/50 p-8">
|
||
{/* STEP 1: Type Selection (Reverted to V3 Names) */}
|
||
{createStep === 1 && (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{KB_TYPES.map((type) => {
|
||
const TypeIcon = type.icon;
|
||
return (
|
||
<button
|
||
key={type.id}
|
||
onClick={() => {
|
||
setSelectedTypeId(type.id);
|
||
setCreateStep(2);
|
||
}}
|
||
className={`relative flex flex-col items-start p-6 rounded-xl border-2 transition-all duration-200 hover:shadow-xl bg-white text-left group min-h-[180px]
|
||
${type.id === 'CUSTOM' ? 'border-dashed border-slate-300 hover:border-slate-500' : 'border-transparent hover:border-blue-500 ring-1 ring-slate-100'}
|
||
`}
|
||
>
|
||
<div className="flex w-full justify-between items-start mb-4">
|
||
<div className={`p-3.5 rounded-xl ${type.bg} ${type.color} group-hover:scale-110 transition-transform duration-300`}>
|
||
<TypeIcon className="w-8 h-8" />
|
||
</div>
|
||
<ArrowRight className="w-5 h-5 text-gray-300 group-hover:text-blue-500 transition-colors" />
|
||
</div>
|
||
|
||
<h3 className="font-bold text-xl text-slate-800 mb-2 group-hover:text-blue-700">{type.name}</h3>
|
||
<p className="text-sm text-slate-500 leading-relaxed mb-4">{type.desc}</p>
|
||
|
||
<div className="flex flex-wrap gap-2 mt-auto">
|
||
{type.tags.map(tag => (
|
||
<span key={tag} className="text-[10px] font-bold px-2 py-1 rounded bg-slate-100 text-slate-600 uppercase tracking-wide border border-slate-200">
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{/* STEP 2: Basic Info */}
|
||
{createStep === 2 && selectedTypeId && (
|
||
<div className="max-w-xl mx-auto mt-6">
|
||
<div className="bg-white p-8 rounded-xl border border-gray-200 shadow-sm space-y-6">
|
||
<div className={`inline-flex items-center space-x-2 px-4 py-1.5 rounded-full text-sm font-bold ${getKbTypeConfig(selectedTypeId).bg} ${getKbTypeConfig(selectedTypeId).color}`}>
|
||
{React.createElement(getKbTypeConfig(selectedTypeId).icon, { className: "w-4 h-4" })}
|
||
<span>正在创建:{getKbTypeConfig(selectedTypeId).name}知识库</span>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-bold text-slate-700 mb-2">知识库名称 <span className="text-red-500">*</span></label>
|
||
<input
|
||
type="text"
|
||
value={formData.name}
|
||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||
placeholder="例如:2024年心衰诊疗指南合集"
|
||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all text-base"
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-bold text-slate-700 mb-2">所属科室 (用于 AI 角色设定)</label>
|
||
<select
|
||
className="w-full px-4 py-3 border border-gray-300 rounded-lg bg-white outline-none focus:ring-2 focus:ring-blue-500 text-base"
|
||
value={formData.department}
|
||
onChange={(e) => setFormData({...formData, department: e.target.value})}
|
||
>
|
||
<option value="Cardiology">心内科</option>
|
||
<option value="Neurology">神经内科</option>
|
||
<option value="Oncology">肿瘤科</option>
|
||
<option value="General">全科</option>
|
||
</select>
|
||
<p className="text-xs text-slate-400 mt-2 flex items-center">
|
||
<User className="w-3 h-3 mr-1" />
|
||
系统将自动为您设定该科室专家的 Prompt 角色。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* STEP 3: Upload */}
|
||
{createStep === 3 && (
|
||
<div className="max-w-3xl mx-auto space-y-6 mt-4">
|
||
<div
|
||
onClick={simulateUpload}
|
||
className="border-2 border-dashed border-gray-300 rounded-xl p-12 flex flex-col items-center justify-center cursor-pointer hover:bg-blue-50 hover:border-blue-500 transition-all group bg-white"
|
||
>
|
||
<div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||
<Upload className="w-10 h-10 text-blue-600" />
|
||
</div>
|
||
<p className="text-xl font-bold text-slate-700">点击上传 PDF 文件</p>
|
||
<p className="text-sm text-slate-400 mt-2">支持高清 PDF 及扫描件 (MinerU 深度解析引擎已就绪)</p>
|
||
</div>
|
||
|
||
{files.length > 0 && (
|
||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||
<div className="px-4 py-3 bg-gray-50 border-b border-gray-100 flex justify-between items-center">
|
||
<span className="text-xs font-bold text-slate-500 uppercase">上传队列 ({files.length})</span>
|
||
</div>
|
||
<div className="max-h-[280px] overflow-y-auto p-2 space-y-2">
|
||
{files.map(f => (
|
||
<div key={f.id} className="flex items-center p-3 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors">
|
||
<div className="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center text-red-500 mr-4 flex-shrink-0">
|
||
<FileText className="w-6 h-6" />
|
||
</div>
|
||
<div className="flex-1 min-w-0 mr-4">
|
||
<div className="flex justify-between mb-1">
|
||
<span className="font-medium text-slate-800 text-sm truncate">{f.name}</span>
|
||
<span className="text-xs text-slate-500">{f.size}</span>
|
||
</div>
|
||
<div className="w-full bg-gray-100 h-1.5 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full transition-all duration-300 ${f.status === 'ready' ? 'bg-green-500' : 'bg-blue-600'}`}
|
||
style={{ width: `${f.progress}%` }}
|
||
></div>
|
||
</div>
|
||
<div className="flex justify-between mt-1">
|
||
<span className={`text-[10px] font-bold ${f.status === 'ready' ? 'text-green-600' : 'text-blue-600'} flex items-center`}>
|
||
{f.status !== 'ready' && <Loader2 className="w-3 h-3 mr-1 animate-spin" />}
|
||
{getStatusText(f.status)}
|
||
</span>
|
||
<span className="text-[10px] text-slate-400">{f.progress}%</span>
|
||
</div>
|
||
</div>
|
||
<button className="text-gray-300 hover:text-red-500 transition-colors">
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Modal Footer */}
|
||
<div className="px-8 py-5 border-t border-gray-100 bg-white flex justify-between items-center z-10">
|
||
<button
|
||
onClick={() => setCreateStep(Math.max(1, createStep - 1))}
|
||
disabled={createStep === 1}
|
||
className={`px-6 py-2.5 rounded-lg font-medium transition-colors ${createStep === 1 ? 'text-gray-300 cursor-not-allowed' : 'text-slate-600 hover:bg-gray-100'}`}
|
||
>
|
||
上一步
|
||
</button>
|
||
|
||
{createStep < 3 ? (
|
||
<button
|
||
onClick={() => {
|
||
if (createStep === 1 && !selectedTypeId) return;
|
||
if (createStep === 2 && !formData.name) return;
|
||
setCreateStep(createStep + 1);
|
||
}}
|
||
disabled={(createStep === 1 && !selectedTypeId) || (createStep === 2 && !formData.name)}
|
||
className={`px-10 py-3 rounded-lg text-white font-bold shadow-md transition-all flex items-center ${
|
||
(createStep === 1 && !selectedTypeId) || (createStep === 2 && !formData.name)
|
||
? 'bg-gray-300 cursor-not-allowed'
|
||
: 'bg-blue-600 hover:bg-blue-700 active:scale-95'
|
||
}`}
|
||
>
|
||
下一步 <ChevronRight className="w-4 h-4 ml-1" />
|
||
</button>
|
||
) : (
|
||
<button
|
||
onClick={() => setIsModalOpen(false)}
|
||
className="px-10 py-3 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-bold shadow-lg hover:shadow-xl transition-all active:scale-95 flex items-center"
|
||
>
|
||
<CheckCircle2 className="w-5 h-5 mr-2" />
|
||
完成并进入工作台
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const root = createRoot(document.getElementById('root'));
|
||
root.render(<DashboardV5 />);
|
||
</script>
|
||
</body>
|
||
</html> |