Files
Edu/docs/standards/ui-design-system.md
SpecialX 2ba4250165
Some checks failed
CI Go / test (push) Has been cancelled
CI Proto / lint (push) Has been cancelled
CI Python / test (push) Has been cancelled
CI TypeScript / test (push) Has been cancelled
feat(p1): complete P1 foundation stage
- 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
2026-07-07 23:39:37 +08:00

47 KiB
Raw Permalink Blame History

Edu 微前端 UI 设计系统

适用于 Edu 项目 4 个微前端teacher-portal / student-portal / parent-portal / admin-portal的统一视觉与交互规范。

本文档从原 CICD 项目Next.js 16 + Tailwind v4 + shadcn/ui的设计令牌体系演进而来针对"当前界面设计不合理"的问题重新确立美学方向:纸感编辑器 + 学术克制 + 单一视觉锚点


目录

  1. 设计哲学
  2. 美学方向
  3. 设计令牌体系(微前端共享)
  4. 布局系统
  5. 组件规范
  6. 动效规范
  7. 微前端架构下的 UI 一致性
  8. 可访问性
  9. 反模式清单
  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 的核心手段。

字体 用途 加载方式
Frauncesserif 主文、标题、引文、节点内容 next/font/google + CSS 变量 --font-serif
Intersans-serif UI 元素、按钮、表单、导航、元信息 next/font/google + CSS 变量 --font-sans
JetBrains Monomono 代码、数据、ID、时间戳、角色标签 next/font/google + CSS 变量 --font-mono

差异化规则:

  • :备课画布的主文用 Fraunces工具条按钮用 Inter
  • :成绩表格的数据用 JetBrains Mono 对齐数字
  • :标题用 Fraunces semibold副标用 Inter regular
  • 不做:全场 Inter 单字体(失去学术质感)
  • 不做:标题用 Inter boldSaaS 感)

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-tight1.2)保持紧凑
  • 正文用 --leading-relaxed1.65)保证可读
  • UI 文字用 --leading-normal1.5

2.5 间距

慷慨留白,呼吸感优先。

  • 基准8px--space-2
  • 段落间距:--space-520px
  • 区块间距:--space-832px~ --space-1248px
  • 页面边距:--space-1872px桌面端
  • 不做:信息密集堆砌,元素间距小于 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 + F10Menu
  • 三栏布局支持 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 为例):

  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 加载,微前端引用变量
强调色 Indigolp-interaction 学术深蓝 hsl(220, 60%, 35%)
背景色 纯白 hsl(0, 0%, 100%) 纸感米白 hsl(40, 20%, 98%)
主文色 hsl(240, 10%, 3.9%) 偏蓝黑 hsl(25, 3%, 15%) 暖炭灰
圆角 0.5rem8px 0.375rem6px更纸感
卡片使用 较多 克制,默认无卡片
字体差异化 Fraunces 用于备课 全局三字体差异化

文档版本v1.0 最后更新2026-07-07 维护者Edu 团队