feat: idCardHash匹配、Roster分页过滤、社保基数调整修复、Modal size支持、Termination版本对比、UI优化
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus } from 'lucide-react'
|
||||
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus, QrCode } from 'lucide-react'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import api from '../lib/api'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
@@ -12,6 +13,14 @@ import Pagination from '../components/ui/Pagination'
|
||||
// 金额格式化:保留两位小数 + 千分位
|
||||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
const terminateReasonMap: Record<string, string> = {
|
||||
NEGOTIATED: '协商解除',
|
||||
FAULT: '员工过错',
|
||||
NONFAULT: '非过错解除',
|
||||
LAYOFF: '经济性裁员',
|
||||
EXPIRED: '合同到期不续签',
|
||||
}
|
||||
|
||||
type DetailTab = 'basic' | 'contract' | 'payslip' | 'overtime' | 'disciplinary' | 'attendance' | 'training' | 'performance' | 'termination' | 'attachment' | 'evidence'
|
||||
|
||||
export default function Roster() {
|
||||
@@ -28,16 +37,34 @@ export default function Roster() {
|
||||
const [showDeptModal, setShowDeptModal] = useState(false)
|
||||
const [deptEmployee, setDeptEmployee] = useState<any>(null)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
const [showBatchRenewModal, setShowBatchRenewModal] = useState(false)
|
||||
const [batchRenewYears, setBatchRenewYears] = useState(3)
|
||||
const [showRenewPreview, setShowRenewPreview] = useState(false)
|
||||
const [previewData, setPreviewData] = useState<any>(null)
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [filterContractStatus, setFilterContractStatus] = useState('')
|
||||
const [showBatchTerminateModal, setShowBatchTerminateModal] = useState(false)
|
||||
const [batchTerminateDate, setBatchTerminateDate] = useState(() => new Date().toISOString().slice(0, 10))
|
||||
const [batchTerminateReason, setBatchTerminateReason] = useState('NEGOTIATED')
|
||||
const [terminatePreviewData, setTerminatePreviewData] = useState<any>(null)
|
||||
|
||||
const { data: employees, isLoading } = useQuery<any[]>({
|
||||
queryKey: ['roster'],
|
||||
const { data: rosterData, isLoading } = useQuery<any>({
|
||||
queryKey: ['roster', page, pageSize, search, filterStatus, filterContractStatus],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/roster') as any
|
||||
return res.data
|
||||
const params: any = { page, pageSize }
|
||||
if (search) params.search = search
|
||||
if (filterStatus) params.status = filterStatus
|
||||
if (filterContractStatus) params.contractStatus = filterContractStatus
|
||||
const res = await api.get('/roster', { params }) as any
|
||||
return res
|
||||
},
|
||||
})
|
||||
|
||||
const employees = rosterData?.data || []
|
||||
const pagination = rosterData?.pagination || { page, pageSize, total: 0, totalPages: 0 }
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: (data: any) => api.post('/employees', data),
|
||||
onSuccess: () => {
|
||||
@@ -95,6 +122,72 @@ export default function Roster() {
|
||||
},
|
||||
})
|
||||
|
||||
const batchRenewMutation = useMutation({
|
||||
mutationFn: (data: { contractIds: string[]; years: number }) => api.post('/employees/contracts/batch-renew', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
setShowBatchRenewModal(false)
|
||||
setShowRenewPreview(false)
|
||||
setSelectedIds(new Set())
|
||||
setPreviewData(null)
|
||||
},
|
||||
})
|
||||
|
||||
const previewRenewMutation = useMutation({
|
||||
mutationFn: (contractIds: string[]) => api.post('/employees/contracts/preview-renew', { contractIds }),
|
||||
onSuccess: (data: any) => {
|
||||
setPreviewData(data.data)
|
||||
setShowRenewPreview(true)
|
||||
},
|
||||
})
|
||||
|
||||
const previewTerminateMutation = useMutation({
|
||||
mutationFn: (items: Array<{ employeeId: string; reason: string; terminationDate: string }>) =>
|
||||
api.post('/termination/batch/preview', { items }),
|
||||
onSuccess: (data: any) => {
|
||||
setTerminatePreviewData(data.data)
|
||||
},
|
||||
})
|
||||
|
||||
const batchTerminateMutation = useMutation({
|
||||
mutationFn: (items: Array<{ employeeId: string; reason: string; terminationDate: string }>) =>
|
||||
api.post('/termination/batch', { items }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
setShowBatchTerminateModal(false)
|
||||
setTerminatePreviewData(null)
|
||||
setSelectedIds(new Set())
|
||||
},
|
||||
})
|
||||
|
||||
const toggleSelect = (id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedIds.size === employees.length) {
|
||||
setSelectedIds(new Set())
|
||||
} else {
|
||||
setSelectedIds(new Set(employees.map((e: any) => e.id)))
|
||||
}
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setSearch('')
|
||||
setFilterStatus('')
|
||||
setFilterContractStatus('')
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const hasActiveFilters = search || filterStatus || filterContractStatus
|
||||
|
||||
const filtered = employees?.filter((e: any) =>
|
||||
!search || e.name.includes(search) || e.department.includes(search)
|
||||
) || []
|
||||
@@ -112,9 +205,45 @@ export default function Roster() {
|
||||
<Input
|
||||
placeholder="搜索姓名/部门"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
|
||||
className="!w-64 shrink-0"
|
||||
/>
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => { setFilterStatus(e.target.value); setPage(1) }}
|
||||
className="text-xs border rounded px-2 py-1.5"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="ACTIVE">在职</option>
|
||||
<option value="PRE_HIRE">预入职</option>
|
||||
<option value="RESIGNED">离职</option>
|
||||
</select>
|
||||
<select
|
||||
value={filterContractStatus}
|
||||
onChange={(e) => { setFilterContractStatus(e.target.value); setPage(1) }}
|
||||
className="text-xs border rounded px-2 py-1.5"
|
||||
>
|
||||
<option value="">全部合同</option>
|
||||
<option value="active">正常</option>
|
||||
<option value="expiring">即将到期</option>
|
||||
<option value="expired">已到期</option>
|
||||
<option value="unsigned">未签合同</option>
|
||||
<option value="unsigned_over_30">未签合同(超30天)</option>
|
||||
<option value="unsigned_over_year">未签合同(已满一年)</option>
|
||||
</select>
|
||||
{hasActiveFilters && (
|
||||
<button onClick={clearFilters} className="text-xs text-gray-400 hover:text-gray-600">清除</button>
|
||||
)}
|
||||
{selectedIds.size > 0 && (
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => setShowBatchRenewModal(true)} className="shrink-0">
|
||||
<Check className="w-4 h-4 mr-1" />批量续签({selectedIds.size})
|
||||
</Button>
|
||||
<Button variant="danger" onClick={() => setShowBatchTerminateModal(true)} className="shrink-0">
|
||||
<UserX className="w-4 h-4 mr-1" />批量解聘({selectedIds.size})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button onClick={() => setShowAddModal(true)} className="shrink-0">
|
||||
<Plus className="w-4 h-4 mr-1" /> 添加员工
|
||||
</Button>
|
||||
@@ -127,11 +256,20 @@ export default function Roster() {
|
||||
<Card><div className="text-center py-8 text-gray-400">暂无员工</div></Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Pagination page={page} pageSize={pageSize} total={filtered.length} onPageChange={setPage} onPageSizeChange={(s) => { setPageSize(s); setPage(1) }} />
|
||||
<Pagination
|
||||
page={pagination.page}
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
onPageChange={(p) => setPage(p)}
|
||||
onPageSizeChange={(s) => { setPageSize(s); setPage(1) }}
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b text-gray-500">
|
||||
<th className="py-2 px-3 text-left w-8">
|
||||
<input type="checkbox" checked={employees.length > 0 && selectedIds.size === employees.length} onChange={toggleSelectAll} />
|
||||
</th>
|
||||
<th className="py-2 px-3 text-left">姓名</th>
|
||||
<th className="py-2 px-3 text-left">部门</th>
|
||||
<th className="py-2 px-3 text-left">状态</th>
|
||||
@@ -139,6 +277,7 @@ export default function Roster() {
|
||||
<th className="py-2 px-3 text-left">离职日期</th>
|
||||
<th className="py-2 px-3 text-right">月薪</th>
|
||||
<th className="py-2 px-3 text-left">合同状态</th>
|
||||
<th className="py-2 px-3 text-left">合同到期</th>
|
||||
<th className="py-2 px-3 text-center">违纪</th>
|
||||
<th className="py-2 px-3 text-center">考勤</th>
|
||||
<th className="py-2 px-3 text-center">培训</th>
|
||||
@@ -148,12 +287,15 @@ export default function Roster() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paged.map((e: any) => (
|
||||
{employees.map((e: any) => (
|
||||
<tr
|
||||
key={e.id}
|
||||
className="border-b last:border-0 cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => setSelectedId(e.id)}
|
||||
>
|
||||
<td className="py-2 px-3" onClick={(ev) => ev.stopPropagation()}>
|
||||
<input type="checkbox" checked={selectedIds.has(e.id)} onChange={() => toggleSelect(e.id)} />
|
||||
</td>
|
||||
<td className="py-2 px-3 font-medium">{e.name}</td>
|
||||
<td className="py-2 px-3 text-gray-500">{e.department}</td>
|
||||
<td className="py-2 px-3">
|
||||
@@ -192,6 +334,21 @@ export default function Roster() {
|
||||
return <span className={`px-2 py-0.5 rounded text-xs ${style}`}>{e.contractStatusText || '无合同'}</span>
|
||||
})()}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-gray-500">
|
||||
{(() => {
|
||||
const endDate = e.latestContract?.endDate
|
||||
if (!endDate) return <span className="text-gray-300">—</span>
|
||||
const end = new Date(endDate)
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
end.setHours(0, 0, 0, 0)
|
||||
const diffDays = Math.ceil((end.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||||
if (diffDays < 0) return <span className="text-danger text-xs">已过期</span>
|
||||
if (diffDays <= 30) return <span className="text-danger font-medium text-xs">{end.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })} ({diffDays}天)</span>
|
||||
if (diffDays <= 90) return <span className="text-amber-600 text-xs">{end.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })} ({diffDays}天)</span>
|
||||
return <span className="text-xs">{end.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })}</span>
|
||||
})()}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-center">
|
||||
{e.counts?.disciplinaryRecords ? (
|
||||
<span className="text-danger font-medium">{e.counts.disciplinaryRecords}</span>
|
||||
@@ -323,6 +480,201 @@ export default function Roster() {
|
||||
error={deptChangeMutation.error as any}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBatchRenewModal && (
|
||||
<Modal open={showBatchRenewModal} onClose={() => { setShowBatchRenewModal(false); setPreviewData(null); }} title="批量续签">
|
||||
<div className="space-y-3">
|
||||
{!previewData ? (
|
||||
<>
|
||||
<p className="text-xs text-gray-500">已选择 {selectedIds.size} 名员工,将为其续签劳动合同。</p>
|
||||
<div>
|
||||
<Label>续签年限</Label>
|
||||
<Select value={String(batchRenewYears)} onChange={(e) => setBatchRenewYears(Number(e.target.value))}>
|
||||
<option value="1">1年</option>
|
||||
<option value="2">2年</option>
|
||||
<option value="3">3年</option>
|
||||
<option value="5">5年</option>
|
||||
</Select>
|
||||
</div>
|
||||
{previewRenewMutation.error && (
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
{(previewRenewMutation.error as any)?.response?.data?.error?.message || '预检失败'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => { setShowBatchRenewModal(false); setPreviewData(null); }}>取消</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const contractIds = employees
|
||||
.filter((e: any) => selectedIds.has(e.id) && e.latestContract?.id)
|
||||
.map((e: any) => e.latestContract.id)
|
||||
if (contractIds.length === 0) {
|
||||
alert('所选员工没有可续签的合同')
|
||||
return
|
||||
}
|
||||
previewRenewMutation.mutate(contractIds)
|
||||
}}
|
||||
disabled={previewRenewMutation.isPending}
|
||||
>
|
||||
{previewRenewMutation.isPending ? '预检中...' : '合规预检'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium">合规预检结果</p>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${previewData.warnings > 0 ? 'bg-amber-100 text-amber-700' : 'bg-green-100 text-green-700'}`}>
|
||||
{previewData.warnings > 0 ? `${previewData.warnings} 项风险提示` : '全部通过'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto space-y-2">
|
||||
{previewData.results.map((r: any) => (
|
||||
<div key={r.contractId} className={`p-2 rounded text-xs ${r.warning ? 'bg-amber-50 border border-amber-200' : 'bg-gray-50'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{r.employeeName}</span>
|
||||
<span className="text-gray-400">{r.department}</span>
|
||||
</div>
|
||||
<div className="text-gray-500 mt-1">
|
||||
第 {r.renewalCount} 次续签 · 在职 {r.yearsSinceHire} 年
|
||||
</div>
|
||||
{r.warning && (
|
||||
<div className="mt-1 text-amber-600">{r.warning}</div>
|
||||
)}
|
||||
<div className="mt-1 text-gray-500">{r.suggestion}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{previewData.warnings > 0 && (
|
||||
<div className="px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs">
|
||||
存在法律风险,建议处理后再继续操作
|
||||
</div>
|
||||
)}
|
||||
{batchRenewMutation.error && (
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
{(batchRenewMutation.error as any)?.response?.data?.error?.message || '续签失败'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setPreviewData(null)}>返回修改</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const contractIds = employees
|
||||
.filter((e: any) => selectedIds.has(e.id) && e.latestContract?.id)
|
||||
.map((e: any) => e.latestContract.id)
|
||||
batchRenewMutation.mutate({ contractIds, years: batchRenewYears })
|
||||
}}
|
||||
disabled={batchRenewMutation.isPending}
|
||||
>
|
||||
{batchRenewMutation.isPending ? '续签中...' : `确认续签 ${selectedIds.size} 人`}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showBatchTerminateModal && (
|
||||
<Modal open={showBatchTerminateModal} onClose={() => { setShowBatchTerminateModal(false); setTerminatePreviewData(null); }} title="批量解聘" size="lg">
|
||||
<div className="space-y-3">
|
||||
{!terminatePreviewData ? (
|
||||
<>
|
||||
<p className="text-xs text-gray-500">已选择 {selectedIds.size} 名员工进行批量解聘。</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>解聘日期</Label>
|
||||
<Input type="date" value={batchTerminateDate} onChange={(e) => setBatchTerminateDate(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>解聘原因</Label>
|
||||
<Select value={batchTerminateReason} onChange={(e) => setBatchTerminateReason(e.target.value)}>
|
||||
<option value="NEGOTIATED">协商解除</option>
|
||||
<option value="LAYOFF">经济性裁员</option>
|
||||
<option value="NONFAULT">非过错解除</option>
|
||||
<option value="FAULT">过错解除</option>
|
||||
<option value="EXPIRED">合同到期不续签</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{previewTerminateMutation.error && (
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
{(previewTerminateMutation.error as any)?.response?.data?.error?.message || '预检失败'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => { setShowBatchTerminateModal(false); setTerminatePreviewData(null); }}>取消</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const items = Array.from(selectedIds).map(id => ({
|
||||
employeeId: id,
|
||||
reason: batchTerminateReason,
|
||||
terminationDate: batchTerminateDate,
|
||||
}))
|
||||
previewTerminateMutation.mutate(items)
|
||||
}}
|
||||
disabled={previewTerminateMutation.isPending}
|
||||
>
|
||||
{previewTerminateMutation.isPending ? '预检中...' : '合规预检'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium">合规预检结果</p>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${terminatePreviewData.warnings > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
|
||||
{terminatePreviewData.warnings > 0 ? `${terminatePreviewData.warnings} 项风险提示` : '全部通过'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto space-y-2">
|
||||
{terminatePreviewData.results.map((r: any) => (
|
||||
<div key={r.employeeId} className={`p-2 rounded text-xs ${r.warnings.length > 0 ? 'bg-red-50 border border-red-200' : 'bg-gray-50'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{r.employeeName}</span>
|
||||
<span className="text-gray-400">{r.department}</span>
|
||||
</div>
|
||||
<div className="text-gray-500 mt-1">
|
||||
{terminateReasonMap[r.reason] || r.reason} · {r.terminationDate}
|
||||
</div>
|
||||
{r.warnings.length > 0 && r.warnings.map((w: string, i: number) => (
|
||||
<div key={i} className="mt-1 text-red-600">{w}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{terminatePreviewData.warnings > 0 && (
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
存在法律风险,建议处理后再继续操作
|
||||
</div>
|
||||
)}
|
||||
{batchTerminateMutation.error && (
|
||||
<div className="px-3 py-2 rounded-md bg-red-50 text-red-700 text-xs">
|
||||
{(batchTerminateMutation.error as any)?.response?.data?.error?.message || '解聘失败'}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setTerminatePreviewData(null)}>返回修改</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
const items = Array.from(selectedIds).map(id => ({
|
||||
employeeId: id,
|
||||
reason: batchTerminateReason,
|
||||
terminationDate: batchTerminateDate,
|
||||
}))
|
||||
batchTerminateMutation.mutate(items)
|
||||
}}
|
||||
disabled={batchTerminateMutation.isPending}
|
||||
>
|
||||
{batchTerminateMutation.isPending ? '解聘中...' : `确认解聘 ${selectedIds.size} 人`}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -583,6 +935,35 @@ function BasicInfo({ profile }: { profile: any }) {
|
||||
<p className="text-xs text-amber-600 mt-2">⚠️ 该员工处于特殊保护期,解聘操作将触发法律风险预警</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 员工端二维码 */}
|
||||
{!editing && profile.phone && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xs font-medium text-gray-600">员工端入口</h3>
|
||||
<Button size="sm" variant="secondary" onClick={() => {
|
||||
const url = `${window.location.origin}/portal/login`
|
||||
navigator.clipboard?.writeText(url)
|
||||
}}>
|
||||
复制链接
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-white p-3 rounded-lg border">
|
||||
<QRCodeSVG
|
||||
value={`${window.location.origin}/portal/login`}
|
||||
size={120}
|
||||
level="M"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<p>员工扫码进入员工端,使用手机号登录</p>
|
||||
<p>可查看工资条、合同信息、确认签署</p>
|
||||
<p className="text-gray-400">链接:{window.location.origin}/portal/login</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user