初始化医疗报告生成项目,添加核心代码文件
This commit is contained in:
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>医疗报告分析系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2889
frontend/package-lock.json
generated
Normal file
2889
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
frontend/package.json
Normal file
26
frontend/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "medical-report-analyzer",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.6.0",
|
||||
"lucide-react": "^0.292.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
261
frontend/src/App.jsx
Normal file
261
frontend/src/App.jsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { FileText, Search, Brain, Check, AlertCircle, Trash2, Layers } from 'lucide-react'
|
||||
import FileUpload from './components/FileUpload'
|
||||
import ReportList from './components/ReportList'
|
||||
import ReportDetail from './components/ReportDetail'
|
||||
import IntegrationResult from './components/IntegrationResult'
|
||||
import BatchUpload from './components/BatchUpload'
|
||||
import { api } from './services/api'
|
||||
|
||||
function App() {
|
||||
const [reports, setReports] = useState([])
|
||||
const [selectedReport, setSelectedReport] = useState(null)
|
||||
const [integrationResult, setIntegrationResult] = useState(null)
|
||||
const [selectedReports, setSelectedReports] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [activeTab, setActiveTab] = useState('batch') // 默认使用批量模式
|
||||
|
||||
const loadReports = async () => {
|
||||
try {
|
||||
const data = await api.getReports()
|
||||
setReports(data.reports || [])
|
||||
} catch (err) {
|
||||
setError('加载报告列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileUpload = async (file) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await api.uploadFile(file)
|
||||
await loadReports()
|
||||
return result
|
||||
} catch (err) {
|
||||
setError('文件上传失败: ' + err.message)
|
||||
throw err
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleOCR = async (fileId) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await api.performOCR(fileId)
|
||||
await loadReports()
|
||||
} catch (err) {
|
||||
setError('OCR识别失败: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnalyze = async (fileId) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
await api.analyzeReport(fileId)
|
||||
await loadReports()
|
||||
const detail = await api.getReportDetail(fileId)
|
||||
setSelectedReport(detail.report)
|
||||
} catch (err) {
|
||||
setError('报告分析失败: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIntegrate = async () => {
|
||||
if (selectedReports.length === 0) {
|
||||
setError('请至少选择一份报告进行整合')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await api.integrateReports(selectedReports)
|
||||
setIntegrationResult(result)
|
||||
} catch (err) {
|
||||
setError('报告整合失败: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (fileId) => {
|
||||
if (!confirm('确定要删除这份报告吗?')) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
await api.deleteReport(fileId)
|
||||
await loadReports()
|
||||
if (selectedReport?.id === fileId) {
|
||||
setSelectedReport(null)
|
||||
}
|
||||
setSelectedReports(prev => prev.filter(id => id !== fileId))
|
||||
} catch (err) {
|
||||
setError('删除失败: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleReportSelection = (reportId) => {
|
||||
setSelectedReports(prev =>
|
||||
prev.includes(reportId)
|
||||
? prev.filter(id => id !== reportId)
|
||||
: [...prev, reportId]
|
||||
)
|
||||
}
|
||||
|
||||
const viewReportDetail = async (reportId) => {
|
||||
try {
|
||||
const detail = await api.getReportDetail(reportId)
|
||||
setSelectedReport(detail.report)
|
||||
setIntegrationResult(null)
|
||||
} catch (err) {
|
||||
setError('加载报告详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleGeneratePDF = async (fileId) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await api.generatePDF(fileId)
|
||||
// 生成成功后直接下载
|
||||
api.downloadPDF(fileId)
|
||||
// 显示成功提示
|
||||
alert('PDF报告生成成功!')
|
||||
} catch (err) {
|
||||
setError('PDF生成失败: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchGenerate = async (files, patientName) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await api.generateComprehensiveReport(files, patientName)
|
||||
// 生成成功后直接下载
|
||||
api.downloadComprehensiveReport(result.pdf_filename)
|
||||
// 显示成功提示
|
||||
alert(`综合健康报告生成成功!\n患者:${result.patient_name}\n报告数量:${result.report_count} 份`)
|
||||
} catch (err) {
|
||||
setError('综合报告生成失败: ' + err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-md">
|
||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<FileText className="w-8 h-8 text-indigo-600" />
|
||||
<h1 className="text-3xl font-bold text-gray-900">医疗报告分析系统</h1>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>系统运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Error Alert */}
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-4 flex items-start">
|
||||
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5 mr-3" />
|
||||
<div>
|
||||
<p className="text-red-800 font-medium">错误</p>
|
||||
<p className="text-red-700 text-sm">{error}</p>
|
||||
</div>
|
||||
<button onClick={() => setError(null)} className="ml-auto text-red-500 hover:text-red-700">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Panel - Upload & Reports */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
{/* 选项卡切换 */}
|
||||
<div className="bg-white rounded-lg shadow-md p-2 flex space-x-2">
|
||||
<button
|
||||
className="flex-1 py-2 px-4 rounded-md font-medium bg-indigo-600 text-white cursor-default"
|
||||
>
|
||||
<Layers className="w-4 h-4 inline mr-2" />
|
||||
批量生成
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* File Upload */}
|
||||
<BatchUpload onGenerate={handleBatchGenerate} loading={loading} />
|
||||
|
||||
{/* Reports List - 只在单个上传模式显示 */}
|
||||
{false && activeTab === 'single' && (
|
||||
<ReportList
|
||||
reports={reports}
|
||||
selectedReports={selectedReports}
|
||||
onToggleSelect={toggleReportSelection}
|
||||
onView={viewReportDetail}
|
||||
onOCR={handleOCR}
|
||||
onAnalyze={handleAnalyze}
|
||||
onDelete={handleDelete}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Integration Button */}
|
||||
{false && reports.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-md p-4">
|
||||
<button
|
||||
onClick={handleIntegrate}
|
||||
disabled={loading || selectedReports.length === 0}
|
||||
className="w-full bg-indigo-600 text-white py-3 rounded-lg font-medium hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center space-x-2 transition-colors"
|
||||
>
|
||||
<Brain className="w-5 h-5" />
|
||||
<span>整合分析 ({selectedReports.length}份)</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Details */}
|
||||
<div className="lg:col-span-2">
|
||||
{integrationResult ? (
|
||||
<IntegrationResult result={integrationResult} onClose={() => setIntegrationResult(null)} />
|
||||
) : selectedReport ? (
|
||||
<ReportDetail
|
||||
report={selectedReport}
|
||||
onClose={() => setSelectedReport(null)}
|
||||
onGeneratePDF={handleGeneratePDF}
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
||||
<Brain className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<p className="text-gray-500 text-lg">上传医疗报告开始分析</p>
|
||||
<p className="text-gray-400 text-sm mt-2">支持 JPG、PNG、PDF 格式</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
191
frontend/src/components/BatchUpload.jsx
Normal file
191
frontend/src/components/BatchUpload.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Upload, X, FileText, User, Loader } from 'lucide-react'
|
||||
|
||||
function BatchUpload({ onGenerate, loading }) {
|
||||
const [selectedFiles, setSelectedFiles] = useState([])
|
||||
const [patientName, setPatientName] = useState('')
|
||||
const [dragActive, setDragActive] = useState(false)
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const files = Array.from(e.target.files)
|
||||
addFiles(files)
|
||||
}
|
||||
|
||||
const addFiles = (files) => {
|
||||
const validFiles = files.filter(file => {
|
||||
const ext = file.name.toLowerCase()
|
||||
return ext.endsWith('.pdf') || ext.endsWith('.jpg') ||
|
||||
ext.endsWith('.jpeg') || ext.endsWith('.png')
|
||||
})
|
||||
|
||||
setSelectedFiles(prev => [...prev, ...validFiles])
|
||||
}
|
||||
|
||||
const removeFile = (index) => {
|
||||
setSelectedFiles(prev => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleDrag = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true)
|
||||
} else if (e.type === 'dragleave') {
|
||||
setDragActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragActive(false)
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
addFiles(files)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
alert('请至少选择一个文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (!patientName.trim()) {
|
||||
alert('请输入患者姓名')
|
||||
return
|
||||
}
|
||||
|
||||
await onGenerate(selectedFiles, patientName.trim())
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', '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]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
|
||||
<Upload className="w-5 h-5 mr-2 text-indigo-600" />
|
||||
批量上传生成综合报告
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
上传多个检测报告(血常规、尿常规、生化检查等),系统将自动识别并生成综合健康报告
|
||||
</p>
|
||||
|
||||
{/* 患者姓名输入 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<User className="w-4 h-4 inline mr-1" />
|
||||
患者姓名 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={patientName}
|
||||
onChange={(e) => setPatientName(e.target.value)}
|
||||
placeholder="请输入患者姓名"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 文件上传区域 */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||
dragActive
|
||||
? 'border-indigo-500 bg-indigo-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
onDragEnter={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Upload className="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||
<p className="text-gray-600 mb-2">
|
||||
拖拽文件到此处,或
|
||||
<label className="text-indigo-600 hover:text-indigo-700 cursor-pointer ml-1">
|
||||
点击选择文件
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
disabled={loading}
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">支持 PDF、JPG、PNG 格式,可多选</p>
|
||||
</div>
|
||||
|
||||
{/* 已选文件列表 */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
||||
已选择 {selectedFiles.length} 个文件
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center flex-1 min-w-0">
|
||||
<FileText className="w-5 h-5 text-gray-400 mr-2 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{formatFileSize(file.size)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={loading}
|
||||
className="ml-2 text-red-500 hover:text-red-700 disabled:opacity-50"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 生成按钮 */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || selectedFiles.length === 0 || !patientName.trim()}
|
||||
className="w-full mt-6 bg-indigo-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center space-x-2 transition-colors"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader className="w-5 h-5 animate-spin" />
|
||||
<span>正在生成综合报告...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-5 h-5" />
|
||||
<span>生成综合健康报告</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-xs text-yellow-800">
|
||||
<strong>注意:</strong>上传的文件仅用于生成报告,处理完成后会自动删除,不会永久保存在系统中。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BatchUpload
|
||||
91
frontend/src/components/FileUpload.jsx
Normal file
91
frontend/src/components/FileUpload.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Upload, Loader } from 'lucide-react'
|
||||
|
||||
function FileUpload({ onUpload, loading }) {
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault()
|
||||
setDragging(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault()
|
||||
setDragging(false)
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
if (files.length > 0) {
|
||||
handleFileSelect(files[0])
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = async (file) => {
|
||||
if (!file) return
|
||||
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert('不支持的文件格式,请上传 JPG、PNG 或 PDF 文件')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await onUpload(file)
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4 flex items-center">
|
||||
<Upload className="w-5 h-5 mr-2 text-indigo-600" />
|
||||
上传医疗报告
|
||||
</h2>
|
||||
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
||||
dragging
|
||||
? 'border-indigo-500 bg-indigo-50'
|
||||
: 'border-gray-300 hover:border-indigo-400'
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<Loader className="w-8 h-8 text-indigo-600 animate-spin mb-2" />
|
||||
<p className="text-gray-600">上传中...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-700 mb-2">拖拽文件到此处或点击上传</p>
|
||||
<p className="text-sm text-gray-500 mb-4">支持 JPG、PNG、PDF 格式</p>
|
||||
<label className="inline-block">
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
onChange={(e) => {
|
||||
if (e.target.files[0]) {
|
||||
handleFileSelect(e.target.files[0])
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<span className="px-4 py-2 bg-indigo-600 text-white rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors">
|
||||
选择文件
|
||||
</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileUpload
|
||||
161
frontend/src/components/IntegrationResult.jsx
Normal file
161
frontend/src/components/IntegrationResult.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React from 'react'
|
||||
import { X, Brain, TrendingUp, AlertTriangle, Lightbulb, Calendar } from 'lucide-react'
|
||||
|
||||
function IntegrationResult({ result, onClose }) {
|
||||
const getSeverityColor = (severity) => {
|
||||
switch (severity) {
|
||||
case '高': return 'bg-red-100 border-red-300 text-red-900'
|
||||
case '中': return 'bg-yellow-100 border-yellow-300 text-yellow-900'
|
||||
case '低': return 'bg-green-100 border-green-300 text-green-900'
|
||||
default: return 'bg-gray-100 border-gray-300 text-gray-900'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md">
|
||||
{/* Header */}
|
||||
<div className="border-b border-gray-200 p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<Brain className="w-6 h-6 mr-2 text-purple-600" />
|
||||
综合分析报告
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
整合了 {result.report_count || result.reports_included?.length || 0} 份医疗报告
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{/* Overall Summary */}
|
||||
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 border border-purple-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-purple-900 mb-3 text-lg">整体健康状况</h3>
|
||||
<p className="text-purple-800 leading-relaxed">
|
||||
{result.integrated_analysis?.overall_summary || result.overall_summary || '暂无摘要'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Health Trends */}
|
||||
{(result.integrated_analysis?.health_trends || result.health_trends) && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-4 flex items-center text-lg">
|
||||
<TrendingUp className="w-5 h-5 mr-2 text-blue-600" />
|
||||
健康趋势
|
||||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{(result.integrated_analysis?.health_trends || result.health_trends).map((trend, index) => (
|
||||
<div key={index} className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-blue-900">{trend}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Priority Concerns */}
|
||||
{(result.integrated_analysis?.priority_concerns || result.priority_concerns) &&
|
||||
(result.integrated_analysis?.priority_concerns || result.priority_concerns).length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-4 flex items-center text-lg">
|
||||
<AlertTriangle className="w-5 h-5 mr-2 text-red-600" />
|
||||
优先关注事项
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{(result.integrated_analysis?.priority_concerns || result.priority_concerns).map((concern, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`border rounded-lg p-4 ${getSeverityColor(concern.severity)}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<p className="font-semibold">{concern.concern}</p>
|
||||
<span className="text-xs font-medium px-2 py-1 rounded">
|
||||
{concern.severity}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm">{concern.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Comprehensive Assessment */}
|
||||
{(result.integrated_analysis?.comprehensive_assessment || result.comprehensive_assessment) && (
|
||||
<div className="bg-gray-50 border border-gray-300 rounded-lg p-5">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">综合评估</h3>
|
||||
<p className="text-gray-800 leading-relaxed">
|
||||
{result.integrated_analysis?.comprehensive_assessment || result.comprehensive_assessment}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Integrated Recommendations */}
|
||||
{(result.integrated_analysis?.integrated_recommendations || result.integrated_recommendations) && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-4 flex items-center text-lg">
|
||||
<Lightbulb className="w-5 h-5 mr-2 text-yellow-600" />
|
||||
综合建议
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{(result.integrated_analysis?.integrated_recommendations || result.integrated_recommendations).map((rec, index) => (
|
||||
<li key={index} className="flex items-start bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<span className="inline-block w-2 h-2 bg-yellow-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
<span className="text-gray-800">{rec}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Follow-up Suggestions */}
|
||||
{(result.integrated_analysis?.follow_up_suggestions || result.follow_up_suggestions) && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-4 flex items-center text-lg">
|
||||
<Calendar className="w-5 h-5 mr-2 text-green-600" />
|
||||
后续跟踪建议
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{(result.integrated_analysis?.follow_up_suggestions || result.follow_up_suggestions).map((suggestion, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
<span className="text-gray-700">{suggestion}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reports Included */}
|
||||
{result.reports_included && result.reports_included.length > 0 && (
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-3 text-sm">包含的报告</h3>
|
||||
<div className="space-y-2">
|
||||
{result.reports_included.map((report, index) => (
|
||||
<div key={index} className="text-sm text-gray-600 bg-gray-50 rounded p-2">
|
||||
<span className="font-medium">{index + 1}. </span>
|
||||
{report.filename}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note */}
|
||||
{result.note && (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<p className="text-xs text-gray-600 italic">{result.note}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntegrationResult
|
||||
184
frontend/src/components/ReportDetail.jsx
Normal file
184
frontend/src/components/ReportDetail.jsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React, { useState } from 'react'
|
||||
import { X, FileText, AlertTriangle, CheckCircle, Activity, Download } from 'lucide-react'
|
||||
|
||||
function ReportDetail({ report, onClose, onGeneratePDF }) {
|
||||
const [generatingPDF, setGeneratingPDF] = useState(false)
|
||||
|
||||
const handleGeneratePDF = async () => {
|
||||
setGeneratingPDF(true)
|
||||
try {
|
||||
await onGeneratePDF(report.id)
|
||||
} finally {
|
||||
setGeneratingPDF(false)
|
||||
}
|
||||
}
|
||||
const analysis = report.analysis || {}
|
||||
|
||||
// 兼容不同结构的返回结果
|
||||
const keyFindings = Array.isArray(analysis.key_findings)
|
||||
? analysis.key_findings.map((item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: item?.finding || item?.text || JSON.stringify(item)
|
||||
)
|
||||
: []
|
||||
|
||||
const abnormalItems = Array.isArray(analysis.abnormal_items)
|
||||
? analysis.abnormal_items
|
||||
: []
|
||||
|
||||
let riskAssessmentText = ''
|
||||
if (typeof analysis.risk_assessment === 'string') {
|
||||
riskAssessmentText = analysis.risk_assessment
|
||||
} else if (analysis.risk_assessment && typeof analysis.risk_assessment === 'object') {
|
||||
const ra = analysis.risk_assessment
|
||||
const parts = []
|
||||
if (Array.isArray(ra.high_risk) && ra.high_risk.length > 0) {
|
||||
parts.push(`【高风险】${ra.high_risk.join(';')}`)
|
||||
}
|
||||
if (Array.isArray(ra.medium_risk) && ra.medium_risk.length > 0) {
|
||||
parts.push(`【中风险】${ra.medium_risk.join(';')}`)
|
||||
}
|
||||
if (Array.isArray(ra.low_risk) && ra.low_risk.length > 0) {
|
||||
parts.push(`【低风险】${ra.low_risk.join(';')}`)
|
||||
}
|
||||
riskAssessmentText = parts.join('\n')
|
||||
}
|
||||
|
||||
const recommendations = Array.isArray(analysis.recommendations)
|
||||
? analysis.recommendations.map((item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: item?.recommendation || item?.text || JSON.stringify(item)
|
||||
)
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md">
|
||||
{/* Header */}
|
||||
<div className="border-b border-gray-200 p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<FileText className="w-6 h-6 mr-2 text-indigo-600" />
|
||||
报告详情
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">{report.filename}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={handleGeneratePDF}
|
||||
disabled={generatingPDF}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>{generatingPDF ? '生成中...' : '生成PDF报告'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{/* Summary */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-blue-900 mb-2 flex items-center">
|
||||
<Activity className="w-5 h-5 mr-2" />
|
||||
摘要
|
||||
</h3>
|
||||
<p className="text-blue-800">{analysis.summary || '暂无摘要'}</p>
|
||||
</div>
|
||||
|
||||
{/* Key Findings */}
|
||||
{keyFindings.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
|
||||
<CheckCircle className="w-5 h-5 mr-2 text-green-600" />
|
||||
关键发现
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{keyFindings.map((finding, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
<span className="text-gray-700">{finding}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Abnormal Items */}
|
||||
{abnormalItems.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
|
||||
<AlertTriangle className="w-5 h-5 mr-2 text-red-600" />
|
||||
异常指标
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{abnormalItems.map((item, index) => (
|
||||
<div key={index} className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<p className="font-medium text-red-900">{item.name || item}</p>
|
||||
{item.value && (
|
||||
<p className="text-sm text-red-700 mt-1">
|
||||
测量值: {item.value} {item.reference && `(参考: ${item.reference})`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Risk Assessment */}
|
||||
{riskAssessmentText && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-3">风险评估</h3>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<p className="text-yellow-900 whitespace-pre-wrap">{riskAssessmentText}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
{recommendations.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-3">建议</h3>
|
||||
<ul className="space-y-2">
|
||||
{recommendations.map((rec, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<span className="inline-block w-2 h-2 bg-indigo-500 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||
<span className="text-gray-700">{rec}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OCR Text */}
|
||||
{report.ocr_text && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-3">原始文本</h3>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<pre className="text-sm text-gray-700 whitespace-pre-wrap font-mono">
|
||||
{report.ocr_text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note */}
|
||||
{analysis.note && (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<p className="text-xs text-gray-600 italic">{analysis.note}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportDetail
|
||||
113
frontend/src/components/ReportList.jsx
Normal file
113
frontend/src/components/ReportList.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react'
|
||||
import { FileText, Search, Brain, Trash2, CheckCircle, Clock, AlertCircle } from 'lucide-react'
|
||||
|
||||
function ReportList({ reports, selectedReports, onToggleSelect, onView, onOCR, onAnalyze, onDelete, loading }) {
|
||||
const getStatusIcon = (report) => {
|
||||
if (report.has_analysis) return <CheckCircle className="w-4 h-4 text-green-500" />
|
||||
if (report.has_ocr) return <Clock className="w-4 h-4 text-blue-500" />
|
||||
return <AlertCircle className="w-4 h-4 text-gray-400" />
|
||||
}
|
||||
|
||||
const getStatusText = (report) => {
|
||||
if (report.has_analysis) return '已分析'
|
||||
if (report.has_ocr) return '已识别'
|
||||
return '已上传'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
报告列表 ({reports.length})
|
||||
</h2>
|
||||
|
||||
{reports.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<FileText className="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
||||
<p>暂无报告</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[500px] overflow-y-auto">
|
||||
{reports.map((report) => (
|
||||
<div
|
||||
key={report.id}
|
||||
className={`border rounded-lg p-4 transition-all ${
|
||||
selectedReports.includes(report.id)
|
||||
? 'border-indigo-500 bg-indigo-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedReports.includes(report.id)}
|
||||
onChange={() => onToggleSelect(report.id)}
|
||||
className="mt-1 w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500"
|
||||
disabled={!report.has_analysis}
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-2 mb-1">
|
||||
<FileText className="w-4 h-4 text-gray-600 flex-shrink-0" />
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{report.filename}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 text-xs text-gray-500 mb-2">
|
||||
{getStatusIcon(report)}
|
||||
<span>{getStatusText(report)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{!report.has_ocr && (
|
||||
<button
|
||||
onClick={() => onOCR(report.id)}
|
||||
disabled={loading}
|
||||
className="text-xs px-2 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-300 flex items-center space-x-1"
|
||||
>
|
||||
<Search className="w-3 h-3" />
|
||||
<span>识别</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{report.has_ocr && !report.has_analysis && (
|
||||
<button
|
||||
onClick={() => onAnalyze(report.id)}
|
||||
disabled={loading}
|
||||
className="text-xs px-2 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 disabled:bg-gray-300 flex items-center space-x-1"
|
||||
>
|
||||
<Brain className="w-3 h-3" />
|
||||
<span>分析</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{report.has_analysis && (
|
||||
<button
|
||||
onClick={() => onView(report.id)}
|
||||
className="text-xs px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600 flex items-center space-x-1"
|
||||
>
|
||||
<FileText className="w-3 h-3" />
|
||||
<span>查看</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onDelete(report.id)}
|
||||
disabled={loading}
|
||||
className="text-xs px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 disabled:bg-gray-300 flex items-center space-x-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportList
|
||||
16
frontend/src/index.css
Normal file
16
frontend/src/index.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
96
frontend/src/services/api.js
Normal file
96
frontend/src/services/api.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const API_BASE_URL = 'http://localhost:8001'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
export const api = {
|
||||
// 上传文件
|
||||
uploadFile: async (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const response = await axios.post(`${API_BASE_URL}/api/upload`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 执行OCR
|
||||
performOCR: async (fileId) => {
|
||||
const response = await apiClient.post(`/api/ocr/${fileId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 分析报告
|
||||
analyzeReport: async (fileId) => {
|
||||
const response = await apiClient.post(`/api/analyze/${fileId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 整合报告
|
||||
integrateReports: async (fileIds) => {
|
||||
const response = await apiClient.post('/api/integrate', fileIds)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取所有报告
|
||||
getReports: async () => {
|
||||
const response = await apiClient.get('/api/reports')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 获取报告详情
|
||||
getReportDetail: async (fileId) => {
|
||||
const response = await apiClient.get(`/api/report/${fileId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 删除报告
|
||||
deleteReport: async (fileId) => {
|
||||
const response = await apiClient.delete(`/api/report/${fileId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 生成PDF报告
|
||||
generatePDF: async (fileId) => {
|
||||
const response = await apiClient.post(`/api/report/${fileId}/pdf`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 下载PDF报告
|
||||
downloadPDF: (fileId) => {
|
||||
window.open(`${API_BASE_URL}/api/report/${fileId}/pdf/download`, '_blank')
|
||||
},
|
||||
|
||||
// 批量上传并生成综合报告
|
||||
generateComprehensiveReport: async (files, patientName = '患者') => {
|
||||
const formData = new FormData()
|
||||
|
||||
// 添加所有文件
|
||||
files.forEach(file => {
|
||||
formData.append('files', file)
|
||||
})
|
||||
|
||||
// 添加患者姓名
|
||||
formData.append('patient_name', patientName)
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_BASE_URL}/api/comprehensive-report`,
|
||||
formData,
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
}
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 下载综合报告
|
||||
downloadComprehensiveReport: (pdfFilename) => {
|
||||
window.open(`${API_BASE_URL}/api/comprehensive-report/download/${pdfFilename}`, '_blank')
|
||||
},
|
||||
}
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
15
frontend/vite.config.js
Normal file
15
frontend/vite.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user