Files
TurboHR/frontend/src/pages/portal/Onboarding.tsx
T
selfrelease 0df8aa77d9 feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出)
- 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条)
- 社保公积金(多城市配置、版本管理、基数调整)
- 解聘管理(6步流程、证据链、工作交接)
- AI 助手(合同审查、风险预测、RAG 知识库)
- Dashboard 仪表盘
- 设置与通知
2026-07-24 13:53:11 +08:00

217 lines
8.3 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 { 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>
)
}