feat: 员工门户UI优化 - PortalLayout/Logo组件/QR扫码登录/合同与政策页面重构

This commit is contained in:
selfrelease
2026-07-27 14:59:01 +08:00
parent 3f39c56f4b
commit 034fcc4111
18 changed files with 896 additions and 297 deletions
+98 -69
View File
@@ -1,13 +1,12 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { FileText, AlertCircle, Check, RefreshCw } from 'lucide-react'
import { FileText, AlertCircle, Check, RefreshCw, Calendar, Briefcase, Clock } from 'lucide-react'
import api from '../../lib/api'
import Card from '../../components/ui/Card'
import Button from '../../components/ui/Button'
import EmptyState from '../../components/ui/EmptyState'
// 金额格式化:保留两位小数 + 千分位
/** 金额格式化:保留两位小数 + 千分位 */
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
const portalApi = api.create({ baseURL: '/api/v1/portal' })
@@ -29,7 +28,6 @@ export default function MyContract() {
},
})
const employee = JSON.parse(localStorage.getItem('portalEmployee') || '{}')
const contract = data
const daysToExpire = contract?.endDate
@@ -52,79 +50,110 @@ export default function MyContract() {
}
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 justify-between mb-6">
<div className="flex items-center gap-2">
<FileText className="w-6 h-6 text-primary" />
<h1 className="text-sm font-semibold"></h1>
<div className="space-y-4">
{/* 页面标题 */}
<h1 className="text-lg font-bold text-gray-900"></h1>
{isLoading ? (
<Card className="p-6">
<div className="animate-pulse space-y-3">
<div className="h-6 bg-gray-100 rounded w-1/2" />
<div className="h-4 bg-gray-100 rounded w-full" />
<div className="h-4 bg-gray-100 rounded w-2/3" />
<div className="h-10 bg-gray-100 rounded-lg w-full" />
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500">{employee.name}</span>
<Link to="/portal/payslip" className="text-sm text-primary"></Link>
</div>
</div>
<Card>
{isLoading ? (
<div className="text-center py-8 text-gray-400">...</div>
) : !contract ? (
<EmptyState title="暂无合同" description="HR 尚未录入您的合同信息" />
) : (
<div className="space-y-3">
{/* 到期提醒 */}
{daysToExpire !== null && daysToExpire <= 30 && daysToExpire >= 0 && (
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-yellow-50 text-yellow-700 text-sm">
<AlertCircle className="w-4 h-4" />
{daysToExpire}
</div>
)}
<div className="space-y-2 text-sm">
<Row label="合同类型" value={contract.contractType === 'FIXED' ? '固定期限' : contract.contractType === 'UNFIXED' ? '无固定期限' : '未签订'} />
<Row label="签订方式" value={contract.signMethod === 'PAPER' ? '纸质合同' : '电子合同'} />
{contract.signDate && <Row label="签订日期" value={new Date(contract.signDate).toISOString().slice(0, 10)} />}
<Row label="合同开始" value={new Date(contract.startDate).toISOString().slice(0, 10)} />
{contract.endDate && <Row label="合同结束" value={new Date(contract.endDate).toISOString().slice(0, 10)} />}
{contract.contractYears > 0 && <Row label="合同期限" value={`${contract.contractYears}`} />}
{contract.probationMonths > 0 && <Row label="试用期" value={`${contract.probationMonths}个月`} />}
{contract.probationSalary > 0 && <Row label="试用期工资" value={`¥${fmt(Number(contract.probationSalary))}`} />}
</div>
{/* 签署确认记录 */}
<div className="border-t pt-3">
<h3 className="font-medium text-sm mb-2"></h3>
{isConfirmed ? (
<div className="flex items-center gap-2 text-sm text-safe">
<Check className="w-4 h-4" />
{new Date(contract.attachmentName.slice(10).split('|')[0]).toLocaleString()}
</div>
) : (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-warning">
<AlertCircle className="w-4 h-4" />
</div>
<Button size="sm" variant="secondary" onClick={handleResend} disabled={resending}>
<RefreshCw className="w-3 h-3 mr-1" />{resending ? '重发中...' : '重发确认链接'}
</Button>
{resendMsg && <div className="text-xs text-gray-500">{resendMsg}</div>}
</div>
)}
</div>
</Card>
) : !contract ? (
<Card className="p-6">
<EmptyState title="暂无合同" description="HR 尚未录入您的合同信息" />
</Card>
) : (
<>
{/* 到期提醒横幅 */}
{daysToExpire !== null && daysToExpire <= 30 && daysToExpire >= 0 && (
<div className="flex items-start gap-2 px-4 py-3 rounded-xl bg-amber-50 text-amber-700 text-sm">
<AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<span> <strong>{daysToExpire}</strong> </span>
</div>
)}
</Card>
</div>
{/* 合同概览卡片 */}
<Card className="p-5">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0">
<FileText className="w-5 h-5 text-primary" />
</div>
<div className="min-w-0">
<div className="text-sm font-semibold text-gray-900 truncate">
{contract.contractType === 'FIXED' ? '固定期限劳动合同' : contract.contractType === 'UNFIXED' ? '无固定期限劳动合同' : '未签订'}
</div>
<div className="text-xs text-gray-500">{contract.signMethod === 'PAPER' ? '纸质合同' : '电子合同'}</div>
</div>
</div>
<div className="space-y-3">
<InfoRow icon={Calendar} label="合同开始" value={new Date(contract.startDate).toISOString().slice(0, 10)} />
{contract.endDate && (
<InfoRow icon={Calendar} label="合同结束" value={new Date(contract.endDate).toISOString().slice(0, 10)} />
)}
{contract.contractYears > 0 && (
<InfoRow icon={Briefcase} label="合同期限" value={`${contract.contractYears}`} />
)}
{contract.signDate && (
<InfoRow icon={Calendar} label="签订日期" value={new Date(contract.signDate).toISOString().slice(0, 10)} />
)}
{contract.probationMonths > 0 && (
<InfoRow icon={Clock} label="试用期" value={`${contract.probationMonths}个月`} />
)}
{contract.probationSalary > 0 && (
<InfoRow icon={Briefcase} label="试用期工资" value={`¥${fmt(Number(contract.probationSalary))}`} />
)}
</div>
</Card>
{/* 签署确认记录 */}
<Card className="p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3"></h3>
{isConfirmed ? (
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
<Check className="w-5 h-5 text-green-600" />
</div>
<div>
<div className="text-sm font-medium text-green-700"></div>
<div className="text-xs text-gray-400">
{new Date(contract.attachmentName.slice(10).split('|')[0]).toLocaleString()}
</div>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm text-amber-600">
<AlertCircle className="w-4 h-4" />
<span></span>
</div>
<Button size="sm" variant="secondary" onClick={handleResend} disabled={resending}>
<RefreshCw className={`w-3.5 h-3.5 mr-1 ${resending ? 'animate-spin' : ''}`} />
{resending ? '重发中...' : '重发确认链接'}
</Button>
{resendMsg && <div className="text-xs text-gray-500">{resendMsg}</div>}
</div>
)}
</Card>
</>
)}
</div>
)
}
function Row({ label, value }: { label: string; value: string }) {
function InfoRow({ icon: Icon, label, value }: { icon: any; label: string; value: string }) {
return (
<div className="flex justify-between">
<span className="text-gray-500">{label}</span>
<span className="font-medium">{value}</span>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-shrink-0">
<Icon className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500">{label}</span>
</div>
<span className="text-sm font-medium text-gray-800 text-right truncate">{value}</span>
</div>
)
}