fix(portal-shell): 管理域 UI 规范合规与 TypeScript 修复
- 替换 41 处原生 select 为 Select 组件封装 - 替换 5 处 window.confirm 为 shadcn AlertDialog - 修复 lesson-plans delete-confirm-dialog 为 AlertDialog - 修复 5 处 Tailwind 任意值 text-[10px] - 修复 graphql-data.ts mutation case 缺少 id 定义 - 修复 use-position-persistence.ts eslint 规则引用
This commit is contained in:
@@ -727,52 +727,74 @@
|
||||
|
||||
> v2.1 架构:单 Next.js App Router 容器 + Micro-kernel 插件系统,替代旧 4 端微前端。关联 spec `2026-07-14-portal-shell-widget-dashboard-design.md`。
|
||||
|
||||
| 场景 | 技术/规则 |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 插件契约跨包共享 | `packages/shared-ts/src/contracts/` 定义 PluginProps/PluginManifest/Layout 类型;`PluginManifest.Component` 用 `unknown`(shared-ts 不依赖 React),前端包引用时断言为 `React.ComponentType<PluginProps>` |
|
||||
| TVars 泛型约束与 TS interface 不兼容 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下,widget 文件用 `interface XxxVars` 会报 TS2344;改用 `type XxxVars = {...}` 类型别名(type alias 满足 index signature,interface 不满足) |
|
||||
| shared-ts contracts 子路径导出 | `package.json` exports 新增 `"./contracts"` 指向 `./dist/contracts/index.js`;hooks 包通过 `@edu/shared-ts/contracts` 引用;typecheck 前需 `pnpm --filter @edu/shared-ts run build` 编译到 dist/ |
|
||||
| Vitest 解析 workspace 子路径导出失败 | vitest.config.ts resolve.alias 需显式添加 `"@edu/shared-ts/contracts"` → 源码路径;Vite 不自动解析 package.json exports 字段 |
|
||||
| portal-shell tsconfig paths 补全 | tsconfig.json paths 需添加 `"@edu/shared-ts/contracts": ["../../packages/shared-ts/src/contracts/index.ts"]`,否则 tsc 报 TS2307 |
|
||||
| 插件 dynamic import 注册 | `Registry.tsx` 用 `next/dynamic`(ssr:false)懒加载 31 个内置插件;loading 展示 `PluginSkeleton` 变体(card/list/table/stats/chart) |
|
||||
| URL Search Params + Zustand 状态分层 | URL 驱动可分享上下文(classId/childId/termId/view/examId/subjectId);Zustand 管理纯 UI 状态(theme/locale/sidebarCollapsed);插件间禁止直接 import |
|
||||
| useWidgetMutation API 签名 | 返回 `{ run, ...result }`,`run` 是 `async (variables: TVars): Promise<TData \| undefined>`;消费者用 `const { run } = useWidgetMutation(...)` 解构 |
|
||||
| usePluginConfig 不直接调 API | fetcher 由消费者注入,保持 @edu/hooks "hooks 不直接调 API" 原则;支持轮询刷新(refreshInterval 默认 5 分钟)+ 网络恢复刷新 + visibilitychange 刷新 |
|
||||
| usePluginStore 注入模式 | `useSyncExternalStore` + `injectPluginStore()` 让 hooks 包不直接依赖特定 store 实例;未注入 store 时返回 DEFAULT_STATE 空操作 |
|
||||
| 28 个内置插件分类 | universal(7) + sidebar(4) + topbar(4) + teacher(4) + student(4) + parent(2) + admin(3);每个插件含 `index.tsx` + `plugin.manifest.ts`,manifest 导出 `manifestMeta: Omit<PluginManifest, "Component">` |
|
||||
| PluginLifecycle 版本兼容 | MVP 只校验 major 版本(`checkVersionCompatibility` 解析 `^`/`~`/`>=` 前缀 + major 数字);完整 semver range 校验待引入 semver 库 |
|
||||
| pnpm install --no-frozen-lockfile | parent-portal package.json 新增 `@opentelemetry/instrumentation-document-load` 后 lockfile 过期;开发环境用 `--no-frozen-lockfile` 安装,CI 用 frozen |
|
||||
| Widget 内联 gql 字面量废弃 | 禁止 `const Q = gql\`...\``直接写在 widget;统一抽取到`lib/api/operations/*.graphql.ts`,widget 只 import `lib/api/<domain>.ts` 的语义化函数(ADR-042) |
|
||||
| 4 层数据访问分层 | Widget(UI)→ API(语义化函数)→ Operations(gql DocumentNode 集中)→ Hook(useWidgetQuery/useWidgetMutation 封装 Apollo);codegen 从 7 子图 schema 生成类型 |
|
||||
| graphql-codegen skipDocumentsValidation | 全局 `skipDocumentsValidation: true` 保留(子图未启动时校验 operations 失败);分域关闭:data-ana 域 dashboard-types.ts 输出已关 `skipDocumentsValidation: false`(P1-7),config 域待 `LayoutTemplateGql.availableSlots` schema 落地后关闭;schema 归一化脚本移除 federation 指令(@key/@requires/@extends)防止误解析 |
|
||||
| useNotifications 命名冲突 | `universal.ts` 与 `topbar.ts` 同时导出 `useNotifications` 在 barrel `index.ts` 冲突(TS2308);topbar 改名 `useNotificationBell`(铃铛专用,限 N 条) |
|
||||
| APQ(Automatic Persisted Queries) | `apollo-client.ts` 用 `createPersistedQueryLink({ sha256 })` + `crypto-hash`;env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭;Link 链顺序:authLink → pqLink → httpLink |
|
||||
| PQ Manifest 生成 | `scripts/generate-pq-manifest.ts` 遍历 `operations/index.ts` 中 DocumentNode,`print(doc)` + `sha256(query)` 写入 `public/pq-manifest.json`;`prebuild` 钩子串联 codegen + generate |
|
||||
| Windows ESM 动态 import 路径 | Node ESM 动态 `import()` 不支持 Windows 盘符路径(`e:\...`),必须 `url.pathToFileURL(path).href` 转 `file://` URL 再 import |
|
||||
| apollo-router PQ manifest 挂载 | docker-compose 把 `apps/portal-shell/public/pq-manifest.json` 挂载到 router `/etc/apollo-router/pq-manifest.json:ro`;entrypoint.sh 启动前校验 `APOLLO_REQUIRE_PQ_MANIFEST=true` 时文件存在性 |
|
||||
| CI 结构性检查三脚本(P1-8) | `scripts/check-route-table.ts`(路由表一致性:实际 `/shell/*` 路由必须登记到 route-permissions.ts)+ `scripts/check-page-count.ts`(页面计数 baseline=13)+ `scripts/check-codegen.ts`(codegen 契约校验:skipDocumentsValidation:false 输出严格校验);CI 接线 `.github/workflows/ci.yml` quality-ts job |
|
||||
| apollo-router 安全限制 | `router.yaml` 配置 `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5`;`supergraph.introspection` 由 env `APOLLO_ROUTER_INTROSPECTION` 控制(生产 false) |
|
||||
| Resolver @RequirePermission 字段级守卫 | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点;50 resolver 审计后补齐 19 个 TS resolver,Python 子图待补 Strawberry/Ariadne 中间件 |
|
||||
| TS interface 不满足 Record 约束 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下 widget 用 `interface XxxVars` 报 TS2344;改 `type XxxVars = {...}` 别名(满足 index signature) |
|
||||
| parent.test.tsx 可选链 | `data?.[0].name` 报 TS2532(`data?.[0]` 可能为 undefined);改 `data?.[0]?.name` 双重可选链 |
|
||||
| PowerShell 不支持 heredoc | `git commit -m "$(cat <<'EOF'...)"` 在 PowerShell 报错;commit 消息写临时文件 `.git/COMMIT_MSG.txt`,用 `git commit -F .git/COMMIT_MSG.txt` |
|
||||
| commitlint body-max-line-length | commit body 每行 ≤100 字符,Plan/Spec 路径过长会超限;移除 URL 行或换行简化 |
|
||||
| commitlint scope-enum | `security` / `graphql` 不在允许 scope 列表;用 `docs` scope 提交审计报告,或用无 scope commit |
|
||||
| Turbopack 不支持 .js 后缀 import | Next 16 默认 Turbopack 无法像 webpack 那样通过 `resolve.extensionAlias` 将 `.js` 映射到 `.ts/.tsx`;`packages/ui-components` 和 `packages/hooks` 源码内部 import 必须去掉 `.js` 后缀(shared-ts 是 NestJS ESM 模式按规则 §3.4 保留 `.js` 后缀,不修改) |
|
||||
| 旧纸感令牌批量迁移 shadcn 标准 | 31 个 widget 全量替换:`bg-paper→bg-background` / `bg-surface→bg-card` / `text-ink→text-foreground` / `border-rule→border` 等 19 项映射;保留 button.tsx 中 `bg-accent`(shadcn 标准 hover 语义令牌,通过 `--accent` CSS 变量定义,非旧纸感 `bg-accent`) |
|
||||
| PERMISSION_BITMAP_ORDER 重复权限点 | `GRADE_READ` 在数组 bit 14 和 bit 53 重复出现,全量编解码 round-trip 测试需用 `[...new Set(PERMISSION_BITMAP_ORDER)]` 去重后再比较 |
|
||||
| jsdom Blob 不支持 .text() | jsdom 的 Blob 实现不提供 `.text()` 方法,`new Response(blob).text()` 也返回 `[object Blob]`;测试 sendBeacon 时用 `vi.stubGlobal("Blob", vi.fn(...))` mock 构造函数,捕获 `parts[0]` 字符串 |
|
||||
| vi.mock 提升导致变量未初始化 | `vi.mock()` 被提升到文件顶部,引用外部变量报 `Cannot access 'X' before initialization`;用 `vi.hoisted()` 将 mock 变量初始化提升到 vi.mock 之前 |
|
||||
| sonner toast 双重性质 mock | toast 既是可调用函数 `toast(msg, opts)` 又有方法 `toast.success/.error/.warning/.info`;mock 时需创建 `vi.fn()` 可调用对象后手动挂载 `.success/.error` 等方法 |
|
||||
| React 19 use() + Suspense jsdom 测试 | `use(promise)` 在 jsdom 中 promise resolve 后不自动触发重新渲染;需用 `await act(async () => { render(...); await Promise.resolve(); })` 包裹 render,并设置 `globalThis.IS_REACT_ACT_ENVIRONMENT = true`(setup 文件) |
|
||||
| 前端错误上报生产端点 | api-gateway 在 `/api/v1/log` 直接处理(不代理到下游),slog 结构化 JSON 日志,64KB body 限制,返回 204;`useErrorReport` 按 `process.env.NODE_ENV` 切换:production→`/api/v1/log`,development→`/api/log`(Next.js API route mock) |
|
||||
| vitest setup 文件配置 | `vitest.config.ts` 的 `setupFiles: ["src/__tests__/setup.ts"]` 注册 `@testing-library/jest-dom/vitest` matchers + 设置 `IS_REACT_ACT_ENVIRONMENT`;`declare global { var IS_REACT_ACT_ENVIRONMENT: boolean \| undefined }` 补类型签名 |
|
||||
| 管理域 GraphQL operation 命名冲突 | admin.graphql.ts 与 grades.graphql.ts/school-settings.graphql.ts 同名 mutation(CreateGrade/UpdateSchool 等)导致 codegen 重复声明;前缀化命名 `AdminCreateGrade`/`AdminUpdateSchool`/`AdminGetSchedulingRules` 区分 |
|
||||
| 管理域 invitation-codes 纯函数时间漂移 | 纯函数 `isInvitationExpired`/`getEffectiveStatus`/`isInvitationRevocable` 调用 `Date.now()` 会造成 SSR/CSR hydration mismatch;参数化注入 `now: number` 由 server page 传入 |
|
||||
| 管理域 StatusBadge 虚假推断状态 | `audit-logs` StatusBadge 用 `log.details ? "success" : ""` 推断 status 违反数据真实性原则;改用 schema 字段 `log.status`,未就绪时显示 "--" 占位符 |
|
||||
| 管理域未实现功能降级模式 | 未实现的 CRUD(如 questions 创建/导入导出、students/teachers/organization 详情)改为按钮 + `notify.info(t("mswNotice"))` 占位,避免死链;不创建实际路由 |
|
||||
| 管理域 @contract-pending 契约标注规则 | schema 未就绪字段在文件头注释 `@contract-pending: <工单>` + `§11.4 登记`;禁止在代码注释中声称 schema 已就绪而实际走 MSW 兜底(plugins 模块原标注造假已修正) |
|
||||
| 管理域 announcements grades 关联编辑 | AnnouncementInput.grades 字段已有 schema 定义;edit 表单用逗号分隔文本输入(简化版),避免下拉多选依赖 useGrades hook;提交时 `split(",").map(trim).filter(Boolean)` 解析 |
|
||||
| 管理域 audit-logs 分页 | ListPageShell pagination slot 支持 prev/next 按钮 + 当前页/总页数显示;URL 状态 `?page=N`,由 `updateQuery("page", ...)` 控制;总页数 `Math.ceil(total / pageSize)` |
|
||||
| 管理域 ai-settings 双权限注释 | AI_CHAT(普通用户访问 private provider)+ AI_CONFIGURE(管理员访问 public/他人 provider);admin 路由仅放行 ["admin"],管理员隐含 AI_CONFIGURE;普通用户 AI_CHAT 走 `/shell/ai-settings`(非 admin 域) |
|
||||
| 管理域 viewports DataScope 注释 | 视口配置属管理员全局视角(DataScope = "all"),不随班级/年级范围收窄;route-permissions.ts 仅放行 ["admin"];无需 ctx.dataScope 上下文 |
|
||||
| 场景 | 技术/规则 |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 插件契约跨包共享 | `packages/shared-ts/src/contracts/` 定义 PluginProps/PluginManifest/Layout 类型;`PluginManifest.Component` 用 `unknown`(shared-ts 不依赖 React),前端包引用时断言为 `React.ComponentType<PluginProps>` |
|
||||
| TVars 泛型约束与 TS interface 不兼容 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下,widget 文件用 `interface XxxVars` 会报 TS2344;改用 `type XxxVars = {...}` 类型别名(type alias 满足 index signature,interface 不满足) |
|
||||
| shared-ts contracts 子路径导出 | `package.json` exports 新增 `"./contracts"` 指向 `./dist/contracts/index.js`;hooks 包通过 `@edu/shared-ts/contracts` 引用;typecheck 前需 `pnpm --filter @edu/shared-ts run build` 编译到 dist/ |
|
||||
| Vitest 解析 workspace 子路径导出失败 | vitest.config.ts resolve.alias 需显式添加 `"@edu/shared-ts/contracts"` → 源码路径;Vite 不自动解析 package.json exports 字段 |
|
||||
| portal-shell tsconfig paths 补全 | tsconfig.json paths 需添加 `"@edu/shared-ts/contracts": ["../../packages/shared-ts/src/contracts/index.ts"]`,否则 tsc 报 TS2307 |
|
||||
| 插件 dynamic import 注册 | `Registry.tsx` 用 `next/dynamic`(ssr:false)懒加载 31 个内置插件;loading 展示 `PluginSkeleton` 变体(card/list/table/stats/chart) |
|
||||
| URL Search Params + Zustand 状态分层 | URL 驱动可分享上下文(classId/childId/termId/view/examId/subjectId);Zustand 管理纯 UI 状态(theme/locale/sidebarCollapsed);插件间禁止直接 import |
|
||||
| useWidgetMutation API 签名 | 返回 `{ run, ...result }`,`run` 是 `async (variables: TVars): Promise<TData \| undefined>`;消费者用 `const { run } = useWidgetMutation(...)` 解构 |
|
||||
| usePluginConfig 不直接调 API | fetcher 由消费者注入,保持 @edu/hooks "hooks 不直接调 API" 原则;支持轮询刷新(refreshInterval 默认 5 分钟)+ 网络恢复刷新 + visibilitychange 刷新 |
|
||||
| usePluginStore 注入模式 | `useSyncExternalStore` + `injectPluginStore()` 让 hooks 包不直接依赖特定 store 实例;未注入 store 时返回 DEFAULT_STATE 空操作 |
|
||||
| 28 个内置插件分类 | universal(7) + sidebar(4) + topbar(4) + teacher(4) + student(4) + parent(2) + admin(3);每个插件含 `index.tsx` + `plugin.manifest.ts`,manifest 导出 `manifestMeta: Omit<PluginManifest, "Component">` |
|
||||
| PluginLifecycle 版本兼容 | MVP 只校验 major 版本(`checkVersionCompatibility` 解析 `^`/`~`/`>=` 前缀 + major 数字);完整 semver range 校验待引入 semver 库 |
|
||||
| pnpm install --no-frozen-lockfile | parent-portal package.json 新增 `@opentelemetry/instrumentation-document-load` 后 lockfile 过期;开发环境用 `--no-frozen-lockfile` 安装,CI 用 frozen |
|
||||
| Widget 内联 gql 字面量废弃 | 禁止 `const Q = gql\`...\``直接写在 widget;统一抽取到`lib/api/operations/*.graphql.ts`,widget 只 import `lib/api/<domain>.ts` 的语义化函数(ADR-042) |
|
||||
| 4 层数据访问分层 | Widget(UI)→ API(语义化函数)→ Operations(gql DocumentNode 集中)→ Hook(useWidgetQuery/useWidgetMutation 封装 Apollo);codegen 从 7 子图 schema 生成类型 |
|
||||
| graphql-codegen skipDocumentsValidation | 全局 `skipDocumentsValidation: true` 保留(子图未启动时校验 operations 失败);分域关闭:data-ana 域 dashboard-types.ts 输出已关 `skipDocumentsValidation: false`(P1-7),config 域待 `LayoutTemplateGql.availableSlots` schema 落地后关闭;schema 归一化脚本移除 federation 指令(@key/@requires/@extends)防止误解析 |
|
||||
| useNotifications 命名冲突 | `universal.ts` 与 `topbar.ts` 同时导出 `useNotifications` 在 barrel `index.ts` 冲突(TS2308);topbar 改名 `useNotificationBell`(铃铛专用,限 N 条) |
|
||||
| APQ(Automatic Persisted Queries) | `apollo-client.ts` 用 `createPersistedQueryLink({ sha256 })` + `crypto-hash`;env `NEXT_PUBLIC_APOLLO_APQ=false` 关闭;Link 链顺序:authLink → pqLink → httpLink |
|
||||
| PQ Manifest 生成 | `scripts/generate-pq-manifest.ts` 遍历 `operations/index.ts` 中 DocumentNode,`print(doc)` + `sha256(query)` 写入 `public/pq-manifest.json`;`prebuild` 钩子串联 codegen + generate |
|
||||
| Windows ESM 动态 import 路径 | Node ESM 动态 `import()` 不支持 Windows 盘符路径(`e:\...`),必须 `url.pathToFileURL(path).href` 转 `file://` URL 再 import |
|
||||
| apollo-router PQ manifest 挂载 | docker-compose 把 `apps/portal-shell/public/pq-manifest.json` 挂载到 router `/etc/apollo-router/pq-manifest.json:ro`;entrypoint.sh 启动前校验 `APOLLO_REQUIRE_PQ_MANIFEST=true` 时文件存在性 |
|
||||
| CI 结构性检查三脚本(P1-8) | `scripts/check-route-table.ts`(路由表一致性:实际 `/shell/*` 路由必须登记到 route-permissions.ts)+ `scripts/check-page-count.ts`(页面计数 baseline=13)+ `scripts/check-codegen.ts`(codegen 契约校验:skipDocumentsValidation:false 输出严格校验);CI 接线 `.github/workflows/ci.yml` quality-ts job |
|
||||
| apollo-router 安全限制 | `router.yaml` 配置 `limits.max_depth=10` / `max_cost=1000` / `max_batch_size=5`;`supergraph.introspection` 由 env `APOLLO_ROUTER_INTROSPECTION` 控制(生产 false) |
|
||||
| Resolver @RequirePermission 字段级守卫 | 每个 GraphQL Resolver 必须用 `@RequirePermission('perm')` 声明权限点;50 resolver 审计后补齐 19 个 TS resolver,Python 子图待补 Strawberry/Ariadne 中间件 |
|
||||
| TS interface 不满足 Record 约束 | `useWidgetQuery<TData, TVars extends Record<string, unknown>>` 约束下 widget 用 `interface XxxVars` 报 TS2344;改 `type XxxVars = {...}` 别名(满足 index signature) |
|
||||
| parent.test.tsx 可选链 | `data?.[0].name` 报 TS2532(`data?.[0]` 可能为 undefined);改 `data?.[0]?.name` 双重可选链 |
|
||||
| PowerShell 不支持 heredoc | `git commit -m "$(cat <<'EOF'...)"` 在 PowerShell 报错;commit 消息写临时文件 `.git/COMMIT_MSG.txt`,用 `git commit -F .git/COMMIT_MSG.txt` |
|
||||
| commitlint body-max-line-length | commit body 每行 ≤100 字符,Plan/Spec 路径过长会超限;移除 URL 行或换行简化 |
|
||||
| commitlint scope-enum | `security` / `graphql` 不在允许 scope 列表;用 `docs` scope 提交审计报告,或用无 scope commit |
|
||||
| Turbopack 不支持 .js 后缀 import | Next 16 默认 Turbopack 无法像 webpack 那样通过 `resolve.extensionAlias` 将 `.js` 映射到 `.ts/.tsx`;`packages/ui-components` 和 `packages/hooks` 源码内部 import 必须去掉 `.js` 后缀(shared-ts 是 NestJS ESM 模式按规则 §3.4 保留 `.js` 后缀,不修改) |
|
||||
| 旧纸感令牌批量迁移 shadcn 标准 | 31 个 widget 全量替换:`bg-paper→bg-background` / `bg-surface→bg-card` / `text-ink→text-foreground` / `border-rule→border` 等 19 项映射;保留 button.tsx 中 `bg-accent`(shadcn 标准 hover 语义令牌,通过 `--accent` CSS 变量定义,非旧纸感 `bg-accent`) |
|
||||
| PERMISSION_BITMAP_ORDER 重复权限点 | `GRADE_READ` 在数组 bit 14 和 bit 53 重复出现,全量编解码 round-trip 测试需用 `[...new Set(PERMISSION_BITMAP_ORDER)]` 去重后再比较 |
|
||||
| jsdom Blob 不支持 .text() | jsdom 的 Blob 实现不提供 `.text()` 方法,`new Response(blob).text()` 也返回 `[object Blob]`;测试 sendBeacon 时用 `vi.stubGlobal("Blob", vi.fn(...))` mock 构造函数,捕获 `parts[0]` 字符串 |
|
||||
| vi.mock 提升导致变量未初始化 | `vi.mock()` 被提升到文件顶部,引用外部变量报 `Cannot access 'X' before initialization`;用 `vi.hoisted()` 将 mock 变量初始化提升到 vi.mock 之前 |
|
||||
| sonner toast 双重性质 mock | toast 既是可调用函数 `toast(msg, opts)` 又有方法 `toast.success/.error/.warning/.info`;mock 时需创建 `vi.fn()` 可调用对象后手动挂载 `.success/.error` 等方法 |
|
||||
| React 19 use() + Suspense jsdom 测试 | `use(promise)` 在 jsdom 中 promise resolve 后不自动触发重新渲染;需用 `await act(async () => { render(...); await Promise.resolve(); })` 包裹 render,并设置 `globalThis.IS_REACT_ACT_ENVIRONMENT = true`(setup 文件) |
|
||||
| 前端错误上报生产端点 | api-gateway 在 `/api/v1/log` 直接处理(不代理到下游),slog 结构化 JSON 日志,64KB body 限制,返回 204;`useErrorReport` 按 `process.env.NODE_ENV` 切换:production→`/api/v1/log`,development→`/api/log`(Next.js API route mock) |
|
||||
| vitest setup 文件配置 | `vitest.config.ts` 的 `setupFiles: ["src/__tests__/setup.ts"]` 注册 `@testing-library/jest-dom/vitest` matchers + 设置 `IS_REACT_ACT_ENVIRONMENT`;`declare global { var IS_REACT_ACT_ENVIRONMENT: boolean \| undefined }` 补类型签名 |
|
||||
| 管理域 GraphQL operation 命名冲突 | admin.graphql.ts 与 grades.graphql.ts/school-settings.graphql.ts 同名 mutation(CreateGrade/UpdateSchool 等)导致 codegen 重复声明;前缀化命名 `AdminCreateGrade`/`AdminUpdateSchool`/`AdminGetSchedulingRules` 区分 |
|
||||
| 管理域 invitation-codes 纯函数时间漂移 | 纯函数 `isInvitationExpired`/`getEffectiveStatus`/`isInvitationRevocable` 调用 `Date.now()` 会造成 SSR/CSR hydration mismatch;参数化注入 `now: number` 由 server page 传入 |
|
||||
| 管理域 StatusBadge 虚假推断状态 | `audit-logs` StatusBadge 用 `log.details ? "success" : ""` 推断 status 违反数据真实性原则;改用 schema 字段 `log.status`,未就绪时显示 "--" 占位符 |
|
||||
| 管理域未实现功能降级模式 | 未实现的 CRUD(如 questions 创建/导入导出、students/teachers/organization 详情)改为按钮 + `notify.info(t("mswNotice"))` 占位,避免死链;不创建实际路由 |
|
||||
| 管理域 @contract-pending 契约标注规则 | schema 未就绪字段在文件头注释 `@contract-pending: <工单>` + `§11.4 登记`;禁止在代码注释中声称 schema 已就绪而实际走 MSW 兜底(plugins 模块原标注造假已修正) |
|
||||
| 管理域 announcements grades 关联编辑 | AnnouncementInput.grades 字段已有 schema 定义;edit 表单用逗号分隔文本输入(简化版),避免下拉多选依赖 useGrades hook;提交时 `split(",").map(trim).filter(Boolean)` 解析 |
|
||||
| 管理域 audit-logs 分页 | ListPageShell pagination slot 支持 prev/next 按钮 + 当前页/总页数显示;URL 状态 `?page=N`,由 `updateQuery("page", ...)` 控制;总页数 `Math.ceil(total / pageSize)` |
|
||||
| 管理域 ai-settings 双权限注释 | AI_CHAT(普通用户访问 private provider)+ AI_CONFIGURE(管理员访问 public/他人 provider);admin 路由仅放行 ["admin"],管理员隐含 AI_CONFIGURE;普通用户 AI_CHAT 走 `/shell/ai-settings`(非 admin 域) |
|
||||
| 管理域 viewports DataScope 注释 | 视口配置属管理员全局视角(DataScope = "all"),不随班级/年级范围收窄;route-permissions.ts 仅放行 ["admin"];无需 ctx.dataScope 上下文 |
|
||||
| 学生域 CICD 迁移整体策略 | 列表页统一改 Card 网格(md:grid-cols-2 lg:grid-cols-3)替代 CICD 表格;详情页用 DetailPageShell + DetailField;富文档自研 markdown 解析(标题/列表/段落/加粗),不引入 react-markdown;图表用纯 SVG 自研(折线/柱状/雷达),不引入 recharts |
|
||||
| 学生域 Server Action → Mutation 适配 | CICD 用 Next.js Server Actions(selectCourseAction/dropCourseAction/joinClassByInvitationCodeAction 等);portal-shell 改写为 Apollo mutation hook(useEnrollCourse/useDropCourse/useJoinClassByInvitationCode),MSW 兜底 |
|
||||
| 学生域 @contract-pending 数据契约清单 | studentDashboard/studentGrades/studentReportCard/studentAttendance/studentCoursePlans/studentCoursePlanDetail/studentSelfDiagnostic/studentErrorBook/studentHomework/studentHomeworkSubmit/studentHomeworkAnalysis/studentLessonPlans/studentLessonPlanView/studentPractice/studentPracticeSession/studentSchedule/studentLeave/electiveCourses/studentElectiveDetail/studentTextbooks/studentTextbookChapters/studentMessageDetail/messageRecipients/sendMessage/userProfile 均待 core-edu/iam/content/ai 服务补齐 |
|
||||
| 学生域角色自适应空态 | 学生未登录或无学生身份时显示 EmptyState + UserX 图标(schedule/learning-center 等);通过 useStudentClasses/useUserProfile hook 获取学生身份 |
|
||||
| 学生域状态分支避免逻辑错误 | homework/[id]/submit 页需根据 submission.status 分流:submitted/graded → ReviewView 复习视图,not_started/in_progress → TakeView 作答视图;始终显示 TakeView 会导致已批改作业仍可"作答"逻辑错误 |
|
||||
| 学生域范围校验契约占位 | lesson-plans/[id]/view 的 assertPlanInScope、textbooks 的学生年级强制过滤等 CICD 服务端校验在 portal-shell 改为前端兜底 + `@contract-pending scope-check` 注释,待 BFF resolver 补齐 |
|
||||
| 学生域 ReviewView 复习视图字段映射 | 复用 homework-analysis-client 的 QuestionAnalysisTable;状态归一化函数 normalizeSubmissionStatus 兼容 MSW 中文与英文枚举;maxAttempts/attemptsUsed 在 QuestionNavPanel 顶部显示 |
|
||||
| 学生域 A4 打印布局 | report-card 用 `.report-card-a4` 容器(width:210mm min-height:297mm padding:15mm)+ `report-card-print.css` 的 @media print 规则;打印按钮 loading 状态 + afterprint 事件重置;rowSpan 合并同学科 Subject 单元格展示多条 records |
|
||||
| 学生域 CoursePlanProgress 进度条 | 用纯 Tailwind div + ARIA role="progressbar" 实现双进度条(完成学时 completedHours/totalHours + 完成项数 completedItems/totalItems),三段色阶(100% 绿、>0 主色、0 灰) |
|
||||
| 学生域 KnowledgePoint 命名冲突 | student-portal.ts 的 `KnowledgePoint`/`useKnowledgePoints` 与 knowledge-graph.ts 同名冲突(TS2308);重命名为 `StudentKnowledgePoint`/`useStudentKnowledgePoints` 避免 barrel index.ts 重复导出 |
|
||||
| 学生域 StudentSelfDiagnostic 命名冲突 | 学生自我诊断(无参数 camelCase)与教师按 studentId 查询(snake_case)同名 `useStudentDiagnostic` 冲突;学生版重命名为 `StudentSelfDiagnostic`/`useStudentSelfDiagnostic`/`GetStudentSelfDiagnostic` |
|
||||
| 学生域 ElectiveStatus 命名冲突 | student-portal.ts 的 `ElectiveStatus` 与其他模块同名冲突(TS2308);重命名为 `StudentElectiveStatus` |
|
||||
| 学生域 UserProfile 命名冲突 | profile.ts 的 `UserProfile` 与 settings.ts 同名冲突(TS2308);profile 版重命名为 `UserProfileDetail`/`useUserProfile` |
|
||||
| 学生域 Card 视图 vs 表格视图切换 | homework-list/error-book-list 等列表页提供 viewMode 切换(card/table),默认 card;Card 视图展示更多字段(attemptsUsed/latestScore/difficulty/reviewCount 等),表格视图作为紧凑模式 |
|
||||
| 学生域 退课确认 AlertDialog | elective 用 AlertDialog 确认退课,含退课理由 textarea(maxLength 255);cancel/confirm 按钮 + loading 状态;成功后 refetch 已选列表 |
|
||||
| 学生域 加入班级邀请码表单 | courses-list 顶部 "加入班级" 按钮 + JoinClassDialog,6 位数字邀请码输入(pattern=[0-9]{6});MSW 兜底(123456 成功/000000 已加入/其他无效);成功后 refetch 课程列表 |
|
||||
| 学生域 messages 共享路由组件创建 | message-detail-client/message-compose-client/message-group-compose-client 3 个组件在 `features/shared/messages/` 下创建;学生/教师/管理员均可访问;group-compose 用原生 `<input type="checkbox">` 多选收件人(项目无 Checkbox 组件,避免引入新依赖) |
|
||||
| 学生域 error-book 变式练习 hook 签名 | `useStartPracticeSession().run` 签名为 `(knowledgePointIds: string[])`;error-book-detail-dialog 调用时需包装为 `[kpId]` 数组,否则报 TS2345 |
|
||||
| 学生域 profile 共享路由 | `/shell/profile` 是所有角色可访问的共享路由(route-permissions.ts 不限角色);ProfileClient 显示双卡片布局(个人信息 + 账户信息)+ 角色专属概览面板(学生显示班级/年级,教师显示教授课程) |
|
||||
| 教师域 CICD 迁移数据层适配 | CICD 用 Server Actions + data-access.ts;portal-shell 改为 useWidgetQuery/useWidgetMutation + MSW 兜底,缺失 hook 用 `@contract-pending` 注释 + 本地 mock 占位 |
|
||||
| 教师域 CICD 迁移 Select/AlertDialog 适配 | CICD 用 Radix Select 复合组件(Trigger/Content/Item)+ AlertDialogTrigger;portal-shell 用原生 Select 的 `options` 数组 API + `useState` 手动管理 AlertDialog open 状态 |
|
||||
| 教师域 CICD 迁移 service-context 改造 | CICD 的 `services/xxx-service-context.tsx` 通过 Server Actions 暴露 service;portal-shell 改为 Context Provider + useWidgetQuery/useWidgetMutation 包装,默认 service 实现为 no-op + notify.info 提示契约未就绪 |
|
||||
|
||||
Reference in New Issue
Block a user