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
+219
View File
@@ -0,0 +1,219 @@
/**
* 员工端二维码弹窗 — 展示员工端登录二维码,支持选择员工生成一次性自动登录链接
*/
import { useState, useEffect } from 'react'
import { QRCodeSVG } from 'qrcode.react'
import { Smartphone, Copy, Check, Search, Loader2, User, ChevronRight } from 'lucide-react'
import Modal from './ui/Modal'
import api from '../lib/api'
interface Employee {
id: string
name: string
phone: string
department: string | null
}
export default function PortalQRModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const [copied, setCopied] = useState(false)
const [employees, setEmployees] = useState<Employee[]>([])
const [loading, setLoading] = useState(false)
const [search, setSearch] = useState('')
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null)
const [autoLoginUrl, setAutoLoginUrl] = useState('')
const [generating, setGenerating] = useState(false)
const portalUrl = `${window.location.origin}/portal/login`
/** 加载员工列表 */
useEffect(() => {
if (!open) return
setLoading(true)
api.get('/roster', { params: { pageSize: 999 } }).then((res: any) => {
const list = res.data?.data || res.data || []
setEmployees(list.map((e: any) => ({
id: e.id,
name: e.name,
phone: e.phone,
department: e.department,
})))
}).catch(() => {}).finally(() => setLoading(false))
}, [open])
/** 选择员工后生成一次性自动登录链接 */
const handleSelectEmployee = async (emp: Employee) => {
setSelectedEmployee(emp)
setGenerating(true)
setAutoLoginUrl('')
try {
const res = await api.post('/portal/auto-login-token', { employeeId: emp.id }) as any
const token = res.data?.token
if (token) {
setAutoLoginUrl(`${window.location.origin}/portal/auto-login?token=${token}`)
}
} catch {
setAutoLoginUrl(portalUrl)
} finally {
setGenerating(false)
}
}
const handleCopy = () => {
navigator.clipboard.writeText(autoLoginUrl || portalUrl)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const handleReset = () => {
setSelectedEmployee(null)
setAutoLoginUrl('')
setSearch('')
}
const filteredEmployees = search
? employees.filter(e => e.name.includes(search) || e.phone.includes(search))
: employees
const displayUrl = autoLoginUrl || portalUrl
return (
<Modal open={open} onClose={() => { handleReset(); onClose() }} title="员工端入口" size="sm">
<div className="py-4">
{/* 未选择员工时:展示员工列表 */}
{!selectedEmployee ? (
<>
<div className="flex items-center gap-2 text-sm text-gray-600 mb-3">
<Smartphone className="w-4 h-4 text-primary" />
<span></span>
</div>
{/* 搜索框 */}
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="搜索姓名或手机号..."
className="w-full pl-9 pr-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
{/* 员工列表 */}
<div className="max-h-64 overflow-y-auto space-y-1">
{loading ? (
<div className="flex items-center justify-center py-6 text-gray-400">
<Loader2 className="w-5 h-5 animate-spin" />
</div>
) : filteredEmployees.length === 0 ? (
<div className="text-center py-6 text-sm text-gray-400"></div>
) : (
filteredEmployees.map(emp => (
<button
key={emp.id}
onClick={() => handleSelectEmployee(emp)}
className="flex items-center justify-between w-full px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors text-left"
>
<div className="flex items-center gap-2 min-w-0">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-primary" />
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-gray-800 truncate">{emp.name}</div>
<div className="text-xs text-gray-400 truncate">
{emp.department || '未分部门'} · {emp.phone}
</div>
</div>
</div>
<ChevronRight className="w-4 h-4 text-gray-300 flex-shrink-0" />
</button>
))
)}
</div>
</>
) : (
/* 已选择员工:展示二维码 */
<>
{/* 员工信息 + 重选按钮 */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<User className="w-4 h-4 text-primary" />
</div>
<div>
<div className="text-sm font-medium text-gray-800">{selectedEmployee.name}</div>
<div className="text-xs text-gray-400">{selectedEmployee.phone}</div>
</div>
</div>
<button
onClick={handleReset}
className="text-xs text-primary hover:text-primary/80"
>
</button>
</div>
{/* 二维码 */}
<div className="flex flex-col items-center">
<div className="p-4 bg-white rounded-xl border-2 border-gray-100 shadow-sm">
{generating ? (
<div className="w-[200px] h-[200px] flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
</div>
) : (
<QRCodeSVG
value={displayUrl}
size={200}
level="M"
includeMargin={false}
/>
)}
</div>
{/* 提示信息 */}
{autoLoginUrl ? (
<p className="mt-3 text-xs text-green-600 flex items-center gap-1">
<Check className="w-3.5 h-3.5" />
10
</p>
) : (
<p className="mt-3 text-xs text-gray-500"></p>
)}
{/* 链接地址 */}
<div className="mt-3 w-full">
<div className="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-lg">
<span className="text-xs text-gray-500 flex-1 truncate">{displayUrl}</span>
<button
onClick={handleCopy}
className="flex items-center gap-1 text-xs text-primary hover:text-primary/80 flex-shrink-0"
>
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
{copied ? '已复制' : '复制'}
</button>
</div>
</div>
</div>
{/* 功能说明 */}
<div className="mt-4 space-y-1.5 text-xs text-gray-500">
<p className="flex items-center gap-1.5">
<Check className="w-3.5 h-3.5 text-green-500" />
</p>
<p className="flex items-center gap-1.5">
<Check className="w-3.5 h-3.5 text-green-500" />
</p>
<p className="flex items-center gap-1.5">
<Check className="w-3.5 h-3.5 text-green-500" />
</p>
</div>
</>
)}
</div>
</Modal>
)
}