视觉统一(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 对齐)
175 lines
5.9 KiB
TypeScript
175 lines
5.9 KiB
TypeScript
'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>
|
||
)
|
||
}
|