110 lines
6.0 KiB
TypeScript
110 lines
6.0 KiB
TypeScript
import { useState, useRef } from "react"
|
||
import { toast } from "sonner"
|
||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||
import { attachmentApi } from '../../lib/api-services'
|
||
import Card from "../../components/ui/Card"
|
||
import Button from "../../components/ui/Button"
|
||
import { Select } from "../../components/ui/Input"
|
||
import { Paperclip, Trash2, Eye, Download } from "lucide-react"
|
||
|
||
export default function AttachmentInfo({ employeeId, attachments }: { employeeId: string; attachments: any[] }) {
|
||
const queryClient = useQueryClient()
|
||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD')
|
||
|
||
const addAttachmentMutation = useMutation({
|
||
mutationFn: (data: any) => attachmentApi.add(data),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }),
|
||
})
|
||
|
||
const deleteAttachmentMutation = useMutation({
|
||
mutationFn: (id: string) => attachmentApi.remove(id),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }),
|
||
})
|
||
|
||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
|
||
// 文件类型校验
|
||
const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
|
||
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
|
||
if (!allowedExts.includes(ext)) {
|
||
toast.error('不支持的文件格式,支持 PDF、图片、Word、Excel 等常见格式')
|
||
return
|
||
}
|
||
|
||
// 文件大小校验(10MB)
|
||
const maxSize = 10 * 1024 * 1024
|
||
if (file.size > maxSize) {
|
||
toast.error(`文件过大,请上传小于 10MB 的文件(当前: ${formatSize(file.size)})`)
|
||
return
|
||
}
|
||
|
||
const reader = new FileReader()
|
||
reader.onload = (event) => {
|
||
const fileUrl = event.target?.result as string
|
||
addAttachmentMutation.mutate({ employeeId, fileName: file.name, fileType, fileUrl, fileSize: file.size })
|
||
}
|
||
reader.readAsDataURL(file)
|
||
}
|
||
|
||
const fileTypeLabels: Record<string, string> = { ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书', CERTIFICATE: '职业资格证书', CONTRACT: '合同扫描件', PHOTO: '员工照片', OTHER: '其他' }
|
||
const fileTypeColors: Record<string, string> = { ID_CARD: 'bg-blue-50 text-blue-600', BANK_CARD: 'bg-green-50 text-safe', EDUCATION: 'bg-amber-50 text-amber-600', CERTIFICATE: 'bg-purple-50 text-purple-600', CONTRACT: 'bg-cyan-50 text-cyan-600', PHOTO: 'bg-pink-50 text-pink-600', OTHER: 'bg-gray-100 text-gray-500' }
|
||
|
||
const formatSize = (bytes: number) => {
|
||
if (!bytes) return '-'
|
||
if (bytes < 1024) return `${bytes}B`
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`
|
||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<h2 className="text-xs font-medium">附件管理({attachments?.length || 0}个)</h2>
|
||
<Card>
|
||
<div className="flex gap-2 mb-3 flex-nowrap items-center">
|
||
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs !w-32 shrink-0">
|
||
<option value="ID_CARD">身份证</option><option value="BANK_CARD">银行卡</option><option value="EDUCATION">学历证书</option><option value="CERTIFICATE">职业资格证书</option><option value="CONTRACT">合同扫描件</option><option value="PHOTO">员工照片</option><option value="OTHER">其他</option>
|
||
</Select>
|
||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
|
||
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={addAttachmentMutation.isPending} className="shrink-0 whitespace-nowrap">
|
||
{addAttachmentMutation.isPending ? '上传中...' : '上传附件'}
|
||
</Button>
|
||
<span className="text-gray-400 text-xs">支持 PDF/JPG/PNG,最大 10MB</span>
|
||
</div>
|
||
{attachments?.length ? (
|
||
<div className="space-y-2">
|
||
{attachments.map((att) => (
|
||
<div key={att.id} className="flex items-center justify-between bg-gray-50 rounded p-2.5 text-xs hover:bg-gray-100">
|
||
<div className="flex items-center gap-2 min-w-0">
|
||
<Paperclip className="w-4 h-4 text-gray-400 shrink-0" />
|
||
<div className="min-w-0">
|
||
<div className="truncate font-medium text-gray-700">{att.fileName}</div>
|
||
<div className="flex items-center gap-2 mt-0.5">
|
||
<span className={`px-1.5 py-0.5 rounded text-xs ${fileTypeColors[att.fileType] || 'bg-gray-100 text-gray-500'}`}>{fileTypeLabels[att.fileType] || att.fileType}</span>
|
||
<span className="text-gray-400">{formatSize(att.fileSize)}</span>
|
||
<span className="text-gray-400">{new Date(att.createdAt).toLocaleDateString('zh-CN')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
||
<button onClick={() => window.open(att.fileUrl, '_blank')} className="text-gray-400 hover:text-blue-600" title="查看">
|
||
<Eye className="w-4 h-4" />
|
||
</button>
|
||
<a href={att.fileUrl} download={att.fileName} className="text-gray-400 hover:text-blue-600" title="下载">
|
||
<Download className="w-4 h-4" />
|
||
</a>
|
||
<button onClick={() => deleteAttachmentMutation.mutate(att.id)} className="text-gray-300 hover:text-danger" title="删除">
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : <div className="text-gray-400 text-xs text-center py-4">暂无附件</div>}
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|