feat: 社保AI建议、合同附件多文件上传预览、删除合同、扩展上传格式
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Plus, Search, Paperclip, Trash2, X, FileText } from 'lucide-react'
|
||||
import { Plus, Search, Paperclip, Trash2, X, FileText, Download, Eye } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import api from '../lib/api'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
@@ -351,6 +352,7 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
const queryClient = useQueryClient()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'CONTRACT_SCAN' | 'EDUCATION' | 'OTHER'>('ID_CARD')
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
|
||||
|
||||
const { data: employee } = useQuery<any>({
|
||||
queryKey: ['employee-detail', employeeId],
|
||||
@@ -379,26 +381,42 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
})
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) 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,
|
||||
})
|
||||
const files = e.target.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
|
||||
const maxSize = 10 * 1024 * 1024
|
||||
|
||||
for (const file of Array.from(files)) {
|
||||
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
|
||||
if (!allowedExts.includes(ext)) {
|
||||
toast.error(`不支持的文件格式: ${file.name}`)
|
||||
continue
|
||||
}
|
||||
if (file.size > maxSize) {
|
||||
toast.error(`文件过大: ${file.name}(最大 10MB)`)
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const fileTypeLabels: Record<string, string> = {
|
||||
ID_CARD: '身份证',
|
||||
BANK_CARD: '银行卡',
|
||||
CONTRACT_SCAN: '合同扫描件',
|
||||
CONTRACT_SCAN: '合同附件',
|
||||
EDUCATION: '学历证书',
|
||||
OTHER: '其他',
|
||||
}
|
||||
@@ -474,13 +492,14 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs">
|
||||
<option value="ID_CARD">身份证</option>
|
||||
<option value="BANK_CARD">银行卡</option>
|
||||
<option value="CONTRACT_SCAN">合同扫描件</option>
|
||||
<option value="CONTRACT_SCAN">合同附件</option>
|
||||
<option value="EDUCATION">学历证书</option>
|
||||
<option value="OTHER">其他</option>
|
||||
</Select>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
@@ -490,9 +509,10 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={addAttachmentMutation.isPending}
|
||||
>
|
||||
{addAttachmentMutation.isPending ? '上传中...' : '上传'}
|
||||
{addAttachmentMutation.isPending ? '上传中...' : '上传附件'}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-2">支持 PDF、图片、Word、Excel 等格式,每个文件最大 10MB</p>
|
||||
|
||||
{attachments && attachments.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
@@ -501,18 +521,26 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
<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">{att.fileName}</div>
|
||||
<button onClick={() => setPreviewUrl(att.fileUrl)} className="text-primary hover:underline truncate text-left">
|
||||
{att.fileName}
|
||||
</button>
|
||||
<div className="text-xs text-gray-400">
|
||||
{fileTypeLabels[att.fileType] || att.fileType} · {new Date(att.createdAt).toLocaleDateString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteAttachmentMutation.mutate(att.id)}
|
||||
className="text-gray-400 hover:text-danger shrink-0 ml-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 shrink-0 ml-2">
|
||||
<a href={att.fileUrl} download={att.fileName} className="text-gray-400 hover:text-primary p-1" title="下载">
|
||||
<Download className="w-4 h-4" />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => deleteAttachmentMutation.mutate(att.id)}
|
||||
className="text-gray-400 hover:text-danger p-1"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -522,6 +550,57 @@ function EmployeeDetailDrawer({ employeeId, onClose }: { employeeId: string; onC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 附件预览弹窗 */}
|
||||
{previewUrl && (() => {
|
||||
const dataToBlobUrl = (dataUrl: string) => {
|
||||
try {
|
||||
const arr = dataUrl.split(',')
|
||||
const mime = arr[0].match(/:(.*?);/)?.[1] || 'application/octet-stream'
|
||||
const bstr = atob(arr[1])
|
||||
const u8 = new Uint8Array(bstr.length)
|
||||
for (let i = 0; i < bstr.length; i++) u8[i] = bstr.charCodeAt(i)
|
||||
return URL.createObjectURL(new Blob([u8], { type: mime }))
|
||||
} catch { return dataUrl }
|
||||
}
|
||||
const blobUrl = previewUrl.startsWith('data:') ? dataToBlobUrl(previewUrl) : previewUrl
|
||||
const mime = previewUrl.startsWith('data:') ? previewUrl.match(/data:(.*?);/)?.[1] || '' : ''
|
||||
const isImage = mime.startsWith('image/')
|
||||
const isPdf = mime === 'application/pdf'
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50" onClick={() => { if (blobUrl !== previewUrl) URL.revokeObjectURL(blobUrl); setPreviewUrl(null) }}>
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full h-[90vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b">
|
||||
<span className="text-sm font-medium">附件预览</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<a href={blobUrl} download="附件" className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||
<Download className="w-3 h-3" />下载
|
||||
</a>
|
||||
<button onClick={() => { if (blobUrl !== previewUrl) URL.revokeObjectURL(blobUrl); setPreviewUrl(null) }} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto flex items-center justify-center p-4">
|
||||
{isImage ? (
|
||||
<img src={blobUrl} alt="附件预览" className="max-w-full max-h-full object-contain" />
|
||||
) : isPdf ? (
|
||||
<embed src={blobUrl} type="application/pdf" className="w-full h-full" />
|
||||
) : (
|
||||
<div className="text-center space-y-3">
|
||||
<FileText className="w-12 h-12 text-gray-300 mx-auto" />
|
||||
<p className="text-sm text-gray-500">此文件格式不支持在线预览</p>
|
||||
<a href={blobUrl} download="附件" className="inline-flex items-center gap-1 text-sm text-primary hover:underline">
|
||||
<Download className="w-4 h-4" />点击下载查看
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user