视觉统一(Zen Minimal × Future Organic 白皮书语言,全站浅色单主题): - 登录页:品牌→由壹点·品牌中心,白纸底+发丝线+柔光光晕,黑按钮 - 作品过稿:深灰头→白皮书,返回链接→返回创意资产库 - 绩效考核:桌面+移动双头部换白皮书,标题改绩效考核,返回改链接 - 组织架构:BoardShell+页面 76 处调色板全量替换(#eef1f4→#fafafa 等) - admin 后台:MaterialsHeader(面包屑治理与设置),Tab 栏墨色高亮 - 深色主题废弃:globals.css 移除媒体查询,tailwind darkMode:'class' 不挂 .dark - 设计文档同步更新:AGENTS.md UI 基线/DESIGN.md/PRODUCT.md/README.md/design-qa.md 个人档案(原个人中心): - API 合并权威人员档案(getEmployeeByUserId → 花名/工号/部门/职位/M-P等级/主管/状态) - 页面白皮书化:权威档案卡(只读 HR 字段)+ 账号区(头像上传)+ 素材/收藏 顶栏平台化: - MaterialsHeader 双模式:首页(五板块导航+全套工具+菜单)/板块(面包屑) - 菜单重排:我的档案/绩效考核/作品过稿看板‖更新记录/提交反馈‖治理与设置‖退出 - 菜单头身份行:花名+职位·部门(权威档案) - 首页排序:01 创意资产库(全员)→ 02 组织管理(管理员)→ 03-05 规划中 - 素材库改名创意资产库,全站同步(索引/导航/卡片/按钮/页脚) - 排行榜换白皮书(白底发丝线+琥珀前三名) 构建修复: - tsconfig 排除 output/(本地审计临时文件不再卡类型检查) - 存储边界门禁豁免 output/(与 .dockerignore 对齐)
532 lines
23 KiB
TypeScript
532 lines
23 KiB
TypeScript
/*
|
||
* 方向契约(云采石场 · seed 32cb33b5)
|
||
* THESIS: 组织架构页让"人"归位到真实组织树——部门即层叠的石料块,人员是块上的铭牌;
|
||
* 拒绝旧后台"平铺列表+堆卡片"的默认布局,用左右结构(左=组织采石剖面,右=人员明细)直接呈现层级与归属。
|
||
* OWN-WORLD: 浅色工业风——石材白/灰蓝地面、切割蓝(#5a66b8)唯一强调色、块状层叠卡片(细边框+轻投影)、
|
||
* 工程数据用等宽数字、状态用绿/琥珀信号色。
|
||
* STORY: 管理员打开页面即看到"公司这座山被切成一层层部门":点哪一层,右侧露出该层成员名单,
|
||
* 等级(M/P)、直属主管、在职状态像工程铭牌一样一目了然;未绑定/未确定人员单独成垛,不混入。
|
||
* FIRST VIEWPORT: 左 300px 组织树(层叠石料块,高亮当前层,节点带人数);右主区顶部=当前层路径+四格工程统计,
|
||
* 下方=人员明细表(花名/工号/职位/等级/主管/状态)。
|
||
* FORM: 云采石场方向(用户从概念骰子选定,替代脚本分配的深海潜水)。
|
||
* FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import { useEffect, useMemo, useState, useCallback } from 'react'
|
||
import { useSession } from 'next-auth/react'
|
||
import { useRouter } from 'next/navigation'
|
||
import BoardShell from '@/components/board/BoardShell'
|
||
|
||
// ---------- 类型 ----------
|
||
interface OrgNode {
|
||
id: string
|
||
name: string
|
||
path: string
|
||
level: number
|
||
parentId: string | null
|
||
count: number
|
||
children: OrgNode[]
|
||
}
|
||
|
||
interface EmployeeRow {
|
||
userId: string
|
||
username: string | null
|
||
realName: string | null
|
||
displayName: string | null
|
||
jobNumber: string | null
|
||
departmentPath: string | null
|
||
departmentName: string | null
|
||
positionTitle: string | null
|
||
positionLevel: string | null
|
||
levelType: string | null
|
||
managerName: string | null
|
||
isLeadership: boolean
|
||
employmentState: string | null
|
||
isActive: boolean
|
||
}
|
||
|
||
interface UnboundUser {
|
||
userId: string
|
||
username: string
|
||
realName: string | null
|
||
oldDepartment: string | null
|
||
}
|
||
|
||
interface OrgResponse {
|
||
tree: OrgNode[]
|
||
employees: EmployeeRow[]
|
||
unclassified: EmployeeRow[]
|
||
unbound: UnboundUser[]
|
||
summary: {
|
||
totalEmployees: number
|
||
activeEmployees: number
|
||
inactiveEmployees: number
|
||
unboundCount: number
|
||
unclassifiedCount: number
|
||
visibleCount: number
|
||
scope: string
|
||
}
|
||
}
|
||
|
||
const LEVEL_TYPE_LABEL: Record<string, string> = {
|
||
M: '管理',
|
||
P: '专业',
|
||
S: '支持',
|
||
}
|
||
|
||
const STATE_LABEL: Record<string, string> = {
|
||
ACTIVE: '在职',
|
||
RESIGNED: '离职',
|
||
UNKNOWN: '未知',
|
||
}
|
||
|
||
// ---------- 组件 ----------
|
||
export default function OrgPage() {
|
||
const { data: session, status } = useSession()
|
||
const router = useRouter()
|
||
|
||
const sessionUser = session?.user as
|
||
| { role?: string; isSuperAdmin?: boolean; managedDepartment?: string | null }
|
||
| undefined
|
||
const canAccess = ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN'].includes(sessionUser?.role || '')
|
||
const isSuperAdmin = Boolean(sessionUser?.isSuperAdmin)
|
||
|
||
const [data, setData] = useState<OrgResponse | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [includeInactive, setIncludeInactive] = useState(false)
|
||
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||
const [selectedGroup, setSelectedGroup] = useState<'org' | 'unclassified' | 'unbound'>('org')
|
||
const [keyword, setKeyword] = useState('')
|
||
|
||
// 权限与重定向
|
||
useEffect(() => {
|
||
if (status === 'unauthenticated') {
|
||
router.push('/login')
|
||
return
|
||
}
|
||
if (status === 'authenticated' && !canAccess) {
|
||
router.push('/')
|
||
}
|
||
}, [status, canAccess, router])
|
||
|
||
// 加载数据
|
||
const fetchData = useCallback(async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
try {
|
||
const res = await fetch(`/api/organization?includeInactive=${includeInactive}`)
|
||
if (!res.ok) {
|
||
const body = await res.json().catch(() => null)
|
||
throw new Error(body?.error || '加载失败')
|
||
}
|
||
const json = (await res.json()) as OrgResponse
|
||
setData(json)
|
||
|
||
// 默认展开第一层,选中第一个有人的顶层节点
|
||
const firstTop = json.tree.find((n) => n.count > 0)
|
||
if (firstTop) {
|
||
setExpanded(new Set([firstTop.id]))
|
||
setSelectedPath(firstTop.path)
|
||
}
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : '加载失败')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [includeInactive])
|
||
|
||
useEffect(() => {
|
||
if (status === 'authenticated' && canAccess) {
|
||
void fetchData()
|
||
}
|
||
}, [status, canAccess, fetchData])
|
||
|
||
// 展平树为可渲染的行(带缩进深度)
|
||
const flatTree = useMemo(() => {
|
||
if (!data) return []
|
||
const rows: Array<{ node: OrgNode; depth: number }> = []
|
||
const walk = (nodes: OrgNode[], depth: number) => {
|
||
for (const node of nodes) {
|
||
rows.push({ node, depth })
|
||
if (expanded.has(node.id) && node.children.length > 0) {
|
||
walk(node.children, depth + 1)
|
||
}
|
||
}
|
||
}
|
||
walk(data.tree, 0)
|
||
return rows
|
||
}, [data, expanded])
|
||
|
||
// 当前选中部门的人员
|
||
const selectedEmployees = useMemo(() => {
|
||
if (!data) return []
|
||
if (selectedGroup === 'unclassified') return data.unclassified
|
||
if (selectedGroup === 'unbound') return []
|
||
if (!selectedPath) return []
|
||
return data.employees.filter((e) => e.departmentPath === selectedPath)
|
||
}, [data, selectedPath, selectedGroup])
|
||
|
||
// 部门负责人计数(当前选中列表)
|
||
const leaderCount = selectedEmployees.filter((e) => e.isLeadership).length
|
||
|
||
// 等级分布(当前选中列表)
|
||
const levelCounts = useMemo(() => {
|
||
const map = new Map<string, number>()
|
||
for (const e of selectedEmployees) {
|
||
if (e.positionLevel) map.set(e.positionLevel, (map.get(e.positionLevel) ?? 0) + 1)
|
||
}
|
||
return Array.from(map.entries()).sort((a, b) => b[1] - a[1])
|
||
}, [selectedEmployees])
|
||
|
||
// 搜索过滤
|
||
const filteredEmployees = useMemo(() => {
|
||
if (!keyword.trim()) return selectedEmployees
|
||
const k = keyword.trim().toLowerCase()
|
||
return selectedEmployees.filter(
|
||
(e) =>
|
||
(e.displayName ?? '').toLowerCase().includes(k) ||
|
||
(e.realName ?? '').toLowerCase().includes(k) ||
|
||
(e.jobNumber ?? '').toLowerCase().includes(k) ||
|
||
(e.positionTitle ?? '').toLowerCase().includes(k),
|
||
)
|
||
}, [selectedEmployees, keyword])
|
||
|
||
const toggleExpand = (id: string) => {
|
||
setExpanded((prev) => {
|
||
const next = new Set(prev)
|
||
if (next.has(id)) next.delete(id)
|
||
else next.add(id)
|
||
return next
|
||
})
|
||
}
|
||
|
||
const selectNode = (node: OrgNode) => {
|
||
setSelectedPath(node.path)
|
||
setSelectedGroup('org')
|
||
if (!expanded.has(node.id) && node.children.length > 0) {
|
||
setExpanded((prev) => new Set(prev).add(node.id))
|
||
}
|
||
}
|
||
|
||
if (status === 'loading' || (status === 'authenticated' && !canAccess)) {
|
||
return <div className="min-h-screen bg-[#fafafa]" />
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{/* direction: cloud-quarry (seed 32cb33b5) — THESIS: 部门即层叠石料块,人员是块上铭牌 */}
|
||
<BoardShell session={session} active="organization">
|
||
<div className="max-w-[1400px] mx-auto px-5 py-6">
|
||
{/* 页头 */}
|
||
<div className="flex items-end justify-between mb-5">
|
||
<div>
|
||
<h1 className="text-[26px] font-bold text-[#1a1c1d] tracking-tight">组织架构</h1>
|
||
<p className="text-[13px] text-[#86898e] mt-1">
|
||
品牌中心人员全景 · 数据源:HR 花名册(每日 02:00 自动同步)
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
{data && (
|
||
<span className="text-[12px] text-[#86898e]">
|
||
共 <b className="text-[#1a1c1d]">{data.summary.visibleCount}</b> 人
|
||
{isSuperAdmin && (
|
||
<span className="ml-2">
|
||
(在职 {data.summary.activeEmployees} / 含离职 {data.summary.inactiveEmployees})
|
||
</span>
|
||
)}
|
||
</span>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => setIncludeInactive((v) => !v)}
|
||
className={`text-[12px] px-3 py-1.5 rounded border transition-colors ${
|
||
includeInactive
|
||
? 'bg-[#5a66b8] text-white border-[#5a66b8]'
|
||
: 'bg-white text-[#5a66b8] border-[#5a66b8] hover:bg-[#eef0fa]'
|
||
}`}
|
||
>
|
||
含离职
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-[#fdf6e9] border border-[#e8c98a] text-[#a0671c] rounded-lg px-4 py-3 text-[13px] mb-4">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{loading && !data ? (
|
||
<div className="bg-white rounded-lg border border-[#e7e8ea] shadow-sm p-10 text-center text-[#86898e] text-sm">
|
||
加载中…
|
||
</div>
|
||
) : data ? (
|
||
<div className="grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-5 items-start">
|
||
{/* 左侧:组织树 */}
|
||
<aside className="bg-white rounded-lg border border-[#e7e8ea] shadow-sm overflow-hidden">
|
||
<div className="px-4 py-3 border-b border-[#eef0f3] flex items-center justify-between">
|
||
<span className="text-[12px] font-semibold text-[#1a1c1d]">组织架构</span>
|
||
<span className="text-[11px] text-[#86898e]">{data.tree.length} 顶层节点</span>
|
||
</div>
|
||
<nav className="max-h-[calc(100vh-260px)] overflow-y-auto py-2" aria-label="组织树">
|
||
{flatTree.map(({ node, depth }) => {
|
||
const hasChildren = node.children.length > 0
|
||
const isExpanded = expanded.has(node.id)
|
||
const isSelected = selectedGroup === 'org' && selectedPath === node.path
|
||
return (
|
||
<div key={node.id}>
|
||
<button
|
||
type="button"
|
||
onClick={() => selectNode(node)}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-colors ${
|
||
isSelected ? 'bg-[#eef0fa]' : 'hover:bg-[#f4f5f7]'
|
||
}`}
|
||
style={{ paddingLeft: `${12 + depth * 16}px` }}
|
||
>
|
||
{hasChildren ? (
|
||
<span
|
||
role="button"
|
||
tabIndex={-1}
|
||
aria-label={isExpanded ? '收起' : '展开'}
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
toggleExpand(node.id)
|
||
}}
|
||
className="shrink-0 cursor-pointer"
|
||
>
|
||
<svg
|
||
className={`w-3 h-3 text-[#86898e] transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
aria-hidden="true"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</span>
|
||
) : (
|
||
<span className="w-3 inline-block" aria-hidden="true" />
|
||
)}
|
||
<span
|
||
className={`text-[13px] truncate ${
|
||
isSelected ? 'text-[#1a1c1d] font-semibold' : 'text-[#1a1c1d]'
|
||
}`}
|
||
>
|
||
{node.name}
|
||
</span>
|
||
<span className="ml-auto text-[11px] font-medium text-[#5a66b8] tabular-nums">
|
||
{node.count}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
)
|
||
})}
|
||
{/* 特殊组 */}
|
||
<div className="border-t border-[#eef0f3] mt-2 pt-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setSelectedGroup('unclassified')
|
||
setSelectedPath(null)
|
||
}}
|
||
className={`w-full flex items-center justify-between px-3 py-2 text-left ${
|
||
selectedGroup === 'unclassified' ? 'bg-[#eef0fa]' : 'hover:bg-[#f4f5f7]'
|
||
}`}
|
||
>
|
||
<span className="text-[13px] text-[#a0671c]">未确定</span>
|
||
<span className="text-[11px] font-medium text-[#a0671c] tabular-nums">
|
||
{data.unclassified.length}
|
||
</span>
|
||
</button>
|
||
{isSuperAdmin && (
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setSelectedGroup('unbound')
|
||
setSelectedPath(null)
|
||
}}
|
||
className={`w-full flex items-center justify-between px-3 py-2 text-left ${
|
||
selectedGroup === 'unbound' ? 'bg-[#eef0fa]' : 'hover:bg-[#f4f5f7]'
|
||
}`}
|
||
>
|
||
<span className="text-[13px] text-[#a0671c]">未绑定账号</span>
|
||
<span className="text-[11px] font-medium text-[#a0671c] tabular-nums">
|
||
{data.unbound.length}
|
||
</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</nav>
|
||
</aside>
|
||
|
||
{/* 右侧:人员明细 */}
|
||
<section className="bg-white rounded-lg border border-[#e7e8ea] shadow-sm overflow-hidden">
|
||
<div className="px-5 py-4 border-b border-[#eef0f3]">
|
||
<div className="flex items-center justify-between gap-4">
|
||
<div className="flex items-center gap-2 min-w-0">
|
||
{selectedGroup === 'org' && selectedPath ? (
|
||
<>
|
||
<span className="text-[11px] bg-[#5a66b8] text-white rounded px-2 py-0.5 font-semibold whitespace-nowrap">
|
||
L{selectedPath.split('/').length}
|
||
</span>
|
||
<span className="text-[14px] font-semibold text-[#1a1c1d] truncate">
|
||
{selectedPath.split('/').pop()}
|
||
</span>
|
||
</>
|
||
) : (
|
||
<span className="text-[14px] font-semibold text-[#1a1c1d]">
|
||
{selectedGroup === 'unclassified' ? '未确定部门' : '未绑定账号'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="relative shrink-0">
|
||
<input
|
||
value={keyword}
|
||
onChange={(e) => setKeyword(e.target.value)}
|
||
placeholder="搜索姓名 / 工号 / 职位"
|
||
className="w-40 md:w-52 text-[13px] px-3 py-1.5 rounded border border-[#d5dae0] bg-[#f4f5f7] focus:outline-none focus:border-[#5a66b8] focus:ring-2 focus:ring-[#eef0fa]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
{selectedGroup === 'org' && selectedPath && (
|
||
<p className="text-[12px] text-[#86898e] mt-1.5 truncate">{selectedPath}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 统计行 */}
|
||
{selectedGroup === 'org' && (
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 px-5 py-4 border-b border-[#eef0f3] bg-[#f4f5f7]">
|
||
<div>
|
||
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
|
||
{selectedEmployees.filter((e) => e.employmentState !== 'RESIGNED').length}
|
||
</div>
|
||
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">在职</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
|
||
{leaderCount}
|
||
</div>
|
||
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">负责人</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
|
||
{levelCounts[0]?.[0] ?? '—'}
|
||
</div>
|
||
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">最常见等级</div>
|
||
</div>
|
||
<div>
|
||
<div className="text-[22px] font-bold text-[#1a1c1d] tabular-nums leading-none">
|
||
{selectedEmployees.filter((e) => e.managerName).length}
|
||
</div>
|
||
<div className="text-[10px] text-[#86898e] mt-1 tracking-wide">有主管记录</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 人员表 */}
|
||
{selectedGroup === 'unbound' ? (
|
||
<div className="p-5">
|
||
<p className="text-[13px] text-[#86898e] mb-3">
|
||
以下账号没有钉钉 ID,无法自动绑定权威人员档案,需人工核对后手动绑定。
|
||
</p>
|
||
<div className="grid gap-2">
|
||
{data.unbound.map((u) => (
|
||
<div
|
||
key={u.userId}
|
||
className="flex items-center justify-between px-4 py-2.5 rounded border border-[#eef0f3] bg-[#f4f5f7]"
|
||
>
|
||
<div>
|
||
<span className="text-[13px] text-[#1a1c1d] font-medium">
|
||
{u.realName ?? u.username}
|
||
</span>
|
||
<span className="text-[12px] text-[#86898e] ml-2">@{u.username}</span>
|
||
</div>
|
||
<span className="text-[12px] text-[#86898e]">{u.oldDepartment || '无部门'}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : filteredEmployees.length === 0 ? (
|
||
<div className="p-10 text-center text-[#86898e] text-sm">该部门暂无人员</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left">
|
||
<thead>
|
||
<tr className="border-b border-[#e7e8ea] bg-[#f4f5f7]">
|
||
<th className="px-5 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">人员</th>
|
||
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">工号</th>
|
||
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">职位</th>
|
||
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">等级</th>
|
||
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">直属主管</th>
|
||
<th className="px-4 py-2.5 text-[10px] font-semibold text-[#86898e] tracking-wider">状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filteredEmployees.map((e) => (
|
||
<tr key={e.userId} className="border-b border-[#eef0f3] hover:bg-[#f4f5f7]">
|
||
<td className="px-5 py-3">
|
||
<div className="flex items-center gap-2.5">
|
||
<span className="w-7 h-7 rounded-full bg-[#eef0fa] text-[#1a1c1d] text-[12px] font-semibold flex items-center justify-center">
|
||
{(e.displayName ?? e.realName ?? '?').slice(0, 1)}
|
||
</span>
|
||
<span className="text-[13px] text-[#1a1c1d] font-medium">
|
||
{e.displayName ?? e.realName ?? e.username}
|
||
</span>
|
||
{e.isLeadership && (
|
||
<span className="text-[10px] text-[#1c8a5a] bg-[#e8f5ee] rounded px-1.5 py-0.5">
|
||
负责人
|
||
</span>
|
||
)}
|
||
</div>
|
||
</td>
|
||
<td className="px-4 py-3 text-[12px] text-[#86898e] tabular-nums">{e.jobNumber ?? '—'}</td>
|
||
<td className="px-4 py-3 text-[13px] text-[#1a1c1d]">{e.positionTitle ?? '—'}</td>
|
||
<td className="px-4 py-3">
|
||
{e.positionLevel ? (
|
||
<span
|
||
className={`text-[11px] font-semibold rounded px-2 py-0.5 ${
|
||
e.levelType === 'M'
|
||
? 'bg-[#fdf6e9] text-[#a0671c]'
|
||
: 'bg-[#eef0fa] text-[#1a1c1d]'
|
||
}`}
|
||
>
|
||
{e.positionLevel}
|
||
{e.levelType ? ` · ${LEVEL_TYPE_LABEL[e.levelType] ?? e.levelType}` : ''}
|
||
</span>
|
||
) : (
|
||
<span className="text-[12px] text-[#b0b6be]">未记录</span>
|
||
)}
|
||
</td>
|
||
<td className="px-4 py-3 text-[13px] text-[#1a1c1d]">{e.managerName ?? '—'}</td>
|
||
<td className="px-4 py-3">
|
||
<span
|
||
className={`text-[11px] rounded-full px-2.5 py-0.5 ${
|
||
e.employmentState === 'RESIGNED'
|
||
? 'text-[#86898e] bg-[#eef0f3]'
|
||
: e.employmentState === 'UNKNOWN'
|
||
? 'text-[#a0671c] bg-[#fdf6e9]'
|
||
: 'text-[#1c8a5a] bg-[#e8f5ee]'
|
||
}`}
|
||
>
|
||
{STATE_LABEL[e.employmentState ?? 'UNKNOWN'] ?? '未知'}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</section>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</BoardShell>
|
||
</>
|
||
)
|
||
}
|