172 lines
7.5 KiB
TypeScript
172 lines
7.5 KiB
TypeScript
import { useState, useRef } from 'react'
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||
import { toast } from 'sonner'
|
||
import { Building2, Upload, Trash2, FileText, AlertCircle, Calendar } from 'lucide-react'
|
||
import { companyFilesApi } from '../lib/api-services'
|
||
import Card from '../components/ui/Card'
|
||
import Button from '../components/ui/Button'
|
||
import { Input, Label, Select } from '../components/ui/Input'
|
||
import EmptyState from '../components/ui/EmptyState'
|
||
|
||
const FILE_TYPES = [
|
||
{ value: 'BUSINESS_LICENSE', label: '营业执照' },
|
||
{ value: 'WORK_HOURS', label: '工时备案' },
|
||
{ value: 'HR_POLICY', label: '制度文件' },
|
||
{ value: 'LABOR_CONTRACT_TEMPLATE', label: '合同模板' },
|
||
{ value: 'OTHER', label: '其他' },
|
||
]
|
||
|
||
const fileTypeLabels: Record<string, string> = Object.fromEntries(FILE_TYPES.map(t => [t.value, t.label]))
|
||
|
||
export default function CompanyFiles() {
|
||
const queryClient = useQueryClient()
|
||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||
const [fileType, setFileType] = useState('BUSINESS_LICENSE')
|
||
const [remark, setRemark] = useState('')
|
||
const [expiryDate, setExpiryDate] = useState('')
|
||
const [filterType, setFilterType] = useState('')
|
||
|
||
const { data: files, isLoading } = useQuery<any[]>({
|
||
queryKey: ['company-files', filterType],
|
||
queryFn: async () => {
|
||
return await companyFilesApi.list(filterType ? { fileType: filterType } : {})
|
||
},
|
||
})
|
||
|
||
const addMutation = useMutation({
|
||
mutationFn: (data: any) => companyFilesApi.add(data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['company-files'] })
|
||
toast.success('文件上传成功')
|
||
setRemark('')
|
||
setExpiryDate('')
|
||
},
|
||
onError: () => toast.error('上传失败'),
|
||
})
|
||
|
||
const deleteMutation = useMutation({
|
||
mutationFn: (id: string) => companyFilesApi.remove(id),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['company-files'] })
|
||
toast.success('已删除')
|
||
},
|
||
})
|
||
|
||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
if (file.size > 10 * 1024 * 1024) {
|
||
toast.error('文件不能超过 10MB')
|
||
return
|
||
}
|
||
const reader = new FileReader()
|
||
reader.onload = () => {
|
||
addMutation.mutate({
|
||
fileName: file.name,
|
||
fileType,
|
||
fileUrl: reader.result as string,
|
||
fileSize: file.size,
|
||
remark: remark || undefined,
|
||
expiryDate: expiryDate || undefined,
|
||
})
|
||
}
|
||
reader.readAsDataURL(file)
|
||
e.target.value = ''
|
||
}
|
||
|
||
const fmtSize = (bytes: number) => {
|
||
if (bytes < 1024) return `${bytes}B`
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<Building2 className="h-5 w-5 text-primary" />
|
||
<h1 className="text-lg font-semibold">公司备用文件</h1>
|
||
</div>
|
||
|
||
<Card>
|
||
<div className="p-4 space-y-3">
|
||
<div className="text-sm text-gray-500">上传营业执照、工时备案文件、公司制度文件、合同模板等公司级文件</div>
|
||
<div className="flex flex-wrap items-end gap-3">
|
||
<div>
|
||
<Label>文件类型</Label>
|
||
<Select value={fileType} onChange={(e) => setFileType(e.target.value)} className="w-36">
|
||
{FILE_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label>备注(选填)</Label>
|
||
<Input value={remark} onChange={(e) => setRemark(e.target.value)} placeholder="如:2024年营业执照" className="w-48" />
|
||
</div>
|
||
<div>
|
||
<Label>有效期(选填)</Label>
|
||
<Input type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)} className="w-40" />
|
||
</div>
|
||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
|
||
<Button onClick={() => fileInputRef.current?.click()} disabled={addMutation.isPending}>
|
||
<Upload className="w-4 h-4 mr-1" />
|
||
{addMutation.isPending ? '上传中...' : '上传文件'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card>
|
||
<div className="p-4">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h2 className="text-sm font-medium">文件列表</h2>
|
||
<Select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="!w-28 text-xs">
|
||
<option value="">全部类型</option>
|
||
{FILE_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||
</Select>
|
||
</div>
|
||
{isLoading ? (
|
||
<div className="text-center py-8 text-gray-500">加载中...</div>
|
||
) : !files || files.length === 0 ? (
|
||
<EmptyState title="暂无文件" description="点击上方上传按钮添加公司文件" />
|
||
) : (
|
||
<div className="space-y-2">
|
||
{files.map((f: any) => {
|
||
const isExpired = f.expiryDate && new Date(f.expiryDate) < new Date()
|
||
const isExpiringSoon = f.expiryDate && !isExpired && (new Date(f.expiryDate).getTime() - new Date().getTime()) < 30 * 24 * 60 * 60 * 1000
|
||
return (
|
||
<div key={f.id} className="flex items-center gap-3 p-3 rounded-md border hover:bg-gray-50">
|
||
<FileText className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm font-medium truncate">{f.fileName}</span>
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">{fileTypeLabels[f.fileType] || f.fileType}</span>
|
||
{isExpired && <span className="text-xs px-1.5 py-0.5 rounded bg-red-50 text-danger flex items-center gap-0.5"><AlertCircle className="w-3 h-3" />已过期</span>}
|
||
{isExpiringSoon && <span className="text-xs px-1.5 py-0.5 rounded bg-amber-50 text-warning flex items-center gap-0.5"><Calendar className="w-3 h-3" />即将到期</span>}
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-0.5">
|
||
{fmtSize(f.fileSize)}
|
||
{f.remark && <span className="ml-2">· {f.remark}</span>}
|
||
{f.expiryDate && <span className="ml-2">· 有效期至 {f.expiryDate.slice(0, 10)}</span>}
|
||
<span className="ml-2">· {new Date(f.createdAt).toLocaleDateString('zh-CN')}</span>
|
||
</div>
|
||
</div>
|
||
<a href={f.fileUrl} download={f.fileName} className="text-xs text-primary hover:underline flex items-center gap-1">
|
||
<FileText className="w-3.5 h-3.5" />下载
|
||
</a>
|
||
<button
|
||
onClick={() => deleteMutation.mutate(f.id)}
|
||
className="p-1.5 rounded hover:bg-red-50 text-gray-400 hover:text-danger"
|
||
title="删除"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|