feat: 附件管理拆为独立tab + 员工端附件上传
管理端: - 花名册详情新增「附件资料」独立tab(人事信息分组下) - 从 BasicInfo 移除附件管理代码到独立 AttachmentsTab 组件 - shared.ts 添加 attachments tab 类型和计数映射 员工端: - 后端新增 GET/POST/DELETE /portal/attachments 接口 - 前端 api-services 添加 portalDelete + 附件 API 方法 - MyProfile 新增附件资料卡片,支持上传/查看/删除附件 - 附件类型:身份证/银行卡/学历证书/职业资格证书/合同/照片/其他
This commit is contained in:
@@ -1130,6 +1130,7 @@ portalAxios.interceptors.response.use(
|
||||
const portalGet = ((url: string, config?: any) => portalAxios.get(url, config)) as any
|
||||
const portalPost = ((url: string, data?: any, config?: any) => portalAxios.post(url, data, config)) as any
|
||||
const portalPut = ((url: string, data?: any, config?: any) => portalAxios.put(url, data, config)) as any
|
||||
const portalDelete = ((url: string, config?: any) => portalAxios.delete(url, config)) as any
|
||||
|
||||
export const portalApi = {
|
||||
/** 登录 */
|
||||
@@ -1198,6 +1199,15 @@ export const portalApi = {
|
||||
/** 更新个人资料 */
|
||||
updateProfile: (data: Record<string, unknown>) =>
|
||||
portalPut('/profile', data).then(unwrap<any>()),
|
||||
/** 获取附件列表 */
|
||||
getAttachments: () =>
|
||||
portalGet('/attachments').then(unwrap<any[]>()),
|
||||
/** 上传附件 */
|
||||
addAttachment: (data: Record<string, unknown>) =>
|
||||
portalPost('/attachments', data).then(unwrap<any>()),
|
||||
/** 删除附件 */
|
||||
removeAttachment: (id: string) =>
|
||||
portalDelete(`/attachments/${id}`),
|
||||
/** 制度列表 */
|
||||
policies: () =>
|
||||
portalGet('/policies').then(unwrap<any[]>()),
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* 员工可查看自己的入职资料,并编辑部分字段(紧急联系人、地址、银行卡、学历等)
|
||||
* 数据直接同步到花名册(Employee 表)
|
||||
*/
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { User, Phone, IdCard, MapPin, Banknote, GraduationCap, AlertCircle, Check, Save, Briefcase, Calendar } from 'lucide-react'
|
||||
import { User, Phone, IdCard, MapPin, Banknote, GraduationCap, AlertCircle, Check, Save, Briefcase, Calendar, Paperclip, Trash2, Eye, Upload } from 'lucide-react'
|
||||
import { portalApi } from '../../lib/api-services'
|
||||
import Card from '../../components/ui/Card'
|
||||
import Button from '../../components/ui/Button'
|
||||
@@ -307,6 +307,136 @@ export default function MyProfile() {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 附件资料卡片 */}
|
||||
<AttachmentCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 附件资料卡片 — 员工可上传/查看/删除自己的附件 */
|
||||
function AttachmentCard() {
|
||||
const queryClient = useQueryClient()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD')
|
||||
|
||||
const { data: attachments = [], isLoading } = useQuery<any[]>({
|
||||
queryKey: ['portal-attachments'],
|
||||
queryFn: () => portalApi.getAttachments(),
|
||||
})
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: (data: Record<string, unknown>) => portalApi.addAttachment(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['portal-attachments'] }),
|
||||
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '上传失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => portalApi.removeAttachment(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['portal-attachments'] }),
|
||||
})
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx']
|
||||
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
|
||||
if (!allowedExts.includes(ext)) {
|
||||
toast.error('不支持的文件格式')
|
||||
return
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast.error('文件过大,请上传小于 10MB 的文件')
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const fileUrl = event.target?.result as string
|
||||
addMutation.mutate({ fileName: file.name, fileType, fileUrl, fileSize: file.size })
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const fileTypeLabels: Record<string, string> = {
|
||||
ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书',
|
||||
CERTIFICATE: '职业资格证书', CONTRACT: '合同扫描件', PHOTO: '员工照片', OTHER: '其他',
|
||||
}
|
||||
const fileTypeColors: Record<string, string> = {
|
||||
ID_CARD: 'bg-blue-50 text-blue-600', BANK_CARD: 'bg-green-50 text-safe',
|
||||
EDUCATION: 'bg-amber-50 text-amber-600', CERTIFICATE: 'bg-purple-50 text-purple-600',
|
||||
CONTRACT: 'bg-cyan-50 text-cyan-600', PHOTO: 'bg-pink-50 text-pink-600', OTHER: 'bg-gray-100 text-gray-500',
|
||||
}
|
||||
const formatSize = (bytes: number) => {
|
||||
if (!bytes) return '-'
|
||||
if (bytes < 1024) return `${bytes}B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Paperclip className="w-4 h-4 text-primary" />
|
||||
<h2 className="text-sm font-medium">附件资料({attachments.length}个)</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center mb-3">
|
||||
<select
|
||||
value={fileType}
|
||||
onChange={(e) => setFileType(e.target.value as any)}
|
||||
className="h-9 px-2 rounded-md border border-gray-300 text-xs"
|
||||
>
|
||||
<option value="ID_CARD">身份证</option>
|
||||
<option value="BANK_CARD">银行卡</option>
|
||||
<option value="EDUCATION">学历证书</option>
|
||||
<option value="CERTIFICATE">职业资格证书</option>
|
||||
<option value="CONTRACT">合同扫描件</option>
|
||||
<option value="PHOTO">员工照片</option>
|
||||
<option value="OTHER">其他</option>
|
||||
</select>
|
||||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
|
||||
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={addMutation.isPending}>
|
||||
<Upload className="w-3.5 h-3.5 mr-1" />
|
||||
{addMutation.isPending ? '上传中...' : '上传附件'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4 text-gray-400 text-xs">加载中...</div>
|
||||
) : attachments.length > 0 ? (
|
||||
<div className="space-y-1.5">
|
||||
{attachments.map((att) => (
|
||||
<div key={att.id} className="flex items-center justify-between bg-gray-50 rounded p-2 text-xs hover:bg-gray-100">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Paperclip className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-gray-700">{att.fileName}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${fileTypeColors[att.fileType] || 'bg-gray-100 text-gray-500'}`}>{fileTypeLabels[att.fileType] || att.fileType}</span>
|
||||
<span className="text-gray-400">{formatSize(att.fileSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
||||
<button onClick={() => window.open(att.fileUrl, '_blank')} className="text-gray-400 hover:text-blue-600" title="查看">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`确认删除「${att.fileName}」?`)) deleteMutation.mutate(att.id)
|
||||
}}
|
||||
className="text-gray-300 hover:text-red-500"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-xs text-center py-4">暂无附件,请点击上方上传</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { attachmentApi } from '../../lib/api-services'
|
||||
import { useConfirm } from '../../hooks/useConfirm'
|
||||
import Card from '../../components/ui/Card'
|
||||
import Button from '../../components/ui/Button'
|
||||
import { Select } from '../../components/ui/Input'
|
||||
import { Paperclip, Trash2, Eye, Download } from 'lucide-react'
|
||||
|
||||
const fileTypeLabels: Record<string, string> = {
|
||||
ID_CARD: '身份证', BANK_CARD: '银行卡', EDUCATION: '学历证书',
|
||||
CERTIFICATE: '职业资格证书', CONTRACT: '合同扫描件', CONTRACT_SCAN: '合同扫描件',
|
||||
PHOTO: '员工照片', OTHER: '其他',
|
||||
TERMINATION_DOC: '离职文件', RETIREMENT_DOC: '退休文件',
|
||||
INJURY_CERT: '工伤证明', MEDICAL_CERT: '医疗证明', PREGNANCY_CERT: '孕期证明',
|
||||
DISCIPLINARY: '违纪文件',
|
||||
}
|
||||
const fileTypeColors: Record<string, string> = {
|
||||
ID_CARD: 'bg-blue-50 text-blue-600', BANK_CARD: 'bg-green-50 text-safe',
|
||||
EDUCATION: 'bg-amber-50 text-amber-600', CERTIFICATE: 'bg-purple-50 text-purple-600',
|
||||
CONTRACT: 'bg-cyan-50 text-cyan-600', CONTRACT_SCAN: 'bg-cyan-50 text-cyan-600',
|
||||
PHOTO: 'bg-pink-50 text-pink-600', OTHER: 'bg-gray-100 text-gray-500',
|
||||
TERMINATION_DOC: 'bg-red-50 text-red-600', RETIREMENT_DOC: 'bg-orange-50 text-orange-600',
|
||||
INJURY_CERT: 'bg-red-50 text-red-600', MEDICAL_CERT: 'bg-orange-50 text-orange-600',
|
||||
PREGNANCY_CERT: 'bg-pink-50 text-pink-600', DISCIPLINARY: 'bg-red-50 text-red-600',
|
||||
}
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (!bytes) return '-'
|
||||
if (bytes < 1024) return `${bytes}B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`
|
||||
}
|
||||
|
||||
export default function AttachmentsTab({ employeeId, attachments }: { employeeId: string; attachments: any[] }) {
|
||||
const queryClient = useQueryClient()
|
||||
const confirm = useConfirm()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD')
|
||||
|
||||
const addAttachmentMutation = useMutation({
|
||||
mutationFn: (data: any) => attachmentApi.add(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }),
|
||||
})
|
||||
|
||||
const deleteAttachmentMutation = useMutation({
|
||||
mutationFn: (id: string) => attachmentApi.remove(id),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }),
|
||||
})
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const allowedExts = ['.pdf', '.jpg', '.jpeg', '.png', '.heic', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx', '.tiff', '.tif']
|
||||
const ext = file.name.toLowerCase().substring(file.name.lastIndexOf('.'))
|
||||
if (!allowedExts.includes(ext)) {
|
||||
toast.error('不支持的文件格式,支持 PDF、图片、Word、Excel 等常见格式')
|
||||
return
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
toast.error('文件过大,请上传小于 10MB 的文件')
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const fileUrl = event.target?.result as string
|
||||
addAttachmentMutation.mutate({ employeeId, fileName: file.name, fileType, fileUrl, fileSize: file.size })
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const handleDelete = async (att: any) => {
|
||||
const ok = await confirm({ title: '删除附件', message: `确认删除「${att.fileName}」?` })
|
||||
if (ok) deleteAttachmentMutation.mutate(att.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xs font-medium">附件资料({attachments?.length || 0}个)</h2>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs !w-28">
|
||||
<option value="ID_CARD">身份证</option>
|
||||
<option value="BANK_CARD">银行卡</option>
|
||||
<option value="EDUCATION">学历证书</option>
|
||||
<option value="CERTIFICATE">职业资格证书</option>
|
||||
<option value="CONTRACT">合同扫描件</option>
|
||||
<option value="PHOTO">员工照片</option>
|
||||
<option value="OTHER">其他</option>
|
||||
</Select>
|
||||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
|
||||
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={addAttachmentMutation.isPending} className="shrink-0 whitespace-nowrap">
|
||||
{addAttachmentMutation.isPending ? '上传中...' : '上传附件'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{attachments?.length ? (
|
||||
<div className="space-y-1.5">
|
||||
{attachments.map((att) => (
|
||||
<div key={att.id} className="flex items-center justify-between bg-gray-50 rounded p-2 text-xs hover:bg-gray-100">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Paperclip className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-gray-700">{att.fileName}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${fileTypeColors[att.fileType] || 'bg-gray-100 text-gray-500'}`}>{fileTypeLabels[att.fileType] || att.fileType}</span>
|
||||
<span className="text-gray-400">{formatSize(att.fileSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
||||
<button onClick={() => window.open(att.fileUrl, '_blank')} className="text-gray-400 hover:text-blue-600" title="查看">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<a href={att.fileUrl} download={att.fileName} className="text-gray-400 hover:text-blue-600" title="下载">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
<button onClick={() => handleDelete(att)} className="text-gray-300 hover:text-danger" title="删除">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400 text-xs text-center py-8">暂无附件,请点击右上角上传</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -563,52 +563,6 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 附件管理 */}
|
||||
<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">附件管理({attachments?.length || 0}个)</h3>
|
||||
{!editing && (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select value={fileType} onChange={(e) => setFileType(e.target.value as any)} className="text-xs !w-28">
|
||||
<option value="ID_CARD">身份证</option><option value="BANK_CARD">银行卡</option><option value="EDUCATION">学历证书</option><option value="CERTIFICATE">职业资格证书</option><option value="CONTRACT">合同扫描件</option><option value="PHOTO">员工照片</option><option value="OTHER">其他</option>
|
||||
</Select>
|
||||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileUpload} />
|
||||
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()} disabled={addAttachmentMutation.isPending} className="shrink-0 whitespace-nowrap">
|
||||
{addAttachmentMutation.isPending ? '上传中...' : '上传附件'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!editing && attachments?.length ? (
|
||||
<div className="space-y-1.5">
|
||||
{attachments.map((att) => (
|
||||
<div key={att.id} className="flex items-center justify-between bg-gray-50 rounded p-2 text-xs hover:bg-gray-100">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Paperclip className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-gray-700">{att.fileName}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${fileTypeColors[att.fileType] || 'bg-gray-100 text-gray-500'}`}>{fileTypeLabels[att.fileType] || att.fileType}</span>
|
||||
<span className="text-gray-400">{formatSize(att.fileSize)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
||||
<button onClick={() => window.open(att.fileUrl, '_blank')} className="text-gray-400 hover:text-blue-600" title="查看">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<a href={att.fileUrl} download={att.fileName} className="text-gray-400 hover:text-blue-600" title="下载">
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
<button onClick={() => deleteAttachmentMutation.mutate(att.id)} className="text-gray-300 hover:text-danger" title="删除">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : !editing ? <div className="text-gray-400 text-xs text-center py-3">暂无附件</div> : null}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import PerformanceInfo from './PerformanceInfo'
|
||||
import TerminationInfo from './TerminationInfo'
|
||||
import ChangeHistoryTab from './ChangeHistoryTab'
|
||||
import EvidenceChain from './EvidenceChain'
|
||||
import AttachmentsTab from './AttachmentsTab'
|
||||
|
||||
/**
|
||||
* 员工详情档案页 — 使用 EmployeeProfileShell 壳组件统一布局
|
||||
@@ -62,6 +63,7 @@ export default function EmployeeProfile({ employeeId, onBack }: { employeeId: st
|
||||
{(activeTab) => (
|
||||
<>
|
||||
{activeTab === 'basic' && <BasicInfo profile={profile} employeeId={employeeId} attachments={profile.attachments} />}
|
||||
{activeTab === 'attachments' && <AttachmentsTab employeeId={employeeId} attachments={profile.attachments} />}
|
||||
{activeTab === 'contract' && <ContractInfo employeeId={employeeId} contracts={profile.contracts} hireDate={profile.hireDate} employee={profile} />}
|
||||
{activeTab === 'payslip' && <PayslipSocialInfo payslips={profile.payslips} socialInsRecords={profile.socialInsRecords} housingFundRecords={profile.housingFundRecords} monthlyProcessRecords={profile.monthlyProcessRecords} employeeId={employeeId} />}
|
||||
{activeTab === 'disciplinary' && <DisciplinaryInfo employeeId={employeeId} records={profile.disciplinaryRecords} />}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const terminateReasonMap: Record<string, string> = {
|
||||
}
|
||||
|
||||
/** 详情页 Tab 类型 */
|
||||
export type DetailTab = 'basic' | 'contract' | 'payslip' | 'attendance' | 'disciplinary' | 'performance' | 'termination' | 'evidence' | 'history'
|
||||
export type DetailTab = 'basic' | 'contract' | 'payslip' | 'attendance' | 'disciplinary' | 'performance' | 'termination' | 'evidence' | 'history' | 'attachments'
|
||||
|
||||
/** Tab 分组 */
|
||||
export type TabGroup = '人事信息' | '考勤绩效' | '风险合规' | '薪酬' | '变更历史'
|
||||
@@ -25,6 +25,7 @@ export const TAB_GROUPS: { group: TabGroup; tabs: { key: DetailTab; label: strin
|
||||
tabs: [
|
||||
{ key: 'basic', label: '基本信息', icon: null },
|
||||
{ key: 'contract', label: '劳动合同', icon: null },
|
||||
{ key: 'attachments', label: '附件资料', icon: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -64,4 +65,5 @@ export const TAB_COUNT_KEYS: Record<string, string> = {
|
||||
disciplinary: 'disciplinaryRecords',
|
||||
performance: 'performanceRecords',
|
||||
termination: 'terminations',
|
||||
attachments: 'attachments',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user