gongxiangp2/components/board/BoardShell.tsx
chenpei 6c6c282203 feat: 全站视觉统一白皮书语言 + 个人档案改造 + 顶栏平台化
视觉统一(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 对齐)
2026-08-15 14:16:20 +08:00

175 lines
5.9 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.

'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { signOut } from 'next-auth/react'
import type { Session } from 'next-auth'
import type { ReactNode } from 'react'
import BrandLogo from '@/components/BrandLogo'
import { DEFAULT_LOGIN_LOGO_URL } from '@/lib/brand-assets'
/*
* BoardShell —— 品牌中心看板顶层骨架(云采石场 · seed 32cb33b5 世界内扩展)
* 顶栏=白色石材地面上的切割导航:当前板块以切割蓝垫层高亮,规划中板块保持"未开采"不可点状态;
* 供看板骨架首页(/)与组织管理(/organization)共用,后续各板块入住时沿用同一顶栏。
*/
export type BoardSection = 'dashboard' | 'organization' | 'assets' | 'business' | 'governance'
interface BoardShellProps {
session?: Session | null
active: BoardSection
children: ReactNode
}
const ADMIN_ROLES = ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN']
const NAV_ITEMS: Array<{
key: BoardSection
label: string
href?: string
planned?: boolean
plannedLabel?: string
adminOnly?: boolean
}> = [
{ key: 'dashboard', label: '中心仪表盘', href: '/' },
{ key: 'assets', label: '创意资产库', href: '/materials' },
{ key: 'organization', label: '组织管理', href: '/organization', adminOnly: true },
{ key: 'business', label: '业务经营', planned: true },
{ key: 'governance', label: '治理与设置', planned: true },
]
export default function BoardShell({ session, active, children }: BoardShellProps) {
const router = useRouter()
// 本壳顶栏为浅色,用浅底适配版 logo(loginLogoUrl);深底版仅用于素材库黑色顶栏
const [brandLogoUrl, setBrandLogoUrl] = useState(DEFAULT_LOGIN_LOGO_URL)
useEffect(() => {
const loadSettings = async () => {
try {
const response = await fetch('/api/system-settings')
if (response.ok) {
const settings = await response.json()
setBrandLogoUrl(settings.loginLogoUrl || DEFAULT_LOGIN_LOGO_URL)
}
} catch {
// 设置加载失败时保留默认 logo
}
}
void loadSettings()
}, [])
const sessionUser = session?.user as
| {
role?: string
name?: string | null
realName?: string | null
username?: string | null
}
| undefined
const isAdmin = ADMIN_ROLES.includes(sessionUser?.role || '')
const displayName = sessionUser?.realName || sessionUser?.name || sessionUser?.username || '成员'
const handleSignOut = async () => {
try {
await signOut({ redirect: false })
await new Promise((resolve) => setTimeout(resolve, 100))
} finally {
router.push('/login')
router.refresh()
}
}
const visibleItems = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin)
const renderNavItem = (item: (typeof visibleItems)[number], compact = false) => {
if (item.planned) {
return (
<span
key={item.key}
aria-disabled="true"
title="规划中,尚未开放"
className={`flex cursor-not-allowed items-center gap-1.5 text-[#6b7075] ${
compact ? 'px-2.5 py-1.5 text-[12px]' : 'px-3 py-2 text-[13px]'
}`}
>
{item.label}
<span className="bg-[#eef0f3] px-1.5 py-0.5 text-[10px]">{item.plannedLabel ?? '规划中'}</span>
</span>
)
}
const isActive = active === item.key
return (
<Link
key={item.key}
href={item.href ?? '/'}
aria-current={isActive ? 'page' : undefined}
className={`transition-colors ${compact ? 'px-2.5 py-1.5 text-[12px]' : 'px-3 py-2 text-[13px]'} ${
isActive
? 'bg-[#eef0fa] font-semibold text-[#1a1c1d] shadow-[inset_0_-2px_0_#5a66b8]'
: 'text-[#1a1c1d] hover:bg-[#f3f5f7]'
}`}
>
{item.label}
</Link>
)
}
return (
<div className="min-h-screen bg-[#fafafa] text-[#1a1c1d]">
<header className="sticky top-0 z-40 border-b border-[#dfe4e9] bg-white">
<div className="mx-auto flex h-[60px] max-w-[1400px] items-center gap-4 px-5">
<Link
href="/"
className="flex min-w-0 items-center gap-2.5"
aria-label="回到品牌中心看板"
>
<BrandLogo
src={brandLogoUrl}
fallbackSrc={DEFAULT_LOGIN_LOGO_URL}
alt=""
width={32}
height={32}
unoptimized
className="h-8 w-8 shrink-0 rounded-md object-cover"
/>
<span className="truncate text-[16px] font-bold tracking-tight text-[#1a1c1d]">
由壹点·品牌中心
</span>
</Link>
{/* 桌面端板块导航 */}
<nav
className="ml-auto hidden items-center gap-1 md:flex"
aria-label="看板板块导航"
>
{visibleItems.map((item) => renderNavItem(item))}
</nav>
<div className="ml-auto flex items-center gap-2.5 md:ml-0">
<span className="hidden max-w-[9rem] truncate text-[12px] text-[#1a1c1d] sm:inline">
{displayName}
</span>
<button
type="button"
onClick={() => void handleSignOut()}
className="border border-[#d5dae0] px-2.5 py-1.5 text-[12px] text-[#1a1c1d] transition-colors hover:border-[#5a66b8] hover:text-[#5a66b8] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5a66b8]/40"
>
退出
</button>
</div>
</div>
{/* 移动端板块导航:横向滚动 */}
<nav
className="flex items-center gap-1 overflow-x-auto border-t border-[#eef0f3] px-4 py-1.5 md:hidden"
aria-label="看板板块导航"
>
{visibleItems.map((item) => renderNavItem(item, true))}
</nav>
</header>
<main>{children}</main>
</div>
)
}