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
@@ -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>
)
}