- Backend: .env file for DB/JWT/Redis/MinIO config, appsettings.json cleared - Backend: Program.cs loads .env at startup (no extra NuGet packages) - Frontend: .env files for VITE_API_URL, api-clients use import.meta.env - Added vite-env.d.ts type declarations for both frontends - All hardcoded localhost:5000 replaced with env variable - Added .env.example template for onboarding
106 lines
4.5 KiB
TypeScript
106 lines
4.5 KiB
TypeScript
import { useState, useRef } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Button } from '@/components/common/Button';
|
||
import { Input } from '@/components/common/Input';
|
||
import { PageHeader } from '@/components/layout/PageHeader';
|
||
import { ToastContainer, toast } from '@/components/common/Toast';
|
||
import * as reportService from '@/services/report.service';
|
||
import styles from './ReportUploadPage.module.css';
|
||
|
||
export function ReportUploadPage() {
|
||
const navigate = useNavigate();
|
||
const fileRef = useRef<HTMLInputElement>(null);
|
||
const [title, setTitle] = useState('');
|
||
const [category, setCategory] = useState('血液检查');
|
||
const [files, setFiles] = useState<File[]>([]);
|
||
const [uploading, setUploading] = useState(false);
|
||
|
||
const categories = ['血液检查', '心电图', '影像学', '尿液检查', '其他'];
|
||
|
||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files) {
|
||
setFiles((prev) => [...prev, ...Array.from(e.target.files!)]);
|
||
}
|
||
if (fileRef.current) fileRef.current.value = '';
|
||
};
|
||
|
||
const removeFile = (index: number) => {
|
||
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
if (!title.trim()) { toast('请输入报告名称', 'error'); return; }
|
||
setUploading(true);
|
||
try {
|
||
// Step 1: Upload files
|
||
const imageUrls: string[] = [];
|
||
for (const file of files) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
const token = JSON.parse(localStorage.getItem('hrt_auth') || '{}')?.state?.token;
|
||
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/files/upload`, {
|
||
method: 'POST',
|
||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||
body: formData,
|
||
});
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
imageUrls.push(data.url);
|
||
}
|
||
}
|
||
|
||
// Step 2: Create report
|
||
await reportService.uploadReport({ title, category, imageUrls });
|
||
toast('上传成功');
|
||
setTimeout(() => navigate('/services/reports'), 800);
|
||
} catch {
|
||
toast('上传失败,请检查后端是否运行', 'error');
|
||
} finally {
|
||
setUploading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="page--no-tab">
|
||
<PageHeader title="上传报告" />
|
||
<div className={styles.form}>
|
||
<Input label="报告名称" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="如:血脂全套检查报告" />
|
||
|
||
<div className={styles.catLabel}>报告类型</div>
|
||
<div className={styles.catGrid}>
|
||
{categories.map((c) => (
|
||
<button key={c} className={`${styles.catChip} ${category === c ? styles.catActive : ''}`} onClick={() => setCategory(c)}>{c}</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className={styles.uploadArea} onClick={() => fileRef.current?.click()}>
|
||
<div style={{ width: 72, height: 72, borderRadius: 18, background: 'var(--color-bg-secondary)', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 12px' }}>
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#9BA0B4" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||
</div>
|
||
<span style={{ fontSize: 14, color: '#6B7280' }}>点击选择报告图片</span>
|
||
<span style={{ fontSize: 11, color: '#9CA3AF' }}>{files.length > 0 ? `已选 ${files.length} 个文件` : '支持 jpg、png、pdf,可多选'}</span>
|
||
</div>
|
||
<input ref={fileRef} type="file" accept="image/*,.pdf" multiple style={{ display: 'none' }} onChange={handleFileChange} />
|
||
|
||
{files.length > 0 && (
|
||
<div className={styles.fileList}>
|
||
{files.map((file, i) => (
|
||
<div key={i} className={styles.fileItem}>
|
||
<span className={styles.fileName}>
|
||
{file.name} ({(file.size / 1024).toFixed(0)}KB)
|
||
</span>
|
||
<button className={styles.fileRemove} onClick={() => removeFile(i)} style={{ fontWeight: 700 }}>×</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<Button variant="primary" size="lg" fullWidth loading={uploading} onClick={handleSubmit}>
|
||
{uploading ? '上传中...' : '提交上传'}
|
||
</Button>
|
||
</div>
|
||
<ToastContainer />
|
||
</div>
|
||
);
|
||
}
|