Files
aars/frontend/src/pages/upload/Upload.jsx
T

279 lines
9.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect } from 'react'
import { Upload, FileSpreadsheet, CheckCircle, AlertCircle, Download, Trash2 } from 'lucide-react'
import api from '../../api'
import toast from 'react-hot-toast'
const SECTIONS = [
{ key: 'accident', label: '意外 Accident', color: 'red' },
{ key: 'attendance', label: '出勤 Attendance', color: 'blue' },
]
export default function UploadPage() {
const [section, setSection] = useState('accident')
const [file, setFile] = useState(null)
const [uploading, setUploading] = useState(false)
const [result, setResult] = useState(null)
const [info, setInfo] = useState(null)
const fileInputRef = useRef()
const [uploadProgress, setUploadProgress] = useState(0)
const currentSection = SECTIONS.find(s => s.key === section)
// Load current file info when section changes
useEffect(() => {
loadSectionInfo()
}, [section])
async function loadSectionInfo() {
try {
const { data } = await api.get(`/api/${section}/info`)
setInfo(data)
} catch (err) {
console.error(err)
}
}
async function handleClear() {
if (!confirm(`確定要刪除 ${section} 的所有數據嗎?這個操作無法撤銷!`)) return
try {
await api.delete(`/api/upload/${section}`)
toast.success(`${section} 數據已清除`)
setInfo({ uploaded: false, rows: 0, columns: 0, headers: [] })
} catch (err) {
toast.error(err.response?.data?.detail || '清除失敗')
}
}
function handleFileChange(e) {
const f = e.target.files[0]
if (f) {
setFile(f)
setResult(null)
}
}
async function handleUpload() {
if (!file) return
setUploading(true)
setResult(null)
setUploadProgress(0)
try {
const formData = new FormData()
formData.append('file', file)
// Simulate progress
const progressInterval = setInterval(() => {
setUploadProgress(p => Math.min(p + 10, 90))
}, 200)
const { data } = await api.post(`/api/upload/${section}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (e) => {
if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total))
}
})
clearInterval(progressInterval)
setUploadProgress(100)
setResult(data)
toast.success(`${section === 'accident' ? '意外' : '出勤'} 數據上傳成功!`)
setFile(null)
loadSectionInfo()
} catch (err) {
toast.error(err.response?.data?.detail || '上傳失敗')
setUploadProgress(0)
} finally {
setUploading(false)
}
}
function handleCancelUpload() {
// Cancel is handled by just resetting the state
setUploading(false)
setUploadProgress(0)
setFile(null)
toast.success('已取消上傳')
}
async function handleDownload() {
try {
const { data: tokenData } = await api.post('/api/auth/login', {
email: 'admin@aars.hk',
password: 'Admin@1234'
})
const token = tokenData.access_token
window.location.href = `https://excel.donton.cloud/api/export/${section}/excel?token=${token}`
} catch (err) {
toast.error('Download failed')
}
}
return (
<div className="max-w-2xl mx-auto space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900">上傳 Excel</h1>
<p className="text-slate-500 mt-1">直接上傳 Excel 檔案作為數據源</p>
</div>
{/* Section selector */}
<div className="card p-4">
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-slate-700">選擇類別</span>
{SECTIONS.map(s => (
<button
key={s.key}
onClick={() => setSection(s.key)}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
section === s.key
? s.color === 'red' ? 'bg-red-600 text-white' : 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{s.label}
</button>
))}
</div>
</div>
{/* Current file info */}
{info && (
<div className="card p-4 bg-slate-50">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-slate-900">
{section === 'accident' ? '意外' : '出勤'} 數據
</div>
<div className="text-sm text-slate-600">
{info.uploaded
? `${info.rows} 行, ${info.columns}`
: '尚未上傳'}
</div>
{info.headers && info.headers.length > 0 && (
<div className="text-xs text-slate-500 mt-1">
欄位{info.headers.slice(0, 5).join(', ')}
{info.headers.length > 5 && '...'}
</div>
)}
</div>
<div className="flex gap-2">
{info.uploaded && (
<button
onClick={handleDownload}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
<Download size={16} />
下載
</button>
)}
{info.uploaded && (
<button
onClick={handleClear}
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
>
<Trash2 size={16} />
清除
</button>
)}
</div>
</div>
</div>
)}
{/* Upload area */}
<div className="card p-8">
<div
className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:border-primary-400 transition-colors cursor-pointer"
onClick={() => fileInputRef.current?.click()}
>
<Upload size={48} className="mx-auto text-slate-400 mb-4" />
<p className="text-lg font-medium text-slate-700">
{file ? file.name : '點擊選擇 Excel 檔案'}
</p>
<p className="text-sm text-slate-500 mt-2">支援 .xlsx, .xls 格式</p>
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.xls"
onChange={handleFileChange}
className="hidden"
/>
</div>
{file && (
<div className="mt-4 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileSpreadsheet size={24} className="text-green-600" />
<div>
<div className="font-medium">{file.name}</div>
<div className="text-sm text-slate-500">
{(file.size / 1024).toFixed(1)} KB
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={handleCancelUpload}
disabled={!uploading}
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-md hover:bg-slate-50 disabled:opacity-50"
>
取消
</button>
<button
onClick={handleUpload}
disabled={uploading}
className="px-6 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50"
>
{uploading ? '上傳中...' : '上傳'}
</button>
</div>
</div>
{uploading && uploadProgress > 0 && (
<div className="w-full bg-slate-200 rounded-full h-2">
<div
className="bg-primary-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
)}
</div>
)}
</div>
{/* Result */}
{result && (
<div className="card p-4 bg-green-50 border border-green-200">
<div className="flex items-center gap-3">
<CheckCircle size={24} className="text-green-600" />
<div>
<div className="font-medium text-green-900">{result.message}</div>
<div className="text-sm text-green-700">
{result.rows} 行數據已就緒
</div>
</div>
</div>
</div>
)}
{/* Instructions */}
<div className="card p-4 bg-amber-50 border border-amber-200">
<div className="flex items-start gap-3">
<AlertCircle size={20} className="text-amber-600 mt-0.5" />
<div>
<div className="font-medium text-amber-900">注意事項</div>
<ul className="text-sm text-amber-800 mt-2 space-y-1">
<li> 上傳的 Excel 將直接作為數據源</li>
<li> 第一行必須是欄位名稱</li>
<li> 之後每行是一條記錄</li>
<li> 現有數據會被新上傳的文件替換</li>
</ul>
</div>
</div>
</div>
</div>
)
}