Files
TurboHR/frontend/src/pages/roster/AttachmentInfo.tsx
T

110 lines
6.0 KiB
TypeScript
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 } 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>
)
}