42e0c650a4
- AI文件审查:.docx上传提取文本,支持多种文档类型 - 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程 - 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word - 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面 - 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤 - 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作 - Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型 - 前后端编译验证全部通过
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
/**
|
|
* 员工端统一布局 — 顶部 Header(Logo + 员工名 + 退出)+ 底部固定 TabBar 导航
|
|
* 所有已登录员工端页面共享此布局
|
|
*/
|
|
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
|
import { DollarSign, FileText, ScrollText, LogOut, CalendarCheck } from 'lucide-react'
|
|
import Logo from '../../components/ui/Logo'
|
|
|
|
const tabItems = [
|
|
{ path: '/portal/payslip', label: '工资条', icon: DollarSign },
|
|
{ path: '/portal/contract', label: '我的合同', icon: FileText },
|
|
{ path: '/portal/attendance', label: '我的考勤', icon: CalendarCheck },
|
|
{ path: '/portal/policies', label: '规章制度', icon: ScrollText },
|
|
]
|
|
|
|
export default function PortalLayout({ children }: { children: React.ReactNode }) {
|
|
const location = useLocation()
|
|
const navigate = useNavigate()
|
|
|
|
const employee = (() => {
|
|
try { return JSON.parse(localStorage.getItem('portalEmployee') || '{}') } catch { return {} }
|
|
})()
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('portalToken')
|
|
localStorage.removeItem('portalEmployee')
|
|
navigate('/portal/login')
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex flex-col">
|
|
{/* 顶部 Header — 安全区 padding 适配刘海屏 */}
|
|
<header className="sticky top-0 z-30 bg-white border-b border-gray-200 pt-safe">
|
|
<div className="max-w-md mx-auto h-14 flex items-center justify-between px-4">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Logo className="w-6 h-6 text-primary flex-shrink-0" />
|
|
<span className="text-sm font-bold text-gray-900 truncate">企业用工专家</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{employee.name && (
|
|
<span className="text-xs text-gray-500 truncate max-w-[60px]">{employee.name}</span>
|
|
)}
|
|
<button
|
|
onClick={handleLogout}
|
|
className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-600 px-2 py-1.5 rounded-md hover:bg-gray-50"
|
|
>
|
|
<LogOut className="w-3.5 h-3.5" />
|
|
退出
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* 主内容区 */}
|
|
<main className="flex-1 max-w-md mx-auto w-full px-4 py-6 pb-28">
|
|
{children}
|
|
</main>
|
|
|
|
{/* 底部固定 TabBar — 安全区 padding 适配 home indicator */}
|
|
<nav className="fixed bottom-0 left-0 right-0 z-30 bg-white border-t border-gray-200 pb-safe">
|
|
<div className="max-w-md mx-auto flex items-center justify-around h-16">
|
|
{tabItems.map((item) => {
|
|
const Icon = item.icon
|
|
const active = location.pathname === item.path ||
|
|
(item.path === '/portal/payslip' && location.pathname.startsWith('/portal/payslip'))
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
to={item.path}
|
|
className={`flex flex-col items-center justify-center gap-0.5 flex-1 h-full transition-colors ${
|
|
active ? 'text-primary' : 'text-gray-400 hover:text-gray-600'
|
|
}`}
|
|
>
|
|
<Icon className={`w-5 h-5 ${active ? 'fill-primary/10' : ''}`} />
|
|
<span className={`text-xs ${active ? 'font-medium' : ''}`}>{item.label}</span>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|