(null);
export function ContextMenuProvider({
children,
}: {
children: React.ReactNode;
}): React.ReactElement {
const [state, setState] = useState<{
x: number;
y: number;
nodeId: string | null;
} | null>(null);
const open = useCallback((event: React.MouseEvent, nodeId: string) => {
event.preventDefault();
setState({ x: event.clientX, y: event.clientY, nodeId });
}, []);
return (
{children}
{state && (
setState(null)}>
{/* 菜单项由业务通过插槽注入 */}
)}
);
}
```
### 5.3 业务组件清单
| 组件 | 微前端 | 说明 |
|------|--------|------|
| StructureTree | teacher / admin | 左侧结构树,支持右键菜单 |
| LessonEditor | teacher | 中间纸面编辑器,Fraunces 主文 |
| NodeProperties | teacher | 右侧节点属性面板 |
| ClassCard | teacher / admin | 班级卡片,**克制使用**(默认列表式) |
| AssignmentCard | teacher / student | 作业卡片,**克制使用** |
| GradeTable | teacher / admin / parent | 成绩表,mono 数字对齐 |
| StudentRoster | teacher / admin | 学生名册,列表式 |
| ScheduleGrid | all | 课表网格 |
| ReportCard | teacher / parent | 成绩报告卡,**唯一允许卡片 + 打印样式** |
### 5.4 右键上下文菜单(强制)
**所有节点支持右键菜单**,菜单项包括:
- 新增子节点
- 编辑节点
- 删除节点
- 复制/移动
- AI 辅助(生成内容/优化措辞/关联推荐)
```tsx
// 节点右键菜单统一封装
export function NodeWithContextMenu({
node,
children,
}: {
node: TreeNode;
children: React.ReactNode;
}): React.ReactElement {
const { open } = useContextMenu();
return (
open(e, node.id)}>
{children}
);
}
```
### 5.5 状态管理
- **React 19 `useOptimistic`**:用于乐观更新(节点编辑、批注提交)
- **zustand 细粒度选择器**:用于跨组件状态共享(选中节点、编辑模式)
- ❌ **不做**:用 Context 共享高频更新状态(导致重渲染)
- ❌ **不做**:用 useState 管理跨组件状态
```tsx
// zustand 细粒度选择器示例
import { create } from "zustand";
import { shallow } from "zustand/shallow";
type EditorStore = {
selectedNodeId: string | null;
editingMode: "view" | "edit" | "ai-assist";
setSelectedNode: (id: string | null) => void;
setEditingMode: (mode: EditorStore["editingMode"]) => void;
};
export const useEditorStore = create((set) => ({
selectedNodeId: null,
editingMode: "view",
setSelectedNode: (id) => set({ selectedNodeId: id }),
setEditingMode: (mode) => set({ editingMode: mode }),
}));
// 组件中:只订阅需要的字段
function TreeView() {
const selectedNodeId = useEditorStore((s) => s.selectedNodeId);
// ...
}
```
---
## 6. 动效规范
### 6.1 动效原则
> **动效用于建立层级和氛围,不是噪音。**
| 原则 | 含义 |
|------|------|
| 有目的 | 每个动效必须传递信息或引导注意力 |
| 克制 | 同屏动效不超过 1 个主要 + 2 个次要 |
| 快速 | 交互反馈 ≤ 200ms,页面过渡 ≤ 300ms |
| 可关闭 | 遵守 `prefers-reduced-motion` |
### 6.2 入场序列(Staggered Reveals)
英雄区元素错峰揭示,建立阅读顺序:
```tsx
// 错峰揭示:标题 → 副标 → 正文 → CTA
function HeroSection() {
return (
Edu
一张可以编辑的学术纸
面向 K12 教学的微前端工作台
);
}
```
```css
/* 全局关键帧 */
@keyframes reveal-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
```
### 6.3 滚动联动(Scroll-Linked)
```tsx
// 使用 useScroll + useTransform
import { useScroll, useTransform } from "framer-motion";
function ScrollContent() {
const { scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0]);
const y = useTransform(scrollYProgress, [0, 0.3], [0, -20]);
return (
{/* 滚动时淡出上移 */}
);
}
```
### 6.4 交互反馈
| 交互 | 动效 | 时长 |
|------|------|------|
| hover 按钮 | `bg-muted` 渐显 | 150ms |
| 选中节点 | 左侧 2px 强调色竖线滑入 | 200ms |
| 打开 Dialog | 淡入 + 轻微下移 | 200ms |
| 切换 Tab | 下划线滑动 | 200ms |
| 提交表单 | 按钮 loading spinner | - |
| 乐观更新 | 内容淡入替换 | 150ms |
### 6.5 微前端场景
| 场景 | 动效 |
|------|------|
| 微前端页面切换 | 顶部 2px 进度条 + 内容淡入 |
| 模块加载占位 | 骨架屏(skeleton),不用 spinner |
| Module Federation 加载失败 | 错误边界 + 重试按钮 |
**做与不做:**
- ✅ **做**:Landing 页错峰揭示建立阅读顺序
- ✅ **做**:选中节点用竖线滑入暗示焦点
- ✅ **做**:加载用骨架屏,不用居中 spinner
- ❌ **不做**:每个元素都加入场动画
- ❌ **不做**:卡片 hover 时 `scale-105` 放大
- ❌ **不做**:装饰性的持续动画(脉冲、闪烁)
---
## 7. 微前端架构下的 UI 一致性
### 7.1 架构总览
```mermaid
flowchart TB
subgraph SHELL["Shell 宿主"]
ROUTER["统一路由
+ 主题切换"]
FONT["字体加载
next/font"]
end
subgraph SHARED["共享层"]
TOKENS["@edu/shared-tokens
令牌包"]
UI["@edu/shared-ui
基础组件"]
UTILS["@edu/shared-utils
cn/权限/类型"]
end
subgraph MF["4 个微前端(Module Federation)"]
T["teacher-portal
教师工作台"]
S["student-portal
学生学习台"]
P["parent-portal
家长查看台"]
A["admin-portal
管理后台"]
end
SHELL -->|暴露| T
SHELL -->|暴露| S
SHELL -->|暴露| P
SHELL -->|暴露| A
TOKENS --> T
TOKENS --> S
TOKENS --> P
TOKENS --> A
UI --> T
UI --> S
UI --> P
UI --> A
```
### 7.2 shared-tokens 包集成
各微前端 `package.json` 依赖:
```json
{
"dependencies": {
"@edu/shared-tokens": "workspace:*"
}
}
```
各微前端 `globals.css`:
```css
@import "tailwindcss";
@import "@edu/shared-tokens/tokens"; /* 汇总入口 */
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
@layer base {
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
}
}
```
### 7.3 共享字体加载策略
**问题**:Module Federation 下,各微前端独立加载字体会导致重复请求与 FOUT。
**方案**:Shell 宿主统一加载字体,通过 CSS 变量共享;微前端只引用变量。
```tsx
// apps/shell/app/layout.tsx
import { Inter, Fraunces, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
const fraunces = Fraunces({
subsets: ["latin"],
display: "swap",
variable: "--font-fraunces",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-jetbrains-mono",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
**shared-tokens 中映射:**
```css
/* packages/shared-tokens/src/tokens/primitive.css */
:root {
--font-family-sans: var(--font-inter), system-ui, sans-serif;
--font-family-serif: var(--font-fraunces), Georgia, serif;
--font-family-mono: var(--font-jetbrains-mono), monospace;
}
```
**做与不做:**
- ✅ **做**:Shell 加载字体,微前端引用变量
- ✅ **做**:`display: swap` 避免 FOIT
- ❌ **不做**:每个微前端各自 `next/font/google` 加载
### 7.4 Module Federation UI 协调
| 协调点 | 策略 |
|--------|------|
| 设计语言 | 统一 shared-tokens,禁止微前端自定义令牌 |
| 组件库 | shared-ui 提供基础组件,微前端可扩展业务组件 |
| 主题切换 | Shell 持有 `class="dark"` 状态,所有微前端响应 |
| 路由 | Shell 统一路由表,微前端注册子路由 |
| 加载态 | 统一骨架屏样式(来自 shared-ui) |
### 7.5 主题切换
```tsx
// apps/shell/components/ThemeProvider.tsx
"use client";
import { useEffect, useState } from "react";
type Theme = "light" | "dark";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState("light");
useEffect(() => {
const saved = localStorage.getItem("edu-theme") as Theme | null;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initial = saved ?? (prefersDark ? "dark" : "light");
setTheme(initial);
document.documentElement.classList.toggle("dark", initial === "dark");
}, []);
const toggle = () => {
const next: Theme = theme === "light" ? "dark" : "light";
setTheme(next);
localStorage.setItem("edu-theme", next);
document.documentElement.classList.toggle("dark", next === "dark");
};
return (
{children}
);
}
```
**做与不做:**
- ✅ **做**:主题状态由 Shell 持有,`class="dark"` 挂在 ``
- ✅ **做**:所有微前端通过 `@edu/shared-tokens` 响应主题
- ❌ **不做**:微前端各自实现主题切换
---
## 8. 可访问性
### 8.1 对比度(WCAG AA)
| 文本类型 | 对比度要求 | 验证 |
|----------|-----------|------|
| 正文(< 18px) | ≥ 4.5:1 | `--foreground` on `--background` |
| 大文本(≥ 18px) | ≥ 3:1 | `--text-secondary` on `--background` |
| 交互元素边框 | ≥ 3:1 | `--border-strong` |
| 焦点指示器 | ≥ 3:1 | `--ring`(学术深蓝) |
**当前令牌对比度验证:**
- 主文 `hsl(25, 3%, 15%)` on 纸面 `hsl(40, 20%, 98%)`:约 14:1 ✅
- 次文 `hsl(30, 5%, 45%)` on 纸面:约 4.6:1 ✅
- 强调 `hsl(220, 60%, 35%)` on 纸面:约 7:1 ✅
### 8.2 键盘导航
- 所有交互元素必须可 `Tab` 聚焦
- 焦点可见:`focus-visible:ring-2 focus-visible:ring-ring`
- 右键菜单支持键盘打开:`Shift + F10` 或 `Menu` 键
- 三栏布局支持 `Tab` 在栏间流转
- 树形结构支持方向键导航(↑↓←→)
```tsx
// 焦点可见样式
```
### 8.3 屏幕阅读器
- 语义化 HTML:`