gongxiangp2/app/page.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

395 lines
18 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.

/*
* 方向契约(用户裁定方向:Zen Minimal × Future Organic 融合 · 2026-08-15)
* THESIS: 看板首页是一张"编辑页"——白纸、黑字、发丝线、超大留白;五个板块排成一列
* 编号索引(作品集列表语言),悬停整行浮现;唯一的柔光来自一枚有机渐变光晕。
* 拒绝卡片阵列、拒绝重色块、拒绝装饰;安静得像一本品牌中心的白皮书。
* OWN-WORLD: 白 #fafafa/#fff、墨 #1a1c1d、正文灰 #6b7075、注脚灰 #86898e、发丝线 #e7e8ea、
* 柔光紫 #6e79c8 系(唯一彩色氛围)+ 浅晕 #eef0fa、微型大写字距标注、
* 黑实心按钮 8px 圆角、无阴影平面化;全部文字对比过 WCAG(≥4.5:1 / 大字 ≥3:1)。
* STORY: 打开即读:巨字「品牌中心」+ 一句矿脉定位;视线沿编号索引下移——
* 01 组织管理、02 资产是活的(可点,行变黑);03 仪表盘在长;04/05 是待启程的灰行。
* 两块已上线板块各有一张白卡给入口;页脚是跟踪字距的微型注脚。
* FIRST VIEWPORT: 极简顶栏(logo+由壹点·品牌中心)→ 超大 600 字重「品牌中心」+ 定位语 →
* 右侧一枚柔光渐变光晕压角 → 下方五条发丝线分隔的编号索引行。
* FORM: 用户在 Stitch 自生成的候选 3(Zen Minimal)与 4(Future Organic)融合,
* 参考作品集文件夹两份 DESIGN.md(Monochrome/Studio Minimalist)裁定。
* FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md
*/
'use client'
import { useEffect, useRef } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
import MaterialsHeader from '@/components/board/MaterialsHeader'
gsap.registerPlugin(useGSAP)
const ADMIN_ROLES = ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN']
function StatusDot({ tone }: { tone: 'live' | 'building' | 'planned' }) {
const tones = { live: 'bg-[#157a4b]', building: 'bg-[#6e79c8]', planned: 'bg-[#9aa0a8]' } as const
return <span aria-hidden="true" className={`inline-block h-[7px] w-[7px] shrink-0 rounded-full ${tones[tone]}`} />
}
const labelCls = 'text-[11px] uppercase tracking-[0.14em] text-[#86898e]'
const focusRing = 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#fafafa]'
/** 会话加载中的骨架行(防止管理员首帧闪灰) */
function SkeletonIndex() {
return (
<div className="border-t border-[#e7e8ea]">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-4 border-b border-[#e7e8ea] py-9 md:gap-8">
<div className="h-3 w-8 shrink-0 animate-pulse bg-[#eceef0]" />
<div className="min-w-0 flex-1 space-y-2.5">
<div className="h-5 w-44 animate-pulse bg-[#eceef0]" />
<div className="h-3 w-72 max-w-full animate-pulse bg-[#f2f3f5]" />
</div>
</div>
))}
</div>
)
}
export default function BoardHomePage() {
const { data: session, status } = useSession()
const router = useRouter()
const pageRef = useRef<HTMLDivElement>(null)
const sessionLoading = status === 'loading'
const sessionUser = session?.user as { role?: string } | undefined
const isAdmin = ADMIN_ROLES.includes(sessionUser?.role || '')
useEffect(() => {
if (status === 'unauthenticated') {
router.replace('/login')
}
}, [status, router])
const indexRows = [
{
no: '01',
title: '创意资产库',
en: 'Creative Assets',
tone: 'live' as const,
status: '运行中',
live: true,
href: '/materials',
desc: '素材浏览、上传、分享与下载——品牌中心的创意资产都在这里,全部功能照常使用。',
},
{
no: '02',
title: '组织管理',
en: 'People & Org',
tone: 'live' as const,
status: '已上线',
live: isAdmin,
href: '/organization',
desc: '人员主档以钉钉唯一值绑定 HR 花名册,组织树与在职状态每日 02:00 自动同步。',
},
{
no: '03',
title: '中心仪表盘',
en: 'Dashboard',
tone: 'building' as const,
status: '建设中',
live: false,
desc: '将接入:核心指标 · 业务趋势 · 六品牌健康度 · 团队产能 · 风险预警',
},
{
no: '04',
title: '业务经营',
en: 'Operations',
tone: 'planned' as const,
status: '规划中',
live: false,
desc: '将包含:新品上新 · 品牌塑造 · 创意生产 · 业绩转化 · 项目协同',
},
{
no: '05',
title: '治理与设置',
en: 'Governance',
tone: 'planned' as const,
status: '规划中',
live: false,
desc: '将包含:权限角色 · 数据口径 · 编辑能力 · 通知机制',
},
]
useGSAP(() => {
if (typeof window.matchMedia !== 'function') return
const motion = gsap.matchMedia()
motion.add('(prefers-reduced-motion: no-preference)', () => {
const tl = gsap.timeline({ defaults: { ease: 'expo.out' } })
tl
// 主标题:对焦揭幕——虚焦+宽字距 → 聚焦收拢(重、慢,配 quint 级缓动)
.fromTo(
'[data-hero-title]',
{ filter: 'blur(16px)', opacity: 0, letterSpacing: '0.16em' },
{
filter: 'blur(0px)',
opacity: 1,
letterSpacing: '-0.03em',
duration: 1.15,
ease: 'power4.out',
clearProps: 'filter,opacity,letterSpacing',
},
)
// 柔光:同语言聚焦绽放
.fromTo(
'[data-orb]',
{ autoAlpha: 0, scale: 0.9, filter: 'blur(12px)' },
{ autoAlpha: 1, scale: 1, filter: 'blur(0px)', duration: 1.3, clearProps: 'filter' },
'-=0.8',
)
// 定位语
.fromTo(
'[data-hero]',
{ y: 12, autoAlpha: 0 },
{ y: 0, autoAlpha: 1, duration: 0.55, clearProps: 'opacity,visibility,transform' },
'-=0.85',
)
// 图纸先行:每行的发丝线依次画出,内容再落位
.fromTo(
'[data-row-line]',
{ scaleX: 0 },
{ scaleX: 1, duration: 0.6, stagger: 0.07 },
'-=0.55',
)
.fromTo(
'[data-row]',
{ y: 14, autoAlpha: 0 },
{ y: 0, autoAlpha: 1, duration: 0.55, stagger: 0.07, clearProps: 'opacity,visibility,transform' },
'-=0.5',
)
// 白卡:虚焦落位(与标题同语言)
.fromTo(
'[data-card]',
{ y: 20, autoAlpha: 0, filter: 'blur(8px)' },
{
y: 0,
autoAlpha: 1,
filter: 'blur(0px)',
duration: 0.75,
stagger: 0.12,
clearProps: 'opacity,visibility,transform,filter',
},
'-=0.4',
)
.fromTo(
'[data-foot]',
{ autoAlpha: 0 },
{ autoAlpha: 1, duration: 0.6, clearProps: 'opacity,visibility' },
'-=0.35',
)
// 柔光光晕的呼吸(极慢、纯 opacity)
gsap.to('[data-orb]', { opacity: 0.75, duration: 5, yoyo: true, repeat: -1, ease: 'sine.inOut' })
})
return () => motion.revert()
}, { scope: pageRef })
return (
// direction: zen-minimal × future-organic fusion (user-curated) — THESIS: 白皮书式的编号索引,悬停浮现
<div ref={pageRef} className="board-home min-h-screen bg-[#fafafa] text-[#1a1c1d]">
<style>{`.board-home ::selection { background: #eef0fa; color: #1a1c1d; }`}</style>
{/* ===== 平台顶栏(首页模式:品牌 + 五大板块导航 + 工具 + 用户菜单) ===== */}
<MaterialsHeader session={session} />
<main className="mx-auto max-w-[1280px] px-6">
{/* ===== 编辑页首:巨字 + 柔光 ===== */}
<section className="relative overflow-hidden pb-16 pt-20 md:pb-24 md:pt-28">
{/* 柔光渐变光晕:唯一的有色氛围(来自 Future Organic) */}
<div
data-orb
aria-hidden="true"
className="pointer-events-none absolute -top-10 right-[-60px] h-[340px] w-[340px] rounded-full opacity-90 md:h-[420px] md:w-[420px]"
style={{
background:
'radial-gradient(circle at 38% 40%, #eef0fa 0%, #dfe3f6 32%, rgba(224,228,248,0.55) 55%, rgba(250,250,250,0) 72%)',
}}
/>
<div className="relative">
{/* 巨型标题:入场对焦揭幕(GSAP blur+tracking),悬停字距呼吸(CSS) */}
<h1
data-hero-title
className="group cursor-default text-[clamp(60px,9vw,132px)] font-semibold leading-[1.04] tracking-[-0.03em] transition-[letter-spacing] duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:tracking-[0.035em]"
>
品牌中心
</h1>
<p data-hero className="mt-6 max-w-xl text-[15px] leading-relaxed text-[#6b7075] md:text-[16px]">
品牌的矿脉,从人到资产到经营,统一开采。
</p>
</div>
</section>
{/* ===== 编号索引(作品集列表语言;加载中骨架化防闪灰) ===== */}
{sessionLoading ? (
<SkeletonIndex />
) : (
<section aria-label="板块索引" className="border-t border-[#e7e8ea]">
{indexRows.map((row) =>
row.live ? (
<Link
key={row.no}
data-row
href={row.href ?? '#'}
className={`group relative grid grid-cols-[44px_1fr_auto] items-baseline gap-4 rounded-sm py-7 transition-colors duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:bg-white md:grid-cols-[64px_1fr_160px_120px_32px] md:gap-8 md:py-9 ${focusRing}`}
>
{/* 基线发丝线:入场画出,悬停加深 */}
<span
data-row-line
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-px origin-left bg-[#e7e8ea] transition-colors duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:bg-[#d9dcdf]"
/>
{/* 悬停定位线:从左划出 */}
<span
aria-hidden="true"
className="absolute inset-x-0 bottom-[-1px] h-[2px] origin-left scale-x-0 bg-[#1a1c1d] transition-transform duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-x-100"
/>
<span className="font-en text-[13px] tabular-nums text-[#86898e] transition-all duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-1 group-hover:text-[#5a66b8]">
{row.no}
</span>
<div className="min-w-0">
<h2 className="text-[24px] font-semibold tracking-tight transition-transform duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-1.5 md:text-[30px]">
{row.title}
</h2>
<p className="mt-1.5 text-[13px] leading-relaxed text-[#6b7075] transition-all duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-1 group-hover:text-[#4a4f55]">
{row.desc}
</p>
</div>
<span className={`font-en hidden text-[11px] uppercase tracking-[0.14em] text-[#86898e] transition-colors duration-[350ms] group-hover:text-[#6b7075] md:inline`}>
{row.en}
</span>
<span className="flex items-center gap-2">
<StatusDot tone={row.tone} />
<span className="text-[12px] text-[#6b7075]">{row.status}</span>
</span>
<span
aria-hidden="true"
className="hidden -translate-x-2 text-[18px] text-[#1a1c1d] opacity-0 transition-all duration-[350ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-0 group-hover:opacity-100 group-focus-visible:translate-x-0 group-focus-visible:opacity-100 md:inline"
>
→
</span>
</Link>
) : (
<div
key={row.no}
data-row
aria-disabled="true"
className="relative grid grid-cols-[44px_1fr_auto] items-baseline gap-4 py-7 md:grid-cols-[64px_1fr_160px_120px_32px] md:gap-8 md:py-9"
>
<span
data-row-line
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-px origin-left bg-[#e7e8ea]"
/>
<span className="font-en text-[13px] tabular-nums text-[#86898e]">{row.no}</span>
<div className="min-w-0">
<h2 className="text-[24px] font-medium tracking-tight text-[#8f9398] md:text-[30px]">
{row.title}
</h2>
<p className="mt-1.5 text-[13px] leading-relaxed text-[#86898e]">{row.desc}</p>
</div>
<span className="font-en hidden text-[11px] uppercase tracking-[0.14em] text-[#86898e] md:inline">
{row.en}
</span>
<span className="flex items-center gap-2">
<StatusDot tone={row.tone} />
<span className="text-[12px] text-[#86898e]">{row.status}</span>
</span>
<span aria-hidden="true" className="hidden text-[18px] text-transparent md:inline">
·
</span>
</div>
),
)}
</section>
)}
{/* ===== 两块已上线板块的入口白卡 ===== */}
{sessionLoading ? (
<div className="grid gap-6 py-16 md:grid-cols-2 md:gap-8 md:py-24">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="h-[260px] animate-pulse rounded-xl border border-[#e7e8ea] bg-white/60" />
))}
</div>
) : (
<section className="grid gap-6 py-16 md:grid-cols-2 md:gap-8 md:py-24">
<div data-card className="rounded-xl border border-[#e7e8ea] bg-white p-8 transition-all duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1.5 hover:border-[#c9ccd1] md:p-10">
<div className="flex items-center gap-2.5">
<StatusDot tone="live" />
<span className="text-[12px] text-[#6b7075]">运行中</span>
<span className={`font-en ml-auto text-[11px] uppercase tracking-[0.14em] ${labelCls}`}>01 / Assets</span>
</div>
<h3 className="mt-5 text-[22px] font-semibold tracking-tight">创意资产库</h3>
<p className="mt-3 text-[14px] leading-relaxed text-[#6b7075]">
素材浏览、上传、分享与下载——创意资产的全部日常都在这里。
</p>
<div className="mt-7">
<Link
href="/materials"
className={`inline-flex min-h-11 items-center rounded-lg bg-[#1a1c1d] px-6 text-[13px] font-medium text-white transition-all duration-200 hover:opacity-80 active:scale-[0.98] ${focusRing}`}
>
进入创意资产库
</Link>
</div>
</div>
<div data-card className="rounded-xl border border-[#e7e8ea] bg-white p-8 transition-all duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1.5 hover:border-[#c9ccd1] md:p-10">
<div className="flex items-center gap-2.5">
<StatusDot tone="live" />
<span className="text-[12px] text-[#6b7075]">已上线</span>
<span className={`font-en ml-auto text-[11px] uppercase tracking-[0.14em] ${labelCls}`}>02 / People</span>
</div>
<h3 className="mt-5 text-[22px] font-semibold tracking-tight">组织管理</h3>
<p className="mt-3 text-[14px] leading-relaxed text-[#6b7075]">
真实组织树、M/P 等级、汇报线与在职状态,数据源 HR 花名册。
</p>
<div className="mt-7 flex flex-wrap gap-3">
{isAdmin ? (
<Link
href="/organization"
className={`inline-flex min-h-11 items-center rounded-lg bg-[#1a1c1d] px-6 text-[13px] font-medium text-white transition-all duration-200 hover:opacity-80 active:scale-[0.98] ${focusRing}`}
>
进入人员架构
</Link>
) : (
<span className="inline-flex min-h-11 items-center rounded-lg border border-[#e7e8ea] px-6 text-[13px] text-[#6b7075]">
需管理员权限
</span>
)}
<Link
href="/performance-survey"
className={`inline-flex min-h-11 items-center rounded-lg border border-[#1a1c1d] bg-white px-6 text-[13px] font-medium transition-all duration-200 hover:bg-[#1a1c1d] hover:text-white active:scale-[0.98] ${focusRing}`}
>
绩效考核
</Link>
</div>
</div>
</section>
)}
{/* ===== 微型注脚 ===== */}
<footer data-foot className="flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-[#e7e8ea] py-10">
<span className="font-en text-[10px] uppercase tracking-[0.2em] text-[#86898e]">Ground</span>
<span className="text-[12px] text-[#6b7075]">
本看板由「由壹点3D素材库」改造而来,素材库现有功能不受影响。
</span>
<Link
href="/materials"
className={`rounded-sm text-[12px] font-medium text-[#5a66b8] underline-offset-4 hover:underline ${focusRing}`}
>
直达创意资产库 →
</Link>
</footer>
</main>
</div>
)
}