- 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
47 KiB
Edu 微前端 UI 设计系统
适用于 Edu 项目 4 个微前端(teacher-portal / student-portal / parent-portal / admin-portal)的统一视觉与交互规范。
本文档从原 CICD 项目(Next.js 16 + Tailwind v4 + shadcn/ui)的设计令牌体系演进而来,针对"当前界面设计不合理"的问题重新确立美学方向:纸感编辑器 + 学术克制 + 单一视觉锚点。
目录
1. 设计哲学
1.1 视觉论点(Visual Thesis)
Edu 不是一个 SaaS Dashboard,而是一张可以编辑的学术纸。
教育产品的核心是"阅读—书写—批注",而非"看图表—点按钮—收通知"。因此 Edu 的视觉论点由三句话定义:
- 纸感编辑器:界面是一张可被书写的纸,不是一组卡片堆砌的面板。
- 学术克制:参考学术书籍排版的克制感(章节标题、引文、注脚、分隔线),不滥用色彩与图形装饰。
- 单一视觉锚点:每屏只有一个焦点(一段文字、一个节点、一张表单),其余皆为衬托。
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 排版阶梯
基于学术书籍排版(章节标题、段落、引文、注脚):
/* 标题层级 */
--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 层分发机制。
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(原始令牌)
/* 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(明色)
/* 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(暗色)
/* 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 暴露
/* 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 常量导出
// 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 三栏布局(核心工作台)
用户明确偏好:左侧结构树 + 中间纸面内容区 + 右侧详情编辑面板。
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 的全画布英雄区模式:
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(复合组件)
用于三栏布局等"一组协同组件":
// 示例:三栏工作台复合组件
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(渲染函数)
用于列表项的灵活定制:
// 示例:节点列表,渲染逻辑由调用方决定
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(状态共享)
用于右键菜单的统一管理:
// 右键菜单 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 辅助(生成内容/优化措辞/关联推荐)
// 节点右键菜单统一封装
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 管理跨组件状态
// 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)
英雄区元素错峰揭示,建立阅读顺序:
// 错峰揭示:标题 → 副标 → 正文 → 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>
);
}
/* 全局关键帧 */
@keyframes reveal-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
6.3 滚动联动(Scroll-Linked)
// 使用 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 架构总览
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 依赖:
{
"dependencies": {
"@edu/shared-tokens": "workspace:*"
}
}
各微前端 globals.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 变量共享;微前端只引用变量。
// 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 中映射:
/* 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 主题切换
// 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在栏间流转 - 树形结构支持方向键导航(↑↓←→)
// 焦点可见样式
<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
// 树形节点无障碍示例
<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 包结构
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 配置:
{
"name": "@edu/shared-tokens",
"exports": {
".": "./src/index.ts",
"./tokens": "./src/tokens/index.css",
"./constants": "./src/constants.ts"
}
}
10.2 各微前端集成方式
每个微前端(以 teacher-portal 为例):
- 依赖声明:
package.json添加@edu/shared-tokens、@edu/shared-ui、@edu/shared-utils - 全局样式:
app/globals.css导入@edu/shared-tokens/tokens - 字体引用:不加载字体,引用 Shell 注入的 CSS 变量
- Tailwind 配置:依赖 shared-tokens 的
@theme inline,无需自定义theme.extend - 主题响应:监听
<html>的class="dark",自动切换语义令牌
集成检查清单:
package.json依赖@edu/shared-tokensglobals.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 团队