Files
TurboHR/frontend/src/components/PortalQRModal.tsx
T
selfrelease 16f22e6622 refactor: 全量迁移前端 API 调用到统一 api-services 服务层
- 新建 api-services-raw.ts 导出原始 axios 方法供特殊端点使用
- 完成 api-services.ts 全领域覆盖(auth/employee/roster/dashboard/attendance/payroll/socialInsurance/commercialInsurance/termination/policies/evidence/audit/calendar/companyFiles/notifications/settings/ai/platform/portal/survey/search)
- 迁移所有 47+ 页面文件:pages/、pages/roster/、pages/portal/、pages/platform/、pages/auth/、pages/dashboard/、pages/compliance/
- 移除所有直接 import api from '../../lib/api' 引用
- 修复 Termination.tsx / WorkProcess.tsx 中 string|null 类型错误
- 修复 SocialInsurance.tsx 中 api-services-raw delete 导入名
- 修复 PlatformLogin.tsx 变量遮蔽问题
- tsc --noEmit 零错误,vite build 成功
2026-08-01 16:13:19 +08:00

219 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 员工端二维码弹窗 — 展示员工端登录二维码,支持选择员工生成一次性自动登录链接
*/
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 { employeeApi, portalApi } from '../lib/api-services'
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)
employeeApi.list({ status: 'ACTIVE' }).then((list: any) => {
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 portalApi.generateAutoLoginToken(emp.id) as any
const token = res?.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>
)
}