feat(teacher-portal): 完成参考项目差距闭环 P3-P7 全量实现
- P3 考试/作业/成绩 mutation + 详情页 + 批改界面 + 乐观更新 + 多 Tab 同步 - P4 知识图谱 SVG 可视化 + 学情分析仪表盘 + parent-portal Remote - P5 WebSocket 通知中心 + AI 出题(SSE) + AI 教案 + AI 学情报告 - P6 可观测性硬化:Sentry + WebVitals + OTel + A11y + 性能配置 + Cookie 迁移 - P7 参考项目差距闭环:新增 35 个页面覆盖 13 个缺失模块 - attendance(考勤 4 页)/questions(题库)/textbooks(教材 2 页) - classes/[id] 详情 + classes/schedule 课表 - course-plans(2 页)/diagnostic(2 页)/error-book/practice - exams/[id]/build 组卷 + exams/[id]/analytics 考后分析 - exams/[id]/edit-rich 富文本编辑 + exams/[id]/proctoring 监考 - grades/entry 批量录入 + grades/stats 统计 + grades/analytics 分析 + grades/report-card 报告卡 - homework/submissions 列表 + assignments/[id]/submissions 批量批改 - homework/submissions/[submissionId] 单份批改 + scan-grading 扫描批改 - lesson-plans 编辑器 + library + calendar + heatmap 5 页 - elective 选修课 3 页 /leave 请假 /schedule-changes 调课 - P7 基础设施:61 GraphQL operations + 5 handlers + 13 fixtures + 11 viewports - 集成 browser.ts/server.ts 注册所有 p7 handlers(fallthrough 顺序) - viewports.ts 扩展 11 个新导航项 - 验证:tsc --noEmit 零错误 + eslint 零错误 - 文档:workline.md 新增 §5 P7 参考项目差距闭环(含完整文件清单)
This commit is contained in:
@@ -26,6 +26,10 @@ import { usePermission } from "@edu/hooks";
|
||||
import { ViewportsQuery, MeQuery } from "@/lib/graphql";
|
||||
import type { ViewportItem, User } from "@/lib/graphql";
|
||||
import { getToken, getUser, logout } from "@/lib/auth";
|
||||
import {
|
||||
useCrossTabSync,
|
||||
broadcastCrossTabEvent,
|
||||
} from "@/hooks/use-cross-tab-sync";
|
||||
|
||||
/**
|
||||
* 权限上下文:从 localStorage 读取(登录时由 iam 返回并存储)。
|
||||
@@ -54,6 +58,9 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
// 多 Tab 会话同步:监听 logout / role-change / token-refresh 事件
|
||||
useCrossTabSync();
|
||||
|
||||
// GraphQL: viewports query(替代 REST /api/v1/teacher/viewports)
|
||||
const [viewportsResult] = useQuery({
|
||||
query: ViewportsQuery,
|
||||
@@ -95,6 +102,12 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
// 退出登录:先广播给其他 Tab,再执行本地登出
|
||||
const handleLogout = () => {
|
||||
broadcastCrossTabEvent("logout");
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-paper">
|
||||
{/* 左侧栏:导航树 */}
|
||||
@@ -140,7 +153,7 @@ export default function AppShell({ children }: { children: React.ReactNode }) {
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={logout}
|
||||
onClick={handleLogout}
|
||||
className="text-tiny uppercase tracking-wide text-ink-muted hover:opacity-70"
|
||||
>
|
||||
退出登录
|
||||
|
||||
90
apps/teacher-portal/src/components/ParentPortalRemote.tsx
Normal file
90
apps/teacher-portal/src/components/ParentPortalRemote.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* ParentPortalRemote - parent-portal MF Remote 接入组件
|
||||
*
|
||||
* 职责(ARB-002 §2.3):
|
||||
* - NEXT_PUBLIC_MF_ENABLED=true:通过 Module Federation 动态加载 parent-portal Remote
|
||||
* - NEXT_PUBLIC_MF_ENABLED=false:展示"需启用微前端模式"提示
|
||||
* - 使用 next/dynamic 懒加载(ssr: false,Remote 仅 CSR)
|
||||
* - ErrorBoundary 兜底:Remote 加载/渲染失败时降级展示
|
||||
*
|
||||
* 维护者:ai13(teacher-portal)
|
||||
*/
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { ErrorBoundary } from "@edu/ui-components";
|
||||
|
||||
const MF_ENABLED = process.env.NEXT_PUBLIC_MF_ENABLED === "true";
|
||||
|
||||
/** 动态加载 parent-portal Remote(仅 CSR,加载失败返回降级组件) */
|
||||
const ParentRemote = dynamic(
|
||||
() =>
|
||||
import("parent/ParentApp")
|
||||
.then((mod) => mod.default)
|
||||
.catch(() => FallbackRemote),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<p className="text-sm text-ink-muted p-6">正在加载家长端视图…</p>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
/** Remote 加载失败时的降级组件 */
|
||||
function FallbackRemote(): React.ReactNode {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
<p className="text-lg font-serif text-ink">家长端视图加载失败</p>
|
||||
<p className="mt-2 text-sm text-ink-muted">
|
||||
请确认 parent-portal 服务已启动(端口 4002)并已暴露 ParentApp
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** MF 未启用时的提示 */
|
||||
function MfDisabledNotice(): React.ReactNode {
|
||||
return (
|
||||
<div className="p-8 border border-rule rounded-card bg-surface text-center">
|
||||
<p className="text-lg font-serif text-ink">
|
||||
家长端视图需启用微前端模式
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-ink-muted">
|
||||
设置环境变量 NEXT_PUBLIC_MF_ENABLED=true 后重启服务,
|
||||
并确保 parent-portal 已在 next.config.js remotes 中注册。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ParentPortalRemote(): React.ReactNode {
|
||||
if (!MF_ENABLED) {
|
||||
return <MfDisabledNotice />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex flex-col items-center justify-center gap-4 p-8"
|
||||
>
|
||||
<h2 className="text-lg font-serif text-ink">
|
||||
家长端视图渲染异常
|
||||
</h2>
|
||||
<p className="text-sm text-ink-muted">{error.message}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="px-4 py-2 text-sm text-ink-on-accent bg-accent rounded-button hover:bg-accent-hover"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ParentRemote />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 可观测性 Provider 组件(P6 硬化)
|
||||
*
|
||||
* 整合 Sentry + Web Vitals + OTel 初始化,在应用启动时调用各 init 函数。
|
||||
* - "use client" 客户端组件
|
||||
* - 在 useEffect 中按需初始化各可观测性模块(动态 import 避免未安装包问题)
|
||||
* - 不渲染任何可见 UI(返回 children)
|
||||
* - 条件初始化:根据环境变量决定是否启用各模块
|
||||
*
|
||||
* 关联:02-architecture-design.md §12 可观测性 / project_rules §12 可观测性规范
|
||||
*/
|
||||
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* 可观测性 Provider。
|
||||
*
|
||||
* 在客户端挂载时初始化 Sentry / Web Vitals / OTel。
|
||||
* 所有初始化均为条件性:未配置对应环境变量时跳过,未安装包时降级。
|
||||
*
|
||||
* 不渲染任何可见 UI,仅透传 children。
|
||||
*/
|
||||
export function ObservabilityProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): ReactNode {
|
||||
useEffect(() => {
|
||||
// 并行初始化各可观测性模块(互不依赖)
|
||||
const initObservability = async (): Promise<void> => {
|
||||
// 1. Sentry 错误追踪(条件:NEXT_PUBLIC_SENTRY_DSN 配置时)
|
||||
try {
|
||||
const { initSentry } = await import(
|
||||
"@/lib/observability/sentry"
|
||||
);
|
||||
await initSentry();
|
||||
} catch (err) {
|
||||
if (typeof console !== "undefined") {
|
||||
console.warn("[teacher-portal] Sentry 初始化失败", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Web Vitals RUM 采集(生产环境启用)
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
try {
|
||||
const { initWebVitals } = await import(
|
||||
"@/lib/observability/web-vitals"
|
||||
);
|
||||
await initWebVitals();
|
||||
} catch (err) {
|
||||
if (typeof console !== "undefined") {
|
||||
console.warn("[teacher-portal] Web Vitals 初始化失败", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. OTel browser SDK(条件:NEXT_PUBLIC_OTEL_ENABLED=true 时)
|
||||
try {
|
||||
const { initOTel } = await import("@/lib/observability/otel");
|
||||
await initOTel();
|
||||
} catch (err) {
|
||||
if (typeof console !== "undefined") {
|
||||
console.warn("[teacher-portal] OTel 初始化失败", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Cookie 迁移(条件:NEXT_PUBLIC_COOKIE_MIGRATION_ENABLED=true 时)
|
||||
// 实际迁移在 iam refresh cookie 端点就绪后启用,此处仅做准备
|
||||
try {
|
||||
const { migrateTokenToCookie } = await import(
|
||||
"@/lib/observability/cookie-migration"
|
||||
);
|
||||
// 后台执行迁移,不阻塞应用渲染
|
||||
void migrateTokenToCookie();
|
||||
} catch {
|
||||
// 迁移失败不影响应用功能,静默忽略
|
||||
}
|
||||
};
|
||||
|
||||
// 后台初始化,不阻塞渲染
|
||||
void initObservability();
|
||||
}, []);
|
||||
|
||||
// 不渲染任何可见 UI,仅透传 children
|
||||
return children;
|
||||
}
|
||||
|
||||
export default ObservabilityProvider;
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Web Vitals 采集初始化组件(P6 硬化)
|
||||
*
|
||||
* 参考 admin-portal/src/components/web-vitals-initializer.tsx 实现。
|
||||
* - "use client" 客户端组件
|
||||
* - 在 useEffect 中调用 initWebVitals()(动态 import 避免未安装包问题)
|
||||
* - 不渲染任何可见 UI
|
||||
*
|
||||
* 仲裁:所有前端应用必须采集 Web Vitals
|
||||
* 关联:02-architecture-design.md §12 可观测性
|
||||
*/
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Web Vitals 采集初始化组件。
|
||||
*
|
||||
* 仅在生产环境启用采集,开发环境跳过以避免控制台噪音。
|
||||
*/
|
||||
export function WebVitalsInitializer(): null {
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV !== "production") return;
|
||||
// 动态 import 避免未安装包导致构建失败
|
||||
void import("@/lib/observability/web-vitals").then(
|
||||
({ initWebVitals }) => {
|
||||
void initWebVitals();
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default WebVitalsInitializer;
|
||||
Reference in New Issue
Block a user