feat: add team functions department navigation

This commit is contained in:
江林铖 2026-09-03 10:00:59 +08:00
parent fe8c17e316
commit b33cef8c44
2 changed files with 90 additions and 55 deletions

View File

@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import TeamFunctionsDashboard from './TeamFunctionsDashboard'
jest.mock('next-auth/react', () => ({
@ -7,24 +7,39 @@ jest.mock('next-auth/react', () => ({
}))
describe('TeamFunctionsDashboard', () => {
it('shows the four teams and their function branches from the approved map', () => {
it('shows department navigation and the selected team introduction', () => {
render(<TeamFunctionsDashboard />)
expect(screen.getByRole('heading', { level: 1, name: '团队职能' })).toBeInTheDocument()
expect(screen.getByRole('heading', { level: 2, name: '品牌中心职责地图' })).toBeInTheDocument()
expect(screen.getByRole('complementary', { name: '团队职能二级部门导航' })).toBeInTheDocument()
for (const team of ['品牌创意A部', '品牌创意B部', '空间设计部', '综合支持部']) {
expect(screen.getByRole('heading', { level: 2, name: team })).toBeInTheDocument()
expect(screen.getByRole('button', { name: new RegExp(team) })).toBeInTheDocument()
}
for (const label of ['服务品牌', '前置能力', '定位', '岗位', '职责']) {
expect(screen.getAllByText(label).length).toBeGreaterThan(0)
}
expect(screen.getByRole('region', { name: '品牌创意A部部门介绍' })).toBeInTheDocument()
expect(screen.getByText('负责朴美、索菲亚、顾家品牌的视觉创意与内容前置策划,统筹品牌调性、商品卖点及页面与短视频创意。')).toBeInTheDocument()
for (const item of ['朴美', '索菲亚', '顾家', '欧意朗', '卓禾', '亿家达', '空间设计师', 'AIGC设计师', '批量生产']) {
expect(screen.getByText(item)).toBeInTheDocument()
for (const item of ['朴美', '索菲亚', '顾家', '品牌视觉策划设计', 'AI+平面+策划', '编导/短视频策划', '品牌调性', '商品卖点', '页面/短视频创意', '经验与复盘']) {
expect(screen.getByRole('region', { name: '品牌创意A部部门介绍' }).querySelector(`[data-team-item="${item}"]`)).not.toBeNull()
}
expect(screen.getByRole('link', { name: '返回部门看板' })).toHaveAttribute('href', '/')
})
it('switches the department introduction from the secondary navigation', () => {
render(<TeamFunctionsDashboard />)
fireEvent.click(screen.getByRole('button', { name: /空间设计部/ }))
expect(screen.getByRole('region', { name: '空间设计部部门介绍' })).toBeInTheDocument()
expect(screen.getByText('负责空间、置景、灯光、机位与拍摄一体化,为内容生产提供场景和拍摄执行能力。')).toBeInTheDocument()
expect(screen.queryByRole('region', { name: '品牌创意A部部门介绍' })).not.toBeInTheDocument()
expect(screen.getByText('美术指导')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /品牌创意B部/ }))
expect(screen.getByRole('region', { name: '品牌创意B部部门介绍' })).toBeInTheDocument()
expect(screen.getByText('欧意朗')).toBeInTheDocument()
})
})

View File

@ -1,5 +1,8 @@
'use client'
import Link from 'next/link'
import { ArrowLeft, Blocks, BriefcaseBusiness, Check, Layers3, UsersRound } from 'lucide-react'
import { useState } from 'react'
import DepartmentShell from './DepartmentShell'
type FunctionBranch = {
@ -10,6 +13,7 @@ type FunctionBranch = {
type TeamFunction = {
name: string
lead: string
description: string
tone: 'coral' | 'navy' | 'blue' | 'teal'
branches: FunctionBranch[]
}
@ -18,6 +22,7 @@ const TEAM_FUNCTIONS: TeamFunction[] = [
{
name: '品牌创意A部',
lead: '伯言',
description: '负责朴美、索菲亚、顾家品牌的视觉创意与内容前置策划,统筹品牌调性、商品卖点及页面与短视频创意。',
tone: 'coral',
branches: [
{ label: '服务品牌', items: ['朴美', '索菲亚', '顾家'] },
@ -28,6 +33,7 @@ const TEAM_FUNCTIONS: TeamFunction[] = [
{
name: '品牌创意B部',
lead: '清云',
description: '负责欧意朗、卓禾、亿家达品牌的视觉创意与内容前置策划,推动品牌辅导、页面及短视频创意落地。',
tone: 'navy',
branches: [
{ label: '服务品牌', items: ['欧意朗', '卓禾', '亿家达'] },
@ -38,6 +44,7 @@ const TEAM_FUNCTIONS: TeamFunction[] = [
{
name: '空间设计部',
lead: '希文',
description: '负责空间、置景、灯光、机位与拍摄一体化,为内容生产提供场景和拍摄执行能力。',
tone: 'blue',
branches: [
{ label: '定位', items: ['空间、置景、灯光、机位、拍摄一体化'] },
@ -48,6 +55,7 @@ const TEAM_FUNCTIONS: TeamFunction[] = [
{
name: '综合支持部',
lead: '无邪',
description: '承接3D、AI、平面、修图、视频后期等生产支持,沉淀资产标准并推进批量生产。',
tone: 'teal',
branches: [
{ label: '岗位', items: ['3D设计师', '平面设计师', 'AIGC设计师', '视频后期', '修图'] },
@ -104,6 +112,7 @@ function Branch({ branch, tone }: { branch: FunctionBranch; tone: TeamFunction['
{branch.items.map((item) => (
<div
key={item}
data-team-item={item}
className="flex min-h-10 items-center gap-2 rounded-lg border px-3 py-2 text-sm font-semibold leading-5"
style={{ borderColor: colors.line, backgroundColor: colors.node, color: '#26364f' }}
>
@ -116,41 +125,11 @@ function Branch({ branch, tone }: { branch: FunctionBranch; tone: TeamFunction['
)
}
function TeamFunctionPanel({ team }: { team: TeamFunction }) {
const colors = toneStyles[team.tone]
return (
<article
aria-labelledby={`team-function-${team.tone}`}
className="overflow-hidden rounded-xl border border-[#dfe5ed] bg-white"
>
<header className="flex flex-col gap-4 px-5 py-5 sm:flex-row sm:items-center sm:justify-between sm:px-6">
<div className="flex min-w-0 items-center gap-3">
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl" style={{ backgroundColor: colors.accentSoft, color: colors.accent }}>
<UsersRound className="h-5 w-5" aria-hidden="true" />
</span>
<div className="min-w-0">
<h2 id={`team-function-${team.tone}`} className="truncate text-xl font-bold tracking-[-0.02em] text-[#101828] sm:text-2xl">
{team.name}
</h2>
<p className="mt-1 text-sm font-semibold" style={{ color: colors.accentMuted }}>负责人:{team.lead}</p>
</div>
</div>
<span className="inline-flex min-h-8 items-center self-start rounded-lg px-3 text-xs font-bold sm:self-auto" style={{ backgroundColor: colors.accentSoft, color: colors.accentMuted }}>
职能地图
</span>
</header>
<div className="border-t border-[#e4e9f0] px-5 py-5 sm:px-6 sm:py-6">
<div className="relative space-y-4 md:space-y-5 md:before:absolute md:before:bottom-5 md:before:left-[116px] md:before:top-5 md:before:w-px md:before:bg-[#e9edf3]">
{team.branches.map((branch) => <Branch key={branch.label} branch={branch} tone={team.tone} />)}
</div>
</div>
</article>
)
}
export default function TeamFunctionsDashboard() {
const [selectedTeamName, setSelectedTeamName] = useState(TEAM_FUNCTIONS[0].name)
const selectedTeam = TEAM_FUNCTIONS.find((team) => team.name === selectedTeamName) || TEAM_FUNCTIONS[0]
const colors = toneStyles[selectedTeam.tone]
return (
<DepartmentShell>
<main className="mx-auto max-w-[1600px] px-4 pb-16 pt-7 sm:px-6 lg:px-8">
@ -164,21 +143,62 @@ export default function TeamFunctionsDashboard() {
</p>
</header>
<section aria-labelledby="team-functions-map-title" className="mt-6 overflow-hidden rounded-xl border border-[#dfe5ed] bg-white shadow-[0_6px_18px_rgba(15,23,42,0.05)]">
<header className="flex flex-col gap-3 border-b border-[#e4e9f0] bg-[#f8fafc] px-5 py-5 sm:flex-row sm:items-end sm:justify-between sm:px-7">
<div>
<h2 id="team-functions-map-title" className="text-xl font-bold tracking-[-0.02em] text-[#101828] sm:text-2xl">品牌中心职责地图</h2>
<p className="mt-1 text-sm leading-6 text-[#66758b]">四个团队、三类职责层级;节点内容按现行团队分工整理。</p>
<section aria-labelledby="team-functions-map-title" className="mt-6 overflow-hidden rounded-xl border border-[#dfe5ed] bg-white shadow-[0_6px_18px_rgba(15,23,42,0.05)] lg:grid lg:grid-cols-[232px_minmax(0,1fr)]">
<aside aria-label="团队职能二级部门导航" className="border-b border-[#e4e9f0] bg-[#f8fafc] lg:border-b-0 lg:border-r">
<div className="hidden border-b border-[#e4e9f0] px-4 py-4 lg:block">
<p className="text-xs font-bold text-[#66758b]">部门</p>
<h2 id="team-functions-map-title" className="mt-1 text-sm font-bold text-[#101828]">品牌中心职责地图</h2>
</div>
<nav className="flex gap-1 overflow-x-auto p-2 lg:block lg:space-y-1 lg:overflow-visible" aria-label="选择部门">
{TEAM_FUNCTIONS.map((team) => {
const teamColors = toneStyles[team.tone]
const selected = selectedTeam.name === team.name
return (
<button
key={team.name}
type="button"
aria-current={selected ? 'page' : undefined}
onClick={() => setSelectedTeamName(team.name)}
className={`flex min-h-[58px] shrink-0 items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] lg:w-full ${selected ? 'bg-[#1677ff] text-white' : 'text-[#4f6077] hover:bg-[#eef2f6] hover:text-[#101828]'}`}
>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: selected ? 'rgba(255,255,255,0.16)' : teamColors.accentSoft, color: selected ? '#fff' : teamColors.accent }}>
<UsersRound className="h-4 w-4" aria-hidden="true" />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-bold">{team.name}</span>
<span className={`mt-0.5 block truncate text-xs ${selected ? 'text-white/75' : 'text-[#667085]'}`}>负责人:{team.lead}</span>
</span>
</button>
)
})}
</nav>
</aside>
<section aria-label={`${selectedTeam.name}部门介绍`} className="min-w-0">
<header className="flex flex-col gap-4 border-b border-[#e4e9f0] px-5 py-5 sm:px-7 sm:py-6 lg:flex-row lg:items-start lg:justify-between">
<div className="flex min-w-0 items-start gap-3">
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl" style={{ backgroundColor: colors.accentSoft, color: colors.accent }}>
<UsersRound className="h-5 w-5" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-xs font-bold" style={{ color: colors.accentMuted }}>当前部门</p>
<h2 className="mt-1 text-2xl font-bold tracking-[-0.02em] text-[#101828]">{selectedTeam.name}</h2>
<p className="mt-1 text-sm font-semibold" style={{ color: colors.accentMuted }}>负责人:{selectedTeam.lead}</p>
</div>
</div>
<div className="flex flex-wrap gap-2 text-xs font-bold text-[#5f7189]" aria-label="职责地图说明">
<span className="inline-flex items-center gap-1.5 rounded-lg bg-white px-3 py-2"><BriefcaseBusiness className="h-3.5 w-3.5" aria-hidden="true" />服务与定位</span>
<span className="inline-flex items-center gap-1.5 rounded-lg bg-white px-3 py-2"><Layers3 className="h-3.5 w-3.5" aria-hidden="true" />能力与岗位</span>
<span className="inline-flex items-center gap-1.5 rounded-lg bg-[#f8fafc] px-3 py-2"><BriefcaseBusiness className="h-3.5 w-3.5" aria-hidden="true" />服务与定位</span>
<span className="inline-flex items-center gap-1.5 rounded-lg bg-[#f8fafc] px-3 py-2"><Layers3 className="h-3.5 w-3.5" aria-hidden="true" />能力与岗位</span>
</div>
</header>
<div className="grid gap-4 p-4 sm:p-6 xl:grid-cols-2">
{TEAM_FUNCTIONS.map((team) => <TeamFunctionPanel key={team.name} team={team} />)}
<div className="px-5 py-5 sm:px-7 sm:py-6">
<p className="max-w-3xl text-base leading-7 text-[#4f6077]">{selectedTeam.description}</p>
<div className="relative mt-6 space-y-4 md:space-y-5 md:before:absolute md:before:bottom-5 md:before:left-[116px] md:before:top-5 md:before:w-px md:before:bg-[#e9edf3]">
{selectedTeam.branches.map((branch) => <Branch key={branch.label} branch={branch} tone={selectedTeam.tone} />)}
</div>
</div>
</section>
</section>
<Link