feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全

- 面包屑导航组件,集成至TopNav header
- 侧边栏菜单分组间距增大,分组间分隔线
- 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计
- 修复Policies.tsx民主程序推进bug(字段名/API路径/参数)
- 用工文本模板变量名英文转中文显示
- 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY)
- 通知示例数据补充
- h2标题统一为text-sm font-medium
- 新增run.md
This commit is contained in:
selfrelease
2026-07-26 20:32:38 +08:00
parent 9cb0d1f63b
commit d79e3baa34
71 changed files with 18561 additions and 3230 deletions
@@ -0,0 +1,279 @@
/**
* 医疗期计算器
* 根据员工工龄和地区计算法定医疗期天数
* 法律依据:《企业职工患病或非因工负伤医疗期规定》(劳部发[1994]479号)
* 上海特殊规定:沪府发[2015]40号
*/
import { useState } from 'react'
import { Calculator, HeartPulse, Info } from 'lucide-react'
import Card from '../../components/ui/Card'
import Button from '../../components/ui/Button'
interface MedicalPeriodResult {
totalMonths: number
cumulativeDays: number
actualDays: number
endDate: string
legalBasis: string
notes: string[]
}
/**
* 计算医疗期
* @param workYears 本单位工作年限
* @param region 地区(上海/全国)
* @param sickDays 累计病休天数
* @param startDate 开始病休日期
*/
function calculateMedicalPeriod(
workYears: number,
region: 'shanghai' | 'national',
sickDays: number,
startDate: string,
): MedicalPeriodResult | null {
if (!startDate || workYears < 0) return null
let totalMonths: number
let cumulativeDays: number
let legalBasis: string
const notes: string[] = []
if (region === 'shanghai') {
// 上海特殊规定:直接按工龄分档
if (workYears < 1) {
totalMonths = 3
cumulativeDays = 6 * 30 // 6个月周期
legalBasis = '《上海市关于本市劳动者在履行劳动合同期间患病或者非因工负伤的医疗期标准的规定》'
} else if (workYears < 4) {
totalMonths = 3
cumulativeDays = 6 * 30
legalBasis = '《上海市关于本市劳动者在履行劳动合同期间患病或者非因工负伤的医疗期标准的规定》'
} else if (workYears < 10) {
totalMonths = 6
cumulativeDays = 12 * 30
legalBasis = '《上海市关于本市劳动者在履行劳动合同期间患病或者非因工负伤的医疗期标准的规定》'
} else {
totalMonths = 9
cumulativeDays = 18 * 30
legalBasis = '《上海市关于本市劳动者在履行劳动合同期间患病或者非因工负伤的医疗期标准的规定》'
}
notes.push('上海地区适用特殊规定,医疗期不按累计病休天数折算')
} else {
// 全国通用规定:劳部发[1994]479号
if (workYears < 5) {
totalMonths = 3
cumulativeDays = 6 * 30 // 6个月内累计病休
legalBasis = '《企业职工患病或非因工负伤医疗期规定》第三条(劳部发[1994]479号)'
} else if (workYears < 10) {
totalMonths = 6
cumulativeDays = 12 * 30 // 12个月内累计病休
legalBasis = '《企业职工患病或非因工负伤医疗期规定》第三条(劳部发[1994]479号)'
} else if (workYears < 15) {
totalMonths = 9
cumulativeDays = 15 * 30 // 15个月内累计病休
legalBasis = '《企业职工患病或非因工负伤医疗期规定》第三条(劳部发[1994]479号)'
} else if (workYears < 20) {
totalMonths = 12
cumulativeDays = 18 * 30 // 18个月内累计病休
legalBasis = '《企业职工患病或非因工负伤医疗期规定》第三条(劳部发[1994]479号)'
} else {
totalMonths = 24
cumulativeDays = 30 * 30 // 30个月内累计病休
legalBasis = '《企业职工患病或非因工负伤医疗期规定》第三条(劳部发[1994]479号)'
}
notes.push(`${cumulativeDays / 30} 个月的累计周期内,病休累计不超过 ${totalMonths} 个月即享有医疗期保护`)
}
// 计算实际可用天数
const actualDays = Math.max(0, totalMonths * 30 - sickDays)
// 计算医疗期结束日期
const start = new Date(startDate)
const endDate = new Date(start)
endDate.setMonth(endDate.getMonth() + totalMonths)
notes.push('医疗期内企业不得解除劳动合同(法定情形除外)')
notes.push('医疗期满后不能从事原工作也不能从事另行安排的工作,企业可提前30天通知或支付代通知金解除')
return {
totalMonths,
cumulativeDays,
actualDays,
endDate: endDate.toISOString().slice(0, 10),
legalBasis,
notes,
}
}
/**
* 医疗期计算器页面
*/
export default function MedicalPeriodCalculator() {
const [region, setRegion] = useState<'national' | 'shanghai'>('national')
const [workYears, setWorkYears] = useState('')
const [sickDays, setSickDays] = useState('0')
const [startDate, setStartDate] = useState('')
const [result, setResult] = useState<MedicalPeriodResult | null>(null)
const handleCalculate = () => {
const years = parseFloat(workYears) || 0
const days = parseInt(sickDays) || 0
const r = calculateMedicalPeriod(years, region, days, startDate)
setResult(r)
}
const handleReset = () => {
setRegion('national')
setWorkYears('')
setSickDays('0')
setStartDate('')
setResult(null)
}
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<HeartPulse className="h-5 w-5 text-primary" />
<h1 className="text-base font-semibold"></h1>
</div>
<Card>
<div className="space-y-3">
{/* 地区选择 */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1"></label>
<select
value={region}
onChange={(e) => setRegion(e.target.value as 'national' | 'shanghai')}
className="w-full px-3 py-2 text-sm border rounded-md focus:outline-none focus:ring-1 focus:ring-primary"
>
<option value="national"></option>
<option value="shanghai"></option>
</select>
</div>
{/* 工龄输入 */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1"></label>
<input
type="number"
value={workYears}
onChange={(e) => setWorkYears(e.target.value)}
placeholder="如:5.5"
step="0.5"
min="0"
className="w-full px-3 py-2 text-sm border rounded-md focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
{/* 病休开始日期 */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1"></label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full px-3 py-2 text-sm border rounded-md focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
{/* 累计病休天数 */}
<div>
<label className="block text-xs font-medium text-gray-700 mb-1"></label>
<input
type="number"
value={sickDays}
onChange={(e) => setSickDays(e.target.value)}
placeholder="0"
min="0"
className="w-full px-3 py-2 text-sm border rounded-md focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
<div className="flex gap-2">
<Button onClick={handleCalculate} className="flex-1">
<Calculator className="w-4 h-4 mr-1" />
</Button>
<Button variant="secondary" onClick={handleReset}>
</Button>
</div>
</div>
</Card>
{/* 计算结果 */}
{result && (
<Card className="border-primary/20 bg-primary/5">
<div className="space-y-3">
<h2 className="text-sm font-medium flex items-center gap-1.5">
<Info className="w-4 h-4 text-primary" />
</h2>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-white">
<div className="text-xs text-gray-500"></div>
<div className="text-lg font-bold text-primary">{result.totalMonths} </div>
</div>
<div className="p-3 rounded-lg bg-white">
<div className="text-xs text-gray-500"></div>
<div className="text-lg font-bold text-primary">{result.cumulativeDays / 30} </div>
</div>
<div className="p-3 rounded-lg bg-white">
<div className="text-xs text-gray-500"></div>
<div className={`text-lg font-bold ${result.actualDays > 0 ? 'text-safe' : 'text-danger'}`}>
{result.actualDays}
</div>
</div>
<div className="p-3 rounded-lg bg-white">
<div className="text-xs text-gray-500"></div>
<div className="text-sm font-bold text-gray-800">{result.endDate}</div>
</div>
</div>
{/* 法律依据 */}
<div className="p-2 rounded-md bg-amber-50 text-xs text-amber-800">
<span className="font-medium"></span>{result.legalBasis}
</div>
{/* 注意事项 */}
<div className="space-y-1">
<div className="text-xs font-medium text-gray-700"></div>
{result.notes.map((note, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs text-gray-600">
<span className="w-1.5 h-1.5 rounded-full bg-primary mt-1 flex-shrink-0" />
{note}
</div>
))}
</div>
</div>
</Card>
)}
{/* 工龄分档表 */}
<Card>
<h2 className="text-sm font-medium mb-2"></h2>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b text-left text-gray-500">
<th className="py-2 pr-3"></th>
<th className="py-2 pr-3"></th>
<th className="py-2 pr-3"></th>
</tr>
</thead>
<tbody className="divide-y">
<tr><td className="py-2 pr-3"> 5 </td><td className="py-2 pr-3">3 </td><td className="py-2 pr-3">6 </td></tr>
<tr><td className="py-2 pr-3">5-10 </td><td className="py-2 pr-3">6 </td><td className="py-2 pr-3">12 </td></tr>
<tr><td className="py-2 pr-3">10-15 </td><td className="py-2 pr-3">9 </td><td className="py-2 pr-3">15 </td></tr>
<tr><td className="py-2 pr-3">15-20 </td><td className="py-2 pr-3">12 </td><td className="py-2 pr-3">18 </td></tr>
<tr><td className="py-2 pr-3">20 </td><td className="py-2 pr-3">24 </td><td className="py-2 pr-3">30 </td></tr>
</tbody>
</table>
</div>
</Card>
</div>
)
}