Files
TurboHR/frontend/src/pages/CompanyFiles.tsx
T

172 lines
7.5 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 { 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>
)
}