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
@@ -0,0 +1,83 @@
/**
* 员工端统一布局 — 顶部 Header(Logo + 员工名 + 退出)+ 底部固定 TabBar 导航
* 所有已登录员工端页面共享此布局
*/
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { DollarSign, FileText, ScrollText, LogOut } 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/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>
)
}
@@ -15,6 +15,7 @@ import {
ChevronDown, ChevronRight,
Building2,
} from 'lucide-react'
import Logo from '../ui/Logo'
interface NavItem {
path: string
@@ -120,7 +121,7 @@ export default function SidebarNav({ mobileOpen, onClose }: { mobileOpen: boolea
>
{/* Logo 区 */}
<div className="h-14 flex items-center gap-2 px-4 border-b border-gray-200 shrink-0">
<Building2 className="w-5 h-5 text-primary" />
<Logo className="w-5 h-5 text-primary" />
<span className="font-bold text-sm text-gray-900"></span>
</div>
+12 -2
View File
@@ -1,17 +1,19 @@
import { Link, useNavigate } from 'react-router-dom'
import { ChevronDown, Settings as SettingsIcon, Bell, Menu, HelpCircle } from 'lucide-react'
import { ChevronDown, Settings as SettingsIcon, Bell, Menu, HelpCircle, Smartphone } from 'lucide-react'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useAuthStore } from '../../store/authStore'
import api from '../../lib/api'
import Breadcrumb from './Breadcrumb'
import HelpModal from '../HelpModal'
import PortalQRModal from '../PortalQRModal'
export default function TopNav({ onMenuClick }: { onMenuClick?: () => void }) {
const navigate = useNavigate()
const { user, logout } = useAuthStore()
const [menuOpen, setMenuOpen] = useState(false)
const [helpOpen, setHelpOpen] = useState(false)
const [portalQROpen, setPortalQROpen] = useState(false)
const { data: dashboardData } = useQuery<any>({
queryKey: ['dashboard'],
@@ -38,8 +40,16 @@ export default function TopNav({ onMenuClick }: { onMenuClick?: () => void }) {
<Breadcrumb />
</div>
{/* 右侧:帮助 + 通知 + 设置 + 用户菜单 */}
{/* 右侧:员工端 + 帮助 + 通知 + 设置 + 用户菜单 */}
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => setPortalQROpen(true)}
className="p-1.5 rounded-md hover:bg-gray-100"
aria-label="员工端入口"
>
<Smartphone className="w-4 h-4 text-gray-600" />
</button>
<PortalQRModal open={portalQROpen} onClose={() => setPortalQROpen(false)} />
<button
onClick={() => setHelpOpen(true)}
className="p-1.5 rounded-md hover:bg-gray-100"