0df8aa77d9
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
217 lines
8.3 KiB
TypeScript
217 lines
8.3 KiB
TypeScript
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>
|
||
)
|
||
}
|