Files
TurboHR/frontend/src/pages/CompanyFiles.tsx
T
selfrelease f523f84c18 ux: 全部页面添加操作说明 PageGuide
为以下17个缺少操作说明的页面添加 PageGuide 组件:
- OrgChart: 组织架构管理
- SupportDashboard: 客服工作台
- Money: 薪酬管理
- AIAssistant: AI 智能助手
- Settings: 系统设置
- Calendar: 人事日历
- Templates: 模板管理
- AuditLog: 审计日志
- Notifications: 通知中心
- MedicalPeriodCalculator: 医疗期计算器
- HealthCheck: 用工健康检查
- AnnualValueReport: 年度价值报表
- CompanyFiles: 公司文件管理
- LeaveApproval: 请假审批
- TrainingRecords: 培训记录
- PerformanceRecords: 绩效记录
- DisciplinaryRecords: 违纪记录

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-15 15:26:02 +08:00

174 lines
7.8 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'
import PageGuide from '../components/ui/PageGuide'
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">
<PageGuide>线</PageGuide>
<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>
)
}