diff --git a/backend/src/routes/ai.routes.ts b/backend/src/routes/ai.routes.ts index d0b5762..9aa1a42 100644 --- a/backend/src/routes/ai.routes.ts +++ b/backend/src/routes/ai.routes.ts @@ -78,15 +78,14 @@ function daysFromNow(date: Date): number { } /** - * 脱敏身份证号:保留前6位和后4位,中间用 * 代替 - * 例:310101199001011234 → 310101********1234 + * 解密身份证号(不脱敏,AI 需要完整身份证号来正确读取生日等信息) */ -function maskIdCard(encrypted: string | null): string { +function decryptIdCard(encrypted: string | null): string { if (!encrypted) return '未登记' try { const plain = decrypt(encrypted) if (plain && plain.length >= 10) { - return plain.slice(0, 6) + '********' + plain.slice(-4) + return plain } return '未登记' } catch { @@ -115,7 +114,7 @@ async function buildOrgContext(orgId: string): Promise { if (e.isPregnant) specialStatus.push('孕期/哺乳期') if (e.isInMedicalPeriod) specialStatus.push('医疗期') if (e.isWorkInjured) specialStatus.push('工伤') - return `- ${e.name}(身份证:${maskIdCard(e.idCardNumber)},${e.department}),入职${e.hireDate.toISOString().slice(0, 10)}(已工作${tenure}),${contract ? `合同:${contract.contractType},${contract.endDate ? `到期${contract.endDate.toISOString().slice(0, 10)}(剩余${daysToExpire}天)` : '无固定期限'}` : '未签合同'}${specialStatus.length > 0 ? `,特殊状态:${specialStatus.join('/')}` : ''}` + return `- ${e.name}(身份证:${decryptIdCard(e.idCardNumber)},${e.department}),入职${e.hireDate.toISOString().slice(0, 10)}(已工作${tenure}),${contract ? `合同:${contract.contractType},${contract.endDate ? `到期${contract.endDate.toISOString().slice(0, 10)}(剩余${daysToExpire}天)` : '无固定期限'}` : '未签合同'}${specialStatus.length > 0 ? `,特殊状态:${specialStatus.join('/')}` : ''}` }).join('\n') const riskSummary = risks.map((r) => `- ${r.title}(${r.level}):${r.description || '无详细描述'}`).join('\n') @@ -265,17 +264,17 @@ router.get('/predict', authMiddleware, async (req: AuthRequest, res, next) => { orgContext = `当前日期:${new Date().toISOString().slice(0, 10)} 员工详情: - 姓名:${emp.name} -- 身份证号:${maskIdCard(emp.idCardNumber)} +- 身份证号:${decryptIdCard(emp.idCardNumber)} - 部门:${emp.department} - 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure}) - 状态:${emp.status} - 特殊状态:${[emp.isPregnant && '孕期/哺乳期', emp.isInMedicalPeriod && '医疗期', emp.isWorkInjured && '工伤'].filter(Boolean).join('、') || '无'} - 合同:${contract ? `${contract.contractType},${contract.startDate.toISOString().slice(0, 10)}至${contract.endDate ? contract.endDate.toISOString().slice(0, 10) : '无固定期限'}${contractDaysLeft !== null ? `(距到期${contractDaysLeft}天)` : ''}` : '未签合同'}${term ? `\n- 离职/解聘记录:${typeMap[term.type] || term.type},原因:${reasonMap[term.reason] || term.reason},离职日期:${term.terminationDate.toISOString().slice(0, 10)},流程状态:${statusMap[term.status] || term.status},经济补偿金:¥${term.compensation.toFixed(2)}${term.resignationReason ? `,离职原因说明:${term.resignationReason}` : ''}${socialInsEnd}${housingFundEnd},${handoverDone ? '工作交接已完成' : '工作交接未完成'},${agreementSigned ? '已签署解除协议' : '未签署解除协议'}` : ''}` - scopeHint = `请只针对员工【${emp.name}(身份证:${maskIdCard(emp.idCardNumber)})】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。` + scopeHint = `请只针对员工【${emp.name}(身份证:${decryptIdCard(emp.idCardNumber)})】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。` } } else if (department) { const employees = await prisma.employee.findMany({ where: { orgId: req.user!.orgId, department, status: 'ACTIVE' }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } }) - const empSummary = employees.map(e => `- ${e.name}(身份证:${maskIdCard(e.idCardNumber)}),入职${e.hireDate.toISOString().slice(0, 10)},${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n') + const empSummary = employees.map(e => `- ${e.name}(身份证:${decryptIdCard(e.idCardNumber)}),入职${e.hireDate.toISOString().slice(0, 10)},${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n') orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}` scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。` } @@ -324,17 +323,17 @@ router.get('/predict-stream', authMiddleware, async (req: AuthRequest, res, next orgContext = `当前日期:${new Date().toISOString().slice(0, 10)} 员工详情: - 姓名:${emp.name} -- 身份证号:${maskIdCard(emp.idCardNumber)} +- 身份证号:${decryptIdCard(emp.idCardNumber)} - 部门:${emp.department} - 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure}) - 状态:${emp.status} - 特殊状态:${[emp.isPregnant && '孕期/哺乳期', emp.isInMedicalPeriod && '医疗期', emp.isWorkInjured && '工伤'].filter(Boolean).join('、') || '无'} - 合同:${contract ? `${contract.contractType},${contract.startDate.toISOString().slice(0, 10)}至${contract.endDate ? contract.endDate.toISOString().slice(0, 10) : '无固定期限'}${contractDaysLeft !== null ? `(距到期${contractDaysLeft}天)` : ''}` : '未签合同'}${term ? `\n- 离职/解聘记录:${typeMap[term.type] || term.type},原因:${reasonMap[term.reason] || term.reason},离职日期:${term.terminationDate.toISOString().slice(0, 10)},流程状态:${statusMap[term.status] || term.status},经济补偿金:¥${term.compensation.toFixed(2)}${term.resignationReason ? `,离职原因说明:${term.resignationReason}` : ''}${socialInsEnd}${housingFundEnd},${handoverDone ? '工作交接已完成' : '工作交接未完成'},${agreementSigned ? '已签署解除协议' : '未签署解除协议'}` : ''}` - scopeHint = `请只针对员工【${emp.name}(身份证:${maskIdCard(emp.idCardNumber)})】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。` + scopeHint = `请只针对员工【${emp.name}(身份证:${decryptIdCard(emp.idCardNumber)})】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。` } } else if (department) { const employees = await prisma.employee.findMany({ where: { orgId: req.user!.orgId, department, status: 'ACTIVE' }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } }) - const empSummary = employees.map(e => `- ${e.name}(身份证:${maskIdCard(e.idCardNumber)}),入职${e.hireDate.toISOString().slice(0, 10)},${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n') + const empSummary = employees.map(e => `- ${e.name}(身份证:${decryptIdCard(e.idCardNumber)}),入职${e.hireDate.toISOString().slice(0, 10)},${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n') orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}` scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。` } diff --git a/frontend/src/pages/portal/PortalLogin.tsx b/frontend/src/pages/portal/PortalLogin.tsx index 0b5357e..d6e787e 100644 --- a/frontend/src/pages/portal/PortalLogin.tsx +++ b/frontend/src/pages/portal/PortalLogin.tsx @@ -1,9 +1,8 @@ import { useState } from 'react' import { useNavigate } from 'react-router-dom' +import { Phone, Lock, ShieldCheck, ArrowRight } from 'lucide-react' import Logo from '../../components/ui/Logo' import { portalApi } from '../../lib/api-services' -import { Input, Label } from '../../components/ui/Input' -import Button from '../../components/ui/Button' type LoginMode = 'password' | 'code' @@ -60,69 +59,130 @@ export default function PortalLogin() { } return ( -
+
-
- - 安职通 — 员工端 + {/* Logo & 标题 */} +
+
+ +
+
+

安职通

+

员工端

+
-
-
+ {/* 登录卡片 */} +
+ {/* Tab 切换 */} +
- {error &&
{error}
} + {error && ( +
+ + {error} +
+ )} {mode === 'password' ? (
-
- - setPhone(e.target.value)} maxLength={11} className="h-11" /> +
+ + setPhone(e.target.value)} + maxLength={11} + className="w-full h-12 pl-11 pr-4 rounded-xl border border-gray-200 bg-gray-50 text-sm focus:bg-white focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-all" + />
-
- - setPassword(e.target.value)} className="h-11" /> +
+ + setPassword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handlePasswordLogin()} + className="w-full h-12 pl-11 pr-4 rounded-xl border border-gray-200 bg-gray-50 text-sm focus:bg-white focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-all" + />
- +
) : (
-
- - setPhone(e.target.value)} maxLength={11} className="h-11" /> +
+ + setPhone(e.target.value)} + maxLength={11} + className="w-full h-12 pl-11 pr-4 rounded-xl border border-gray-200 bg-gray-50 text-sm focus:bg-white focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-all" + />
-
-
- - setCode(e.target.value)} maxLength={6} className="h-11" /> -
-
- +
+
+ + setCode(e.target.value)} + maxLength={6} + onKeyDown={(e) => e.key === 'Enter' && handleCodeLogin()} + className="w-full h-12 pl-11 pr-4 rounded-xl border border-gray-200 bg-gray-50 text-sm focus:bg-white focus:border-primary focus:ring-1 focus:ring-primary outline-none transition-all" + />
+
{codeSent && displayedCode && ( -
+
验证码:{displayedCode}(开发阶段直接显示,生产环境将发送短信)
)} - +
)}
+ +

登录即表示同意《用户协议》和《隐私政策》

)