- monorepo: pnpm workspace + go.work + pyproject.toml + commitlint/husky - infra: docker-compose (minimal + full profiles) + init-sql + prometheus - arch-scan: multi-language scanner skeleton (TS/Go/Python/Proto) - shared-proto: buf v2 + classes.proto (ClassService CRUD contract) - api-gateway: Go/Gin + JWT HS256 auth + reverse proxy + request ID - classes: NestJS golden template (error system + observability + middleware + CRUD + tests) - teacher-portal: Next.js + paper-feel UI design system - CI/CD: 4 workflows (go/ts/py/proto) - docs: migration guide + project_rules + coding-standards + git-workflow + ui-design-system + 004 + 9 module READMEs + known-issues + spec/plan migration + roadmap
1521 lines
47 KiB
Markdown
1521 lines
47 KiB
Markdown
# 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<br/>npm 包"]
|
||
end
|
||
|
||
subgraph L1["Layer 1 · Primitive(原始令牌)"]
|
||
PRIM["primitive.css<br/>色板/字号/间距/阴影/字体<br/>业务代码不直接引用"]
|
||
end
|
||
|
||
subgraph L2["Layer 2 · Semantic(语义令牌)"]
|
||
LIGHT["semantic-light.css<br/>:root 明色"]
|
||
DARK["semantic-dark.css<br/>.dark 暗色"]
|
||
NS["module namespace<br/>--lp-* / --grade-* 等"]
|
||
end
|
||
|
||
subgraph L3["Layer 3 · Tailwind 暴露"]
|
||
THEME["tailwind-theme.css<br/>@theme inline<br/>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 -- <reason>` 注释):
|
||
- PWA manifest
|
||
- 邮件 HTML 内联样式
|
||
- 图表 SVG 固定画布尺寸
|
||
- loading.tsx 占位骨架
|
||
|
||
---
|
||
|
||
## 4. 布局系统
|
||
|
||
### 4.1 三栏布局(核心工作台)
|
||
|
||
用户明确偏好:**左侧结构树 + 中间纸面内容区 + 右侧详情编辑面板**。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph LEFT["左侧 · 结构树"]
|
||
L1["学科/章节/课时<br/>树形导航"]
|
||
L2["右键上下文菜单<br/>新增/删除/AI 辅助"]
|
||
end
|
||
|
||
subgraph CENTER["中间 · 纸面内容区"]
|
||
C1["单一视觉锚点<br/>当前编辑的内容"]
|
||
C2["Fraunces serif 主文<br/>纸感米白底"]
|
||
C3["最少干扰<br/>无工具条悬浮"]
|
||
end
|
||
|
||
subgraph RIGHT["右侧 · 详情编辑面板"]
|
||
R1["节点属性<br/>表单/元数据"]
|
||
R2["AI 辅助建议<br/>侧边抽屉"]
|
||
R3["关联资源<br/>列表式"]
|
||
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["单一视觉锚点<br/>一句标题 + 一段引文"]
|
||
H2["错峰揭示动画<br/>staggered reveals"]
|
||
end
|
||
|
||
subgraph BODY["正文区"]
|
||
B1["分栏叙事<br/>非卡片网格"]
|
||
B2["滚动联动<br/>scroll-linked 透明度/位移"]
|
||
end
|
||
|
||
subgraph CTA["收尾区"]
|
||
C1["单一行动召唤<br/>不堆砌多个按钮"]
|
||
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<WorkbenchContextValue | null>(null);
|
||
|
||
function useWorkbench(): WorkbenchContextValue {
|
||
const ctx = useContext(WorkbenchContext);
|
||
if (!ctx) {
|
||
throw new Error("Workbench 子组件必须用在 <Workbench> 内");
|
||
}
|
||
return ctx;
|
||
}
|
||
|
||
// 根组件:提供 Context,不渲染具体布局
|
||
export function Workbench({
|
||
children,
|
||
selectedNodeId,
|
||
onSelectNode,
|
||
}: {
|
||
children: React.ReactNode;
|
||
selectedNodeId: string | null;
|
||
onSelectNode: (id: string) => void;
|
||
}): React.ReactElement {
|
||
return (
|
||
<WorkbenchContext.Provider value={{ selectedNodeId, onSelectNode }}>
|
||
<div className="grid h-screen grid-cols-[260px_1fr_380px]">
|
||
{children}
|
||
</div>
|
||
</WorkbenchContext.Provider>
|
||
);
|
||
}
|
||
|
||
// 子组件:各自渲染自己的栏,通过 Context 协同
|
||
export function WorkbenchTree({ children }: { children: React.ReactNode }): React.ReactElement {
|
||
return (
|
||
<aside className="border-r border-border bg-sidebar overflow-y-auto">
|
||
{children}
|
||
</aside>
|
||
);
|
||
}
|
||
|
||
export function WorkbenchPaper({ children }: { children: React.ReactNode }): React.ReactElement {
|
||
return (
|
||
<main className="bg-background overflow-y-auto px-18 py-12">
|
||
{children}
|
||
</main>
|
||
);
|
||
}
|
||
|
||
export function WorkbenchDetail({ children }: { children: React.ReactNode }): React.ReactElement {
|
||
return (
|
||
<aside className="border-l border-border bg-background-elevated overflow-y-auto">
|
||
{children}
|
||
</aside>
|
||
);
|
||
}
|
||
|
||
// 使用方式:组合而非配置
|
||
<Workbench selectedNodeId={id} onSelectNode={setId}>
|
||
<WorkbenchTree>
|
||
<StructureTree />
|
||
</WorkbenchTree>
|
||
<WorkbenchPaper>
|
||
<LessonEditor />
|
||
</WorkbenchPaper>
|
||
<WorkbenchDetail>
|
||
<NodeProperties />
|
||
</WorkbenchDetail>
|
||
</Workbench>
|
||
```
|
||
|
||
#### 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 (
|
||
<ul className="divide-y divide-border-subtle">
|
||
{nodes.map((node) => (
|
||
<li
|
||
key={node.id}
|
||
onClick={() => onSelectNode(node.id)}
|
||
className="cursor-pointer px-4 py-2 hover:bg-muted"
|
||
>
|
||
{renderItem(node, node.id === selectedNodeId)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
);
|
||
}
|
||
|
||
// 调用方决定渲染
|
||
<NodeList
|
||
nodes={lessons}
|
||
renderItem={(node, isSelected) => (
|
||
<span className={isSelected ? "font-serif text-primary" : "font-serif"}>
|
||
{node.title}
|
||
</span>
|
||
)}
|
||
/>
|
||
```
|
||
|
||
#### 5.2.3 Context Providers(状态共享)
|
||
|
||
用于右键菜单的统一管理:
|
||
|
||
```tsx
|
||
// 右键菜单 Context
|
||
type ContextMenuState = {
|
||
open: (event: React.MouseEvent, nodeId: string) => void;
|
||
};
|
||
|
||
const ContextMenuContext = createContext<ContextMenuState | null>(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 (
|
||
<ContextMenuContext.Provider value={{ open }}>
|
||
{children}
|
||
{state && (
|
||
<DropdownMenu open onOpenChange={() => setState(null)}>
|
||
<DropdownMenuContent style={{ left: state.x, top: state.y }}>
|
||
{/* 菜单项由业务通过插槽注入 */}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
</ContextMenuContext.Provider>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 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 (
|
||
<div onContextMenu={(e) => open(e, node.id)}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 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<EditorStore>((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 (
|
||
<section className="h-screen flex flex-col justify-center px-18">
|
||
<h1
|
||
className="font-serif text-size-9 text-foreground"
|
||
style={{ animation: "reveal-up 0.6s var(--ease-out) 0ms both" }}
|
||
>
|
||
Edu
|
||
</h1>
|
||
<p
|
||
className="font-sans text-size-5 text-text-secondary mt-4"
|
||
style={{ animation: "reveal-up 0.6s var(--ease-out) 120ms both" }}
|
||
>
|
||
一张可以编辑的学术纸
|
||
</p>
|
||
<p
|
||
className="font-sans text-size-4 text-text-tertiary mt-2 max-w-prose"
|
||
style={{ animation: "reveal-up 0.6s var(--ease-out) 240ms both" }}
|
||
>
|
||
面向 K12 教学的微前端工作台
|
||
</p>
|
||
</section>
|
||
);
|
||
}
|
||
```
|
||
|
||
```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 (
|
||
<motion.div style={{ opacity, y }}>
|
||
{/* 滚动时淡出上移 */}
|
||
</motion.div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 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["统一路由<br/>+ 主题切换"]
|
||
FONT["字体加载<br/>next/font"]
|
||
end
|
||
|
||
subgraph SHARED["共享层"]
|
||
TOKENS["@edu/shared-tokens<br/>令牌包"]
|
||
UI["@edu/shared-ui<br/>基础组件"]
|
||
UTILS["@edu/shared-utils<br/>cn/权限/类型"]
|
||
end
|
||
|
||
subgraph MF["4 个微前端(Module Federation)"]
|
||
T["teacher-portal<br/>教师工作台"]
|
||
S["student-portal<br/>学生学习台"]
|
||
P["parent-portal<br/>家长查看台"]
|
||
A["admin-portal<br/>管理后台"]
|
||
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 (
|
||
<html lang="zh-CN" suppressHydrationWarning>
|
||
<body className={`${inter.variable} ${fraunces.variable} ${jetbrainsMono.variable}`}>
|
||
{children}
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|
||
```
|
||
|
||
**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<Theme>("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 (
|
||
<ThemeContext.Provider value={{ theme, toggle }}>
|
||
{children}
|
||
</ThemeContext.Provider>
|
||
);
|
||
}
|
||
```
|
||
|
||
**做与不做:**
|
||
|
||
- ✅ **做**:主题状态由 Shell 持有,`class="dark"` 挂在 `<html>`
|
||
- ✅ **做**:所有微前端通过 `@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
|
||
// 焦点可见样式
|
||
<button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
|
||
按钮
|
||
</button>
|
||
```
|
||
|
||
### 8.3 屏幕阅读器
|
||
|
||
- 语义化 HTML:`<nav>` `<main>` `<aside>` `<article>`
|
||
- 图标按钮必须有 `aria-label`
|
||
- 动态内容用 `aria-live="polite"` 通告
|
||
- 树形结构用 `role="tree"` + `role="treeitem"` + `aria-expanded`
|
||
- 表单字段必须有 `<label>` 或 `aria-label`
|
||
|
||
```tsx
|
||
// 树形节点无障碍示例
|
||
<li
|
||
role="treeitem"
|
||
aria-expanded={hasChildren}
|
||
aria-selected={isSelected}
|
||
className="focus-visible:ring-2 focus-visible:ring-ring"
|
||
>
|
||
{node.title}
|
||
</li>
|
||
```
|
||
|
||
### 8.4 焦点可见性
|
||
|
||
- ❌ **不做**:`outline: none` 后不提供替代焦点样式
|
||
- ✅ **做**:用 `focus-visible:ring-2` 替代 `outline`
|
||
- ✅ **做**:焦点指示器对比度 ≥ 3:1
|
||
|
||
---
|
||
|
||
## 9. 反模式清单
|
||
|
||
### 9.1 禁止清单(强制)
|
||
|
||
| # | 反模式 | 原因 | 正确做法 |
|
||
|---|--------|------|----------|
|
||
| 1 | 通用 SaaS 卡片网格作为首屏 | 缺乏视觉锚点,AI slop | 单一视觉锚点 + 分栏叙事 |
|
||
| 2 | 紫色渐变白底 | 通用 SaaS 感 | 纸感米白纯色底 |
|
||
| 3 | Inter 单字体全场 | 失去学术质感 | Fraunces 主文 + Inter UI + Mono 数据 |
|
||
| 4 | 过度图标化 | 视觉干扰,违背克制 | 文字优先,图标仅用于无文字场景 |
|
||
| 5 | 居中布局堆砌 | SaaS 模板感 | 编辑式左对齐排版 |
|
||
| 6 | 均匀圆角 `rounded-2xl` | 单调,无层级 | 按层级分级圆角,纸感用小圆角 |
|
||
| 7 | 装饰性阴影 `shadow-2xl` | 视觉噪音 | 默认无阴影,必要时用 `--shadow-1` |
|
||
| 8 | 多个竞争强调色 | 视觉焦点散乱 | 单一学术深蓝强调 |
|
||
| 9 | `#hex` 颜色字面量 | 绕过令牌系统 | `hsl(var(--*))` 或 Tailwind 类 |
|
||
| 10 | `font-size: 14px` 硬编码 | 绕过令牌系统 | `var(--font-size-4)` |
|
||
| 11 | `w-[137px]` 任意值 | 绕过间距系统 | `--space-*` 或 Tailwind 默认阶梯 |
|
||
| 12 | 每个元素加入场动画 | 动效噪音 | 仅英雄区错峰揭示 |
|
||
| 13 | 卡片 hover `scale-105` | 装饰性动效 | 用 `bg-muted` 反馈 |
|
||
| 14 | 持续脉冲/闪烁动画 | 注意力干扰 | 仅用于真正需要警告的场景 |
|
||
| 15 | `dangerouslySetInnerHTML` 未清洗 | XSS 风险 | 先 DOMPurify 清洗 |
|
||
| 16 | `role === "xxx"` 硬编码 | 权限散落 | `usePermission().hasPermission()` |
|
||
| 17 | 业务代码引用 Primitive 层 | 破坏分层 | 仅引用 Semantic 层 |
|
||
| 18 | 微前端自定义令牌 | 破坏一致性 | 统一 `@edu/shared-tokens` |
|
||
|
||
### 9.2 推荐清单
|
||
|
||
| # | 推荐模式 | 适用场景 |
|
||
|---|----------|----------|
|
||
| 1 | 三栏工作台 | 编辑类页面(备课、批注、详情) |
|
||
| 2 | 全画布英雄区 | Landing 页 |
|
||
| 3 | 列表 + 分隔线 | 数据列表(成绩、名册、通知) |
|
||
| 4 | 表格 + mono 数字 | 结构化数据 |
|
||
| 5 | 左竖线引文 | 引用、批注、AI 建议 |
|
||
| 6 | Tabs 下划线式 | 同级内容切换 |
|
||
| 7 | Sheet 右侧抽屉 | 详情面板、AI 辅助 |
|
||
| 8 | Compound Components | 一组协同组件 |
|
||
| 9 | Render Props | 列表项灵活定制 |
|
||
| 10 | zustand 细粒度选择器 | 跨组件状态共享 |
|
||
| 11 | `useOptimistic` | 乐观更新 |
|
||
| 12 | 骨架屏 | 加载占位 |
|
||
|
||
---
|
||
|
||
## 10. 实施清单
|
||
|
||
### 10.1 shared-tokens 包结构
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph PKG["@edu/shared-tokens"]
|
||
P1["src/tokens/primitive.css"]
|
||
P2["src/tokens/semantic-light.css"]
|
||
P3["src/tokens/semantic-dark.css"]
|
||
P4["src/tokens/module-lesson.css"]
|
||
P5["src/tokens/module-grade.css"]
|
||
P6["src/tokens/tailwind-theme.css"]
|
||
P7["src/tokens/index.css"]
|
||
C1["src/constants.ts<br/>SPACE/FONT_SIZE/Z_INDEX"]
|
||
T1["src/types.ts"]
|
||
E1["src/index.ts"]
|
||
end
|
||
|
||
subgraph EXPORTS["包导出"]
|
||
E_CSS["./tokens → index.css"]
|
||
E_TS["". → index.ts"]
|
||
E_PKG["package.json exports 字段"]
|
||
end
|
||
|
||
P1 --> P7
|
||
P2 --> P7
|
||
P3 --> P7
|
||
P4 --> P7
|
||
P5 --> P7
|
||
P6 --> P7
|
||
P7 --> E_CSS
|
||
C1 --> E1
|
||
T1 --> E1
|
||
E1 --> E_TS
|
||
```
|
||
|
||
**package.json exports 配置:**
|
||
|
||
```json
|
||
{
|
||
"name": "@edu/shared-tokens",
|
||
"exports": {
|
||
".": "./src/index.ts",
|
||
"./tokens": "./src/tokens/index.css",
|
||
"./constants": "./src/constants.ts"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.2 各微前端集成方式
|
||
|
||
每个微前端(以 teacher-portal 为例):
|
||
|
||
1. **依赖声明**:`package.json` 添加 `@edu/shared-tokens`、`@edu/shared-ui`、`@edu/shared-utils`
|
||
2. **全局样式**:`app/globals.css` 导入 `@edu/shared-tokens/tokens`
|
||
3. **字体引用**:不加载字体,引用 Shell 注入的 CSS 变量
|
||
4. **Tailwind 配置**:依赖 shared-tokens 的 `@theme inline`,无需自定义 `theme.extend`
|
||
5. **主题响应**:监听 `<html>` 的 `class="dark"`,自动切换语义令牌
|
||
|
||
**集成检查清单:**
|
||
|
||
- [ ] `package.json` 依赖 `@edu/shared-tokens`
|
||
- [ ] `globals.css` 导入 `@import "@edu/shared-tokens/tokens"`
|
||
- [ ] 不出现 `next/font/google` 字体加载
|
||
- [ ] 不出现 `#hex` 颜色字面量
|
||
- [ ] 不出现 `'Inter'` / `'Fraunces'` / `'JetBrains Mono'` 字面量
|
||
- [ ] 不出现 `font-size: Npx` 硬编码
|
||
- [ ] 不出现 `w-[Npx]` 任意值
|
||
|
||
### 10.3 组件库抽取策略
|
||
|
||
```
|
||
packages/shared-ui/
|
||
├─ package.json # @edu/shared-ui
|
||
├─ src/
|
||
│ ├─ primitives/ # shadcn 基础组件(纸感调校)
|
||
│ │ ├─ button.tsx
|
||
│ │ ├─ input.tsx
|
||
│ │ ├─ dialog.tsx
|
||
│ │ ├─ sheet.tsx
|
||
│ │ ├─ dropdown-menu.tsx
|
||
│ │ ├─ tabs.tsx
|
||
│ │ ├─ table.tsx
|
||
│ │ └─ tooltip.tsx
|
||
│ ├─ patterns/ # 组合模式组件
|
||
│ │ ├─ workbench.tsx # 三栏工作台
|
||
│ │ ├─ context-menu-provider.tsx # 右键菜单
|
||
│ │ └─ node-list.tsx # 节点列表
|
||
│ ├─ skeleton/ # 骨架屏
|
||
│ │ ├─ tree-skeleton.tsx
|
||
│ │ ├─ paper-skeleton.tsx
|
||
│ │ └─ table-skeleton.tsx
|
||
│ └─ index.ts
|
||
└─ README.md
|
||
```
|
||
|
||
**抽取原则:**
|
||
|
||
- ✅ **做**:4 个微前端都用到的组件才抽取到 shared-ui
|
||
- ✅ **做**:业务专属组件留在各自微前端
|
||
- ❌ **不做**:提前抽取"可能复用"的组件
|
||
- ❌ **不做**:shared-ui 依赖任何微前端
|
||
|
||
### 10.4 设计审查清单
|
||
|
||
每次 PR 提交前,对照以下清单审查:
|
||
|
||
#### 视觉与美学
|
||
|
||
- [ ] 当前屏有且仅有一个视觉焦点
|
||
- [ ] 无卡片堆砌(默认无卡片,必要时才用)
|
||
- [ ] 无装饰性阴影(仅 `--shadow-1` 用于浮层)
|
||
- [ ] 无多个强调色竞争(仅学术深蓝)
|
||
- [ ] 字体差异化正确(serif 主文 / sans UI / mono 数据)
|
||
|
||
#### 令牌合规
|
||
|
||
- [ ] 无 `#hex` 颜色字面量
|
||
- [ ] 无字体字面量(`'Inter'` 等)
|
||
- [ ] 无 `font-size: Npx` 硬编码
|
||
- [ ] 无 `w-[Npx]` 任意值
|
||
- [ ] 业务代码不引用 Primitive 层
|
||
|
||
#### 布局
|
||
|
||
- [ ] 三栏布局比例正确(260 / 1fr / 380)
|
||
- [ ] 页面边距 `--space-18`(桌面)/ `--space-4`(移动)
|
||
- [ ] 留白充足(段落 `--space-5`,区块 `--space-8` 以上)
|
||
|
||
#### 交互
|
||
|
||
- [ ] 所有节点支持右键上下文菜单
|
||
- [ ] 交互反馈 ≤ 200ms
|
||
- [ ] 动效遵守 `prefers-reduced-motion`
|
||
- [ ] 无装饰性持续动画
|
||
|
||
#### 可访问性
|
||
|
||
- [ ] 对比度 ≥ WCAG AA
|
||
- [ ] 所有交互元素可 `Tab` 聚焦
|
||
- [ ] 焦点可见(`focus-visible:ring-2`)
|
||
- [ ] 图标按钮有 `aria-label`
|
||
- [ ] 树形结构有 `role="tree"` 等语义
|
||
|
||
#### 微前端一致性
|
||
|
||
- [ ] 使用 `@edu/shared-tokens`,不自定义令牌
|
||
- [ ] 不加载字体,引用 Shell 变量
|
||
- [ ] 主题切换响应 `<html>` 的 `class="dark"`
|
||
|
||
---
|
||
|
||
## 附录:与原 CICD 项目的差异
|
||
|
||
| 维度 | CICD(原) | Edu(新) |
|
||
|------|-----------|-----------|
|
||
| 架构 | Next.js 单应用 | 微服务 + 4 微前端 Module Federation |
|
||
| 令牌分发 | 应用内 `src/app/styles/tokens/` | `@edu/shared-tokens` npm 包 |
|
||
| 字体加载 | 应用内 `next/font` | Shell 加载,微前端引用变量 |
|
||
| 强调色 | Indigo(lp-interaction) | 学术深蓝 `hsl(220, 60%, 35%)` |
|
||
| 背景色 | 纯白 `hsl(0, 0%, 100%)` | 纸感米白 `hsl(40, 20%, 98%)` |
|
||
| 主文色 | `hsl(240, 10%, 3.9%)` 偏蓝黑 | `hsl(25, 3%, 15%)` 暖炭灰 |
|
||
| 圆角 | `0.5rem`(8px) | `0.375rem`(6px,更纸感) |
|
||
| 卡片使用 | 较多 | 克制,默认无卡片 |
|
||
| 字体差异化 | Fraunces 用于备课 | 全局三字体差异化 |
|
||
|
||
---
|
||
|
||
**文档版本**:v1.0
|
||
**最后更新**:2026-07-07
|
||
**维护者**:Edu 团队
|