From d49d2114251375889776f9f74dfeca881b9ecc2d Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:27:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(teacher-portal):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=8F=82=E8=80=83=E9=A1=B9=E7=9B=AE=E5=B7=AE=E8=B7=9D=E9=97=AD?= =?UTF-8?q?=E7=8E=AF=20P3-P7=20=E5=85=A8=E9=87=8F=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 参考项目差距闭环(含完整文件清单) --- apps/teacher-portal/.eslintrc.a11y.js | 97 ++ apps/teacher-portal/package.json | 15 +- apps/teacher-portal/size-limit.json | 25 + .../src/app/(app)/ai-assist/page.tsx | 348 +++++++ .../src/app/(app)/ai-lesson-plan/page.tsx | 259 +++++ .../src/app/(app)/ai-report/page.tsx | 289 ++++++ .../src/app/(app)/analytics/TrendChart.tsx | 162 ++++ .../app/(app)/analytics/[studentId]/page.tsx | 170 ++++ .../src/app/(app)/analytics/page.tsx | 287 ++++++ .../src/app/(app)/attendance/page.tsx | 341 +++++++ .../src/app/(app)/attendance/report/page.tsx | 337 +++++++ .../src/app/(app)/attendance/sheet/page.tsx | 344 +++++++ .../src/app/(app)/attendance/stats/page.tsx | 551 +++++++++++ .../src/app/(app)/classes/[id]/page.tsx | 492 ++++++++++ .../src/app/(app)/classes/schedule/page.tsx | 250 +++++ .../src/app/(app)/course-plans/[id]/page.tsx | 190 ++++ .../src/app/(app)/course-plans/page.tsx | 366 ++++++++ .../(app)/diagnostic/class/[classId]/page.tsx | 235 +++++ .../src/app/(app)/diagnostic/page.tsx | 195 ++++ .../src/app/(app)/elective/[id]/edit/page.tsx | 397 ++++++++ .../src/app/(app)/elective/create/page.tsx | 240 +++++ .../src/app/(app)/elective/page.tsx | 244 +++++ .../src/app/(app)/error-book/page.tsx | 519 ++++++++++ .../app/(app)/exams/[id]/analytics/page.tsx | 486 ++++++++++ .../app/(app)/exams/[id]/edit-rich/page.tsx | 328 +++++++ .../src/app/(app)/exams/[id]/page.tsx | 228 +++++ .../app/(app)/exams/[id]/proctoring/page.tsx | 388 ++++++++ .../src/app/(app)/exams/new/page.tsx | 249 +++++ .../src/app/(app)/exams/page.tsx | 54 +- .../src/app/(app)/grades/analytics/charts.tsx | 598 ++++++++++++ .../src/app/(app)/grades/analytics/page.tsx | 217 +++++ .../src/app/(app)/grades/entry/page.tsx | 325 +++++++ .../src/app/(app)/grades/page.tsx | 477 +++++++++- .../src/app/(app)/grades/report-card/page.tsx | 280 ++++++ .../src/app/(app)/grades/stats/page.tsx | 285 ++++++ .../src/app/(app)/homework/[id]/page.tsx | 319 +++++++ .../assignments/[id]/submissions/page.tsx | 267 ++++++ .../src/app/(app)/homework/new/page.tsx | 210 +++++ .../src/app/(app)/homework/page.tsx | 54 +- .../submissions/[submissionId]/page.tsx | 486 ++++++++++ .../[submissionId]/scan-grading/page.tsx | 448 +++++++++ .../app/(app)/homework/submissions/page.tsx | 203 ++++ .../knowledge-graph/KnowledgeGraphView.tsx | 399 ++++++++ .../src/app/(app)/knowledge-graph/page.tsx | 90 ++ .../src/app/(app)/leave/page.tsx | 607 ++++++++++++ .../(app)/lesson-plans/[planId]/edit/page.tsx | 497 ++++++++++ .../app/(app)/lesson-plans/calendar/page.tsx | 217 +++++ .../app/(app)/lesson-plans/heatmap/page.tsx | 377 ++++++++ .../app/(app)/lesson-plans/library/page.tsx | 185 ++++ .../src/app/(app)/lesson-plans/new/page.tsx | 220 +++++ .../src/app/(app)/lesson-plans/page.tsx | 162 ++++ .../src/app/(app)/notifications/page.tsx | 260 +++++ .../src/app/(app)/practice/page.tsx | 451 +++++++++ .../src/app/(app)/questions/page.tsx | 886 ++++++++++++++++++ .../src/app/(app)/schedule-changes/page.tsx | 629 +++++++++++++ .../src/app/(app)/settings/page.tsx | 154 ++- .../src/app/(app)/textbooks/[id]/page.tsx | 454 +++++++++ .../src/app/(app)/textbooks/page.tsx | 478 ++++++++++ apps/teacher-portal/src/app/layout.tsx | 9 +- .../src/components/AppShell.tsx | 15 +- .../src/components/ParentPortalRemote.tsx | 90 ++ .../src/components/observability-provider.tsx | 90 ++ .../src/components/web-vitals-initializer.tsx | 36 + .../src/hooks/use-cross-tab-sync.ts | 105 +++ .../src/hooks/use-notifications-websocket.ts | 175 ++++ apps/teacher-portal/src/lib/graphql-p4.ts | 122 +++ apps/teacher-portal/src/lib/graphql-p5.ts | 137 +++ .../src/lib/graphql-p7-admin.ts | 713 ++++++++++++++ .../src/lib/graphql-p7-advanced.ts | 499 ++++++++++ .../src/lib/graphql-p7-exams.ts | 628 +++++++++++++ .../src/lib/graphql-p7-grades.ts | 418 +++++++++ .../src/lib/graphql-p7-insights.ts | 737 +++++++++++++++ apps/teacher-portal/src/lib/graphql.ts | 170 ++++ .../src/lib/observability/a11y.ts | 272 ++++++ .../src/lib/observability/cookie-migration.ts | 189 ++++ .../src/lib/observability/env.ts | 113 +++ .../src/lib/observability/otel.ts | 140 +++ .../src/lib/observability/performance.ts | 230 +++++ .../src/lib/observability/sentry.ts | 193 ++++ .../src/lib/observability/web-vitals.ts | 131 +++ apps/teacher-portal/src/mocks/browser.ts | 20 +- apps/teacher-portal/src/mocks/fixtures/ai.ts | 94 ++ .../src/mocks/fixtures/analytics.ts | 105 +++ .../src/mocks/fixtures/attendance.ts | 501 ++++++++++ .../src/mocks/fixtures/classes-extended.ts | 265 ++++++ .../src/mocks/fixtures/course-plans.ts | 218 +++++ .../src/mocks/fixtures/diagnostic.ts | 233 +++++ .../src/mocks/fixtures/elective.ts | 298 ++++++ .../src/mocks/fixtures/error-book.ts | 182 ++++ .../src/mocks/fixtures/exam-analytics.ts | 353 +++++++ .../src/mocks/fixtures/exams.ts | 69 +- .../src/mocks/fixtures/grades-extended.ts | 403 ++++++++ .../src/mocks/fixtures/grades.ts | 17 +- .../mocks/fixtures/homework-submissions.ts | 344 +++++++ .../src/mocks/fixtures/homework.ts | 83 +- .../src/mocks/fixtures/knowledge-graph.ts | 146 +++ .../src/mocks/fixtures/leave-requests.ts | 458 +++++++++ .../src/mocks/fixtures/lesson-plan-heatmap.ts | 157 ++++ .../src/mocks/fixtures/lesson-plans.ts | 448 +++++++++ .../src/mocks/fixtures/notifications.ts | 77 ++ .../src/mocks/fixtures/practice.ts | 152 +++ .../src/mocks/fixtures/proctoring.ts | 154 +++ .../src/mocks/fixtures/questions.ts | 391 ++++++++ .../src/mocks/fixtures/scan-grading.ts | 163 ++++ .../src/mocks/fixtures/textbooks.ts | 249 +++++ .../src/mocks/fixtures/viewports.ts | 136 +++ apps/teacher-portal/src/mocks/handlers-p4.ts | 108 +++ apps/teacher-portal/src/mocks/handlers-p5.ts | 137 +++ .../src/mocks/handlers-p7-admin.ts | 249 +++++ .../src/mocks/handlers-p7-advanced.ts | 241 +++++ .../src/mocks/handlers-p7-exams.ts | 268 ++++++ .../src/mocks/handlers-p7-grades.ts | 163 ++++ .../src/mocks/handlers-p7-insights.ts | 299 ++++++ apps/teacher-portal/src/mocks/handlers.ts | 51 +- apps/teacher-portal/src/mocks/server.ts | 20 +- apps/teacher-portal/src/types/mf-remotes.d.ts | 15 + .../worklines/teacher-portal_workline.md | 175 +++- 117 files changed, 30867 insertions(+), 108 deletions(-) create mode 100644 apps/teacher-portal/.eslintrc.a11y.js create mode 100644 apps/teacher-portal/size-limit.json create mode 100644 apps/teacher-portal/src/app/(app)/ai-assist/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/ai-lesson-plan/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/ai-report/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/analytics/TrendChart.tsx create mode 100644 apps/teacher-portal/src/app/(app)/analytics/[studentId]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/analytics/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/attendance/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/attendance/report/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/attendance/sheet/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/attendance/stats/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/classes/[id]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/classes/schedule/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/course-plans/[id]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/course-plans/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/diagnostic/class/[classId]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/diagnostic/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/elective/[id]/edit/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/elective/create/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/elective/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/error-book/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/exams/[id]/analytics/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/exams/[id]/edit-rich/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/exams/[id]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/exams/[id]/proctoring/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/exams/new/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/grades/analytics/charts.tsx create mode 100644 apps/teacher-portal/src/app/(app)/grades/analytics/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/grades/entry/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/grades/report-card/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/grades/stats/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/homework/[id]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/homework/assignments/[id]/submissions/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/homework/new/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/homework/submissions/[submissionId]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/homework/submissions/[submissionId]/scan-grading/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/homework/submissions/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/knowledge-graph/KnowledgeGraphView.tsx create mode 100644 apps/teacher-portal/src/app/(app)/knowledge-graph/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/leave/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/lesson-plans/[planId]/edit/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/lesson-plans/calendar/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/lesson-plans/heatmap/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/lesson-plans/library/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/lesson-plans/new/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/lesson-plans/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/notifications/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/practice/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/questions/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/schedule-changes/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/textbooks/[id]/page.tsx create mode 100644 apps/teacher-portal/src/app/(app)/textbooks/page.tsx create mode 100644 apps/teacher-portal/src/components/ParentPortalRemote.tsx create mode 100644 apps/teacher-portal/src/components/observability-provider.tsx create mode 100644 apps/teacher-portal/src/components/web-vitals-initializer.tsx create mode 100644 apps/teacher-portal/src/hooks/use-cross-tab-sync.ts create mode 100644 apps/teacher-portal/src/hooks/use-notifications-websocket.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p4.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p5.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p7-admin.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p7-advanced.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p7-exams.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p7-grades.ts create mode 100644 apps/teacher-portal/src/lib/graphql-p7-insights.ts create mode 100644 apps/teacher-portal/src/lib/observability/a11y.ts create mode 100644 apps/teacher-portal/src/lib/observability/cookie-migration.ts create mode 100644 apps/teacher-portal/src/lib/observability/env.ts create mode 100644 apps/teacher-portal/src/lib/observability/otel.ts create mode 100644 apps/teacher-portal/src/lib/observability/performance.ts create mode 100644 apps/teacher-portal/src/lib/observability/sentry.ts create mode 100644 apps/teacher-portal/src/lib/observability/web-vitals.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/ai.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/analytics.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/attendance.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/classes-extended.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/course-plans.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/diagnostic.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/elective.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/error-book.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/exam-analytics.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/grades-extended.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/homework-submissions.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/knowledge-graph.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/leave-requests.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/lesson-plan-heatmap.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/lesson-plans.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/notifications.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/practice.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/proctoring.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/questions.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/scan-grading.ts create mode 100644 apps/teacher-portal/src/mocks/fixtures/textbooks.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p4.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p5.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p7-admin.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p7-advanced.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p7-exams.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p7-grades.ts create mode 100644 apps/teacher-portal/src/mocks/handlers-p7-insights.ts create mode 100644 apps/teacher-portal/src/types/mf-remotes.d.ts diff --git a/apps/teacher-portal/.eslintrc.a11y.js b/apps/teacher-portal/.eslintrc.a11y.js new file mode 100644 index 0000000..29f75ae --- /dev/null +++ b/apps/teacher-portal/.eslintrc.a11y.js @@ -0,0 +1,97 @@ +/** + * ESLint A11y 配置(P6 硬化) + * + * - 配置 eslint-plugin-jsx-a11y(error 级别) + * - 包含 WCAG 2.2 AA 规则集 + * + * 这是独立配置文件,不修改主 eslint 配置。 + * 使用方式:eslint -c .eslintrc.a11y.js src + * + * 注意:eslint-plugin-jsx-a11y 尚未安装,CI 统一安装后启用。 + * 关联:02-architecture-design.md §12 可观测性 / WCAG 2.2 AA + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + ecmaFeatures: { jsx: true }, + }, + plugins: ["jsx-a11y"], + rules: { + // WCAG 2.2 AA 规则集(全部 error 级别) + + // 1.1.1 非文本内容:所有图片必须有 alt 属性 + "jsx-a11y/alt-text": "error", + + // 1.3.1 信息与关系:表单控件必须有 label + "jsx-a11y/label-has-associated-control": "error", + "jsx-a11y/control-has-associated-label": "error", + + // 1.4.1 颜色使用:不仅依赖颜色传达信息 + "jsx-a11y/no-noninteractive-element-interactions": "error", + + // 2.1.1 键盘可达性:所有交互元素必须键盘可操作 + "jsx-a11y/no-static-element-interactions": "error", + + // 2.1.2 无键盘陷阱:焦点必须能移出组件 + "jsx-a11y/no-autofocus": "error", + + // 2.4.1 跳过块:提供跳过重复内容的机制 + "jsx-a11y/anchor-is-valid": "error", + + // 2.4.4 链接目的:链接文本必须有意义 + "jsx-a11y/anchor-has-content": "error", + + // 2.4.6 标题与标签:标题必须描述性 + "jsx-a11y/heading-has-content": "error", + + // 3.1.2 每种语言的部分:html lang 属性 + "jsx-a11y/html-has-lang": "error", + "jsx-a11y/lang": "error", + + // 3.2.2 输入时:表单提交可预测 + "jsx-a11y/no-onchange": "error", + + // 3.3.2 标签或指令:表单输入需要描述 + "jsx-a11y/tabindex-no-positive": "error", + + // 4.1.2 名称、角色、值:ARIA 属性正确使用 + "jsx-a11y/aria-props": "error", + "jsx-a11y/aria-proptypes": "error", + "jsx-a11y/aria-unsupported-elements": "error", + "jsx-a11y/role-has-required-aria-props": "error", + "jsx-a11y/role-supports-aria-props": "error", + + // 4.1.3 状态消息:ARIA live regions + "jsx-a11y/aria-activedescendant-has-tabindex": "error", + + // 额外规则:交互元素的角色 + "jsx-a11y/interactive-supports-focus": "error", + "jsx-a11y/no-interactive-element-to-noninteractive-role": "error", + "jsx-a11y/no-noninteractive-element-to-interactive-role": "error", + "jsx-a11y/no-redundant-roles": "error", + "jsx-a11y/role": "error", + + // 媒体可访问性 + "jsx-a11y/media-has-caption": "error", + + // 鼠标/指针事件可访问性 + "jsx-a11y/mouse-events-have-key-events": "error", + "jsx-a11y/click-events-have-key-events": "error", + + // 分散元素(blink/marquee 禁用) + "jsx-a11y/no-distracting-elements": "error", + }, + overrides: [ + { + // 允许在测试文件中使用 jsx-a11y 断言 + files: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"], + rules: { + "jsx-a11y/anchor-is-valid": "off", + }, + }, + ], +}; diff --git a/apps/teacher-portal/package.json b/apps/teacher-portal/package.json index 0b632a7..a8d2672 100644 --- a/apps/teacher-portal/package.json +++ b/apps/teacher-portal/package.json @@ -13,21 +13,34 @@ "@edu/hooks": "workspace:*", "@edu/ui-components": "workspace:*", "@edu/ui-tokens": "workspace:*", + "@opentelemetry/context-zone": "^1.28.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation-document-load": "^0.57.0", + "@opentelemetry/instrumentation-fetch": "^0.57.0", + "@opentelemetry/instrumentation-xml-http-request": "^0.57.0", + "@opentelemetry/sdk-trace-web": "^1.28.0", + "@sentry/nextjs": "^8.0.0", "graphql": "^16.8.0", "next": "^14.2.0", "react": "^18.3.0", "react-dom": "^18.3.0", - "urql": "^2.2.0" + "urql": "^2.2.0", + "web-vitals": "^4.2.0" }, "devDependencies": { "@module-federation/nextjs-mf": "^8.3.0", + "@next/bundle-analyzer": "^14.2.0", "@types/node": "^22.0.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.0", + "axe-core": "^4.10.0", "eslint": "^9.0.0", + "eslint-plugin-jsx-a11y": "^6.9.0", "msw": "^2.4.0", "postcss": "^8.4.0", + "size-limit": "^11.1.0", "tailwindcss": "^3.4.0", "typescript": "^5.6.0" }, diff --git a/apps/teacher-portal/size-limit.json b/apps/teacher-portal/size-limit.json new file mode 100644 index 0000000..2abb6b4 --- /dev/null +++ b/apps/teacher-portal/size-limit.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/ai/size-limit/main/schema.json", + "name": "teacher-portal bundle size limits", + "description": "P6 性能预算:Shell <150KB / Remote <80KB / CSS <50KB", + "checks": [ + { + "name": "Shell (main bundle)", + "path": ".next/static/chunks/main-*.js", + "limit": "150 KB", + "gzip": true + }, + { + "name": "Remote entry", + "path": ".next/static/chunks/remoteEntry-*.js", + "limit": "80 KB", + "gzip": true + }, + { + "name": "CSS", + "path": ".next/static/css/*.css", + "limit": "50 KB", + "gzip": true + } + ] +} diff --git a/apps/teacher-portal/src/app/(app)/ai-assist/page.tsx b/apps/teacher-portal/src/app/(app)/ai-assist/page.tsx new file mode 100644 index 0000000..c5ce7db --- /dev/null +++ b/apps/teacher-portal/src/app/(app)/ai-assist/page.tsx @@ -0,0 +1,348 @@ +"use client"; + +/** + * AI 辅助出题页面(P5) + * + * 数据来源(02-architecture-design.md §6 AI 辅助): + * - GraphQL GenerateQuestionMutation + * + * 功能: + * - 表单:科目 / 题型 / 难度 / 知识点 / 题目数量 + * - 生成题目(mutation)+ SSE 流式效果模拟(setInterval 逐字显示) + * - 完整题目展示(含选项、答案、解析) + * - 复制到剪贴板 / 再生成一组 + * - 生成历史(本次会话内) + * + * 维护者:ai13(teacher-portal) + */ + +import { useEffect, useRef, useState } from "react"; +import { useMutation } from "urql"; +import { GenerateQuestionMutation } from "@/lib/graphql-p5"; +import type { GeneratedQuestion, GenerateQuestionInput } from "@/lib/graphql-p5"; + +const SUBJECTS = ["数学", "语文", "英语", "物理", "化学"]; +const QUESTION_TYPES = ["选择题", "填空题", "解答题"]; +const DIFFICULTIES = ["简单", "中等", "困难"]; + +const STREAM_INTERVAL_MS = 25; + +function buildQuestionText(q: GeneratedQuestion): string { + return [ + q.question, + "", + ...q.options, + "", + `答案:${q.answer}`, + "", + `解析:${q.explanation}`, + ].join("\n"); +} + +export default function AiAssistPage(): JSX.Element { + const [subject, setSubject] = useState(SUBJECTS[0] ?? "数学"); + const [questionType, setQuestionType] = useState( + QUESTION_TYPES[0] ?? "选择题", + ); + const [difficulty, setDifficulty] = useState(DIFFICULTIES[1] ?? "中等"); + const [knowledgePoints, setKnowledgePoints] = useState("导数应用"); + const [count, setCount] = useState(1); + + const [isGenerating, setIsGenerating] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [streamingText, setStreamingText] = useState(""); + const [generated, setGenerated] = useState(null); + const [history, setHistory] = useState([]); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + const streamTimerRef = useRef | null>(null); + const [, generateQuestion] = useMutation(GenerateQuestionMutation); + + useEffect(() => { + return () => { + if (streamTimerRef.current) clearInterval(streamTimerRef.current); + }; + }, []); + + const handleGenerate = async (): Promise => { + if (isGenerating || isStreaming) return; + setIsGenerating(true); + setError(null); + setGenerated(null); + setStreamingText(""); + setCopied(false); + + const input: GenerateQuestionInput = { + subject, + questionType, + difficulty, + knowledgePoints, + count, + }; + + const res = await generateQuestion({ input }); + setIsGenerating(false); + + if (res.error || !res.data?.generateQuestion) { + setError(res.error?.message ?? "生成失败,请重试"); + return; + } + + const q = res.data.generateQuestion as GeneratedQuestion; + const full = q.question; + + setIsStreaming(true); + let i = 0; + streamTimerRef.current = setInterval(() => { + i += 1; + setStreamingText(full.slice(0, i)); + if (i >= full.length) { + if (streamTimerRef.current) { + clearInterval(streamTimerRef.current); + streamTimerRef.current = null; + } + setIsStreaming(false); + setGenerated(q); + setHistory((prev) => [q, ...prev].slice(0, 20)); + } + }, STREAM_INTERVAL_MS); + }; + + const handleCopy = (): void => { + if (!generated) return; + const text = buildQuestionText(generated); + if (typeof navigator !== "undefined" && navigator.clipboard) { + void navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + } + }; + + return ( +
+
+

AI 辅助出题

+

+ GraphQL GenerateQuestionMutation · SSE 流式输出模拟(P5) +

+
+ +
+ + {/* 生成表单 */} +
+

出题参数

+
+ + + + + +
+ +
+ + {generated && !isStreaming && ( + + )} +
+
+ + {error && ( +
+

{error}

+
+ )} + + {/* 生成结果(流式 + 完整) */} + {(isGenerating || isStreaming || streamingText || generated) && ( +
+
+

+ 生成结果 + {isGenerating && ( + + 正在请求 AI… + + )} + {isStreaming && ( + + 流式输出中… + + )} +

+ {generated && !isStreaming && ( + + )} +
+
+ + {isStreaming || !generated ? ( +
+ {streamingText} + {isStreaming && ( + + )} +
+ ) : ( +
+

+ {generated.question} +

+
    + {generated.options.map((opt) => ( +
  • + {opt} +
  • + ))} +
+

+ + 答案 + {" "} + {generated.answer} +

+

+ + 解析 + + {"\n"} + {generated.explanation} +

+
+ )} +
+ )} + + {/* 生成历史 */} + {history.length > 0 && ( +
+
+

+ 生成历史 + + {history.length} 条 + +

+
+
+
    + {history.map((h) => ( +
  • +
    +

    {h.question}

    +

    + 答案:{h.answer} +

    +
    +
    + {h.id} +
    +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/apps/teacher-portal/src/app/(app)/ai-lesson-plan/page.tsx b/apps/teacher-portal/src/app/(app)/ai-lesson-plan/page.tsx new file mode 100644 index 0000000..a2e0ed6 --- /dev/null +++ b/apps/teacher-portal/src/app/(app)/ai-lesson-plan/page.tsx @@ -0,0 +1,259 @@ +"use client"; + +/** + * AI 教案生成页面(P5) + * + * 数据来源(02-architecture-design.md §6 AI 辅助): + * - GraphQL GenerateLessonPlanMutation + * + * 功能: + * - 表单:班级 / 科目 / 课时主题 / 教学目标 + * - 生成教案(mutation)+ 流式输出模拟(setInterval 逐字显示) + * - 完整教案展示 + * - 导出为 .md 文件 + * + * 维护者:ai13(teacher-portal) + */ + +import { useEffect, useRef, useState } from "react"; +import { useMutation, useQuery } from "urql"; +import { GenerateLessonPlanMutation } from "@/lib/graphql-p5"; +import type { + GeneratedLessonPlan, + GenerateLessonPlanInput, +} from "@/lib/graphql-p5"; +import { ClassesQuery } from "@/lib/graphql"; +import type { Class } from "@/lib/graphql"; +import { Loading } from "@edu/ui-components"; + +const SUBJECTS = ["数学", "语文", "英语", "物理", "化学"]; +const STREAM_INTERVAL_MS = 12; + +export default function AiLessonPlanPage(): JSX.Element { + const [classId, setClassId] = useState(""); + const [subject, setSubject] = useState(SUBJECTS[0] ?? "数学"); + const [topic, setTopic] = useState("函数的单调性与极值"); + const [objectives, setObjectives] = useState( + "理解单调性、掌握极值求法、能解决实际问题", + ); + + const [isGenerating, setIsGenerating] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [streamingText, setStreamingText] = useState(""); + const [generated, setGenerated] = useState(null); + const [error, setError] = useState(null); + + const streamTimerRef = useRef | null>(null); + const [, generateLessonPlan] = useMutation(GenerateLessonPlanMutation); + const [classesResult] = useQuery({ query: ClassesQuery }); + const classes = (classesResult.data?.classes as Class[] | undefined) ?? []; + + useEffect(() => { + if (!classId) { + const first = classes[0]; + if (first) setClassId(first.id); + } + }, [classes, classId]); + + useEffect(() => { + return () => { + if (streamTimerRef.current) clearInterval(streamTimerRef.current); + }; + }, []); + + const handleGenerate = async (): Promise => { + if (isGenerating || isStreaming) return; + setIsGenerating(true); + setError(null); + setGenerated(null); + setStreamingText(""); + + const input: GenerateLessonPlanInput = { + classId, + subject, + topic, + objectives, + }; + + const res = await generateLessonPlan({ input }); + setIsGenerating(false); + + if (res.error || !res.data?.generateLessonPlan) { + setError(res.error?.message ?? "生成失败,请重试"); + return; + } + + const lp = res.data.generateLessonPlan as GeneratedLessonPlan; + const full = lp.content; + + setIsStreaming(true); + let i = 0; + streamTimerRef.current = setInterval(() => { + i += 1; + setStreamingText(full.slice(0, i)); + if (i >= full.length) { + if (streamTimerRef.current) { + clearInterval(streamTimerRef.current); + streamTimerRef.current = null; + } + setIsStreaming(false); + setGenerated(lp); + } + }, STREAM_INTERVAL_MS); + }; + + const handleExport = (): void => { + if (!generated) return; + const blob = new Blob([generated.content], { + type: "text/markdown;charset=utf-8", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `lesson-plan-${generated.id}.md`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+

AI 教案生成

+

+ GraphQL GenerateLessonPlanMutation · 流式输出模拟(P5) +

+
+ +
+ + {/* 表单 */} +
+

教案参数

+ {classesResult.fetching && classes.length === 0 ? ( + + ) : ( +
+ + + +