feat: AIHR 智能人力资源管理系统初始提交

- 员工花名册管理(加密存储、导入导出)
- 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条)
- 社保公积金(多城市配置、版本管理、基数调整)
- 解聘管理(6步流程、证据链、工作交接)
- AI 助手(合同审查、风险预测、RAG 知识库)
- Dashboard 仪表盘
- 设置与通知
This commit is contained in:
selfrelease
2026-07-24 13:53:11 +08:00
commit 0df8aa77d9
109 changed files with 38190 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
import { useState, useRef } from 'react'
import { useSearchParams } from 'react-router-dom'
import { ClipboardList, Check, FileText, X } from 'lucide-react'
import api from '../../lib/api'
import Card from '../../components/ui/Card'
import Button from '../../components/ui/Button'
import { Input, Label } from '../../components/ui/Input'
const FILE_TYPES = [
{ key: 'ID_CARD_FRONT', label: '身份证正面' },
{ key: 'ID_CARD_BACK', label: '身份证反面' },
{ key: 'EDUCATION', label: '学历证明' },
{ key: 'BANK_CARD', label: '银行卡照片' },
{ key: 'OTHER', label: '其他材料' },
]
interface UploadedFile {
fileType: string
fileName: string
fileUrl: string
fileSize: number
}
export default function Onboarding() {
const [params] = useSearchParams()
const token = params.get('token') || ''
const [orgName, setOrgName] = useState('')
const [loading, setLoading] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [error, setError] = useState('')
const [uploading, setUploading] = useState(false)
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const [currentFileType, setCurrentFileType] = useState('ID_CARD_FRONT')
const [form, setForm] = useState({
name: '',
phone: '',
idCard: '',
emergencyContact: '',
emergencyPhone: '',
address: '',
bankCard: '',
bankName: '',
})
// 获取链接信息
useState(() => {
if (token) {
api.get(`/portal/onboarding/${token}`).then((res: any) => {
setOrgName(res.data.orgName)
}).catch((err: any) => {
setError(err.response?.data?.error?.message || '链接无效')
})
}
})
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
setError('')
try {
const formData = new FormData()
formData.append('file', file)
formData.append('fileType', currentFileType)
const res = await api.post(`/portal/onboarding/${token}/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
}) as any
setUploadedFiles([...uploadedFiles, res.data.data])
} catch (err: any) {
setError(err.response?.data?.error?.message || '文件上传失败')
} finally {
setUploading(false)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
const removeFile = (idx: number) => {
setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
}
const handleSubmit = async () => {
setError('')
setLoading(true)
try {
await api.post('/portal/onboarding', { ...form, token, attachments: uploadedFiles })
setSubmitted(true)
} catch (err: any) {
setError(err.response?.data?.error?.message || '提交失败')
} finally {
setLoading(false)
}
}
if (submitted) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<div className="max-w-sm w-full text-center">
<Check className="w-16 h-16 text-safe mx-auto mb-4" />
<h1 className="text-sm font-semibold mb-2"></h1>
<p className="text-sm text-gray-500">HR </p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 px-4 py-6">
<div className="max-w-md mx-auto">
<div className="flex items-center gap-2 mb-6">
<ClipboardList className="w-6 h-6 text-primary" />
<h1 className="text-sm font-semibold"></h1>
</div>
{orgName && (
<div className="mb-4 text-sm text-gray-600">
{orgName}
</div>
)}
{error && (
<div className="mb-4 px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">{error}</div>
)}
<Card>
<div className="space-y-3">
<div>
<Label> *</Label>
<Input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="请输入姓名" />
</div>
<div>
<Label> *</Label>
<Input type="tel" value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="请输入手机号" maxLength={11} />
</div>
<div>
<Label> *</Label>
<Input value={form.idCard} onChange={(e) => setForm({ ...form, idCard: e.target.value })} placeholder="请输入身份证号" maxLength={18} />
</div>
<div>
<Label></Label>
<Input value={form.emergencyContact} onChange={(e) => setForm({ ...form, emergencyContact: e.target.value })} placeholder="选填" />
</div>
<div>
<Label></Label>
<Input type="tel" value={form.emergencyPhone} onChange={(e) => setForm({ ...form, emergencyPhone: e.target.value })} placeholder="选填" maxLength={11} />
</div>
<div>
<Label></Label>
<Input value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} placeholder="选填" />
</div>
<div>
<Label></Label>
<Input value={form.bankCard} onChange={(e) => setForm({ ...form, bankCard: e.target.value })} placeholder="选填" />
</div>
<div>
<Label></Label>
<Input value={form.bankName} onChange={(e) => setForm({ ...form, bankName: e.target.value })} placeholder="选填" />
</div>
{/* 文件上传区域 */}
<div className="border-t pt-3">
<Label></Label>
<div className="flex flex-wrap gap-2 mb-2">
{FILE_TYPES.map((ft) => (
<button
key={ft.key}
type="button"
onClick={() => setCurrentFileType(ft.key)}
className={`px-2 py-1 rounded text-xs ${currentFileType === ft.key ? 'bg-primary text-white' : 'bg-gray-100 text-gray-600'}`}
>
{ft.label}
</button>
))}
</div>
<input
ref={fileInputRef}
type="file"
accept=".jpg,.jpeg,.png,.pdf,.bmp"
onChange={handleFileUpload}
className="hidden"
/>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="w-full py-2 border-2 border-dashed border-gray-300 rounded-md text-xs text-gray-500 hover:border-primary"
>
{uploading ? '上传中...' : `点击上传${FILE_TYPES.find(f => f.key === currentFileType)?.label || ''}`}
</button>
{uploadedFiles.length > 0 && (
<div className="mt-2 space-y-1">
{uploadedFiles.map((f, i) => (
<div key={i} className="flex items-center justify-between px-2 py-1 bg-gray-50 rounded text-xs">
<div className="flex items-center gap-1 min-w-0">
<FileText className="w-3 h-3 flex-shrink-0 text-gray-400" />
<span className="truncate">{FILE_TYPES.find(ft => ft.key === f.fileType)?.label || f.fileType}: {f.fileName}</span>
</div>
<button onClick={() => removeFile(i)} className="text-gray-400 hover:text-danger flex-shrink-0">
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
)}
</div>
<Button className="w-full" onClick={handleSubmit} disabled={loading || !form.name || !form.phone || !form.idCard}>
{loading ? '提交中...' : '提交'}
</Button>
<div className="text-xs text-gray-400 text-center">📌 HR </div>
</div>
</Card>
</div>
</div>
)
}