feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { PenTool, Check, AlertCircle } from 'lucide-react'
|
||||
import api from '../../lib/api'
|
||||
import Card from '../../components/ui/Card'
|
||||
import Button from '../../components/ui/Button'
|
||||
|
||||
// 金额格式化:保留两位小数 + 千分位
|
||||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
export default function ContractConfirm() {
|
||||
const [params] = useSearchParams()
|
||||
const token = params.get('token') || ''
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [agreed, setAgreed] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [confirmed, setConfirmed] = useState(false)
|
||||
const [verifyCode, setVerifyCode] = useState('')
|
||||
const [sendingCode, setSendingCode] = useState(false)
|
||||
const [codeSent, setCodeSent] = useState(false)
|
||||
const [devCode, setDevCode] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
api.get(`/portal/contract-confirm/${token}`).then((res: any) => {
|
||||
setData(res.data)
|
||||
}).catch((err: any) => {
|
||||
setError(err.response?.data?.error?.message || '链接无效或已过期')
|
||||
}).finally(() => setLoading(false))
|
||||
} else {
|
||||
setError('缺少 token 参数')
|
||||
setLoading(false)
|
||||
}
|
||||
}, [token])
|
||||
|
||||
const handleSendCode = async () => {
|
||||
setSendingCode(true)
|
||||
setError('')
|
||||
try {
|
||||
const res = await api.post('/portal/contract-confirm/send-code', { token }) as any
|
||||
setCodeSent(true)
|
||||
setDevCode(res.data?.data?.code || '')
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error?.message || '验证码发送失败')
|
||||
} finally {
|
||||
setSendingCode(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await api.post('/portal/contract-confirm', { token, agreed: true, verifyCode })
|
||||
setConfirmed(true)
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error?.message || '确认失败')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmed) {
|
||||
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">已记录您的签署确认时间和 IP 地址。</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">
|
||||
<PenTool className="w-6 h-6 text-primary" />
|
||||
<h1 className="text-sm font-semibold">合同签署确认</h1>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-400">加载中...</div>
|
||||
) : error ? (
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 text-danger">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</Card>
|
||||
) : data ? (
|
||||
<Card>
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-600">
|
||||
{data.orgName} 与 {data.employeeName} 的劳动合同
|
||||
</div>
|
||||
|
||||
{data.contract && (
|
||||
<div className="space-y-2 text-sm">
|
||||
<Row label="合同类型" value={data.contract.contractType === 'FIXED' ? '固定期限' : data.contract.contractType === 'UNFIXED' ? '无固定期限' : '未签订'} />
|
||||
{data.contract.contractYears > 0 && <Row label="合同期限" value={`${data.contract.contractYears}年`} />}
|
||||
<Row label="合同开始" value={new Date(data.contract.startDate).toISOString().slice(0, 10)} />
|
||||
{data.contract.endDate && <Row label="合同结束" value={new Date(data.contract.endDate).toISOString().slice(0, 10)} />}
|
||||
{data.contract.probationMonths > 0 && <Row label="试用期" value={`${data.contract.probationMonths}个月`} />}
|
||||
{data.contract.probationSalary > 0 && <Row label="试用期工资" value={`¥${fmt(Number(data.contract.probationSalary))}`} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
|
||||
我已阅读合同内容,确认签署
|
||||
</label>
|
||||
|
||||
{/* 验证码区域 */}
|
||||
{agreed && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={verifyCode}
|
||||
onChange={(e) => setVerifyCode(e.target.value)}
|
||||
placeholder="请输入6位验证码"
|
||||
maxLength={6}
|
||||
className="flex-1 px-3 py-2 rounded-md border border-gray-300 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendCode}
|
||||
disabled={sendingCode || codeSent}
|
||||
className="px-3 py-2 rounded-md bg-gray-100 text-xs font-medium disabled:opacity-50 whitespace-nowrap"
|
||||
>
|
||||
{sendingCode ? '发送中' : codeSent ? '已发送' : '发送验证码'}
|
||||
</button>
|
||||
</div>
|
||||
{devCode && (
|
||||
<div className="text-xs text-blue-500">开发模式验证码:{devCode}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button className="w-full" onClick={handleConfirm} disabled={!agreed || submitting || !verifyCode}>
|
||||
{submitting ? '确认中...' : '确认签署'}
|
||||
</Button>
|
||||
<div className="text-xs text-gray-400 text-center">📌 确认后将记录签署时间、IP 地址和设备信息</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">{label}</span>
|
||||
<span className="font-medium">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user