# Edu 微前端 UI 设计系统 > 适用于 Edu 项目 4 个微前端(teacher-portal / student-portal / parent-portal / admin-portal)的统一视觉与交互规范。 > > 本文档从原 CICD 项目(Next.js 16 + Tailwind v4 + shadcn/ui)的设计令牌体系演进而来,针对"当前界面设计不合理"的问题重新确立美学方向:**纸感编辑器 + 学术克制 + 单一视觉锚点**。 --- ## 目录 1. [设计哲学](#1-设计哲学) 2. [美学方向](#2-美学方向) 3. [设计令牌体系(微前端共享)](#3-设计令牌体系微前端共享) 4. [布局系统](#4-布局系统) 5. [组件规范](#5-组件规范) 6. [动效规范](#6-动效规范) 7. [微前端架构下的 UI 一致性](#7-微前端架构下的-ui-一致性) 8. [可访问性](#8-可访问性) 9. [反模式清单](#9-反模式清单) 10. [实施清单](#10-实施清单) --- ## 1. 设计哲学 ### 1.1 视觉论点(Visual Thesis) > **Edu 不是一个 SaaS Dashboard,而是一张可以编辑的学术纸。** 教育产品的核心是"阅读—书写—批注",而非"看图表—点按钮—收通知"。因此 Edu 的视觉论点由三句话定义: 1. **纸感编辑器**:界面是一张可被书写的纸,不是一组卡片堆砌的面板。 2. **学术克制**:参考学术书籍排版的克制感(章节标题、引文、注脚、分隔线),不滥用色彩与图形装饰。 3. **单一视觉锚点**:每屏只有一个焦点(一段文字、一个节点、一张表单),其余皆为衬托。 ### 1.2 核心原则 | 原则 | 含义 | 落地手段 | |------|------|----------| | 内容优先 | 内容是主角,UI 是配角 | 默认无卡片、无装饰阴影,用分隔线/留白/排版层级划分 | | 排版为王 | 用字体、字号、字重、行高建立层级,而非用色块和图标 | serif 主文 / sans UI / mono 数据三字体差异化 | | 克制装饰 | 删除一切不传递信息的视觉元素 | 单一强调色、最少图标、无渐变、无装饰阴影 | | 单一焦点 | 每屏只有一个视觉锚点 | 大留白、视觉层级递减、动效引导 | ### 1.3 反模式(禁止) - 通用 SaaS 卡片网格作为首屏 - 紫色渐变白底 - Inter 单字体全场使用 - 过度图标化(每个标题配图标、每行配 emoji) - 居中布局堆砌(hero 居中标题 + 居中副标 + 居中按钮) - 均匀圆角(所有元素 `rounded-2xl`) - 装饰性阴影(卡片下方 `shadow-2xl`) - 多个竞争强调色(同时出现蓝、紫、绿、橙) --- ## 2. 美学方向 ### 2.1 风格定位 **编辑式克制(Editorial Restraint)** 借鉴学术期刊与教辅书籍的排版美学:清晰的章节层级、慷慨的留白、serif 主文带来的"可被认真阅读"的质感。不追求"现代感"与"科技感",追求"可读、可写、可信赖"。 ### 2.2 色调 | 角色 | 颜色 | HSL | 用途 | |------|------|-----|------| | 纸面背景 | 纸感米白 | `hsl(40, 20%, 98%)` | 主背景,模拟纸张底色 | | 主文 | 深炭灰 | `hsl(0, 0%, 15%)` | 正文、标题,不用纯黑避免刺眼 | | 次文 | 暖灰 | `hsl(30, 5%, 45%)` | 元信息、辅助说明、注脚 | | 强调(唯一) | 学术深蓝 | `hsl(220, 60%, 35%)` | 链接、激活态、关键操作 | | 边框 | 细线灰 | `hsl(30, 10%, 90%)` | 分隔线、表格线、节点边框 | | 危险 | 砖红 | `hsl(0, 70%, 45%)` | 删除、错误,仅此一种红色 | | 成功 | 墨绿 | `hsl(142, 50%, 35%)` | 仅用于状态指示,不作装饰 | **铁律:全屏同时只能出现一个强调色(学术深蓝)。** 危险色与成功色仅用于状态语义,不参与视觉竞争。 ### 2.3 字体策略 三字体差异化是 Edu 区别于通用 SaaS 的核心手段。 | 字体 | 用途 | 加载方式 | |------|------|----------| | Fraunces(serif) | 主文、标题、引文、节点内容 | `next/font/google` + CSS 变量 `--font-serif` | | Inter(sans-serif) | UI 元素、按钮、表单、导航、元信息 | `next/font/google` + CSS 变量 `--font-sans` | | JetBrains Mono(mono) | 代码、数据、ID、时间戳、角色标签 | `next/font/google` + CSS 变量 `--font-mono` | **差异化规则:** - ✅ **做**:备课画布的主文用 Fraunces,工具条按钮用 Inter - ✅ **做**:成绩表格的数据用 JetBrains Mono 对齐数字 - ✅ **做**:标题用 Fraunces semibold,副标用 Inter regular - ❌ **不做**:全场 Inter 单字体(失去学术质感) - ❌ **不做**:标题用 Inter bold(SaaS 感) ### 2.4 排版阶梯 基于学术书籍排版(章节标题、段落、引文、注脚): ```css /* 标题层级 */ --font-size-9: 32px; /* 页面标题(H1)—— Fraunces semibold */ --font-size-8: 24px; /* 区块标题(H2)—— Fraunces semibold */ --font-size-7: 20px; /* 章节标题(H3)—— Fraunces medium */ --font-size-6: 18px; /* 小节标题(H4)—— Fraunces medium */ /* 正文层级 */ --font-size-5: 16px; /* 正文 —— Fraunces regular,行高 1.65 */ --font-size-4: 14px; /* UI 元素 —— Inter regular */ --font-size-3: 13.5px;/* 节点主文 —— Inter regular,纸感优化 */ --font-size-2: 13px; /* 元信息 —— Inter regular */ --font-size-1: 12px; /* 角色标签、注脚 —— Inter medium */ --font-size-0: 9px; /* 微标签 —— Mono semibold uppercase */ ``` **行高策略:** - 标题用 `--leading-tight`(1.2)保持紧凑 - 正文用 `--leading-relaxed`(1.65)保证可读 - UI 文字用 `--leading-normal`(1.5) ### 2.5 间距 **慷慨留白,呼吸感优先。** - 基准:8px(`--space-2`) - 段落间距:`--space-5`(20px) - 区块间距:`--space-8`(32px)~ `--space-12`(48px) - 页面边距:`--space-18`(72px)桌面端 - ❌ **不做**:信息密集堆砌,元素间距小于 8px --- ## 3. 设计令牌体系(微前端共享) ### 3.1 三层架构 沿用原 CICD 项目的双层令牌体系,新增"微前端共享包"作为第 0 层分发机制。 ```mermaid flowchart TB subgraph L0["Layer 0 · 共享分发"] PKG["@edu/shared-tokens
npm 包"] end subgraph L1["Layer 1 · Primitive(原始令牌)"] PRIM["primitive.css
色板/字号/间距/阴影/字体
业务代码不直接引用"] end subgraph L2["Layer 2 · Semantic(语义令牌)"] LIGHT["semantic-light.css
:root 明色"] DARK["semantic-dark.css
.dark 暗色"] NS["module namespace
--lp-* / --grade-* 等"] end subgraph L3["Layer 3 · Tailwind 暴露"] THEME["tailwind-theme.css
@theme inline
bg-* / text-* / font-*"] end subgraph APPS["微前端消费"] T["teacher-portal"] S["student-portal"] P["parent-portal"] A["admin-portal"] end PKG --> PRIM PRIM --> LIGHT PRIM --> DARK LIGHT --> NS DARK --> NS NS --> THEME THEME --> T THEME --> S THEME --> P THEME --> A ``` **分层规则:** | 层级 | 职责 | 谁能引用 | |------|------|----------| | Layer 0 | npm 包分发,导出 CSS 文件 + TS 常量 | 各微前端 `package.json` 依赖 | | Layer 1 | 原始色板/字号/间距/阴影 | 仅 Layer 2 引用,**业务代码禁止直接引用** | | Layer 2 | 语义令牌(明暗双份) | 业务代码唯一引用入口 | | Layer 3 | `@theme inline` 暴露为 Tailwind 类 | 业务代码通过 `bg-*` / `text-*` / `font-*` 使用 | ### 3.2 令牌文件结构(shared-tokens 包) ``` packages/shared-tokens/ ├─ package.json # @edu/shared-tokens ├─ src/ │ ├─ tokens/ │ │ ├─ primitive.css # Layer 1 原始令牌 │ │ ├─ semantic-light.css # Layer 2 明色语义 │ │ ├─ semantic-dark.css # Layer 2 暗色语义 │ │ ├─ module-lesson.css # 备课模块命名空间 --lp-* │ │ ├─ module-grade.css # 成绩模块命名空间 --grade-* │ │ ├─ tailwind-theme.css # Layer 3 Tailwind 暴露 │ │ └─ index.css # 汇总入口 │ ├─ constants.ts # TS 常量(间距/字号/z-index) │ ├─ types.ts # 令牌类型定义 │ └─ index.ts # 包入口 └─ README.md ``` ### 3.3 具体令牌定义 #### Layer 1: Primitive(原始令牌) ```css /* packages/shared-tokens/src/tokens/primitive.css */ :root { /* ============ 色板(学术克制) ============ */ /* 中性色板(基于 Stone 暖灰,纸感) */ --color-paper-50: 40 20% 99%; --color-paper-100: 40 20% 98%; /* 纸面背景 */ --color-paper-200: 40 15% 96%; --color-paper-300: 38 12% 92%; --color-paper-400: 35 10% 88%; --color-paper-500: 32 8% 80%; --color-paper-600: 30 6% 65%; --color-paper-700: 30 5% 45%; /* 次文 */ --color-paper-800: 28 4% 30%; --color-paper-900: 25 3% 15%; /* 主文(深炭灰) */ --color-paper-950: 20 2% 8%; /* 强调色(学术深蓝,唯一) */ --color-accent-300: 220 60% 55%; --color-accent-500: 220 60% 35%; /* 默认强调 */ --color-accent-700: 220 65% 25%; /* 语义状态色(仅状态语义,不参与装饰竞争) */ --color-danger-500: 0 70% 45%; /* 砖红 */ --color-success-500: 142 50% 35%;/* 墨绿 */ --color-warning-500: 38 80% 45%; /* 琥珀,仅用于警告 */ /* ============ 字号阶梯 ============ */ --font-size-0: 9px; /* 微标签 */ --font-size-1: 12px; /* 元信息/注脚 */ --font-size-2: 13px; /* 节点元信息 */ --font-size-3: 13.5px; /* 节点主文(纸感优化) */ --font-size-4: 14px; /* UI 元素/按钮 */ --font-size-5: 16px; /* 正文(Fraunces) */ --font-size-6: 18px; /* H4 */ --font-size-7: 20px; /* H3 */ --font-size-8: 24px; /* H2 */ --font-size-9: 32px; /* H1 */ /* ============ 间距阶梯(8px 基准) ============ */ --space-0: 0; --space-0_5: 0.125rem; /* 2px */ --space-1: 0.25rem; /* 4px */ --space-1_5: 0.375rem; /* 6px */ --space-2: 0.5rem; /* 8px —— 基准 */ --space-3: 0.75rem; /* 12px */ --space-4: 1rem; /* 16px */ --space-5: 1.25rem; /* 20px */ --space-6: 1.5rem; /* 24px */ --space-8: 2rem; /* 32px */ --space-10: 2.5rem; /* 40px */ --space-12: 3rem; /* 48px */ --space-16: 4rem; /* 64px */ --space-18: 4.5rem; /* 72px —— 桌面页边距 */ /* ============ 阴影阶梯(克制使用) ============ */ --shadow-1: 0 1px 2px rgba(15, 15, 15, 0.04); --shadow-2: 0 1px 3px rgba(15, 15, 15, 0.06), 0 1px 2px rgba(15, 15, 15, 0.04); --shadow-3: 0 4px 6px rgba(15, 15, 15, 0.05), 0 2px 4px rgba(15, 15, 15, 0.04); --shadow-4: 0 1px 2px rgba(15, 15, 15, 0.04), 0 8px 24px rgba(15, 15, 15, 0.04); --shadow-5: 0 1px 2px rgba(15, 15, 15, 0.06), 0 12px 36px rgba(15, 15, 15, 0.08); /* ============ 字体家族 ============ */ --font-family-sans: 'Inter', system-ui, sans-serif; --font-family-serif: 'Fraunces', Georgia, serif; --font-family-mono: 'JetBrains Mono', monospace; /* ============ 行高阶梯 ============ */ --leading-tight: 1.2; --leading-snug: 1.35; --leading-normal: 1.5; --leading-relaxed: 1.65; --leading-loose: 1.8; /* ============ 字重阶梯 ============ */ --weight-regular: 400; --weight-medium: 500; --weight-semibold: 600; --weight-bold: 700; /* ============ 动效 ============ */ --duration-fast: 150ms; --duration-normal: 200ms; --duration-slow: 300ms; --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* ============ z-index ============ */ --z-dropdown: 1000; --z-sticky: 1100; --z-modal: 1300; --z-popover: 1400; --z-toast: 1500; } ``` #### Layer 2: Semantic(明色) ```css /* packages/shared-tokens/src/tokens/semantic-light.css */ :root { /* ============ shadcn 标准令牌(纸感调校) ============ */ --background: 40 20% 98%; /* 纸面米白,非纯白 */ --foreground: 25 3% 15%; /* 深炭灰,非纯黑 */ --card: 40 20% 99%; /* 卡片略亮于背景(克制使用) */ --card-foreground: 25 3% 15%; --popover: 40 20% 99%; --popover-foreground: 25 3% 15%; --primary: 220 60% 35%; /* 学术深蓝,唯一强调 */ --primary-foreground: 40 20% 98%; --secondary: 40 15% 96%; --secondary-foreground: 25 3% 15%; --muted: 40 12% 94%; --muted-foreground: 30 5% 45%; /* 暖灰次文 */ --accent: 220 60% 35%; --accent-foreground: 40 20% 98%; --destructive: 0 70% 45%; /* 砖红 */ --destructive-foreground: 40 20% 98%; --border: 30 10% 90%; /* 细线灰 */ --input: 30 10% 90%; --ring: 220 60% 35%; --radius: 0.375rem; /* 6px,偏小圆角,纸感 */ /* ============ 扩展语义令牌 ============ */ --background-elevated: 40 20% 99%; /* 浮层略亮 */ --background-sunken: 40 15% 96%; /* 凹陷区略暗 */ --text-primary: 25 3% 15%; --text-secondary: 30 5% 45%; --text-tertiary: 30 5% 65%; --border-strong: 30 10% 75%; --border-subtle: 30 10% 95%; /* ============ 业务语义 ============ */ --diff-add: 142 50% 35%; --diff-add-bg: 142 50% 35% / 0.1; --diff-remove: 0 70% 45%; --diff-remove-bg: 0 70% 45% / 0.1; /* ============ 状态色(仅状态语义) ============ */ --status-success: 142 50% 35%; --status-warning: 38 80% 45%; --status-danger: 0 70% 45%; /* ============ sidebar ============ */ --sidebar-background: 40 15% 96%; --sidebar-foreground: 25 3% 25%; --sidebar-primary: 220 60% 35%; --sidebar-primary-foreground: 40 20% 98%; --sidebar-accent: 40 12% 92%; --sidebar-accent-foreground: 25 3% 15%; --sidebar-border: 30 10% 88%; --sidebar-ring: 220 60% 35%; } ``` #### Layer 2: Semantic(暗色) ```css /* packages/shared-tokens/src/tokens/semantic-dark.css */ .dark { --background: 220 13% 8%; /* 深炭蓝黑,非纯黑 */ --foreground: 40 15% 92%; --card: 220 13% 10%; --card-foreground: 40 15% 92%; --popover: 220 13% 10%; --popover-foreground: 40 15% 92%; --primary: 220 60% 60%; /* 暗色下强调色提亮 */ --primary-foreground: 220 13% 8%; --secondary: 220 10% 15%; --secondary-foreground: 40 15% 92%; --muted: 220 10% 15%; --muted-foreground: 40 8% 65%; --accent: 220 60% 60%; --accent-foreground: 220 13% 8%; --destructive: 0 70% 55%; --destructive-foreground: 40 15% 92%; --border: 220 10% 18%; --input: 220 10% 18%; --ring: 220 60% 60%; --radius: 0.375rem; --background-elevated: 220 13% 11%; --background-sunken: 220 13% 6%; --text-primary: 40 15% 92%; --text-secondary: 40 8% 65%; --text-tertiary: 40 8% 45%; --border-strong: 220 10% 35%; --border-subtle: 220 10% 14%; --diff-add: 142 50% 55%; --diff-add-bg: 142 50% 55% / 0.15; --diff-remove: 0 70% 60%; --diff-remove-bg: 0 70% 60% / 0.15; --status-success: 142 50% 55%; --status-warning: 38 80% 55%; --status-danger: 0 70% 55%; --sidebar-background: 220 13% 6%; --sidebar-foreground: 40 12% 85%; --sidebar-primary: 220 60% 60%; --sidebar-primary-foreground: 220 13% 8%; --sidebar-accent: 220 10% 15%; --sidebar-accent-foreground: 40 15% 92%; --sidebar-border: 220 10% 15%; --sidebar-ring: 220 60% 60%; } ``` #### Layer 3: Tailwind 暴露 ```css /* packages/shared-tokens/src/tokens/tailwind-theme.css */ @theme inline { /* shadcn 标准颜色 */ --color-background: hsl(var(--background)); --color-foreground: hsl(var(--foreground)); --color-card: hsl(var(--card)); --color-card-foreground: hsl(var(--card-foreground)); --color-popover: hsl(var(--popover)); --color-popover-foreground: hsl(var(--popover-foreground)); --color-primary: hsl(var(--primary)); --color-primary-foreground: hsl(var(--primary-foreground)); --color-secondary: hsl(var(--secondary)); --color-secondary-foreground: hsl(var(--secondary-foreground)); --color-muted: hsl(var(--muted)); --color-muted-foreground: hsl(var(--muted-foreground)); --color-accent: hsl(var(--accent)); --color-accent-foreground: hsl(var(--accent-foreground)); --color-destructive: hsl(var(--destructive)); --color-destructive-foreground: hsl(var(--destructive-foreground)); --color-border: hsl(var(--border)); --color-input: hsl(var(--input)); --color-ring: hsl(var(--ring)); /* 扩展语义 */ --color-background-elevated: hsl(var(--background-elevated)); --color-background-sunken: hsl(var(--background-sunken)); --color-text-primary: hsl(var(--text-primary)); --color-text-secondary: hsl(var(--text-secondary)); --color-text-tertiary: hsl(var(--text-tertiary)); --color-border-strong: hsl(var(--border-strong)); --color-border-subtle: hsl(var(--border-subtle)); /* 状态色 */ --color-status-success: hsl(var(--status-success)); --color-status-warning: hsl(var(--status-warning)); --color-status-danger: hsl(var(--status-danger)); /* diff */ --color-diff-add: hsl(var(--diff-add)); --color-diff-add-bg: hsl(var(--diff-add-bg)); --color-diff-remove: hsl(var(--diff-remove)); --color-diff-remove-bg: hsl(var(--diff-remove-bg)); /* 字体 */ --font-sans: var(--font-family-sans); --font-serif: var(--font-family-serif); --font-mono: var(--font-family-mono); /* 字号 */ --text-size-0: var(--font-size-0); --text-size-1: var(--font-size-1); --text-size-2: var(--font-size-2); --text-size-3: var(--font-size-3); --text-size-4: var(--font-size-4); --text-size-5: var(--font-size-5); --text-size-6: var(--font-size-6); --text-size-7: var(--font-size-7); --text-size-8: var(--font-size-8); --text-size-9: var(--font-size-9); /* 圆角 */ --radius-sm: calc(var(--radius) - 2px); --radius-md: var(--radius); --radius-lg: calc(var(--radius) + 2px); --radius-xl: calc(var(--radius) + 4px); --radius-full: 9999px; /* 阴影 */ --shadow-1: var(--shadow-1); --shadow-2: var(--shadow-2); --shadow-3: var(--shadow-3); --shadow-4: var(--shadow-4); --shadow-5: var(--shadow-5); /* 动效 */ --duration-fast: var(--duration-fast); --duration-normal: var(--duration-normal); --duration-slow: var(--duration-slow); /* z-index */ --z-dropdown: var(--z-dropdown); --z-sticky: var(--z-sticky); --z-modal: var(--z-modal); --z-popover: var(--z-popover); --z-toast: var(--z-toast); } ``` ### 3.4 TS 常量导出 ```typescript // packages/shared-tokens/src/constants.ts /** 间距常量(px,用于 canvas/SVG 等无法用 CSS 变量的场景) */ export const SPACE = { 0: 0, 0_5: 2, 1: 4, 1_5: 6, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 8: 32, 10: 40, 12: 48, 16: 64, 18: 72, } as const; /** 字号常量(px) */ export const FONT_SIZE = { 0: 9, 1: 12, 2: 13, 3: 13.5, 4: 14, 5: 16, 6: 18, 7: 20, 8: 24, 9: 32, } as const; /** z-index 常量 */ export const Z_INDEX = { dropdown: 1000, sticky: 1100, modal: 1300, popover: 1400, toast: 1500, } as const; ``` ### 3.5 强制规则 - ✅ **做**:业务代码使用 `bg-background` / `text-foreground` / `font-serif` 等 Tailwind 类 - ✅ **做**:需要动态计算时使用 `hsl(var(--primary))` - ❌ **不做**:TSX/TS 中出现 `#hex` 颜色字面量 - ❌ **不做**:出现 `'Inter'` / `'Fraunces'` / `'JetBrains Mono'` 字面量,统一用 `var(--font-family-*)` - ❌ **不做**:出现 `font-size: 14px`,统一用 `var(--font-size-4)` - ❌ **不做**:使用 `w-[137px]` 任意值,映射到 `--space-*` 或 Tailwind 默认阶梯 - ❌ **不做**:业务代码直接引用 Primitive 层(`--color-paper-*` / `--font-size-*`) **豁免场景**(需 `// eslint-disable-next-line no-restricted-syntax -- ` 注释): - PWA manifest - 邮件 HTML 内联样式 - 图表 SVG 固定画布尺寸 - loading.tsx 占位骨架 --- ## 4. 布局系统 ### 4.1 三栏布局(核心工作台) 用户明确偏好:**左侧结构树 + 中间纸面内容区 + 右侧详情编辑面板**。 ```mermaid flowchart LR subgraph LEFT["左侧 · 结构树"] L1["学科/章节/课时
树形导航"] L2["右键上下文菜单
新增/删除/AI 辅助"] end subgraph CENTER["中间 · 纸面内容区"] C1["单一视觉锚点
当前编辑的内容"] C2["Fraunces serif 主文
纸感米白底"] C3["最少干扰
无工具条悬浮"] end subgraph RIGHT["右侧 · 详情编辑面板"] R1["节点属性
表单/元数据"] R2["AI 辅助建议
侧边抽屉"] R3["关联资源
列表式"] end LEFT -->|选中节点| CENTER CENTER -->|编辑属性| RIGHT RIGHT -->|应用变更| CENTER ``` **三栏比例建议:** | 栏 | 比例 | 内容 | 字体 | |----|------|------|------| | 左侧结构树 | 20%(260px 固定) | 树形导航、章节列表 | Inter 14px | | 中间纸面 | 50%(自适应) | 主编辑区、阅读区 | Fraunces 16px | | 右侧详情 | 30%(380px 固定) | 属性面板、AI 辅助 | Inter 13px | **做与不做:** - ✅ **做**:左侧树用细线分隔,节点用文字而非图标 - ✅ **做**:中间纸面用 `--background` 纸感米白,主文用 Fraunces - ✅ **做**:右侧面板用 `--background-elevated` 略亮区分 - ✅ **做**:所有节点支持右键上下文菜单 - ❌ **不做**:三栏都用卡片包裹(破坏纸感) - ❌ **不做**:左侧树用图标代替文字(过度图标化) ### 4.2 全画布英雄区(Landing 页) Landing 页采用 frontend-skill 的全画布英雄区模式: ```mermaid flowchart TB subgraph HERO["全屏英雄区(100vh)"] H1["单一视觉锚点
一句标题 + 一段引文"] H2["错峰揭示动画
staggered reveals"] end subgraph BODY["正文区"] B1["分栏叙事
非卡片网格"] B2["滚动联动
scroll-linked 透明度/位移"] end subgraph CTA["收尾区"] C1["单一行动召唤
不堆砌多个按钮"] end ``` **做与不做:** - ✅ **做**:英雄区全屏占满,单一焦点(一句标题) - ✅ **做**:标题用 Fraunces,副标用 Inter - ✅ **做**:英雄区文字左对齐或编辑式排版,不居中堆砌 - ❌ **不做**:居中标题 + 居中副标 + 居中按钮的 SaaS 模板 - ❌ **不做**:英雄区放渐变背景或装饰图形 ### 4.3 卡片克制 **默认无卡片,用以下手段替代:** | 替代手段 | 适用场景 | |----------|----------| | 分栏(columns) | 并列内容 | | 分隔线(border-b) | 列表项 | | 留白(padding/margin) | 区块划分 | | 缩进 + 左竖线 | 引文、注脚 | | 表格 | 结构化数据 | **何时可用卡片:** - 仅当内容需要"被拎出来强调"时(如成绩报告卡打印) - 卡片用 `--card` 略亮背景 + 1px `--border` 边框,**不加阴影** **做与不做:** - ✅ **做**:列表用 `divide-y divide-border` 分隔线 - ✅ **做**:引文用 `border-l-2 border-border pl-4` 左竖线 - ❌ **不做**:每个内容块都包 `rounded-2xl shadow-lg border` - ❌ **不做**:卡片用 `shadow-2xl` 装饰性阴影 ### 4.4 网格与间距 - 基准:8px(`--space-2`) - 所有间距必须是 4px 的整数倍 - 桌面页边距:72px(`--space-18`) - 移动页边距:16px(`--space-4`) - 段落间距:20px(`--space-5`) - 区块间距:32px~48px(`--space-8` ~ `--space-12`) --- ## 5. 组件规范 ### 5.1 基础组件库 基于 shadcn/ui,但做纸感调校: | 组件 | 来源 | 纸感调校 | |------|------|----------| | Button | shadcn | 默认 `variant=ghost`,强调用 `variant=default`(学术深蓝) | | Input | shadcn | 边框 `--border`,聚焦 `--ring` 学术深蓝 | | Dialog | shadcn | 圆角 `--radius`,无阴影或仅 `--shadow-2` | | Sheet | shadcn | 右侧抽屉用于详情面板 | | DropdownMenu | shadcn | 右键上下文菜单基础 | | Tooltip | shadcn | 仅用于图标按钮,文字按钮不加 | | Tabs | shadcn | 下划线式,非胶囊式 | | Table | shadcn | 细线分隔,数据用 JetBrains Mono | | Toast | shadcn | 极简,无图标 | **做与不做:** - ✅ **做**:Tabs 用 `underline` 模式 - ✅ **做**:数据表格用 `font-mono` 对齐数字 - ❌ **不做**:所有按钮都加图标 - ❌ **不做**:Dialog 用大圆角 + 大阴影 ### 5.2 组件组合模式 结合 composition-patterns 与 shadcn,使用以下模式: #### 5.2.1 Compound Components(复合组件) 用于三栏布局等"一组协同组件": ```tsx // 示例:三栏工作台复合组件 import { createContext, useContext } from "react"; type WorkbenchContextValue = { selectedNodeId: string | null; onSelectNode: (id: string) => void; }; const WorkbenchContext = createContext(null); function useWorkbench(): WorkbenchContextValue { const ctx = useContext(WorkbenchContext); if (!ctx) { throw new Error("Workbench 子组件必须用在 内"); } return ctx; } // 根组件:提供 Context,不渲染具体布局 export function Workbench({ children, selectedNodeId, onSelectNode, }: { children: React.ReactNode; selectedNodeId: string | null; onSelectNode: (id: string) => void; }): React.ReactElement { return (
{children}
); } // 子组件:各自渲染自己的栏,通过 Context 协同 export function WorkbenchTree({ children }: { children: React.ReactNode }): React.ReactElement { return ( ); } export function WorkbenchPaper({ children }: { children: React.ReactNode }): React.ReactElement { return (
{children}
); } export function WorkbenchDetail({ children }: { children: React.ReactNode }): React.ReactElement { return ( ); } // 使用方式:组合而非配置 ``` #### 5.2.2 Render Props(渲染函数) 用于列表项的灵活定制: ```tsx // 示例:节点列表,渲染逻辑由调用方决定 type TreeNode = { id: string; title: string; type: "chapter" | "lesson" | "activity"; }; export function NodeList({ nodes, renderItem, }: { nodes: TreeNode[]; renderItem: (node: TreeNode, isSelected: boolean) => React.ReactNode; }): React.ReactElement { const { selectedNodeId, onSelectNode } = useWorkbench(); return (
    {nodes.map((node) => (
  • onSelectNode(node.id)} className="cursor-pointer px-4 py-2 hover:bg-muted" > {renderItem(node, node.id === selectedNodeId)}
  • ))}
); } // 调用方决定渲染 ( {node.title} )} /> ``` #### 5.2.3 Context Providers(状态共享) 用于右键菜单的统一管理: ```tsx // 右键菜单 Context type ContextMenuState = { open: (event: React.MouseEvent, nodeId: string) => void; }; const ContextMenuContext = createContext(null); export function ContextMenuProvider({ children, }: { children: React.ReactNode; }): React.ReactElement { const [state, setState] = useState<{ x: number; y: number; nodeId: string | null; } | null>(null); const open = useCallback((event: React.MouseEvent, nodeId: string) => { event.preventDefault(); setState({ x: event.clientX, y: event.clientY, nodeId }); }, []); return ( {children} {state && ( setState(null)}> {/* 菜单项由业务通过插槽注入 */} )} ); } ``` ### 5.3 业务组件清单 | 组件 | 微前端 | 说明 | |------|--------|------| | StructureTree | teacher / admin | 左侧结构树,支持右键菜单 | | LessonEditor | teacher | 中间纸面编辑器,Fraunces 主文 | | NodeProperties | teacher | 右侧节点属性面板 | | ClassCard | teacher / admin | 班级卡片,**克制使用**(默认列表式) | | AssignmentCard | teacher / student | 作业卡片,**克制使用** | | GradeTable | teacher / admin / parent | 成绩表,mono 数字对齐 | | StudentRoster | teacher / admin | 学生名册,列表式 | | ScheduleGrid | all | 课表网格 | | ReportCard | teacher / parent | 成绩报告卡,**唯一允许卡片 + 打印样式** | ### 5.4 右键上下文菜单(强制) **所有节点支持右键菜单**,菜单项包括: - 新增子节点 - 编辑节点 - 删除节点 - 复制/移动 - AI 辅助(生成内容/优化措辞/关联推荐) ```tsx // 节点右键菜单统一封装 export function NodeWithContextMenu({ node, children, }: { node: TreeNode; children: React.ReactNode; }): React.ReactElement { const { open } = useContextMenu(); return (
open(e, node.id)}> {children}
); } ``` ### 5.5 状态管理 - **React 19 `useOptimistic`**:用于乐观更新(节点编辑、批注提交) - **zustand 细粒度选择器**:用于跨组件状态共享(选中节点、编辑模式) - ❌ **不做**:用 Context 共享高频更新状态(导致重渲染) - ❌ **不做**:用 useState 管理跨组件状态 ```tsx // zustand 细粒度选择器示例 import { create } from "zustand"; import { shallow } from "zustand/shallow"; type EditorStore = { selectedNodeId: string | null; editingMode: "view" | "edit" | "ai-assist"; setSelectedNode: (id: string | null) => void; setEditingMode: (mode: EditorStore["editingMode"]) => void; }; export const useEditorStore = create((set) => ({ selectedNodeId: null, editingMode: "view", setSelectedNode: (id) => set({ selectedNodeId: id }), setEditingMode: (mode) => set({ editingMode: mode }), })); // 组件中:只订阅需要的字段 function TreeView() { const selectedNodeId = useEditorStore((s) => s.selectedNodeId); // ... } ``` --- ## 6. 动效规范 ### 6.1 动效原则 > **动效用于建立层级和氛围,不是噪音。** | 原则 | 含义 | |------|------| | 有目的 | 每个动效必须传递信息或引导注意力 | | 克制 | 同屏动效不超过 1 个主要 + 2 个次要 | | 快速 | 交互反馈 ≤ 200ms,页面过渡 ≤ 300ms | | 可关闭 | 遵守 `prefers-reduced-motion` | ### 6.2 入场序列(Staggered Reveals) 英雄区元素错峰揭示,建立阅读顺序: ```tsx // 错峰揭示:标题 → 副标 → 正文 → CTA function HeroSection() { return (

Edu

一张可以编辑的学术纸

面向 K12 教学的微前端工作台

); } ``` ```css /* 全局关键帧 */ @keyframes reveal-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } ``` ### 6.3 滚动联动(Scroll-Linked) ```tsx // 使用 useScroll + useTransform import { useScroll, useTransform } from "framer-motion"; function ScrollContent() { const { scrollYProgress } = useScroll(); const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0]); const y = useTransform(scrollYProgress, [0, 0.3], [0, -20]); return ( {/* 滚动时淡出上移 */} ); } ``` ### 6.4 交互反馈 | 交互 | 动效 | 时长 | |------|------|------| | hover 按钮 | `bg-muted` 渐显 | 150ms | | 选中节点 | 左侧 2px 强调色竖线滑入 | 200ms | | 打开 Dialog | 淡入 + 轻微下移 | 200ms | | 切换 Tab | 下划线滑动 | 200ms | | 提交表单 | 按钮 loading spinner | - | | 乐观更新 | 内容淡入替换 | 150ms | ### 6.5 微前端场景 | 场景 | 动效 | |------|------| | 微前端页面切换 | 顶部 2px 进度条 + 内容淡入 | | 模块加载占位 | 骨架屏(skeleton),不用 spinner | | Module Federation 加载失败 | 错误边界 + 重试按钮 | **做与不做:** - ✅ **做**:Landing 页错峰揭示建立阅读顺序 - ✅ **做**:选中节点用竖线滑入暗示焦点 - ✅ **做**:加载用骨架屏,不用居中 spinner - ❌ **不做**:每个元素都加入场动画 - ❌ **不做**:卡片 hover 时 `scale-105` 放大 - ❌ **不做**:装饰性的持续动画(脉冲、闪烁) --- ## 7. 微前端架构下的 UI 一致性 ### 7.1 架构总览 ```mermaid flowchart TB subgraph SHELL["Shell 宿主"] ROUTER["统一路由
+ 主题切换"] FONT["字体加载
next/font"] end subgraph SHARED["共享层"] TOKENS["@edu/shared-tokens
令牌包"] UI["@edu/shared-ui
基础组件"] UTILS["@edu/shared-utils
cn/权限/类型"] end subgraph MF["4 个微前端(Module Federation)"] T["teacher-portal
教师工作台"] S["student-portal
学生学习台"] P["parent-portal
家长查看台"] A["admin-portal
管理后台"] end SHELL -->|暴露| T SHELL -->|暴露| S SHELL -->|暴露| P SHELL -->|暴露| A TOKENS --> T TOKENS --> S TOKENS --> P TOKENS --> A UI --> T UI --> S UI --> P UI --> A ``` ### 7.2 shared-tokens 包集成 各微前端 `package.json` 依赖: ```json { "dependencies": { "@edu/shared-tokens": "workspace:*" } } ``` 各微前端 `globals.css`: ```css @import "tailwindcss"; @import "@edu/shared-tokens/tokens"; /* 汇总入口 */ @plugin "tailwindcss-animate"; @plugin "@tailwindcss/typography"; @custom-variant dark (&:where(.dark, .dark *)); @layer base { body { @apply bg-background text-foreground; font-feature-settings: "rlig" 1, "calt" 1; } } ``` ### 7.3 共享字体加载策略 **问题**:Module Federation 下,各微前端独立加载字体会导致重复请求与 FOUT。 **方案**:Shell 宿主统一加载字体,通过 CSS 变量共享;微前端只引用变量。 ```tsx // apps/shell/app/layout.tsx import { Inter, Fraunces, JetBrains_Mono } from "next/font/google"; const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter", }); const fraunces = Fraunces({ subsets: ["latin"], display: "swap", variable: "--font-fraunces", }); const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], display: "swap", variable: "--font-jetbrains-mono", }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` **shared-tokens 中映射:** ```css /* packages/shared-tokens/src/tokens/primitive.css */ :root { --font-family-sans: var(--font-inter), system-ui, sans-serif; --font-family-serif: var(--font-fraunces), Georgia, serif; --font-family-mono: var(--font-jetbrains-mono), monospace; } ``` **做与不做:** - ✅ **做**:Shell 加载字体,微前端引用变量 - ✅ **做**:`display: swap` 避免 FOIT - ❌ **不做**:每个微前端各自 `next/font/google` 加载 ### 7.4 Module Federation UI 协调 | 协调点 | 策略 | |--------|------| | 设计语言 | 统一 shared-tokens,禁止微前端自定义令牌 | | 组件库 | shared-ui 提供基础组件,微前端可扩展业务组件 | | 主题切换 | Shell 持有 `class="dark"` 状态,所有微前端响应 | | 路由 | Shell 统一路由表,微前端注册子路由 | | 加载态 | 统一骨架屏样式(来自 shared-ui) | ### 7.5 主题切换 ```tsx // apps/shell/components/ThemeProvider.tsx "use client"; import { useEffect, useState } from "react"; type Theme = "light" | "dark"; export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState("light"); useEffect(() => { const saved = localStorage.getItem("edu-theme") as Theme | null; const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; const initial = saved ?? (prefersDark ? "dark" : "light"); setTheme(initial); document.documentElement.classList.toggle("dark", initial === "dark"); }, []); const toggle = () => { const next: Theme = theme === "light" ? "dark" : "light"; setTheme(next); localStorage.setItem("edu-theme", next); document.documentElement.classList.toggle("dark", next === "dark"); }; return ( {children} ); } ``` **做与不做:** - ✅ **做**:主题状态由 Shell 持有,`class="dark"` 挂在 `` - ✅ **做**:所有微前端通过 `@edu/shared-tokens` 响应主题 - ❌ **不做**:微前端各自实现主题切换 --- ## 8. 可访问性 ### 8.1 对比度(WCAG AA) | 文本类型 | 对比度要求 | 验证 | |----------|-----------|------| | 正文(< 18px) | ≥ 4.5:1 | `--foreground` on `--background` | | 大文本(≥ 18px) | ≥ 3:1 | `--text-secondary` on `--background` | | 交互元素边框 | ≥ 3:1 | `--border-strong` | | 焦点指示器 | ≥ 3:1 | `--ring`(学术深蓝) | **当前令牌对比度验证:** - 主文 `hsl(25, 3%, 15%)` on 纸面 `hsl(40, 20%, 98%)`:约 14:1 ✅ - 次文 `hsl(30, 5%, 45%)` on 纸面:约 4.6:1 ✅ - 强调 `hsl(220, 60%, 35%)` on 纸面:约 7:1 ✅ ### 8.2 键盘导航 - 所有交互元素必须可 `Tab` 聚焦 - 焦点可见:`focus-visible:ring-2 focus-visible:ring-ring` - 右键菜单支持键盘打开:`Shift + F10` 或 `Menu` 键 - 三栏布局支持 `Tab` 在栏间流转 - 树形结构支持方向键导航(↑↓←→) ```tsx // 焦点可见样式 ``` ### 8.3 屏幕阅读器 - 语义化 HTML:`