chore(teacher-portal): merge teacher-portal full implementation into main

This commit is contained in:
SpecialX
2026-07-10 19:19:19 +08:00
48 changed files with 2888 additions and 1333 deletions

View File

@@ -14,7 +14,8 @@
},
"peerDependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react-dom": "^18.3.0",
"urql": "^2.2.0"
},
"devDependencies": {
"@types/react": "^18.3.0",

View File

@@ -51,6 +51,12 @@ export type { UseToastReturn } from "./use-toast.js";
export { useTraceId } from "./use-trace-id.js";
export type { UseTraceIdReturn } from "./use-trace-id.js";
export {
useGraphQLClient,
GraphQLClientContext,
} from "./use-graphql-client.js";
export type { UseGraphQLClientReturn } from "./use-graphql-client.js";
// 共享类型
export type {
UserSession,

View File

@@ -18,10 +18,11 @@ export interface UserSession {
}
/**
* 数据范围(对齐 iam DataScope 6 级
* 数据范围(对齐 ARB-001 §1.2 schema DataScope enum
* 与 apps/teacher-portal/src/lib/graphql.ts DataScope 保持一致
*/
export type DataScope =
"ALL" | "SCHOOL" | "GRADE" | "CLASS" | "SUBJECT" | "SELF";
"SELF" | "CLASS" | "GRADE" | "SCHOOL" | "DISTRICT" | "ALL";
/**
* 权限点检查结果

View File

@@ -0,0 +1,53 @@
import { useContext, useMemo } from "react";
import type { Client } from "urql";
/**
* useGraphQLClient - 获取 urql client 单例
*
* 用途:从 Shell 注入的 GraphQLProvider Context 中获取 urql client。
* 总裁裁决 §2.17 方案 AShellteacher-portal暴露 GraphQLProvider
* Remotestudent/parent/admin复用同一 client 单例,避免多实例 + 缓存不一致。
*
* 注意:本 hook 必须在 GraphQLProvider 内部调用,否则抛出错误。
*
* @example
* const client = useGraphQLClient();
* const [result] = useQuery({ query: DashboardQuery });
*
* MF 暴露ARB-002 §2.2 exposes './useGraphQLClient'
*/
// GraphQLClientContext 由 Shellapps/teacher-portal/src/app/providers.tsx创建并注入。
// 此处通过 createContext 占位Shell 在初始化时通过 Provider value 覆盖。
// 为避免 hooks 包直接依赖 urqlpeerDependencyContext 值类型用 Clienturql 类型)。
// urql 在 apps/* 的 dependencies 中,通过 peerDependencies 声明。
import { createContext } from "react";
/**
* GraphQL Client Context
*
* Shellteacher-portal通过 <Context.Provider value={client}> 注入。
* Remotestudent/parent/admin-portal通过 MF shared 复用同一 Context 实例。
*/
export const GraphQLClientContext = createContext<Client | null>(null);
GraphQLClientContext.displayName = "GraphQLClientContext";
export interface UseGraphQLClientReturn {
/** urql client 单例 */
client: Client;
}
export function useGraphQLClient(): UseGraphQLClientReturn {
const client = useContext(GraphQLClientContext);
if (!client) {
throw new Error(
"useGraphQLClient 必须在 GraphQLProvider 内部调用。" +
"请确保组件树被 <GraphQLProvider> 包裹apps/teacher-portal/src/app/providers.tsx。",
);
}
return useMemo(() => ({ client }), [client]);
}

View File

@@ -29,13 +29,13 @@ export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { error: null };
override state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
override componentDidCatch(error: Error, info: ErrorInfo): void {
this.props.onError?.(error, info);
}
@@ -44,7 +44,7 @@ export class ErrorBoundary extends Component<
this.setState({ error: null });
};
render(): ReactNode {
override render(): ReactNode {
const { error } = this.state;
const { children, fallback } = this.props;

View File

@@ -79,5 +79,5 @@ function getTailwindPrefix(token: string): string | null {
const match = token.match(
/^(bg|text|p|m|px|py|mx|my|w|h|min-h|min-w|border|rounded|shadow|font|leading|tracking|gap|space|flex|grid|col|row|inset|top|right|bottom|left|z|opacity|transition|duration|delay|animate|hover|focus|sm|md|lg|xl|2xl)-/,
);
return match ? match[1] : null;
return match?.[1] ?? null;
}

View File

@@ -4,5 +4,16 @@
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts"
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./primitive.css": "./src/primitive.css",
"./semantic-light.css": "./src/semantic-light.css",
"./semantic-dark.css": "./src/semantic-dark.css",
"./tailwind-theme.css": "./src/tailwind-theme.css",
"./all.css": "./src/all.css"
}
}

View File

@@ -0,0 +1,13 @@
/**
* ui-tokens 全量样式入口
*
* 导入顺序Layer 1 Primitive → Layer 2 Semanticlight + dark→ Layer 3 Tailwind
*
* 用法apps/teacher-portal/src/app/globals.css
* @import '@edu/ui-tokens/all.css';
*/
@import './primitive.css';
@import './semantic-light.css';
@import './semantic-dark.css';
@import './tailwind-theme.css';

View File

@@ -0,0 +1,112 @@
/**
* Layer 1: Primitive Tokens原始色板/字号/间距/阴影)
*
* 业务代码不直接引用本层令牌,仅 Layer 2 Semantic 引用。
* 维护者ai13teacher-portal
* 关联project_rules §3.10 设计令牌规范
*
* 命名规范:
* - 颜色:--color-<hue>-<level>HSL 分量,供 Layer 2 组合)
* - 字号:--font-size-<n>1-9 阶梯)
* - 间距:--space-<n>4px 基准阶梯)
* - 阴影:--shadow-<n>
* - 字重:--font-weight-<name>
* - 圆角:--radius-<name>
*/
:root {
/* ============ 颜色原始色板HSL 分量,非完整颜色) ============ */
/* 中性色(纸张/墨色) */
--color-paper-h: 40;
--color-paper-s: 20%;
--color-paper-l-50: 50%;
--color-paper-l-98: 98%;
--color-paper-l-99: 99%;
--color-ink-h: 25;
--color-ink-s: 3%;
--color-ink-l-15: 15%;
--color-ink-l-45: 45%;
--color-ink-l-60: 60%;
/* 强调色(深蓝) */
--color-accent-h: 220;
--color-accent-s: 60%;
--color-accent-l-35: 35%;
--color-accent-l-50: 50%;
/* 分隔线(暖灰) */
--color-rule-h: 30;
--color-rule-s: 10%;
--color-rule-l-85: 85%;
--color-rule-l-90: 90%;
/* 语义原始色 */
--color-success-h: 142;
--color-success-s: 71%;
--color-success-l-45: 45%;
--color-warning-h: 38;
--color-warning-s: 92%;
--color-warning-l-50: 50%;
--color-danger-h: 0;
--color-danger-s: 84%;
--color-danger-l-60: 60%;
--color-info-h: 199;
--color-info-s: 89%;
--color-info-l-48: 48%;
/* ============ 字号阶梯1-91 最小 9 最大) ============ */
--font-size-1: 0.75rem; /* 12px - 辅助说明 */
--font-size-2: 0.875rem; /* 14px - 次要正文 */
--font-size-3: 1rem; /* 16px - 正文 body */
--font-size-4: 1.125rem; /* 18px - 强调正文 */
--font-size-5: 1.25rem; /* 20px - 小标题 */
--font-size-6: 1.5rem; /* 24px - 区块标题 */
--font-size-7: 1.875rem; /* 30px - 页面标题 */
--font-size-8: 2.25rem; /* 36px - Hero */
--font-size-9: 3rem; /* 48px - 大数字 */
/* ============ 间距阶梯4px 基准) ============ */
--space-1: 0.25rem; /* 4px */
--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-20: 5rem; /* 80px */
/* ============ 阴影 ============ */
--shadow-1: 0 1px 2px 0 hsl(25 3% 15% / 0.05);
--shadow-2: 0 1px 3px 0 hsl(25 3% 15% / 0.1), 0 1px 2px -1px hsl(25 3% 15% / 0.1);
--shadow-3: 0 4px 6px -1px hsl(25 3% 15% / 0.1), 0 2px 4px -2px hsl(25 3% 15% / 0.1);
--shadow-4: 0 10px 15px -3px hsl(25 3% 15% / 0.1), 0 4px 6px -4px hsl(25 3% 15% / 0.1);
/* ============ 字重 ============ */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* ============ 圆角 ============ */
--radius-sm: 0.25rem; /* 4px */
--radius-md: 0.375rem; /* 6px */
--radius-lg: 0.5rem; /* 8px */
--radius-full: 9999px;
/* ============ 行高 ============ */
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* ============ 字间距 ============ */
--letter-spacing-tight: -0.01em;
--letter-spacing-normal: 0;
--letter-spacing-wide: 0.025em;
}

View File

@@ -0,0 +1,66 @@
/**
* Layer 2: Semantic Tokens - Dark Theme暗色语义令牌
*
* 维护者ai13teacher-portal
* 关联project_rules §3.10
*
* 暗色主题覆盖,通过 [data-theme="dark"] 或 prefers-color-scheme: dark 激活。
*/
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--bg-paper: hsl(var(--color-ink-h) var(--color-ink-s) var(--color-ink-l-15));
--bg-surface: hsl(var(--color-ink-h) var(--color-ink-s) 20%);
--bg-subtle: hsl(var(--color-ink-h) var(--color-ink-s) 25%);
--color-ink: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-98));
--color-ink-muted: hsl(var(--color-paper-h) var(--color-paper-s) 70%);
--color-ink-subtle: hsl(var(--color-paper-h) var(--color-paper-s) 60%);
--color-ink-on-accent: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-99));
--color-accent: hsl(var(--color-accent-h) var(--color-accent-s) var(--color-accent-l-50));
--color-accent-hover: hsl(var(--color-accent-h) var(--color-accent-s) 60%);
--color-accent-subtle: hsl(var(--color-accent-h) var(--color-accent-s) 25%);
--color-rule: hsl(var(--color-ink-h) var(--color-ink-s) 25%);
--color-rule-strong: hsl(var(--color-ink-h) var(--color-ink-s) 30%);
--color-border: var(--color-rule);
--color-input-bg: var(--bg-surface);
--shadow-sm: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
--shadow-md: 0 1px 3px 0 hsl(0 0% 0% / 0.4), 0 1px 2px -1px hsl(0 0% 0% / 0.4);
--shadow-lg: 0 4px 6px -1px hsl(0 0% 0% / 0.4), 0 2px 4px -2px hsl(0 0% 0% / 0.4);
--shadow-xl: 0 10px 15px -3px hsl(0 0% 0% / 0.5), 0 4px 6px -4px hsl(0 0% 0% / 0.5);
}
}
[data-theme="dark"] {
color-scheme: dark;
--bg-paper: hsl(var(--color-ink-h) var(--color-ink-s) var(--color-ink-l-15));
--bg-surface: hsl(var(--color-ink-h) var(--color-ink-s) 20%);
--bg-subtle: hsl(var(--color-ink-h) var(--color-ink-s) 25%);
--color-ink: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-98));
--color-ink-muted: hsl(var(--color-paper-h) var(--color-paper-s) 70%);
--color-ink-subtle: hsl(var(--color-paper-h) var(--color-paper-s) 60%);
--color-ink-on-accent: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-99));
--color-accent: hsl(var(--color-accent-h) var(--color-accent-s) var(--color-accent-l-50));
--color-accent-hover: hsl(var(--color-accent-h) var(--color-accent-s) 60%);
--color-accent-subtle: hsl(var(--color-accent-h) var(--color-accent-s) 25%);
--color-rule: hsl(var(--color-ink-h) var(--color-ink-s) 25%);
--color-rule-strong: hsl(var(--color-ink-h) var(--color-ink-s) 30%);
--color-border: var(--color-rule);
--color-input-bg: var(--bg-surface);
--shadow-sm: 0 1px 2px 0 hsl(0 0% 0% / 0.3);
--shadow-md: 0 1px 3px 0 hsl(0 0% 0% / 0.4), 0 1px 2px -1px hsl(0 0% 0% / 0.4);
--shadow-lg: 0 4px 6px -1px hsl(0 0% 0% / 0.4), 0 2px 4px -2px hsl(0 0% 0% / 0.4);
--shadow-xl: 0 10px 15px -3px hsl(0 0% 0% / 0.5), 0 4px 6px -4px hsl(0 0% 0% / 0.4);
}

View File

@@ -0,0 +1,82 @@
/**
* Layer 2: Semantic Tokens - Light Theme亮色语义令牌
*
* 业务代码唯一引用入口:业务 TSX/CSS 引用 var(--color-*) / var(--font-*) 等。
* 维护者ai13teacher-portal
* 关联project_rules §3.10、03-long-term-architecture.md §1.5
*
* 引用 Layer 1 Primitive 组合成完整语义值。
*/
:root {
color-scheme: light;
/* ============ 背景与表面 ============ */
--bg-paper: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-98));
--bg-surface: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-99));
--bg-subtle: hsl(var(--color-rule-h) var(--color-rule-s) var(--color-rule-l-90));
/* ============ 文字 ============ */
--color-ink: hsl(var(--color-ink-h) var(--color-ink-s) var(--color-ink-l-15));
--color-ink-muted: hsl(var(--color-ink-h) var(--color-ink-s) var(--color-ink-l-45));
--color-ink-subtle: hsl(var(--color-ink-h) var(--color-ink-s) var(--color-ink-l-60));
--color-ink-on-accent: hsl(var(--color-paper-h) var(--color-paper-s) var(--color-paper-l-99));
/* ============ 强调色 ============ */
--color-accent: hsl(var(--color-accent-h) var(--color-accent-s) var(--color-accent-l-35));
--color-accent-hover: hsl(var(--color-accent-h) var(--color-accent-s) var(--color-accent-l-50));
--color-accent-subtle: hsl(var(--color-accent-h) var(--color-accent-s) var(--color-rule-l-90));
/* ============ 分隔线 ============ */
--color-rule: hsl(var(--color-rule-h) var(--color-rule-s) var(--color-rule-l-90));
--color-rule-strong: hsl(var(--color-rule-h) var(--color-rule-s) var(--color-rule-l-85));
/* ============ 语义色 ============ */
--color-success: hsl(var(--color-success-h) var(--color-success-s) var(--color-success-l-45));
--color-warning: hsl(var(--color-warning-h) var(--color-warning-s) var(--color-warning-l-50));
--color-danger: hsl(var(--color-danger-h) var(--color-danger-s) var(--color-danger-l-60));
--color-info: hsl(var(--color-info-h) var(--color-info-s) var(--color-info-l-48));
/* ============ 边框/输入 ============ */
--color-border: var(--color-rule);
--color-border-focus: var(--color-accent);
--color-input-bg: var(--bg-surface);
/* ============ 字体族(通过 var 引用,禁止字面量) ============ */
--font-family-sans: var(--font-inter, system-ui), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-family-serif: var(--font-fraunces, Georgia), "Times New Roman", serif;
--font-family-mono: var(--font-jetbrains-mono, "SF Mono"), Monaco, Consolas, monospace;
/* ============ 语义字号 ============ */
--font-size-body: var(--font-size-3);
--font-size-small: var(--font-size-2);
--font-size-tiny: var(--font-size-1);
--font-size-heading-1: var(--font-size-7);
--font-size-heading-2: var(--font-size-6);
--font-size-heading-3: var(--font-size-5);
--font-size-display: var(--font-size-9);
--font-size-large-number: var(--font-size-8);
/* ============ 语义间距 ============ */
--space-xs: var(--space-1);
--space-sm: var(--space-2);
--space-md: var(--space-4);
--space-lg: var(--space-6);
--space-xl: var(--space-8);
--space-2xl: var(--space-12);
/* ============ 语义阴影 ============ */
--shadow-sm: var(--shadow-1);
--shadow-md: var(--shadow-2);
--shadow-lg: var(--shadow-3);
--shadow-xl: var(--shadow-4);
/* ============ 语义圆角 ============ */
--radius-default: var(--radius-md);
--radius-card: var(--radius-lg);
--radius-button: var(--radius-md);
/* ============ 过渡 ============ */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@@ -0,0 +1,31 @@
/**
* Layer 3: Tailwind Theme MappingTailwind 主题映射)
*
* 将 Layer 2 Semantic 令牌映射为 Tailwind 类名bg-* / text-* / font-* 等)。
* 维护者ai13teacher-portal
* 关联project_rules §3.10、03-long-term-architecture.md §1.5
*
* Tailwind 3.4 通过 tailwind.config.js 的 theme.extend 映射;
* Tailwind 4 将改用 @theme inline 指令(未来升级时本文件可转为 @theme 块)。
*
* 使用方式(业务组件):
* <div className="bg-paper text-ink font-serif">...</div>
* <p className="text-sm text-ink-muted">...</p>
*
* 对应 tailwind.config.js 中:
* colors: { paper: 'var(--bg-paper)', ink: 'var(--color-ink)', ... }
* fontFamily: { sans: 'var(--font-family-sans)', serif: 'var(--font-family-serif)', ... }
*/
/* 显式引用语义令牌,确保 CSS 变量在构建产物中可用 */
@layer base {
:root {
/* Tailwind 颜色类映射(与 tailwind.config.js theme.extend.colors 对齐) */
--tw-color-paper: var(--bg-paper);
--tw-color-surface: var(--bg-surface);
--tw-color-ink: var(--color-ink);
--tw-color-ink-muted: var(--color-ink-muted);
--tw-color-accent: var(--color-accent);
--tw-color-rule: var(--color-rule);
}
}