diff --git a/.superpowers/brainstorm/41500-1782168322.9344/content/anchor-timeline-layout.html b/.superpowers/brainstorm/41500-1782168322.9344/content/anchor-timeline-layout.html new file mode 100644 index 0000000..41c3fc5 --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/content/anchor-timeline-layout.html @@ -0,0 +1,273 @@ +

课文锚点时间线布局

+

课文作为主轴,节点锚定到课文位置,形成教学流程时间线

+ +
+
备课编辑器 — 课文锚点时间线
+
+
+ +
+
+ 📖 + 秋天(第一课时) + 语文 · 一年级上册 · 第一单元 +
+
+ 💾 已保存 · 2 分钟前 + 📋 版本历史 + 💾 保存 +
+
+ + +
+ + +
+ + +
+ +
1
+
2
+
3
+
4
+
5
+
+ + +
+
秋天
+ +

+ 天气凉了,树叶黄了, + 一片片叶子从树上落下来。 +

+

+ 天空那么蓝,那么高。 + 一群大雁往南飞, +

+

+ 一会儿排成个"人"字, + 一会儿排成个"一"字。 +

+

+ 啊!秋天来了! +

+ + +
+ 💡 选中文字可"关联节点",或从右侧拖动节点到课文某字前 +
+
+
+ + +
+
教学流程时间线
+ + +
+ + +
+
+ 1 + 💡 导入 + 📍 锚定 +
+
+ "天气凉了,树叶黄了" → 提问:你见过秋天的树叶吗? +
+
+ + +
+
+ 2 + 📝 文本研习 + 📍 锚定 +
+
+ "天空那么蓝,那么高" → 赏析:叠词的运用 +
+
+ + +
+
+ 3 + 📚 新授 + 📍 锚定 +
+
+ "一会儿排成个'人'字" → 讲解:大雁南飞 +
+
+ + +
+
+ 4 + ✏️ 练习 + 📍 锚定 +
+
+ "一会儿排成个'一'字" → 3 道题 +
+
+ + +
+
+ 5 + 📌 小结 + 📍 锚定 +
+
+ "啊!秋天来了!" → 总结全文 +
+
+ + +
+
未锚定节点
+
+ + +
+
+ 🎯 教学目标 + 全局 +
+
+
+
+ ⭐ 重难点 + 全局 +
+
+
+
+ 🏠 作业 + 课后 +
+
+
+
+ 📋 板书设计 + 全局 +
+
+
+
+ 💭 教学反思 + 课后 +
+
+ + +
+ + 添加节点 +
+
+
+
+
+
+
+ +
+

核心交互:两种锚定方式

+
+
+
方式 1:拖动节点到课文某字前
+
+
+
+
右侧节点
+
💡 导入
+
+
+
+
课文
+
+ 天气凉了,|树叶黄了,
+ 一片片叶子从树上落下来。 +
+
💡 节点锚定到此位置
+
+
+
+
+
+
方式 2:选中文字 → 关联节点
+
+
+
1. 选中文字
+
+ 天空那么蓝,那么高。 +
+
+
+
+
2. 弹出菜单选择节点
+
+ 💡 导入 + 📝 文本研习 + 📚 新授 + ✏️ 练习 + 📌 小结 + + 新建节点 +
+
+
+
+
+
+ +
+

数据模型:锚点(Anchor)

+
+
// 节点锚点 — 记录节点与课文位置的关联
+
interface NodeAnchor {
+
  nodeId: string; // 关联的节点 ID
+
  type: "point" | "range"; // 点锚点 or 范围锚点
+
  start: number; // 课文纯文本偏移量(字符)
+
  end?: number; // range 锚点的结束偏移
+
  textPreview?: string; // 锚定文字预览(便于回显)
+
}
+
+
// LessonPlanDocument 扩展
+
interface LessonPlanDocument {
+
  version: 3; // 升级到 v3
+
  nodes: LessonPlanNode[];
+
  edges: LessonPlanEdge[]; // 保留:节点间连线
+
  anchors: NodeAnchor[]; // 新增:节点与课文的锚点
+
}
+
+
+ +
+

这个设计的优势

+
+
+

优势

+
    +
  • 教学流程可视化:节点按课文位置排序,天然形成时间线
  • +
  • 节点与课文强关联:每个节点对应课文的哪部分一目了然
  • +
  • 双模式锚定:拖动(点锚点)+ 选文字(范围锚点)
  • +
  • 保留连线能力:节点间仍可连线(如"导入→新授"流程线)
  • +
  • 未锚定节点:目标/重难点/作业/板书/反思等全局节点不强制锚定
  • +
+
+
+

需要注意

+
    +
  • 课文偏移量需基于纯文本(Markdown 渲染后需映射)
  • +
  • 课文内容变更后锚点可能失效(需重新定位或提示)
  • +
  • 数据结构升级到 v3,需迁移现有 v2 数据
  • +
+
+
+
diff --git a/.superpowers/brainstorm/41500-1782168322.9344/content/canvas-anchor-layout.html b/.superpowers/brainstorm/41500-1782168322.9344/content/canvas-anchor-layout.html new file mode 100644 index 0000000..b9d300a --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/content/canvas-anchor-layout.html @@ -0,0 +1,292 @@ +

画布式锚点布局 — 正文固定 + 节点散布 + 连线关联

+

保留 React Flow 画布交互,正文为不可移动但可缩放的中央容器,节点通过连线关联正文锚点

+ +
+
备课编辑器 — 画布视图(默认状态)
+
+
+ + +
+
+ 📖 + 秋天(第一课时) + 语文 · 一年级上册 +
+
+ 💾 已保存 + 📋 版本 + 💾 保存 +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ 📜 课文正文 + 🔒 固定 +
+
+
秋天
+

+ 天气凉了,树叶黄了, +

+

+ 天空那么蓝,那么高。 +

+

+ 一群大雁往南飞, +

+

+ 一会儿排成个"人"字, +

+

+ 一会儿排成个"一"字。 +

+

+ 啊!秋天来了! +

+
+ +
🔍
+
+ + + +
+
+ 1 + 💡 导入 +
+
提问:你见过秋天的树叶吗?
+ +
+
+ + +
+
+ 2 + 📝 文本研习 +
+
赏析:叠词的运用
+
+
+ + + +
+
+ 3 + 📚 新授 +
+
讲解:大雁南飞
+
+
+ + +
+
+ 4 + ✏️ 练习 +
+
3 道题
+
+
+ + +
+
+ 5 + 📌 小结 +
+
总结全文
+
+
+ + +
+
🎯 教学目标
+
+
+
⭐ 重难点
+
+ + +
+
🏠 作业
+
+
+
📋 板书设计
+
+
+
💭 教学反思
+
+ + +
+
+
+
+
+
+ + +
+ + 添加节点 +
+ + +
+ 连线默认 10% 透明度 · 选中节点时完整显示 +
+
+
+
+
+ + +
+

选中节点时的连线显示对比

+
+
+
默认状态 — 连线 10% 透明度
+
+ + + + 导入 + + 课文 + +
连线几乎不可见,画布干净
+
+
+
+
选中"导入"节点 — 连线 100% 显示
+
+ + + + + 导入 + + 课文 + +
连线完整显示,高亮锚点位置
+
+
+
+
+ + +
+

数据模型设计

+
+
// 正文容器节点(特殊节点类型,不可拖动,可缩放)
+
interface TextbookContentNode extends LessonPlanNode {
+
  type: "textbook_content"; // 新增节点类型
+
  data: {
+
    chapterId: string; // 关联教材章节
+
    content: string; // Markdown 正文(缓存)
+
    zoom: number; // 缩放比例 0.5-2.0
+
  };
+
  position: { x: number; y: number }; // 固定位置(不可拖动)
+
  draggable: false; // React Flow 节点锁定
+
}
+
+
// 锚点连线(节点 → 正文位置)
+
interface AnchorEdge extends LessonPlanEdge {
+
  type: "anchor"; // 锚点连线(vs "flow" 流程连线)
+
  source: string; // 节点 ID
+
  target: string; // 正文节点 ID
+
  targetHandle: string; // "anchor:123:145"(正文偏移量 start:end)
+
}
+
+
// LessonPlanDocument v3
+
interface LessonPlanDocument {
+
  version: 3;
+
  nodes: LessonPlanNode[]; // 含 1 个 textbook_content + N 个教学节点
+
  edges: AnchorEdge | FlowEdge[]; // 锚点连线 + 流程连线
+
}
+
+
+ + +
+

核心交互流程

+
+
+

🔗 锚定节点到正文

+
    +
  1. 教师选中正文某段文字(或某个字)
  2. +
  3. 选中后弹出浮动菜单:"关联节点 →"
  4. +
  5. 从下拉列表选择已有节点,或"新建节点"
  6. +
  7. 创建 AnchorEdge,source=节点, target=正文节点, targetHandle="anchor:start:end"
  8. +
  9. 正文对应文字高亮显示(节点颜色)
  10. +
  11. 连线默认 10% 透明,选中节点时 100%
  12. +
+
+
+

🖱️ 拖动节点到正文

+
    +
  1. 教师从右侧节点列表拖动一个节点
  2. +
  3. 拖动过程中,正文区域高亮可放置区域
  4. +
  5. 拖到正文某个字前释放
  6. +
  7. 创建点锚点(point anchor),targetHandle="anchor:pos"
  8. +
  9. 节点自动定位到正文旁边(左或右空位)
  10. +
  11. 连线默认 10% 透明,选中时完整显示
  12. +
+
+
+
+ +
+

设计要点

+
+
+

优势

+
    +
  • 保留画布交互:缩放/平移/拖动节点,与当前备课模块一致
  • +
  • 正文固定居中:不可拖动,始终是视觉中心
  • +
  • 连线语义化:anchor 锚点连线 vs flow 流程连线
  • +
  • 透明度策略:默认 10%,选中时 100%,画布不杂乱
  • +
  • 正文可缩放:教师可放大正文便于阅读
  • +
+
+
+

技术挑战

+
    +
  • 正文偏移量需基于纯文本(Markdown 渲染后映射)
  • +
  • 正文内容变更后锚点需重新定位
  • +
  • React Flow 自定义节点需处理正文渲染
  • +
  • 数据结构升级 v2 → v3,需迁移
  • +
+
+
+
diff --git a/.superpowers/brainstorm/41500-1782168322.9344/content/layout-options.html b/.superpowers/brainstorm/41500-1782168322.9344/content/layout-options.html new file mode 100644 index 0000000..e83983c --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/content/layout-options.html @@ -0,0 +1,226 @@ +

备课模块布局方案对比

+

3 种布局方案 — 课文固定中央,教学节点围绕组织

+ +
+ +
+
+
+
+ 📖 秋天(第一课时) + 💾 已保存 +
+
+ +
+
课前
+
🎯 教学目标
+
⭐ 重难点
+
💡 导入
+
📝 文本研习
+
+ +
+
📜 课文正文
+
+ 天气凉了,树叶黄了,
+ 一片片叶子从树上落下来。
+ 天空那么蓝,那么高
+ 一群大雁往南飞,
+ 一会儿排成个"人"字,
+ 一会儿排成个"一"字。
+ 啊!秋天来了! +
+
💡 选中文字可添加批注
+
+ +
+
课中
+
📚 新授
+
✏️ 练习
+
📌 小结
+
课后
+
🏠 作业
+
📋 板书设计
+
💭 教学反思
+
+
+
+
+
+

A. 三栏布局(课前/课文/课后)

+

课文固定中央(琥珀色边框),左侧"课前"节点(目标/重难点/导入/文本研习),右侧"课中+课后"节点(新授/练习/小结/作业/板书/反思)。节点按教学流程纵向排列。点击节点在右侧抽屉编辑。

+
+
+ + +
+
+
+
+ 📖 秋天(第一课时) + 💾 已保存 +
+
+ +
+
🎯 教学目标
+
⭐ 重难点
+
+ +
+
+
💡 导入
+
📝 文本研习
+
+
+
📜 课文正文
+
天气凉了,树叶黄了...
天空那么蓝...
一群大雁往南飞...
+
+
+
📚 新授
+
✏️ 练习
+
+
+ +
+
📌 小结
+
🏠 作业
+
📋 板书
+
💭 反思
+
+
+
+
+
+

B. 上下分区布局

+

顶部目标/重难点横排,中央课文 + 左右导入/文本研习/新授/练习,底部小结/作业/板书/反思横排。按"目标→导入→课文→新授→小结"的阅读顺序自然流动。视觉层次更清晰。

+
+
+ + +
+
+
+
+ 📖 秋天(第一课时) + 💾 已保存 +
+
+ +
+
+
📜 课文正文
+
+ 天气凉了,树叶黄了,
+ 一片片叶子从树上落下来。
+ 天空那么蓝,那么高
+ 一群大雁往南飞... +
+
+
📝 文本研习(批注)
+
+ +
+
教学流程
+
+ 1 + 🎯 教学目标 +
+
+ 2 + ⭐ 重难点 +
+
+ 3 + 💡 导入 +
+
+ 4 + 📚 新授 +
+
+ 5 + ✏️ 练习 +
+
+ 6 + 📌 小结 +
+
+ 7 + 🏠 作业 +
+
+ 8 + 📋 板书 +
+
+ 9 + 💭 反思 +
+
+
+
+
+
+

C. 左课文 + 右时间线

+

左侧课文正文(固定)+ 文本研习批注,右侧教学流程时间线(编号 1-9 按顺序)。点击时间线节点展开编辑抽屉。最贴近传统教案本格式,结构清晰。

+
+
+
+ +
+

三种方案的核心差异

+
+
+

方案 A 三栏

+
    +
  • 课文始终居中可见
  • +
  • 课前/课后分区直观
  • +
  • 节点可拖动微调位置
  • +
+
+
+

方案 A 三栏

+
    +
  • 三栏可能拥挤(小屏)
  • +
  • 教学流程顺序不够明显
  • +
+
+
+
+
+

方案 B 上下分区

+
    +
  • 视觉层次最清晰
  • +
  • 阅读顺序自然(上→下)
  • +
  • 课文居中突出
  • +
+
+
+

方案 B 上下分区

+
    +
  • 节点位置较固定
  • +
  • 纵向空间需求大
  • +
+
+
+
+
+

方案 C 左课文+右时间线

+
    +
  • 最接近传统教案
  • +
  • 教学流程顺序最明确
  • +
  • 课文阅读体验最佳
  • +
+
+
+

方案 C 左课文+右时间线

+
    +
  • 节点画布感弱(更像列表)
  • +
  • 失去节点图连线能力
  • +
+
+
+
diff --git a/.superpowers/brainstorm/41500-1782168322.9344/content/placeholder-markers.html b/.superpowers/brainstorm/41500-1782168322.9344/content/placeholder-markers.html new file mode 100644 index 0000000..deb4820 --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/content/placeholder-markers.html @@ -0,0 +1,251 @@ +

正文占位符标记布局

+

正文中嵌入占位符标记(特殊符号),默认接近透明,选中节点时完整显示

+ +
+
备课编辑器 — 默认状态(占位符 10% 透明度)
+
+
+ + +
+
+ 📖 + 秋天(第一课时) + 语文 · 一年级上册 +
+
+ 💾 已保存 + 💾 保存 +
+
+ + +
+ + + + + + + + + + + +
+
+ 📜 课文正文 + 🔒 固定 +
+
+
秋天
+

+ + 天气凉了,树叶黄了, +

+

+ + 天空那么蓝,那么高。 +

+

+ 一群大雁往南飞, +

+

+ 一会儿排成个"人"字, +

+

+ 一会儿排成个"一"字。 +

+

+ 啊!秋天来了! +

+
+
+ + +
+
+ 1 + 💡 导入 +
+
提问:你见过秋天的树叶吗?
+
+ +
+
+ 2 + 📝 文本研习 +
+
赏析:叠词的运用
+
+ + +
+
+ 3 + 📚 新授 +
+
讲解:大雁南飞
+
+ +
+
+ 4 + ✏️ 练习 +
+
3 道题
+
+ +
+
+ 5 + 📌 小结 +
+
总结全文
+
+ +
+ 占位符默认 10% · 选中节点时 100% 显示 +
+
+
+
+
+ + +
+

选中"导入"节点时的状态变化

+
+
+
默认 — 占位符 ① 10% 透明度
+
+
+ 天气凉了,树叶黄了, +
+
+ 占位符几乎不可见 · 画布干净 +
+
+
+
+
选中"导入"节点 — 占位符 ① 100% + 连线显示
+
+
+ 天气凉了,树叶黄了, +
+
+ 占位符完整显示 · 连线高亮 · 锚定位置清晰 +
+
+
+
+
+ + +
+

占位符样式选项

+

选择占位符在正文中的视觉呈现方式

+
+
+
+
+ 天气凉了 +
+
+
+

数字圆圈

+

①②③④⑤ — 与节点编号对应,简洁清晰

+
+
+
+
+
+ 💡天气凉了 +
+
+
+

节点图标

+

💡📝📚✏️📌 — 与节点类型图标一致,直观

+
+
+
+
+
+ 天气凉了 +
+
+
+

彩色圆点

+

● — 极简,颜色对应节点,不干扰阅读

+
+
+
+
+
+ 【1】天气凉了 +
+
+
+

方括号编号

+

【1】【2】【3】— 类似脚注标记,学术感

+
+
+
+
+ + +
+

占位符数据模型

+
+
// 正文中的占位符标记
+
interface ContentPlaceholder {
+
  id: string; // 占位符 ID
+
  nodeId: string; // 关联的节点 ID
+
  offset: number; // 在正文纯文本中的字符偏移量
+
  label: string; // 显示的标记("①" / "💡" / "●" / "【1】")
+
  color: string; // 节点颜色(用于占位符背景)
+
}
+
+
// 正文渲染时注入占位符
+
function renderContentWithPlaceholders(
+
  content: string, // Markdown 原文
+
  placeholders: ContentPlaceholder[]
+
): string {
+
  // 按 offset 排序,在对应位置插入占位符标记
+
  // 渲染为 <span class="placeholder" data-node-id="xxx">①</span>
+
}
+
+
// CSS 透明度控制
+
.placeholder { opacity: 0.1; transition: opacity 0.2s; }
+
.placeholder.active { opacity: 1; }
+
.placeholder:hover { opacity: 0.6; }
+
+
+ +
+

交互流程

+
+
+

🔗 添加占位符

+
    +
  1. 教师点击正文某个位置(光标位置)
  2. +
  3. 或选中一段文字后释放
  4. +
  5. 弹出菜单:"在此处添加节点 →"
  6. +
  7. 选择节点类型或已有节点
  8. +
  9. 在正文对应位置插入占位符标记
  10. +
  11. 创建 AnchorEdge 连线
  12. +
+
+
+

👁️ 选中节点时的视觉反馈

+
    +
  1. 点击画布上的某个节点
  2. +
  3. 该节点对应的占位符 opacity 从 0.1 → 1
  4. +
  5. 连线从 10% → 100% 显示
  6. +
  7. 占位符添加 active 样式(边框/阴影)
  8. +
  9. 其他占位符保持 10% 透明度
  10. +
  11. 点击空白处恢复默认状态
  12. +
+
+
+
diff --git a/.superpowers/brainstorm/41500-1782168322.9344/content/two-anchor-types.html b/.superpowers/brainstorm/41500-1782168322.9344/content/two-anchor-types.html new file mode 100644 index 0000000..9d97dd0 --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/content/two-anchor-types.html @@ -0,0 +1,376 @@ +

两种锚定方式的视觉规则

+

范围锚定(选文本)vs 点锚定(插入占位符)— 默认/选中状态对比

+ + +
+

视觉规则总览

+
+
// 两种锚定方式
+
type AnchorType = "range" | "point";
+
+
// 范围锚定(选一段文本关联节点)
+
// 文本背景色 = 节点颜色
+
.range-anchor {
+
  background-color: var(--node-color); // 节点颜色
+
  opacity: 0; // 默认完全透明(和正常文本一样)
+
}
+
.range-anchor.active {
+
  opacity: 0.3; // 选中节点时显示背景色
+
}
+
+
// 点锚定(在文本中插入占位符)
+
.point-anchor {
+
  opacity: 0.3; // 默认半透明
+
}
+
.point-anchor.active {
+
  opacity: 1; // 选中节点时不透明
+
}
+
+
+ + +
+

完整画布 — 默认状态

+
+
默认状态(未选中任何节点)
+
+
+ + +
+ + + + + + + + + + + +
+
+ 📜 课文正文 + 🔒 固定 +
+
+
秋天
+

+ + 天气凉了,树叶黄了, +

+

+ + 天空那么蓝,那么高。 +

+

+ 一群大雁往南飞, +

+

+ + 一会儿排成个"人"字 + + , +

+

+ + 啊!秋天来了! +

+
+
+ + +
+
+ 1 + 💡 导入 + 范围 +
+
提问:你见过秋天的树叶吗?
+
+ +
+
+ 2 + 📝 文本研习 + +
+
赏析:叠词的运用
+
+ + +
+
+ 3 + 📚 新授 + 范围 +
+
讲解:大雁南飞
+
+ +
+
+ 4 + ✏️ 练习 + +
+
3 道题
+
+ +
+
+ 5 + 📌 小结 + +
+
总结全文
+
+ +
+
🔵 范围锚定:默认 opacity:0
+
🔴 点锚定:默认 opacity:0.3
+
+
+
+
+
+
+ + +
+

选中"导入"节点(范围锚定)— 文本背景显示

+
+
选中节点 1 — "天气凉了"背景色显示
+
+
+
+ + + + + + + + + + + + + + +
+
+
秋天
+

+ + 天气凉了,树叶黄了, +

+

+ + 天空那么蓝,那么高。 +

+

一群大雁往南飞,

+

+ 一会儿排成个"人"字 + , +

+

+ 啊!秋天来了! +

+
+
+ + +
+
+ 1 + 💡 导入 + 范围·选中 +
+
提问:你见过秋天的树叶吗?
+
+ + +
+
+ 2 + 📝 文本研习 +
+
+ +
+
+ 3 + 📚 新授 +
+
+ +
+ ✅ 选中"导入"节点 → "天气凉了"背景显示 +
+
+
+
+
+
+ + +
+

选中"文本研习"节点(点锚定)— 占位符不透明

+
+
选中节点 2 — 占位符 ② 100% 显示
+
+
+
+ + + + + + + + + + + +
+
+
秋天
+

+ + 天气凉了,树叶黄了, +

+

+ + 天空那么蓝,那么高。 +

+

一群大雁往南飞,

+

+ 一会儿排成个"人"字 + , +

+

+ 啊!秋天来了! +

+
+
+ +
+
+ 1 + 💡 导入 +
+
+ + +
+
+ 2 + 📝 文本研习 + 点·选中 +
+
赏析:叠词的运用
+
+ +
+
+ 3 + 📚 新授 +
+
+ +
+ ✅ 选中"文本研习"节点 → 占位符 ② 不透明显示 +
+
+
+
+
+
+ + +
+

两种锚定方式对比

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性范围锚定(选文本)点锚定(插入占位符)
触发方式选中一段文字 → 关联节点点击文本某位置 → 插入占位符
视觉表现文本背景色 = 节点颜色插入标记符号(①②③)
默认透明度 + opacity: 0 + (完全透明,和正常文本一样) + + opacity: 0.3 + (半透明,隐约可见) +
选中时透明度 + opacity: 0.3 + (背景色显示) + + opacity: 1 + (不透明,完整显示) +
连线透明度 + 默认 opacity: 0.1 · 选中时 opacity: 1 +
适用场景节点与具体文字内容相关(如赏析某词、讲解某句)节点对应文本某个位置(如在此处开始导入、在此处小结)
+
+
+ + +
+

数据模型

+
+
// 锚点 — 统一接口,区分 type
+
interface NodeAnchor {
+
  id: string;
+
  nodeId: string; // 关联的节点
+
  type: "range" | "point"; // 两种锚定方式
+
  start: number; // 正文纯文本偏移量
+
  end?: number; // range 锚定的结束偏移(point 无)
+
  textPreview?: string; // range 锚定的文字预览
+
}
+
+
// 渲染规则
+
function getAnchorStyle(anchor: NodeAnchor, isActive: boolean) {
+
  if (anchor.type === "range") {
+
    return { backgroundColor: getNodeColor(anchor.nodeId), opacity: isActive ? 0.3 : 0 };
+
  } else { // point
+
    return { opacity: isActive ? 1 : 0.3 };
+
  }
+
}
+
+
diff --git a/.superpowers/brainstorm/41500-1782168322.9344/state/server-stopped b/.superpowers/brainstorm/41500-1782168322.9344/state/server-stopped new file mode 100644 index 0000000..904e1ba --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1782143663726} diff --git a/.superpowers/brainstorm/41500-1782168322.9344/state/session-info.txt b/.superpowers/brainstorm/41500-1782168322.9344/state/session-info.txt new file mode 100644 index 0000000..a99a513 --- /dev/null +++ b/.superpowers/brainstorm/41500-1782168322.9344/state/session-info.txt @@ -0,0 +1,3 @@ +sessionDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344 +stateDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344\state +contentDir=e:\Desktop\CICD\.superpowers\brainstorm\41500-1782168322.9344\content diff --git a/.tsc_out.txt b/.tsc_out.txt new file mode 100644 index 0000000..d927273 --- /dev/null +++ b/.tsc_out.txt @@ -0,0 +1 @@ +src/app/(dashboard)/teacher/textbooks/error.tsx(3,10): error TS2305: Module '"@/shared/components/route-error"' has no exported member 'RouteError'. diff --git a/bugs/screenshots/announcements.png b/bugs/screenshots/announcements.png new file mode 100644 index 0000000..7819cc8 Binary files /dev/null and b/bugs/screenshots/announcements.png differ diff --git a/bugs/screenshots/login_failure.png b/bugs/screenshots/login_failure.png new file mode 100644 index 0000000..3800e4a Binary files /dev/null and b/bugs/screenshots/login_failure.png differ diff --git a/bugs/screenshots/messages.png b/bugs/screenshots/messages.png new file mode 100644 index 0000000..9ec5b43 Binary files /dev/null and b/bugs/screenshots/messages.png differ diff --git a/bugs/screenshots/teacher_attendance_error.png b/bugs/screenshots/teacher_attendance_error.png new file mode 100644 index 0000000..ebf3598 Binary files /dev/null and b/bugs/screenshots/teacher_attendance_error.png differ diff --git a/bugs/screenshots/teacher_attendance_sheet_error.png b/bugs/screenshots/teacher_attendance_sheet_error.png new file mode 100644 index 0000000..5b9b2dc Binary files /dev/null and b/bugs/screenshots/teacher_attendance_sheet_error.png differ diff --git a/bugs/screenshots/teacher_attendance_stats_error.png b/bugs/screenshots/teacher_attendance_stats_error.png new file mode 100644 index 0000000..a28afd1 Binary files /dev/null and b/bugs/screenshots/teacher_attendance_stats_error.png differ diff --git a/bugs/screenshots/teacher_classes.png b/bugs/screenshots/teacher_classes.png new file mode 100644 index 0000000..6267d6f Binary files /dev/null and b/bugs/screenshots/teacher_classes.png differ diff --git a/bugs/screenshots/teacher_classes_error.png b/bugs/screenshots/teacher_classes_error.png new file mode 100644 index 0000000..56a90de Binary files /dev/null and b/bugs/screenshots/teacher_classes_error.png differ diff --git a/bugs/screenshots/teacher_classes_my_error.png b/bugs/screenshots/teacher_classes_my_error.png new file mode 100644 index 0000000..9634b8d Binary files /dev/null and b/bugs/screenshots/teacher_classes_my_error.png differ diff --git a/bugs/screenshots/teacher_classes_schedule_error.png b/bugs/screenshots/teacher_classes_schedule_error.png new file mode 100644 index 0000000..0b305c5 Binary files /dev/null and b/bugs/screenshots/teacher_classes_schedule_error.png differ diff --git a/bugs/screenshots/teacher_classes_students_error.png b/bugs/screenshots/teacher_classes_students_error.png new file mode 100644 index 0000000..773f4f8 Binary files /dev/null and b/bugs/screenshots/teacher_classes_students_error.png differ diff --git a/bugs/screenshots/teacher_course-plans_error.png b/bugs/screenshots/teacher_course-plans_error.png new file mode 100644 index 0000000..ea3e273 Binary files /dev/null and b/bugs/screenshots/teacher_course-plans_error.png differ diff --git a/bugs/screenshots/teacher_dashboard_error.png b/bugs/screenshots/teacher_dashboard_error.png new file mode 100644 index 0000000..38200a1 Binary files /dev/null and b/bugs/screenshots/teacher_dashboard_error.png differ diff --git a/bugs/screenshots/teacher_diagnostic.png b/bugs/screenshots/teacher_diagnostic.png new file mode 100644 index 0000000..d6fa78d Binary files /dev/null and b/bugs/screenshots/teacher_diagnostic.png differ diff --git a/bugs/screenshots/teacher_diagnostic_error.png b/bugs/screenshots/teacher_diagnostic_error.png new file mode 100644 index 0000000..6a98c5d Binary files /dev/null and b/bugs/screenshots/teacher_diagnostic_error.png differ diff --git a/bugs/screenshots/teacher_elective_error.png b/bugs/screenshots/teacher_elective_error.png new file mode 100644 index 0000000..05bcfc6 Binary files /dev/null and b/bugs/screenshots/teacher_elective_error.png differ diff --git a/bugs/screenshots/teacher_error-book.png b/bugs/screenshots/teacher_error-book.png new file mode 100644 index 0000000..a8efce9 Binary files /dev/null and b/bugs/screenshots/teacher_error-book.png differ diff --git a/bugs/screenshots/teacher_error-book_error.png b/bugs/screenshots/teacher_error-book_error.png new file mode 100644 index 0000000..eb9dc07 Binary files /dev/null and b/bugs/screenshots/teacher_error-book_error.png differ diff --git a/bugs/screenshots/teacher_exams_all_error.png b/bugs/screenshots/teacher_exams_all_error.png new file mode 100644 index 0000000..681bfe1 Binary files /dev/null and b/bugs/screenshots/teacher_exams_all_error.png differ diff --git a/bugs/screenshots/teacher_exams_create_error.png b/bugs/screenshots/teacher_exams_create_error.png new file mode 100644 index 0000000..2ec3cee Binary files /dev/null and b/bugs/screenshots/teacher_exams_create_error.png differ diff --git a/bugs/screenshots/teacher_exams_error.png b/bugs/screenshots/teacher_exams_error.png new file mode 100644 index 0000000..e91adb6 Binary files /dev/null and b/bugs/screenshots/teacher_exams_error.png differ diff --git a/bugs/screenshots/teacher_grades.png b/bugs/screenshots/teacher_grades.png new file mode 100644 index 0000000..d48c58f Binary files /dev/null and b/bugs/screenshots/teacher_grades.png differ diff --git a/bugs/screenshots/teacher_grades_analytics_error.png b/bugs/screenshots/teacher_grades_analytics_error.png new file mode 100644 index 0000000..1998865 Binary files /dev/null and b/bugs/screenshots/teacher_grades_analytics_error.png differ diff --git a/bugs/screenshots/teacher_grades_entry.png b/bugs/screenshots/teacher_grades_entry.png new file mode 100644 index 0000000..d48c58f Binary files /dev/null and b/bugs/screenshots/teacher_grades_entry.png differ diff --git a/bugs/screenshots/teacher_grades_stats.png b/bugs/screenshots/teacher_grades_stats.png new file mode 100644 index 0000000..d48c58f Binary files /dev/null and b/bugs/screenshots/teacher_grades_stats.png differ diff --git a/bugs/screenshots/teacher_homework_assignments_create_error.png b/bugs/screenshots/teacher_homework_assignments_create_error.png new file mode 100644 index 0000000..d4b63fb Binary files /dev/null and b/bugs/screenshots/teacher_homework_assignments_create_error.png differ diff --git a/bugs/screenshots/teacher_homework_assignments_error.png b/bugs/screenshots/teacher_homework_assignments_error.png new file mode 100644 index 0000000..ebf8239 Binary files /dev/null and b/bugs/screenshots/teacher_homework_assignments_error.png differ diff --git a/bugs/screenshots/teacher_homework_error.png b/bugs/screenshots/teacher_homework_error.png new file mode 100644 index 0000000..e504a77 Binary files /dev/null and b/bugs/screenshots/teacher_homework_error.png differ diff --git a/bugs/screenshots/teacher_homework_submissions.png b/bugs/screenshots/teacher_homework_submissions.png new file mode 100644 index 0000000..cab2452 Binary files /dev/null and b/bugs/screenshots/teacher_homework_submissions.png differ diff --git a/bugs/screenshots/teacher_homework_submissions_error.png b/bugs/screenshots/teacher_homework_submissions_error.png new file mode 100644 index 0000000..f6de446 Binary files /dev/null and b/bugs/screenshots/teacher_homework_submissions_error.png differ diff --git a/bugs/screenshots/teacher_lesson-plans_error.png b/bugs/screenshots/teacher_lesson-plans_error.png new file mode 100644 index 0000000..1910bda Binary files /dev/null and b/bugs/screenshots/teacher_lesson-plans_error.png differ diff --git a/bugs/screenshots/teacher_lesson-plans_new_error.png b/bugs/screenshots/teacher_lesson-plans_new_error.png new file mode 100644 index 0000000..e42606e Binary files /dev/null and b/bugs/screenshots/teacher_lesson-plans_new_error.png differ diff --git a/bugs/screenshots/teacher_questions_error.png b/bugs/screenshots/teacher_questions_error.png new file mode 100644 index 0000000..ddca749 Binary files /dev/null and b/bugs/screenshots/teacher_questions_error.png differ diff --git a/bugs/screenshots/teacher_schedule-changes.png b/bugs/screenshots/teacher_schedule-changes.png new file mode 100644 index 0000000..e89cfc9 Binary files /dev/null and b/bugs/screenshots/teacher_schedule-changes.png differ diff --git a/bugs/screenshots/teacher_schedule-changes_error.png b/bugs/screenshots/teacher_schedule-changes_error.png new file mode 100644 index 0000000..75937d9 Binary files /dev/null and b/bugs/screenshots/teacher_schedule-changes_error.png differ diff --git a/bugs/screenshots/teacher_textbooks_error.png b/bugs/screenshots/teacher_textbooks_error.png new file mode 100644 index 0000000..2fda262 Binary files /dev/null and b/bugs/screenshots/teacher_textbooks_error.png differ diff --git a/build-output.txt b/build-output.txt new file mode 100644 index 0000000..1edc643 --- /dev/null +++ b/build-output.txt @@ -0,0 +1,1734 @@ + +> cicd@0.1.0 build +> next build + +node.exe : [baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data +, please update: `npm i baseline-browser-mapping@latest -D` +At line:1 char:1 ++ & "C:\Program Files\nodejs/node.exe" "C:\Program Files\nodejs/node_mo ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: ([baseline-brows...ping@latest -D`:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ▲ Next.js 16.0.10 (Turbopack) + - Environments: .env + + Creating an optimized production build ... +[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please up +date: `npm i baseline-browser-mapping@latest -D` + +> Build error occurred +Error: Turbopack build failed with 38 errors: +./src/modules/audit/retention.ts:1:1 +Ecmascript file had an error +> 1 | import "server-only" +  | ^^^^^[3 +1m^^^^^^[ +31m^^^^^^ +[31m^^^ +  2 | +  3 | import { lt } from "drizzle-orm" +  4 | + +You're importing a component that needs "server-only". That only works in a Server Component which is not supported in +the pages/ directory. Read more: https://nextjs.org/docs/app/building-your-application/rendering/server-components + + + +Import traces: + App Route: + ./src/modules/audit/retention.ts + ./src/app/api/cron/audit-cleanup/route.ts + + Server Component: + ./src/modules/audit/retention.ts + ./src/modules/audit/actions.ts + ./src/modules/audit/services/admin-audit-service.ts + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx + + Client Component Browser: + ./src/modules/audit/retention.ts [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/audit/retention.ts [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + +./src/modules/course-plans/actions.ts:488:17 +Ecmascript file had an error +  486 | // 预留埋点接口,供前端调用以记录关键操作。 +  487 | // 当前为空实现,后续接入监控 SDK 时只需修改此函数。 +> 488 | export function trackCoursePlanEvent( +  | ^^^^ +^^^^^^[31 +m^^^^^^[3 +1m^^^^ +  489 | event: string, +  490 | properties?: Record<string, unknown[33 +m> +  491 | ): void { + +Server Actions must be async functions. + + + +./src/modules/exams/actions.ts:49:1 +Ecmascript file had an error +  47 | // Re-export 从拆分文件迁移的导出,保持外部 import 路径不变 +  48 | export type { AutoMarkResult } from "./ai-pipeline/auto-mark" +> 49 | export { autoMarkExamAction } from "./ai-pipeline/auto-mar +k" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^ +  50 | export { createExamFromRichEditorAction, updateExamFromRichEditorAction } fro +m "./actions-rich-editor" +  51 | export type { AiPreviewData, AiRewriteQuestionData } from + "./ai-pipeline" +  52 | + +Only async functions are allowed to be exported in a "use server" file. + + +Import traces: + Client Component Browser: + ./src/modules/exams/actions.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/actions.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/settings/data-access-system-settings.ts:1:1 +Ecmascript file had an error +> 1 | import "server-only" +  | ^^^^^[3 +1m^^^^^^[ +31m^^^^^^ +[31m^^^ +  2 | +  3 | import { and, eq } from "drizzle-orm" +  4 | + +You're importing a component that needs "server-only". That only works in a Server Component which is not supported in +the pages/ directory. Read more: https://nextjs.org/docs/app/building-your-application/rendering/server-components + + + +Import traces: + #1 [Server Component]: + ./src/modules/settings/data-access-system-settings.ts + ./src/modules/settings/actions-system-settings.ts + + #2 [App Route]: + ./src/modules/settings/data-access-system-settings.ts + ./src/modules/audit/retention.ts + ./src/app/api/cron/audit-cleanup/route.ts + + #3 [Server Component]: + ./src/modules/settings/data-access-system-settings.ts + ./src/modules/settings/data-access-two-factor.ts + ./src/modules/auth/services/two-factor-service.ts + ./src/modules/auth/actions.ts + ./src/app/(auth)/register/page.tsx + + #4 [Client Component Browser]: + ./src/modules/settings/data-access-system-settings.ts [Client Component Browser] + ./src/modules/audit/retention.ts [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + #5 [Client Component SSR]: + ./src/modules/settings/data-access-system-settings.ts [Client Component SSR] + ./src/modules/audit/retention.ts [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + #6 [Edge Instrumentation]: + ./src/modules/settings/data-access-system-settings.ts + ./src/modules/settings/data-access-two-factor.ts + ./src/modules/auth/services/two-factor-service.ts + ./src/modules/auth/services/login-service.ts + ./src/auth.ts + ./src/modules/classes/data-access.ts + ./src/modules/homework/data-access.ts + ./src/instrumentation.ts + + #7 [Instrumentation]: + ./src/modules/settings/data-access-system-settings.ts + ./src/modules/settings/data-access-two-factor.ts + ./src/modules/auth/services/two-factor-service.ts + ./src/modules/auth/services/login-service.ts + ./src/auth.ts + ./src/modules/classes/data-access.ts + ./src/modules/homework/data-access.ts + ./src/instrumentation.ts + + +./node_modules/mysql2/lib/base/connection.js:18:13 +Module not found: Can't resolve 'net' +  16 | 'use strict'; +  17 | +> 18 | const Net = require('net'); +  | ^^^^^ +^^^^^ +^^^^ +  19 | const Tls = require('tls'); +  20 | const Timers = require('timers'); +  21 | const EventEmitter = require('events').EventEmi +tter; + + + +Import traces: + App Route: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/app/api/search/route.ts + + Edge Instrumentation: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Instrumentation: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Server Component: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/modules/elective/data-access.ts + ./src/app/(dashboard)/admin/elective/[id]/edit/page.tsx + + Client Component Browser: + ./node_modules/mysql2/lib/base/connection.js [Client Component Browser] + ./node_modules/mysql2/lib/promise/connection.js [Client Component Browser] + ./node_modules/mysql2/promise.js [Client Component Browser] + ./src/shared/db/index.ts [Client Component Browser] + ./src/modules/audit/retention.ts [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + Client Component SSR: + ./node_modules/mysql2/lib/base/connection.js [Client Component SSR] + ./node_modules/mysql2/lib/promise/connection.js [Client Component SSR] + ./node_modules/mysql2/promise.js [Client Component SSR] + ./src/shared/db/index.ts [Client Component SSR] + ./src/modules/audit/retention.ts [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + +./node_modules/mysql2/lib/server.js:3:13 +Module not found: Can't resolve 'net' +  1 | 'use strict'; +  2 | +> 3 | const net = require('net'); +  | ^^^^^ +[22m^^^^^^ +^^^ +  4 | const EventEmitter = require('events').EventEmit +ter; +  5 | +  6 | const Connection = require('./connection'); + + + +Import traces: + App Route: + ./node_modules/mysql2/lib/server.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/app/api/search/route.ts + + Edge Instrumentation: + ./node_modules/mysql2/lib/server.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Instrumentation: + ./node_modules/mysql2/lib/server.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Server Component: + ./node_modules/mysql2/lib/server.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/modules/textbooks/data-access.ts + ./src/app/(dashboard)/teacher/textbooks/[id]/page.tsx + + Client Component Browser: + ./node_modules/mysql2/lib/server.js [Client Component Browser] + ./node_modules/mysql2/index.js [Client Component Browser] + ./node_modules/drizzle-orm/mysql2/driver.js [Client Component Browser] + ./src/shared/db/index.ts [Client Component Browser] + ./src/modules/audit/retention.ts [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + Client Component SSR: + ./node_modules/mysql2/lib/server.js [Client Component SSR] + ./node_modules/mysql2/index.js [Client Component SSR] + ./node_modules/drizzle-orm/mysql2/driver.js [Client Component SSR] + ./src/shared/db/index.ts [Client Component SSR] + ./src/modules/audit/retention.ts [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + +./node_modules/mysql2/lib/auth_plugins/sha256_password.js:6:13 +Module not found: Can't resolve 'tls' +  4 | const crypto = require('crypto'); +  5 | const { xorRotating } = require('../auth_41'); +> 6 | const Tls = require('tls'); +  | ^^^^^ +[22m^^^^^^ +^^^ +  7 | +  8 | const REQUEST_SERVER_KEY_PACKET = Buffer.from[3 +9m([1]); +  9 | + + + +Import traces: + App Route: + ./node_modules/mysql2/lib/auth_plugins/sha256_password.js + ./node_modules/mysql2/lib/auth_plugins/index.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/app/api/search/route.ts + + Edge Instrumentation: + ./node_modules/mysql2/lib/auth_plugins/sha256_password.js + ./node_modules/mysql2/lib/auth_plugins/index.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Instrumentation: + ./node_modules/mysql2/lib/auth_plugins/sha256_password.js + ./node_modules/mysql2/lib/auth_plugins/index.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Server Component: + ./node_modules/mysql2/lib/auth_plugins/sha256_password.js + ./node_modules/mysql2/lib/auth_plugins/index.js + ./node_modules/mysql2/index.js + ./node_modules/drizzle-orm/mysql2/driver.js + ./src/shared/db/index.ts + ./src/modules/audit/data-access.ts + ./src/app/(dashboard)/admin/audit-logs/data-changes/page.tsx + + Client Component Browser: + ./node_modules/mysql2/lib/auth_plugins/sha256_password.js [Client Component Browser] + ./node_modules/mysql2/lib/auth_plugins/index.js [Client Component Browser] + ./node_modules/mysql2/index.js [Client Component Browser] + ./node_modules/drizzle-orm/mysql2/driver.js [Client Component Browser] + ./src/shared/db/index.ts [Client Component Browser] + ./src/modules/audit/retention.ts [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + Client Component SSR: + ./node_modules/mysql2/lib/auth_plugins/sha256_password.js [Client Component SSR] + ./node_modules/mysql2/lib/auth_plugins/index.js [Client Component SSR] + ./node_modules/mysql2/index.js [Client Component SSR] + ./node_modules/drizzle-orm/mysql2/driver.js [Client Component SSR] + ./src/shared/db/index.ts [Client Component SSR] + ./src/modules/audit/retention.ts [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + +./node_modules/mysql2/lib/base/connection.js:19:13 +Module not found: Can't resolve 'tls' +  17 | +  18 | const Net = require('net'); +> 19 | const Tls = require('tls'); +  | ^^^^^ +^^^^^ +^^^^ +  20 | const Timers = require('timers'); +  21 | const EventEmitter = require('events').EventEmi +tter; +  22 | const Readable = require('stream').Readable[39 +m; + + + +Import traces: + App Route: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/app/api/search/route.ts + + Edge Instrumentation: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Instrumentation: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/modules/exams/data-access.ts + ./src/instrumentation.ts + + Server Component: + ./node_modules/mysql2/lib/base/connection.js + ./node_modules/mysql2/lib/promise/connection.js + ./node_modules/mysql2/promise.js + ./src/shared/db/index.ts + ./src/modules/elective/data-access.ts + ./src/app/(dashboard)/admin/elective/[id]/edit/page.tsx + + Client Component Browser: + ./node_modules/mysql2/lib/base/connection.js [Client Component Browser] + ./node_modules/mysql2/lib/promise/connection.js [Client Component Browser] + ./node_modules/mysql2/promise.js [Client Component Browser] + ./src/shared/db/index.ts [Client Component Browser] + ./src/modules/audit/retention.ts [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component Browser] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + + Client Component SSR: + ./node_modules/mysql2/lib/base/connection.js [Client Component SSR] + ./node_modules/mysql2/lib/promise/connection.js [Client Component SSR] + ./node_modules/mysql2/promise.js [Client Component SSR] + ./src/shared/db/index.ts [Client Component SSR] + ./src/modules/audit/retention.ts [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Client Component SSR] + ./src/modules/audit/components/audit-retention-settings.tsx [Server Component] + ./src/modules/audit/components/audit-overview-view.tsx [Server Component] + ./src/app/(dashboard)/admin/audit-logs/overview/page.tsx [Server Component] + +https://nextjs.org/docs/messages/module-not-found + + +./src/modules/audit +Invalid import +'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component. +The error was caused by importing 'src/modules/audit' + + + +./src/modules/settings +Invalid import +'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component. +The error was caused by importing 'src/modules/settings' + + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export createAiExamAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export createAiExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export createAiExamAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export createAiExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export createExamAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export createExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export createExamAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export createExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export deleteExamAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export deleteExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export deleteExamAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export deleteExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export duplicateExamAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export duplicateExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript) +. +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export duplicateExamAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export duplicateExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export getExamPreviewAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export getExamPreviewAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript +). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export getExamPreviewAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export getExamPreviewAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export getGradesAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export getGradesAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export getGradesAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export getGradesAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-rich-form.tsx:20:1 +Export getGradesAction doesn't exist in target module +  18 | import { ResizablePanel } from "@/shared/components/ui/resizabl +e-panel" +  19 | import { ScrollArea } from "@/shared/components/ui/scroll-area"[39 +m +> 20 | import { getGradesAction, getSubjectsAction } from [ +32m"../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^ +  21 | import { autoMarkExamAction } from "../ai-pipeline/auto-mark" +  22 | import { +  23 | createExamFromRichEditorAction, + +The export getGradesAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-rich-form.tsx + ./src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx + + +./src/modules/exams/components/exam-rich-form.tsx:20:1 +Export getGradesAction doesn't exist in target module +  18 | import { ResizablePanel } from "@/shared/components/ui/resizabl +e-panel" +  19 | import { ScrollArea } from "@/shared/components/ui/scroll-area"[39 +m +> 20 | import { getGradesAction, getSubjectsAction } from [ +32m"../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^ +  21 | import { autoMarkExamAction } from "../ai-pipeline/auto-mark" +  22 | import { +  23 | createExamFromRichEditorAction, + +The export getGradesAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-rich-form.tsx + ./src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export getSubjectsAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export getSubjectsAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-form.tsx:11:1 +Export getSubjectsAction doesn't exist in target module +  9 | +  10 | import { Form } from "@/shared/components/ui/form" +> 11 | import { createAiExamAction, createExamAction, getSub +jectsAction, getGradesAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^ +  12 | import { getAiProviderSummaries, type AiProviderSummary } from + "@/modules/settings/actions" +  13 | +  14 | import { formSchema, type ExamFormValues, type PreviewSna +pshotMeta, defaultValues } from "./exam-form-types" + +The export getSubjectsAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-form.tsx + ./src/app/(dashboard)/teacher/exams/create/page.tsx + + +./src/modules/exams/components/exam-rich-form.tsx:20:1 +Export getSubjectsAction doesn't exist in target module +  18 | import { ResizablePanel } from "@/shared/components/ui/resizabl +e-panel" +  19 | import { ScrollArea } from "@/shared/components/ui/scroll-area"[39 +m +> 20 | import { getGradesAction, getSubjectsAction } from [ +32m"../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^ +  21 | import { autoMarkExamAction } from "../ai-pipeline/auto-mark" +  22 | import { +  23 | createExamFromRichEditorAction, + +The export getSubjectsAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-rich-form.tsx + ./src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx + + +./src/modules/exams/components/exam-rich-form.tsx:20:1 +Export getSubjectsAction doesn't exist in target module +  18 | import { ResizablePanel } from "@/shared/components/ui/resizabl +e-panel" +  19 | import { ScrollArea } from "@/shared/components/ui/scroll-area"[39 +m +> 20 | import { getGradesAction, getSubjectsAction } from [ +32m"../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^ +  21 | import { autoMarkExamAction } from "../ai-pipeline/auto-mark" +  22 | import { +  23 | createExamFromRichEditorAction, + +The export getSubjectsAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-rich-form.tsx + ./src/app/(dashboard)/teacher/exams/[id]/edit-rich/page.tsx + + +./src/modules/exams/components/exam-preview-utils.ts:5:1 +Export previewAiExamAction doesn't exist in target module +  3 | import { toast } from "sonner" +  4 | import type { useTranslations } from "next-intl" +> 5 | import { previewAiExamAction, type AiPreviewData } [3 +6mfrom "../actions" +  | ^^^^^[3 +1m^^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^ +  6 | import type { ExamNode } from "./assembly/selected-question-list"[3 +9m +  7 | import type { Question } from "@/modules/questions/types" +  8 | import type { EditableQuestionContent, ExamFormValues,[39 +m PreviewBackgroundTask, PreviewSnapshotMeta } from "./exam-form-types"[3 +9m + +The export previewAiExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript) +. +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-preview-utils.ts [Client Component Browser] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-preview-utils.ts [Client Component SSR] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/exams/components/exam-preview-utils.ts:5:1 +Export previewAiExamAction doesn't exist in target module +  3 | import { toast } from "sonner" +  4 | import type { useTranslations } from "next-intl" +> 5 | import { previewAiExamAction, type AiPreviewData } [3 +6mfrom "../actions" +  | ^^^^^[3 +1m^^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^ +  6 | import type { ExamNode } from "./assembly/selected-question-list"[3 +9m +  7 | import type { Question } from "@/modules/questions/types" +  8 | import type { EditableQuestionContent, ExamFormValues,[39 +m PreviewBackgroundTask, PreviewSnapshotMeta } from "./exam-form-types"[3 +9m + +The export previewAiExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-preview-utils.ts [Client Component Browser] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-preview-utils.ts [Client Component SSR] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/exams/hooks/use-exam-preview.ts:7:1 +Export previewAiExamAction doesn't exist in target module +  5 | import type { UseFormReturn } from "react-hook-form" +  6 | +> 7 | import { previewAiExamAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^ +  8 | import type { ExamFormValues } from "../components/exam-form-types" + +  9 | import { +  10 | buildPreviewNodes, + +The export previewAiExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript) +. +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/exams/hooks/use-exam-preview.ts:7:1 +Export previewAiExamAction doesn't exist in target module +  5 | import type { UseFormReturn } from "react-hook-form" +  6 | +> 7 | import { previewAiExamAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^ +  8 | import type { ExamFormValues } from "../components/exam-form-types" + +  9 | import { +  10 | buildPreviewNodes, + +The export previewAiExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/exams/hooks/use-exam-preview-rewrite.ts:9:1 +Export regenerateAiQuestionAction doesn't exist in target module +  7 | import type { ExamFormValues } from "../components/exam-form-ty +pes" +  8 | import { findPreviewQuestionNode, parseEditableContent } from "../c +omponents/exam-preview-utils" +> 9 | import { regenerateAiQuestionAction } from "../actions"[3 +9m +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^ +  10 | +  11 | type Translator = ReturnType<typeof useTranslations +[33m> +  12 | + +The export regenerateAiQuestionAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecma +script). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/hooks/use-exam-preview-rewrite.ts [Client Component Browser] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/hooks/use-exam-preview-rewrite.ts [Client Component SSR] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/exams/hooks/use-exam-preview-rewrite.ts:9:1 +Export regenerateAiQuestionAction doesn't exist in target module +  7 | import type { ExamFormValues } from "../components/exam-form-ty +pes" +  8 | import { findPreviewQuestionNode, parseEditableContent } from "../c +omponents/exam-preview-utils" +> 9 | import { regenerateAiQuestionAction } from "../actions"[3 +9m +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^ +  10 | +  11 | type Translator = ReturnType<typeof useTranslations +[33m> +  12 | + +The export regenerateAiQuestionAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascr +ipt). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/hooks/use-exam-preview-rewrite.ts [Client Component Browser] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Client Component Browser] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/hooks/use-exam-preview-rewrite.ts [Client Component SSR] + ./src/modules/exams/hooks/use-exam-preview.ts [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Client Component SSR] + ./src/modules/exams/components/exam-form.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/create/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export updateExamAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export updateExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-actions.tsx:35:1 +Export updateExamAction doesn't exist in target module +  33 | } from "@/shared/components/ui/dialog" +  34 | +> 35 | import { deleteExamAction, duplicateExamAction, updat +eExamAction, getExamPreviewAction } from "../actions" +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^^[2 +2m^^^^^^[ +22m^^^^^^ +[22m^^^^^^ +^^^^^ +^^^^^^[1 +m^^^^^^[ +1m^^^^^^ +[1m^^^^^^ +  36 | import { Exam } from "../types" +  37 | import { ExamPaperPreview } from "./assembly/exam-paper-preview"[3 +9m +  38 | import type { ExamNode } from "./assembly/selected-question-list"[ +39m + +The export updateExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import traces: + Client Component Browser: + ./src/modules/exams/components/exam-actions.tsx [Client Component Browser] + ./src/modules/exams/components/exam-columns.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Client Component Browser] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + Client Component SSR: + ./src/modules/exams/components/exam-actions.tsx [Client Component SSR] + ./src/modules/exams/components/exam-columns.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Client Component SSR] + ./src/modules/exams/components/exam-data-table.tsx [Server Component] + ./src/app/(dashboard)/teacher/exams/all/page.tsx [Server Component] + + +./src/modules/exams/components/exam-assembly.tsx:16:1 +Export updateExamAction doesn't exist in target module +  14 | import type { Question } from "@/modules/questions/types" +  15 | import type { QuestionType } from "@/modules/questions/types" +> 16 | import { updateExamAction } from "@/modules/exams/actions" + +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^ +  17 | import { getQuestionsAction } from "@/modules/questions/actions" +  18 | import { StructureEditor } from "./assembly/structure-editor" +  19 | import { QuestionBankList } from "./assembly/question-bank-list"[3 +9m + +The export updateExamAction was not found in module [project]/src/modules/exams/actions.ts [app-client] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-assembly.tsx + ./src/app/(dashboard)/teacher/exams/[id]/build/page.tsx + + +./src/modules/exams/components/exam-assembly.tsx:16:1 +Export updateExamAction doesn't exist in target module +  14 | import type { Question } from "@/modules/questions/types" +  15 | import type { QuestionType } from "@/modules/questions/types" +> 16 | import { updateExamAction } from "@/modules/exams/actions" + +  | ^^^^^[ +31m^^^^^^ +[31m^^^^^^ +^^^^^^[39 +m^^^^^^[3 +9m^^^^^^[ +39m^^^^^^ +[39m^^^^^^ +^^^^^^[22 +m^^^^^ +  17 | import { getQuestionsAction } from "@/modules/questions/actions" +  18 | import { StructureEditor } from "./assembly/structure-editor" +  19 | import { QuestionBankList } from "./assembly/question-bank-list"[3 +9m + +The export updateExamAction was not found in module [project]/src/modules/exams/actions.ts [app-ssr] (ecmascript). +The module has no exports at all. +All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the req +uested export doesn't exist. + +Import trace: + Server Component: + ./src/modules/exams/components/exam-assembly.tsx + ./src/app/(dashboard)/teacher/exams/[id]/build/page.tsx + + + at (./src/modules/audit/retention.ts:1:1) + at (./src/modules/course-plans/actions.ts:488:17) + at (./src/modules/exams/actions.ts:49:1) + at (./src/modules/settings/data-access-system-settings.ts:1:1) + at (https://nextjs.org/docs/messages/module-not-found) + at (https://nextjs.org/docs/messages/module-not-found) + at (https://nextjs.org/docs/messages/module-not-found) + at (https://nextjs.org/docs/messages/module-not-found) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-rich-form.tsx:20:1) + at (./src/modules/exams/components/exam-rich-form.tsx:20:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-form.tsx:11:1) + at (./src/modules/exams/components/exam-rich-form.tsx:20:1) + at (./src/modules/exams/components/exam-rich-form.tsx:20:1) + at (./src/modules/exams/components/exam-preview-utils.ts:5:1) + at (./src/modules/exams/components/exam-preview-utils.ts:5:1) + at (./src/modules/exams/hooks/use-exam-preview.ts:7:1) + at (./src/modules/exams/hooks/use-exam-preview.ts:7:1) + at (./src/modules/exams/hooks/use-exam-preview-rewrite.ts:9:1) + at (./src/modules/exams/hooks/use-exam-preview-rewrite.ts:9:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-actions.tsx:35:1) + at (./src/modules/exams/components/exam-assembly.tsx:16:1) + at (./src/modules/exams/components/exam-assembly.tsx:16:1) diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..c31d989 --- /dev/null +++ b/cookies.txt @@ -0,0 +1,4 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + diff --git a/debug-exams.png b/debug-exams.png new file mode 100644 index 0000000..92922f9 Binary files /dev/null and b/debug-exams.png differ diff --git a/docs/architecture/004_architecture_impact_map.md b/docs/architecture/004_architecture_impact_map.md index 96db0ca..3f5b9df 100644 --- a/docs/architecture/004_architecture_impact_map.md +++ b/docs/architecture/004_architecture_impact_map.md @@ -1255,7 +1255,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" - Lib(✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分,✅ v4-P2-6 新增 scope-check):`toNumber` / `normalize`(位于 `lib/grade-utils.ts`);`buildScopeClassFilter(scope, currentUserId?)`(P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`,P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数);`assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因:actions.ts 是 "use server" 文件要求所有 export 为 async,而 assertClassInScope 是同步函数) - Stats-service(✅ P1-1 新增):`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`(从 3 个 data-access 文件抽取的纯函数,使数据层专注 DB I/O,统计逻辑可独立测试) - Export(✅ v4-P1-12 新增):`exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` / `exportStudentGradeRecordsToExcel`(v4-P1-12 新增:导出单个学生成绩单家长视角,仅含成绩明细 + 个人统计,不含班级数据,scope 为 children 自动按 studentId 过滤)/ `formatDateForFile`(已迁移至 shared/lib/utils) -- Components(✅ P1-5 新增):`WidgetBoundary`(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增:管理员全校成绩汇总卡片,4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色) +- Components(✅ P1-5 新增,✅ Phase 4.6 拆分扩展):`WidgetBoundary`(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增:管理员全校成绩汇总卡片,4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色)/ `BatchGradeEntrySelectors`(✅ Phase 4.6 新增:批量录入试卷/班级选择器子组件)/ `BatchGradeEntryToolbar`(✅ Phase 4.6 新增:批量录入底部工具栏)/ `GradeRecordRow`(✅ Phase 4.6 新增:成绩记录行渲染:桌面表格+移动端卡片)/ `GradeRecordDetail`(✅ Phase 4.6 新增:成绩记录编辑/删除对话框集合,导出 EditableFields 类型与 toEditableFields 工具函数) **依赖关系**: - 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds)、`exams`(✅ 2026-06-24 新增:通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)、`error-book`(✅ 2026-06-25 新增:通过 error-book data-access.collectFromExamSubmission 在成绩录入后自动采集考试错题)、`diagnostic`(通过 diagnostic data-access.updateMasteryFromExamScore 更新掌握度) @@ -1365,25 +1365,25 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" | `components/school-wide-summary-card.tsx` | - | v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格) | | `components/score-cell.tsx` | 41 | v4-P1-7 新增:成绩单元格组件,根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名 | | `components/grade-trend-card.tsx` | 69 | 趋势卡片(v2-P2-9 修复:a11y;v2-P1-4:i18n;P3 修复:NaN 日期检查 + fullScore > 0 守卫) | -| `components/grade-record-list.tsx` | 200+ | 成绩记录列表(v2-P1-4:i18n;P3 修复:safeActionCall 包装删除操作;v4-P1-7:使用 ScoreCell;v4-P1-10:overflow-x-auto;v3-P3-2 新增:多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框,接入 bulkDeleteGradeRecordsAction) | | `components/grade-distribution-chart.tsx` | 100 | 分数分布图(v2-P1-4:i18n) | | `components/subject-comparison-chart.tsx` | 62 | 科目对比图(v2-P1-4:i18n) | | `components/class-comparison-chart.tsx` | 194 | 班级对比图(v2-P1-4:i18n;v3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) | | `components/grade-trend-chart.tsx` | 59 | 趋势图(v2-P1-4:i18n) | | `components/grade-record-form.tsx` | 177 | 录入表单(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall 包装提交) | -| `components/batch-grade-entry.tsx` | 422 | 2026-06-24 完全重写:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。**P1-6/P1-7 重构(2026-06-25)**:拆分为 3 个子组件 + 1 个 Hook(BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo),主组件从 604 行降至 422 行;撤销数据使用 isUndoData 类型守卫替代 as { ids: string[]; timestamp: number } 断言;window.confirm() 已替换为声明式 AlertDialog。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?(原 v2-P2-7 Label htmlFor;v2-P1-4 i18n;P3 safeActionCall + localStorage 安全检查;v3-P2 服务端草稿;v3-P3-1 下载 CSV 模板;v4-P3-2 新手引导均已替换) | +| `components/batch-grade-entry.tsx` | 376 | 2026-06-24 完全重写:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。**P1-6/P1-7 重构(2026-06-25)**:拆分为 3 个子组件 + 1 个 Hook(BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo);**Phase 4.6 重构(2026-07-06)**:进一步拆出 BatchGradeEntrySelectors(试卷/班级选择器含未选态)+ BatchGradeEntryToolbar(保存/清除按钮+粘贴提示),主组件从 457 行降至 376 行,容器仅保留状态编排与 Server Action 调用。撤销数据使用 isUndoData 类型守卫替代 as { ids: string[]; timestamp: number } 断言;window.confirm() 已替换为声明式 AlertDialog。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId? | +| `components/batch-grade-entry-selectors.tsx` | 124 | **Phase 4.6 新增(2026-07-06)**:批量录入试卷/班级选择器子组件(含未选试卷态引导)。从 batch-grade-entry.tsx 拆分,承接试卷选择器、班级选择器、试卷信息徽章三个区块的渲染 | +| `components/batch-grade-entry-toolbar.tsx` | 53 | **Phase 4.6 新增(2026-07-06)**:批量录入底部工具栏子组件(清除按钮 + 保存按钮 + 粘贴提示)。从 batch-grade-entry.tsx 拆分 | | `components/batch-grade-entry-table.tsx` | 115 | **P1-6 新增(2026-06-25)**:批量录入 Excel 式表格子组件(行=学生,列=题目,末列=总分自动计算)。支持多行多列 Excel 粘贴、Enter 跳下一行、Tab 跳下一格、分数校验(超过题目满分标红)。接受 inputRefs 供父组件管理输入焦点 | | `components/batch-grade-entry-stats.tsx` | 68 | **P1-6 新增(2026-06-25)**:批量录入统计栏子组件,显示已录入/总数/均分/最高/最低/无效分数徽章,含学生搜索框过滤。导出 BatchGradeEntryStatsData 接口 | | `components/batch-grade-entry-dialog.tsx` | 58 | **P1-6 新增(2026-06-25)**:批量录入切换确认对话框子组件(替代 window.confirm())。导出 PendingSwitch 联合类型 { kind: "exam"; examId; message } \| { kind: "class"; classId; message } \| null | | `hooks/use-batch-grade-entry-undo.ts` | 86 | **P1-6/P1-7 新增(2026-06-25)**:批量录入撤销 Hook。封装撤销令牌的序列化、sessionStorage 读写、5 分钟 TTL 过期检查、撤销 Action 调用。包含 isUndoData 类型守卫函数(替代 as 断言),使用 in 运算符做安全属性访问 | -| `components/grade-filters.tsx` | 76 | 过滤器(v2-P1-4:i18n;**P2-2(2026-06-25)**:补全 : JSX.Element 返回值类型) | | `components/student-grade-summary.tsx` | 107 | 学生成绩摘要(v2-P1-4:i18n) | | `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作;**P2-2(2026-06-25)**:handleExport 补全 : Promise 返回值类型) | -| `components/analytics-filters.tsx` | 86+ | 分析过滤器(v2-P1-4:i18n;v3-P2 新增:exams/currentExamId/currentSemester props,添加学期和考试筛选 ChipNav) | | `components/stats-class-selector.tsx` | 40 | 统计班级选择器(v2-P1-4:i18n) | -| `components/grade-stats-card.tsx` | 74 | 统计卡片(v2-P1-4:i18n) | -| `components/class-grade-report.tsx` | 90 | 班级成绩报告(v2-P1-4:i18n) | -| `components/grade-query-filters.tsx` | 96 | 查询过滤器(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;**P2-2(2026-06-25)**:补全 : JSX.Element 返回值类型) | +| `components/class-grade-report.tsx` | 130 | 班级成绩报告(v2-P1-4:i18n;**Phase 4.6 重构(2026-07-06)**:内联原 GradeStatsCard 实现,使用 StatsGrid + 8 个 StatCard(average/median/max/min/stdDev/passRate/excellentRate/count),含 count=0 空态 Card) | +| `components/grade-record-list.tsx` | 232 | 成绩记录列表(v2-P1-4:i18n;P3 修复:safeActionCall 包装删除操作;v4-P1-7:使用 ScoreCell;v4-P1-10:overflow-x-auto;v3-P3-2 新增:多选复选框 + 全选 + 批量删除工具栏 + 批量删除确认对话框;**Phase 4.6 重构(2026-07-06)**:行渲染(桌面表格+移动端卡片)拆分至 GradeRecordRow,编辑/删除/批量删除对话框拆分至 GradeRecordDetail,主组件从 523 行降至 232 行) | +| `components/grade-record-row.tsx` | 175 | **Phase 4.6 新增(2026-07-06)**:成绩记录行渲染子组件,承接桌面端表格行 + 移动端卡片堆叠视图,含 v3-P3-2 多选复选框 + 全选 + v4-P1-7 ScoreCell 颜色编码 | +| `components/grade-record-detail.tsx` | 215 | **Phase 4.6 新增(2026-07-06)**:成绩记录详情/编辑/删除对话框集合,含编辑表单、单条删除确认、批量删除确认三个对话框。导出 EditableFields 类型与 toEditableFields 工具函数 | | `components/report-card-view.tsx` | 323 | **P3-1 新增(2026-06-26)**:A4 打印格式成绩报告卡服务端组件。含表头(学校名+标题+学期)、学生信息网格、科目成绩表格(rowSpan 合并科目名/排名列)、汇总统计(均分/班排名/及格率/优秀率)、教师评语+签字区。使用 globals.css `@media print` 规则控制打印布局 | | `components/report-card-print-button.tsx` | 106 | **P3-1 新增(2026-06-26)**:跳转至报告卡页面的客户端按钮,支持学年/学期筛选 | | `components/report-card-print-action.tsx` | 48 | **P3-1 新增(2026-06-26)**:调用 window.print() 的客户端按钮组件,含 loading 状态和错误 toast | @@ -1396,6 +1396,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions" > - **grade-record-list.tsx**:编辑保存 `isSaving` useState → `useOptimistic` + `useTransition`。`handleEditSave` 构造 `optimisticRecord` 在 `startSaveTransition` 内 `addOptimisticRecord` + `await updateGradeRecordAction` + `router.refresh()`;两处 `records.map` 改为 `optimisticRecords.map`(桌面表格 + 移动端卡片视图)即时显示乐观记录 > - **关键决策**:成功后调用 `router.refresh()` 同步数据源,让 `useOptimistic` 回滚到最新服务端值;不为删除操作使用 useOptimistic,因为删除需要服务端确认(外键约束、关联记录检查等) +> 架构变更(2026-07-06,Phase 4.6 — grades 模块组件化重构闭环): +> - **batch-grade-entry.tsx**(457→376 行):拆出 `BatchGradeEntrySelectors`(试卷/班级选择器含未选态)+ `BatchGradeEntryToolbar`(保存/清除按钮+粘贴提示),容器仅保留状态编排与 Server Action 调用 +> - **grade-record-list.tsx**(523→232 行):拆出 `GradeRecordRow`(行渲染:桌面表格+移动端卡片)+ `GradeRecordDetail`(编辑/单条删除/批量删除对话框集合) +> - **删除 4 个重复组件**:`grade-stats-card.tsx`(迁移至 `StatsGrid`,由 class-grade-report.tsx 直接使用)/ `grade-filters.tsx`(迁移至调用方 `student/grades/student-grade-filters.tsx`,复用 `FilterBar` + `FilterSearchInput`)/ `grade-query-filters.tsx`(迁移至调用方 `teacher/grades/teacher-grade-query-filters.tsx`,复用 `FilterBar` + Select)/ `analytics-filters.tsx`(迁移至调用方 `teacher/grades/analytics/analytics-grade-filters.tsx`,复用 `FilterBar` + `ChipNav`) + --- ## 2.7 classes(班级模块)— 耦合最严重 diff --git a/docs/architecture/005_architecture_data.json b/docs/architecture/005_architecture_data.json index 1f4ce72..6071897 100644 --- a/docs/architecture/005_architecture_data.json +++ b/docs/architecture/005_architecture_data.json @@ -5,8 +5,8 @@ "generatedAt": "2026-06-17", "formatVersion": "1.1", "rule": "每次文件修改后须同步更新本文件", - "lastUpdate": "缓存策略落地专项重构完成(2026-07-05):全栈缓存一致性重构,覆盖全部 28 个模块。基础设施层:新增 shared/lib/cache/ 9 个文件(types/memory-store/redis-store/store-factory/cache-fn/invalidation-map/client-invalidation-map/invalidate/index)+ 共享 redis-client.ts + 客户端 query-keys.ts 工厂。核心机制:(1) cacheFn 双层包装器 — 外层 react.cache 请求级 memoization,内层 cacheStore.getOrSet 跨请求/跨实例缓存(按 tag 失效,TTL 可选);(2) INVALIDATION_MAP 集中式失效映射表 — 扩展至 203 个 actionId,每个声明 { tags, queryKeys, paths } 三段副作用;(3) invalidateFor 三步编排 — CacheStore.invalidateTags → revalidateTag × N → revalidatePath × N(Next.js 16 revalidateTag 第二参数必填 \"default\");(4) CLIENT_INVALIDATION_MAP 客户端可见子集(仅 queryKeys 字段,避免拉入 server-only 依赖);(5) CACHE_DRIVER 环境变量切换 memory/redis driver,复用 rate-limit 模式。迁移范围:data-access 83 个文件 250+ 查询函数迁移至 cacheFn(双导出 raw + 包装版本,单测可 mock);actions 43 个文件 247 处 revalidatePath 替换为 invalidateFor;INVALIDATION_MAP 覆盖全部 28 个模块。Hook 双模式:useActionQuery 传 queryKey 走 useQuery 跨页共享缓存,不传走旧 useEffect+useState;useActionMutation 传 mutationFn+actionId 自动 invalidate,旧 mutate(action) 仍可用。ESLint 强制规则:no-restricted-syntax 禁止 Server Actions 中直接调用 revalidatePath/revalidateTag,仅 shared/lib/cache/invalidate.ts 豁免;data-access 查询函数双导出 raw 模式便于单测 mock。验证:npx tsc --noEmit 零错误;npm run lint 零 no-restricted-syntax 错误(仅 3 个 pre-existing react-hooks/refs 错误位于未修改文件);npx vitest run src/shared/lib/cache/ 25/25 测试通过。详细文档见 004 第 1.1.6 节。", - "lastUpdated": "2026-07-05" + "lastUpdate": "Phase 4.6 grades 模块组件化重构闭环完成(2026-07-06):(1) 拆分 batch-grade-entry.tsx 巨型文件 — 将试卷/班级选择器(含未选态引导)拆至 batch-grade-entry-selectors.tsx、底部工具栏(清除/保存/粘贴提示)拆至 batch-grade-entry-toolbar.tsx,主组件从 457 行降至 374 行,容器仅负责状态编排与 Server Action 调用;(2) 拆分 grade-record-list.tsx 巨型文件 — 行渲染(桌面表格 + 移动端卡片堆叠)拆至 grade-record-row.tsx、编辑/单条删除/批量删除对话框集合拆至 grade-record-detail.tsx(导出 EditableFields 类型 + toEditableFields 工具函数),主组件从 523 行降至 231 行;(3) 迁移 4 个重复组件到共享底座并删除原文件 — grade-stats-card.tsx → StatsGrid + StatItem 组合(class-grade-report.tsx 内联 8 个统计项)、grade-filters.tsx → app 层 student-grade-filters.tsx(复用 FilterBar + FilterSearchInput + Select + nuqs useQueryState)、grade-query-filters.tsx → app 层 teacher-grade-query-filters.tsx(复用 FilterBar + Select + useRouter/useSearchParams)、analytics-filters.tsx → app 层 analytics-grade-filters.tsx(复用 FilterBar + ChipNav,保留原 buildHref URL 构造逻辑)。同步更新:StatItem/ChipNav/FilterBar/FilterSearchInput 的 usedBy 列表、GradeStats/isGradeType/isSemester 的 usedBy 列表、BatchGradeEntry 与 GradeRecordList 与 ClassGradeReport 的 purpose/props/deps、新增 4 个子组件条目(BatchGradeEntrySelectors/BatchGradeEntryToolbar/GradeRecordRow/GradeRecordDetail)、删除 4 个旧组件条目(GradeStatsCard/GradeQueryFilters/AnalyticsFilters 及 grades/components/grade-filters 隐式条目)。验证:npx tsc --noEmit 零错误;npm run lint 仅 3 个 pre-existing react-hooks/refs 错误(位于未修改的 use-exam-preview-tasks.ts);grep 确认旧组件名(GradeStatsCard/GradeFilters/GradeQueryFilters/AnalyticsFilters)无业务代码残留。前次更新:缓存策略落地专项重构完成(2026-07-05),覆盖全部 28 个模块,新增 shared/lib/cache/ 9 个文件 + INVALIDATION_MAP 203 个 actionId,data-access 83 文件 250+ 查询函数迁移至 cacheFn 双导出模式,actions 43 文件 247 处 revalidatePath 替换为 invalidateFor,详细见 004 第 1.1.6 节。", + "lastUpdated": "2026-07-06" }, "architectureOverview": { "layers": [ @@ -1767,20 +1767,21 @@ "name": "StatItem", "file": "components/ui/stat-item.tsx", "props": "{ label, value, icon?, hint?, valueClassName? }", - "purpose": "紧凑统计项(label+icon+value+hint,用于统计面板网格),P1-a 重构从 attendance-stats-card、grade-stats-card 两处重复实现抽取", + "purpose": "紧凑统计项(label+icon+value+hint,用于统计面板网格),P1-a 重构从 attendance-stats-card、grade-stats-card 两处重复实现抽取。Phase 4.6:grades 模块 grade-stats-card.tsx 已删除,class-grade-report.tsx 改用 StatsGrid + StatItem 组合", "internalDeps": [ "cn" ], "usedBy": [ "attendance/components/attendance-stats-card.tsx", - "grades/components/grade-stats-card.tsx" + "grades/components/class-grade-report.tsx", + "shared/components/ui/stats-grid.tsx" ] }, { "name": "ChipNav", "file": "components/ui/chip-nav.tsx", "props": "{ options: ChipNavOption[], currentId, buildHref: (id) => string, size?: 'sm'|'xs', allOption?, className? }", - "purpose": "芯片导航组(通过 URL search params 切换筛选维度,Link 跳转),P1-b 重构从 stats-class-selector、attendance-stats-class-selector、analytics-filters 三处重复实现抽取", + "purpose": "芯片导航组(通过 URL search params 切换筛选维度,Link 跳转),P1-b 重构从 stats-class-selector、attendance-stats-class-selector、analytics-filters 三处重复实现抽取。Phase 4.6:grades 模块 analytics-filters.tsx 已删除,analytics-grade-filters.tsx(app 层)改用 FilterBar + ChipNav 组合", "internalDeps": [ "Link", "cn" @@ -1788,7 +1789,7 @@ "usedBy": [ "grades/components/stats-class-selector.tsx", "attendance/components/attendance-stats-class-selector.tsx", - "grades/components/analytics-filters.tsx" + "app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx" ] }, { @@ -1810,7 +1811,7 @@ "name": "FilterBar", "file": "components/ui/filter-bar.tsx", "props": "{ children, hasFilters?, onReset?, layout?: 'default'|'wrap'|'between', gapClassName?, className?, resetClassName? }", - "purpose": "筛选栏容器(统一布局壳 + Reset 按钮),P3-b 重构从 exam/textbook/question/audit-log/login-log filters 五处重复布局抽取。URL 状态管理方式(nuqs/router/callback)由各模块自行处理", + "purpose": "筛选栏容器(统一布局壳 + Reset 按钮),P3-b 重构从 exam/textbook/question/audit-log/login-log filters 五处重复布局抽取。URL 状态管理方式(nuqs/router/callback)由各模块自行处理。Phase 4.6:grades 模块 3 个 filter 组件迁移至 app 层并复用 FilterBar(student-grade-filters / teacher-grade-query-filters / analytics-grade-filters)", "internalDeps": [ "Button", "cn" @@ -1820,14 +1821,17 @@ "textbooks/components/textbook-filters.tsx", "questions/components/question-filters.tsx", "audit/components/audit-log-filters.tsx", - "audit/components/login-log-filters.tsx" + "audit/components/login-log-filters.tsx", + "app/(dashboard)/student/grades/student-grade-filters.tsx", + "app/(dashboard)/teacher/grades/teacher-grade-query-filters.tsx", + "app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx" ] }, { "name": "FilterSearchInput", "file": "components/ui/filter-bar.tsx", "props": "{ value, onChange, placeholder?, className?, inputClassName? }", - "purpose": "筛选栏搜索框(带 Search 图标的 Input),P3-b 重构从 exam/textbook/question filters 三处重复搜索框抽取", + "purpose": "筛选栏搜索框(带 Search 图标的 Input),P3-b 重构从 exam/textbook/question filters 三处重复搜索框抽取。Phase 4.6:grades 模块 student-grade-filters 复用", "internalDeps": [ "Input", "Search (lucide-react)", @@ -1836,7 +1840,8 @@ "usedBy": [ "exams/components/exam-filters.tsx", "textbooks/components/textbook-filters.tsx", - "questions/components/question-filters.tsx" + "questions/components/question-filters.tsx", + "app/(dashboard)/student/grades/student-grade-filters.tsx" ] }, { @@ -14095,7 +14100,7 @@ "definition": "{ count, average, median, stdDev, passRate, excellentRate, maxScore, minScore }", "usedBy": [ "data-access", - "components/grade-stats-card" + "components/class-grade-report" ] }, { @@ -14440,20 +14445,20 @@ "name": "isGradeType", "signature": "(v: unknown) => v is GradeRecordType", "file": "lib/type-guards.ts", - "purpose": "类型守卫:验证 unknown 值是否为合法的 GradeRecordType。P2-9 修复:使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v),消除 as 断言", + "purpose": "类型守卫:验证 unknown 值是否为合法的 GradeRecordType。P2-9 修复:使用 GRADE_TYPES.some((t) => t === v) 替代 (GRADE_TYPES as readonly string[]).includes(v),消除 as 断言。Phase 4.6:原调用方 grade-filters/grade-query-filters 已删除,改由 app 层 student-grade-filters / teacher-grade-query-filters 复用", "usedBy": [ - "grades/components/grade-filters", - "grades/components/grade-query-filters" + "app/(dashboard)/student/grades/student-grade-filters", + "app/(dashboard)/teacher/grades/teacher-grade-query-filters" ] }, { "name": "isSemester", "signature": "(v: unknown) => v is GradeRecordSemester", "file": "lib/type-guards.ts", - "purpose": "类型守卫:验证 unknown 值是否为合法的 GradeRecordSemester。P2-9 修复:使用 SEMESTERS.some((s) => s === v) 替代 (SEMESTERS as readonly string[]).includes(v),消除 as 断言", + "purpose": "类型守卫:验证 unknown 值是否为合法的 GradeRecordSemester。P2-9 修复:使用 SEMESTERS.some((s) => s === v) 替代 (SEMESTERS as readonly string[]).includes(v),消除 as 断言。Phase 4.6:原调用方 grade-filters/grade-query-filters 已删除,改由 app 层 student-grade-filters / teacher-grade-query-filters 复用", "usedBy": [ - "grades/components/grade-filters", - "grades/components/grade-query-filters" + "app/(dashboard)/student/grades/student-grade-filters", + "app/(dashboard)/teacher/grades/teacher-grade-query-filters" ] }, { @@ -14550,7 +14555,7 @@ { "name": "BatchGradeEntry", "file": "components/batch-grade-entry.tsx", - "purpose": "2026-06-24 重新设计:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。P1-6/P1-7 重构:拆分为 3 个子组件 + 1 个自定义 Hook(BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo),主组件从 604 行降至 422 行。撤销数据使用 isUndoData 类型守卫替代 as 断言。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?", + "purpose": "2026-06-24 重新设计:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。P1-6/P1-7 重构:拆分为 3 个子组件 + 1 个自定义 Hook(BatchGradeEntryTable + BatchGradeEntryStats + BatchGradeEntryDialog + useBatchGradeEntryUndo),主组件从 604 行降至 422 行。撤销数据使用 isUndoData 类型守卫替代 as 断言。Phase 4.6 重构:进一步拆分试卷/班级选择器(含未选态引导)至 BatchGradeEntrySelectors、底部工具栏(清除/保存/粘贴提示)至 BatchGradeEntryToolbar,主组件降至 374 行,容器仅负责状态编排与 Server Action 调用。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?", "props": "{ exams: ExamOptionForEntry[]; classes: ClassOption[]; classGradeMap: Record; exam: ExamForGradeEntry | null; students: Student[]; defaultExamId?: string; defaultClassId?: string }", "deps": [ "exams/types.ExamOptionForEntry", @@ -14560,6 +14565,8 @@ "grades/components/batch-grade-entry-table.BatchGradeEntryTable", "grades/components/batch-grade-entry-stats.BatchGradeEntryStats", "grades/components/batch-grade-entry-dialog.BatchGradeEntryDialog", + "grades/components/batch-grade-entry-selectors.BatchGradeEntrySelectors", + "grades/components/batch-grade-entry-toolbar.BatchGradeEntryToolbar", "grades/hooks/use-batch-grade-entry-undo.useBatchGradeEntryUndo", "shared/lib/action-utils.safeActionCall", "shared/components/ui/card", @@ -14606,20 +14613,82 @@ "grades/components/batch-grade-entry" ] }, + { + "name": "BatchGradeEntrySelectors", + "file": "components/batch-grade-entry-selectors.tsx", + "purpose": "Phase 4.6 新增:批量录入试卷/班级选择器子组件。未选试卷时显示单选试卷 Card + Info 引导;已选试卷时显示双列选择器(试卷 Select + 班级 Select)+ 试卷信息 Badge(题目数/总分/满分)", + "props": "{ exams, classes, exam, defaultExamId?, defaultClassId?, onExamChange, onClassChange }", + "deps": [ + "shared/components/ui/card", + "shared/components/ui/select", + "shared/components/ui/badge" + ], + "usedBy": [ + "grades/components/batch-grade-entry" + ] + }, + { + "name": "BatchGradeEntryToolbar", + "file": "components/batch-grade-entry-toolbar.tsx", + "purpose": "Phase 4.6 新增:批量录入底部工具栏子组件(清除按钮 + 保存按钮 + Excel 粘贴提示)。保存按钮在 hasInvalidScores 时禁用", + "props": "{ isSubmitting, hasInvalidScores, onClear, onSubmit }", + "deps": [ + "shared/components/ui/button" + ], + "usedBy": [ + "grades/components/batch-grade-entry" + ] + }, { "name": "GradeRecordList", "file": "components/grade-record-list.tsx", - "purpose": "成绩列表(含查询筛选、删除对话框)" + "purpose": "成绩记录列表容器。Phase 4.6 重构:行渲染(桌面表格 + 移动端卡片堆叠)拆分至 GradeRecordRow,编辑/单条删除/批量删除对话框拆分至 GradeRecordDetail,主组件从 523 行降至 231 行,容器仅负责状态编排(useOptimistic + useTransition)与 Server Action 调用。Phase 4.5:编辑保存改用 useOptimistic + useTransition,在列表中即时显示乐观记录。v3-P3-2:批量选择与删除", + "props": "{ records: GradeRecordListItem[] }", + "deps": [ + "grades/actions.deleteGradeRecordAction", + "grades/actions.updateGradeRecordAction", + "grades/actions.bulkDeleteGradeRecordsAction", + "grades/components/grade-record-row.GradeRecordRow", + "grades/components/grade-record-detail.GradeRecordDetail", + "grades/components/grade-record-detail.toEditableFields", + "shared/lib/action-utils.safeActionCall", + "shared/components/ui/button" + ], + "usedBy": [ + "teacher/grades/page.tsx" + ] }, { - "name": "GradeStatsCard", - "file": "components/grade-stats-card.tsx", - "purpose": "统计卡片(均分、中位数、标准差、及格率、优秀率、最高分、最低分)" + "name": "GradeRecordRow", + "file": "components/grade-record-row.tsx", + "purpose": "Phase 4.6 新增:成绩记录行渲染子组件。桌面端 Table 行(含复选框 + 学生/科目/考试/分数/满分/类型/学期/备注/操作)+ 移动端卡片堆叠视图(role=list/listitem)。复选框支持单行切换与全选切换。操作按钮触发编辑与单条删除回调", + "props": "{ records, selectedIds, onToggleRow, onToggleSelectAll, onEdit, onDelete }", + "deps": [ + "shared/components/ui/table", + "shared/components/ui/checkbox", + "shared/components/ui/button", + "shared/components/ui/status-badge" + ], + "usedBy": [ + "grades/components/grade-record-list" + ] }, { - "name": "GradeQueryFilters", - "file": "components/grade-query-filters.tsx", - "purpose": "查询筛选器(班级、科目、考试类型、学期)" + "name": "GradeRecordDetail", + "file": "components/grade-record-detail.tsx", + "purpose": "Phase 4.6 新增:成绩记录详情对话框集合子组件,包含编辑对话框(title/score/fullScore/type/semester/remark 表单)、单条删除确认对话框、批量删除确认对话框。导出 EditableFields 类型 + toEditableFields(r: GradeRecordListItem): EditableFields 工具函数,用于将列表项转换为表单可编辑字段", + "props": "{ editTarget, deleteId, isBulkDeleteDialogOpen, isSaving, isDeleting, isBulkDeleting, bulkDeleteCount, onEditTargetChange, onCancelEdit, onSaveEdit, onCancelDelete, onConfirmDelete, onCloseBulkDelete, onConfirmBulkDelete }", + "deps": [ + "shared/components/ui/dialog", + "shared/components/ui/button", + "shared/components/ui/label", + "shared/components/ui/input", + "shared/components/ui/select", + "shared/components/ui/alert-dialog" + ], + "usedBy": [ + "grades/components/grade-record-list" + ] }, { "name": "StudentGradeSummary", @@ -14629,7 +14698,7 @@ { "name": "ClassGradeReport", "file": "components/class-grade-report.tsx", - "purpose": "班级成绩报表(含统计+排名)" + "purpose": "班级成绩报表(含统计+排名)。Phase 4.6 重构:删除 GradeStatsCard 依赖,改用 StatsGrid + StatItem 组合内联渲染 8 个统计项(average/median/max/min/stdDev/passRate/excellentRate/count),count=0 时显示空态 Card" }, { "name": "ExportButton", @@ -14678,20 +14747,6 @@ "shared/components/ui/chart" ] }, - { - "name": "AnalyticsFilters", - "file": "components/analytics-filters.tsx", - "purpose": "成绩分析页筛选器(班级、科目、年级 Link 筛选按钮组,含 focus-visible 焦点样式;v3-P2 新增:exams/currentExamId/currentSemester props,添加学期和考试筛选 ChipNav)", - "props": "{ classId?, gradeId?, subjectId?, currentSemester?, currentExamId?, exams: { id, title }[] }", - "deps": [ - "next/link", - "shared/lib/utils.cn", - "shared/components/ui/chip-nav" - ], - "usedBy": [ - "teacher/grades/analytics/page.tsx" - ] - }, { "name": "StatsClassSelector", "file": "components/stats-class-selector.tsx", diff --git a/lint-phase2b.log b/lint-phase2b.log new file mode 100644 index 0000000..a90f0ba Binary files /dev/null and b/lint-phase2b.log differ diff --git a/public/uploads/2026-06/ulthqxd89p6d4ikjv7m7mf3l.png b/public/uploads/2026-06/ulthqxd89p6d4ikjv7m7mf3l.png new file mode 100644 index 0000000..a9002c7 Binary files /dev/null and b/public/uploads/2026-06/ulthqxd89p6d4ikjv7m7mf3l.png differ diff --git a/public/uploads/2026-06/v86mvtnjoejz3l189hrf01ty.png b/public/uploads/2026-06/v86mvtnjoejz3l189hrf01ty.png new file mode 100644 index 0000000..c885d50 Binary files /dev/null and b/public/uploads/2026-06/v86mvtnjoejz3l189hrf01ty.png differ diff --git a/public/uploads/2026-06/zofz1jxqbx3z7te0mbknw9j9.png b/public/uploads/2026-06/zofz1jxqbx3z7te0mbknw9j9.png new file mode 100644 index 0000000..33d3ace Binary files /dev/null and b/public/uploads/2026-06/zofz1jxqbx3z7te0mbknw9j9.png differ diff --git a/screenshots/01-login.png b/screenshots/01-login.png new file mode 100644 index 0000000..d881f50 Binary files /dev/null and b/screenshots/01-login.png differ diff --git a/screenshots/02-form-filled.png b/screenshots/02-form-filled.png new file mode 100644 index 0000000..125ff86 Binary files /dev/null and b/screenshots/02-form-filled.png differ diff --git a/screenshots/03-after-login.png b/screenshots/03-after-login.png new file mode 100644 index 0000000..4f71950 Binary files /dev/null and b/screenshots/03-after-login.png differ diff --git a/src/modules/attendance/components/attendance-filters.tsx b/src/app/(dashboard)/admin/attendance/admin-attendance-filters.tsx similarity index 62% rename from src/modules/attendance/components/attendance-filters.tsx rename to src/app/(dashboard)/admin/attendance/admin-attendance-filters.tsx index fc6e7aa..9fe19be 100644 --- a/src/modules/attendance/components/attendance-filters.tsx +++ b/src/app/(dashboard)/admin/attendance/admin-attendance-filters.tsx @@ -1,5 +1,6 @@ "use client" +import type { JSX } from "react" import { useRouter, useSearchParams } from "next/navigation" import { useCallback } from "react" import { useTranslations } from "next-intl" @@ -12,19 +13,26 @@ import { SelectValue, } from "@/shared/components/ui/select" import { Input } from "@/shared/components/ui/input" +import { FilterBar } from "@/shared/components/ui/filter-bar" +import { + ATTENDANCE_STATUS_OPTIONS, + ATTENDANCE_STATUS_LABEL_KEYS, +} from "@/shared/constants/attendance-status" -import { ATTENDANCE_STATUS_OPTIONS, ATTENDANCE_STATUS_LABEL_KEYS } from "../constants" - -type Option = { id: string; name: string } - -interface AttendanceFiltersProps { - classes: Option[] +interface AdminAttendanceFiltersProps { + classes: Array<{ id: string; name: string }> } -export function AttendanceFilters({ classes }: AttendanceFiltersProps) { +/** + * 管理端考勤筛选器(基于共享底座 FilterBar + Select)。 + * + * 组件化重构:从 modules/attendance/components/attendance-filters.tsx 内联至调用方目录, + * 直接复用 shared 底座组件,消除模块级 wrapper。 + */ +export function AdminAttendanceFilters({ classes }: AdminAttendanceFiltersProps): JSX.Element { + const t = useTranslations("attendance") const router = useRouter() const searchParams = useSearchParams() - const t = useTranslations("attendance") const updateParam = useCallback( (key: string, value: string) => { @@ -42,13 +50,22 @@ export function AttendanceFilters({ classes }: AttendanceFiltersProps) { const classId = searchParams.get("classId") ?? "all" const status = searchParams.get("status") ?? "all" const date = searchParams.get("date") ?? "" + const hasFilters = classId !== "all" || status !== "all" || date !== "" return ( -
+ { + router.push("/admin/attendance") + }} + >
- + updateParam("status", v)}> - + @@ -80,8 +99,11 @@ export function AttendanceFilters({ classes }: AttendanceFiltersProps) {
- + updateParam("date", e.target.value)} @@ -89,6 +111,6 @@ export function AttendanceFilters({ classes }: AttendanceFiltersProps) { aria-label={t("filters.date")} />
-
+ ) } diff --git a/src/app/(dashboard)/admin/attendance/page.tsx b/src/app/(dashboard)/admin/attendance/page.tsx index 6dc1669..4cb4160 100644 --- a/src/app/(dashboard)/admin/attendance/page.tsx +++ b/src/app/(dashboard)/admin/attendance/page.tsx @@ -1,10 +1,11 @@ import Link from "next/link" import type { JSX } from "react" -import { BarChart3, ClipboardList } from "lucide-react" +import { BarChart3, CheckCircle2, ClipboardList, Clock, FileText, LogOut, School, Users, XCircle } from "lucide-react" import { getTranslations } from "next-intl/server" import { Button } from "@/shared/components/ui/button" import { EmptyState } from "@/shared/components/ui/empty-state" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { requirePermission, getAuthContext } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getSearchParam, type SearchParams } from "@/shared/lib/utils" @@ -13,8 +14,7 @@ import { getGrades } from "@/modules/school/data-access" import { getAttendanceRecords, getAttendanceStats } from "@/modules/attendance/data-access" import { getClassComparison } from "@/modules/attendance/data-access-stats" import { getAttendanceGradeCorrelation } from "@/modules/attendance/data-access-correlation" -import { AttendanceFilters } from "@/modules/attendance/components/attendance-filters" -import { AttendanceStatsCards } from "@/modules/attendance/components/attendance-stats-cards" +import { AdminAttendanceFilters } from "./admin-attendance-filters" import { AttendanceRecordList } from "@/modules/attendance/components/attendance-record-list" import { AttendancePageLayout } from "@/modules/attendance/components/attendance-page-layout" import { ClassComparisonCard } from "@/modules/attendance/components/class-comparison-card" @@ -82,6 +82,21 @@ export default async function AdminAttendancePage({ const gradeOptions = grades.map((g) => ({ id: g.id, name: g.name })) + const statsItems = [ + { label: t("stats.totalRecords"), value: stats.total, icon: FileText, color: "text-blue-500" }, + { label: t("stats.present"), value: stats.present, icon: CheckCircle2, color: "text-green-500" }, + { label: t("stats.absent"), value: stats.absent, icon: XCircle, color: "text-red-500" }, + { label: t("stats.late"), value: stats.late, icon: Clock, color: "text-yellow-500" }, + { label: t("stats.earlyLeave"), value: stats.earlyLeave, icon: LogOut, color: "text-orange-500" }, + { label: t("stats.schoolActivity"), value: stats.schoolActivity, icon: School, color: "text-cyan-500" }, + { + label: t("stats.attendanceRate"), + value: `${stats.presentRate.toFixed(1)}%`, + icon: Users, + color: "text-primary", + }, + ] + const header = (
@@ -100,8 +115,8 @@ export default async function AdminAttendancePage({ return ( } - filters={} + stats={} + filters={} >
{result.items.length === 0 && !classId && !status && !date ? ( diff --git a/src/app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx b/src/app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx index da7281e..5d9c9f9 100644 --- a/src/app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx +++ b/src/app/(dashboard)/admin/elective/_components/stats-cards-loader.tsx @@ -1,12 +1,46 @@ import type { JSX } from "react" +import { BookOpen, Users, Gauge, Shuffle } from "lucide-react" +import { getTranslations } from "next-intl/server" + import { getElectiveOverviewStats } from "@/modules/elective/data-access-stats" -import { ElectiveStatsCards } from "@/modules/elective/components/elective-stats-cards" +import { StatsGrid } from "@/shared/components/ui/stats-grid" /** * 管理员概览统计卡片 - 异步 RSC loader * 独立 Suspense 边界,可流式渲染 */ export async function StatsCardsLoader(): Promise { - const stats = await getElectiveOverviewStats() - return + const [stats, t] = await Promise.all([ + getElectiveOverviewStats(), + getTranslations("elective"), + ]) + + const items = [ + { + label: t("stats.totalCourses"), + value: stats.totalCourses, + icon: BookOpen, + color: "text-blue-500", + }, + { + label: t("stats.totalEnrolled"), + value: stats.totalEnrolled, + icon: Users, + color: "text-green-500", + }, + { + label: t("stats.avgUtilization"), + value: t("stats.utilizationRate", { rate: stats.avgUtilization }), + icon: Gauge, + color: "text-purple-500", + }, + { + label: t("stats.pendingLottery"), + value: stats.pendingLottery, + icon: Shuffle, + color: "text-orange-500", + }, + ] + + return } diff --git a/src/app/(dashboard)/admin/error-book/page.tsx b/src/app/(dashboard)/admin/error-book/page.tsx index 882e302..d579714 100644 --- a/src/app/(dashboard)/admin/error-book/page.tsx +++ b/src/app/(dashboard)/admin/error-book/page.tsx @@ -1,13 +1,14 @@ import type { JSX } from "react" import type { Metadata } from "next" import { Suspense } from "react" -import { BarChart3 } from "lucide-react" +import { BarChart3, BookOpen, Brain, CheckCircle2, Clock, TrendingUp } from "lucide-react" import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { EmptyState } from "@/shared/components/ui/empty-state" import { Skeleton } from "@/shared/components/ui/skeleton" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { WidgetBoundary } from "@/shared/components/widget-boundary" import { getParam, type SearchParams } from "@/shared/lib/search-params" @@ -23,7 +24,6 @@ import { } from "@/modules/error-book/data-access-analytics" import { TopWrongQuestions } from "@/modules/error-book/components/top-wrong-questions" import { SubjectTabs } from "@/modules/error-book/components/subject-tabs" -import { AnalyticsStatsCards } from "@/modules/error-book/components/analytics-stats-cards" import { SubjectDistributionChart } from "@/modules/error-book/components/subject-distribution-chart" import { KnowledgePointWeaknessChart } from "@/modules/error-book/components/knowledge-point-weakness-chart" import { ChapterWeaknessChart } from "@/modules/error-book/components/chapter-weakness-chart" @@ -120,6 +120,15 @@ async function AdminErrorBookContent({ .sort((a, b) => b.totalCount - a.totalCount) .slice(0, 50) + const analyticsAvg = (limitedStudentIds.length > 0 ? totalErrorItems / limitedStudentIds.length : 0).toFixed(1) + const analyticsItems = [ + { label: t("analyticsStats.coverage"), value: studentsWithErrorBook.length, icon: BookOpen, color: "text-blue-600 dark:text-blue-400", valueClassName: "text-blue-600 dark:text-blue-400", description: t("analyticsStats.coverageSub", { total: limitedStudentIds.length }) }, + { label: t("analyticsStats.totalErrors"), value: totalErrorItems, icon: TrendingUp, color: "text-rose-600 dark:text-rose-400", valueClassName: "text-rose-600 dark:text-rose-400", description: t("analyticsStats.totalErrorsSub", { avg: analyticsAvg }) }, + { label: t("analyticsStats.avgMastery"), value: `${Math.round(averageMasteryRate * 100)}%`, icon: CheckCircle2, color: "text-emerald-600 dark:text-emerald-400", valueClassName: "text-emerald-600 dark:text-emerald-400", description: averageMasteryRate >= 0.6 ? t("analyticsStats.avgMasteryGood") : t("analyticsStats.avgMasteryNeedImprove") }, + { label: t("analyticsStats.dueReview"), value: totalDueReview, icon: Clock, color: "text-amber-600 dark:text-amber-400", valueClassName: "text-amber-600 dark:text-amber-400", description: totalDueReview > 0 ? t("analyticsStats.dueReviewNeedAttention") : t("analyticsStats.dueReviewNone") }, + { label: t("analyticsStats.knowledgePoints"), value: knowledgePointCount, icon: Brain, color: "text-purple-600 dark:text-purple-400", valueClassName: "text-purple-600 dark:text-purple-400", description: knowledgePointCount > 5 ? t("analyticsStats.knowledgePointsWide") : t("analyticsStats.knowledgePointsFocused") }, + ] + return (
@@ -139,14 +148,7 @@ async function AdminErrorBookContent({ {/* 统计卡片 */} - + {/* 学科错题分布图(仅在"全部学科"视图下显示) */} diff --git a/src/app/(dashboard)/parent/error-book/page.tsx b/src/app/(dashboard)/parent/error-book/page.tsx index eabe7e1..36c8483 100644 --- a/src/app/(dashboard)/parent/error-book/page.tsx +++ b/src/app/(dashboard)/parent/error-book/page.tsx @@ -1,5 +1,5 @@ import type { JSX } from "react" -import { Users } from "lucide-react" +import { BookX, Clock, GraduationCap, Repeat, Sparkles, Users } from "lucide-react" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" @@ -7,6 +7,7 @@ import { EmptyState } from "@/shared/components/ui/empty-state" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Badge } from "@/shared/components/ui/badge" import { Progress } from "@/shared/components/ui/progress" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { WidgetBoundary } from "@/shared/components/widget-boundary" import { formatNumber } from "@/shared/lib/utils" @@ -16,7 +17,6 @@ import { getTopWrongQuestionsByStudentIds, getKnowledgePointWeakness, } from "@/modules/error-book/data-access-analytics" -import { ErrorBookStatsCards } from "@/modules/error-book/components/error-book-stats-cards" import { TopWrongQuestions } from "@/modules/error-book/components/top-wrong-questions" import { getTranslations } from "next-intl/server" @@ -56,6 +56,11 @@ export default async function ParentErrorBookPage(): Promise { getKnowledgePointWeakness(childrenIds, 5), ]) + const singleStats = childStatsList[0] + const singleMasteredPercent = singleStats.totalCount > 0 + ? Math.round(singleStats.masteredRate * 100) + : 0 + return (
@@ -66,7 +71,16 @@ export default async function ParentErrorBookPage(): Promise { {childrenIds.length === 1 ? ( // 单子女:直接展示统计卡片 - + 0 }, + ]} + /> ) : ( // 多子女:每个子女一张卡片 diff --git a/src/app/(dashboard)/student/elective/_components/available-courses-loader.tsx b/src/app/(dashboard)/student/elective/_components/available-courses-loader.tsx index 3fe58e9..9a098c7 100644 --- a/src/app/(dashboard)/student/elective/_components/available-courses-loader.tsx +++ b/src/app/(dashboard)/student/elective/_components/available-courses-loader.tsx @@ -1,7 +1,7 @@ import type { JSX } from "react" import { getAvailableCoursesForStudent, getStudentSelections } from "@/modules/elective/data-access-selections" import { StudentAvailableCoursesSection } from "@/modules/elective/components/student-selection-view" -import { ElectiveFilters } from "@/modules/elective/components/elective-filters" +import { ElectiveFilters } from "./elective-filters" import { getParam, type SearchParams } from "@/shared/lib/search-params" /** diff --git a/src/modules/elective/components/elective-filters.tsx b/src/app/(dashboard)/student/elective/_components/elective-filters.tsx similarity index 92% rename from src/modules/elective/components/elective-filters.tsx rename to src/app/(dashboard)/student/elective/_components/elective-filters.tsx index c0efd76..4a9c413 100644 --- a/src/modules/elective/components/elective-filters.tsx +++ b/src/app/(dashboard)/student/elective/_components/elective-filters.tsx @@ -36,7 +36,7 @@ export function ElectiveFilters() {
updateParam("classId", v)}> + + + + + {t("filters.allClasses")} + {classes.map((c) => ( + + {c.name} + + ))} + + +
+ +
+ + +
+ +
+ + updateParam("date", e.target.value)} + className="h-9" + aria-label={t("filters.date")} + /> +
+ + ) +} diff --git a/src/app/(dashboard)/teacher/error-book/page.tsx b/src/app/(dashboard)/teacher/error-book/page.tsx index 198701f..435fb5e 100644 --- a/src/app/(dashboard)/teacher/error-book/page.tsx +++ b/src/app/(dashboard)/teacher/error-book/page.tsx @@ -1,12 +1,13 @@ import type { JSX } from "react" import { Suspense } from "react" -import { BarChart3 } from "lucide-react" +import { BarChart3, BookOpen, Brain, CheckCircle2, Clock, TrendingUp } from "lucide-react" import { getTranslations } from "next-intl/server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { EmptyState } from "@/shared/components/ui/empty-state" import { Skeleton } from "@/shared/components/ui/skeleton" +import { StatsGrid } from "@/shared/components/ui/stats-grid" import { WidgetBoundary } from "@/shared/components/widget-boundary" import { getParam, type SearchParams } from "@/shared/lib/search-params" import { getStudentIdsByClassIds, getClassIdsByGradeIds } from "@/modules/classes/data-access" @@ -23,7 +24,6 @@ import { import { TopWrongQuestions } from "@/modules/error-book/components/top-wrong-questions" import { SubjectTabs } from "@/modules/error-book/components/subject-tabs" import { ClassFilter } from "@/modules/error-book/components/class-filter" -import { AnalyticsStatsCards } from "@/modules/error-book/components/analytics-stats-cards" import { ClassErrorBarChart } from "@/modules/error-book/components/class-error-bar-chart" import { KnowledgePointWeaknessChart } from "@/modules/error-book/components/knowledge-point-weakness-chart" import { ChapterWeaknessChart } from "@/modules/error-book/components/chapter-weakness-chart" @@ -128,6 +128,15 @@ async function TeacherErrorBookContent({ // 按错题数降序排列 const sortedSummaries = [...summaries].sort((a, b) => b.totalCount - a.totalCount) + const analyticsAvg = (queryStudentIds.length > 0 ? totalErrorItems / queryStudentIds.length : 0).toFixed(1) + const analyticsItems = [ + { label: t("analyticsStats.coverage"), value: studentsWithErrorBook.length, icon: BookOpen, color: "text-blue-600 dark:text-blue-400", valueClassName: "text-blue-600 dark:text-blue-400", description: t("analyticsStats.coverageSub", { total: queryStudentIds.length }) }, + { label: t("analyticsStats.totalErrors"), value: totalErrorItems, icon: TrendingUp, color: "text-rose-600 dark:text-rose-400", valueClassName: "text-rose-600 dark:text-rose-400", description: t("analyticsStats.totalErrorsSub", { avg: analyticsAvg }) }, + { label: t("analyticsStats.avgMastery"), value: `${Math.round(averageMasteryRate * 100)}%`, icon: CheckCircle2, color: "text-emerald-600 dark:text-emerald-400", valueClassName: "text-emerald-600 dark:text-emerald-400", description: averageMasteryRate >= 0.6 ? t("analyticsStats.avgMasteryGood") : t("analyticsStats.avgMasteryNeedImprove") }, + { label: t("analyticsStats.dueReview"), value: totalDueReview, icon: Clock, color: "text-amber-600 dark:text-amber-400", valueClassName: "text-amber-600 dark:text-amber-400", description: totalDueReview > 0 ? t("analyticsStats.dueReviewNeedAttention") : t("analyticsStats.dueReviewNone") }, + { label: t("analyticsStats.knowledgePoints"), value: knowledgePointCount, icon: Brain, color: "text-purple-600 dark:text-purple-400", valueClassName: "text-purple-600 dark:text-purple-400", description: knowledgePointCount > 5 ? t("analyticsStats.knowledgePointsWide") : t("analyticsStats.knowledgePointsFocused") }, + ] + return (
@@ -159,14 +168,7 @@ async function TeacherErrorBookContent({ {/* 统计卡片 */} - + {/* 班级错题对比图(仅在"全部班级"视图下显示) */} diff --git a/src/modules/grades/components/analytics-filters.tsx b/src/app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx similarity index 81% rename from src/modules/grades/components/analytics-filters.tsx rename to src/app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx index 6982c83..087cbf7 100644 --- a/src/modules/grades/components/analytics-filters.tsx +++ b/src/app/(dashboard)/teacher/grades/analytics/analytics-grade-filters.tsx @@ -2,8 +2,9 @@ import type { JSX } from "react" import { getTranslations } from "next-intl/server" import { ChipNav } from "@/shared/components/ui/chip-nav" +import { FilterBar } from "@/shared/components/ui/filter-bar" -interface AnalyticsFiltersProps { +interface AnalyticsGradeFiltersProps { classes: Array<{ id: string; name: string }> grades: Array<{ id: string; name: string }> subjects: Array<{ id: string; name: string }> @@ -15,7 +16,17 @@ interface AnalyticsFiltersProps { currentSemester: string } -export async function AnalyticsFilters({ +/** + * 教师成绩分析页过滤器(基于共享底座 FilterBar + ChipNav)。 + * + * Phase 4.6 重构:从 `modules/grades/components/analytics-filters.tsx` 内联至调用方目录, + * 删除模块级 wrapper,直接复用 shared 底座组件。 + * + * 设计选择:本场景是多维度 chip 切换(班级/学科/年级/学期/考试), + * 每个 ChipNav 通过 buildHref + Link 实现 URL 导航(无需 client state), + * 因此使用 ChipNav 而非 Select;外层用 FilterBar 统一布局外壳。 + */ +export async function AnalyticsGradeFilters({ classes, grades, subjects, @@ -25,7 +36,7 @@ export async function AnalyticsFilters({ currentGradeId, currentExamId, currentSemester, -}: AnalyticsFiltersProps): Promise { +}: AnalyticsGradeFiltersProps): Promise { const t = await getTranslations("grades") const buildHref = (overrides: { classId?: string @@ -53,12 +64,10 @@ export async function AnalyticsFilters({ overrides.gradeId !== undefined ? overrides.gradeId : currentGradeId ) } - // v3-P2-7: examId 筛选 const examId = overrides.examId !== undefined ? overrides.examId : currentExamId if (examId && examId !== "all") { params.set("examId", examId) } - // v3-P2-7: semester 筛选 const semester = overrides.semester !== undefined ? overrides.semester : currentSemester if (semester && semester !== "all") { params.set("semester", semester) @@ -79,8 +88,8 @@ export async function AnalyticsFilters({ return (
-
-
+ +
{t("analytics.class")}
-
+
{t("analytics.subject")}
-
+
{t("analytics.classComparisonLabel")}
@@ -115,9 +124,9 @@ export async function AnalyticsFilters({ className="gap-1.5" />
-
+ - {/* v3-P2-7: 学期和考试筛选 */} + {/* 学期和考试筛选 */}
{t("analytics.semester")}
diff --git a/src/app/(dashboard)/teacher/grades/analytics/page.tsx b/src/app/(dashboard)/teacher/grades/analytics/page.tsx index 6e070a9..0c8daef 100644 --- a/src/app/(dashboard)/teacher/grades/analytics/page.tsx +++ b/src/app/(dashboard)/teacher/grades/analytics/page.tsx @@ -23,7 +23,7 @@ import { GradeTrendChart } from "@/modules/grades/components/grade-trend-chart" import { ClassComparisonChart } from "@/modules/grades/components/class-comparison-chart" import { SubjectComparisonChart } from "@/modules/grades/components/subject-comparison-chart" import { GradeDistributionChart } from "@/modules/grades/components/grade-distribution-chart" -import { AnalyticsFilters } from "@/modules/grades/components/analytics-filters" +import { AnalyticsGradeFilters } from "./analytics-grade-filters" import { KnowledgePointMasteryChart } from "@/modules/grades/components/knowledge-point-mastery-chart" import { getClassMasterySummary } from "@/modules/diagnostic/data-access" import { WidgetBoundary } from "@/shared/components/widget-boundary" @@ -142,7 +142,7 @@ export default async function GradeAnalyticsPage({
- ({ id: c.id, name: c.name }))} grades={allGrades.map((g) => ({ id: g.id, name: g.name }))} subjects={allSubjects.map((s) => ({ id: s.id, name: s.name ?? "Unknown" }))} diff --git a/src/app/(dashboard)/teacher/grades/page.tsx b/src/app/(dashboard)/teacher/grades/page.tsx index 5de3ba3..a2dd013 100644 --- a/src/app/(dashboard)/teacher/grades/page.tsx +++ b/src/app/(dashboard)/teacher/grades/page.tsx @@ -12,11 +12,11 @@ import { getParam, type SearchParams } from "@/shared/lib/search-params" import { getTeacherClasses } from "@/modules/classes/data-access" import { getGradeRecords } from "@/modules/grades/data-access" import { getSubjectOptions } from "@/modules/school/data-access" -import { GradeQueryFilters } from "@/modules/grades/components/grade-query-filters" import { GradeRecordList } from "@/modules/grades/components/grade-record-list" import { ExportButton } from "@/modules/grades/components/export-button" import { ExcelImportDialog } from "@/modules/grades/components/excel-import-dialog" import { isGradeType, isSemester } from "@/modules/grades/lib/type-guards" +import { TeacherGradeQueryFilters } from "./teacher-grade-query-filters" export const dynamic = "force-dynamic" @@ -104,7 +104,7 @@ export default async function TeacherGradesPage({
- + {total === 0 && !hasFilters ? ( + subjects: Array<{ id: string; name: string }> } -export function GradeQueryFilters({ classes, subjects }: GradeQueryFiltersProps): JSX.Element { +/** + * 教师端成绩查询过滤器(基于共享底座 FilterBar + Select)。 + * + * Phase 4.6 重构:从 `modules/grades/components/grade-query-filters.tsx` 内联至调用方目录, + * 删除模块级 wrapper,直接复用 shared 底座组件。 + */ +export function TeacherGradeQueryFilters({ classes, subjects }: TeacherGradeQueryFiltersProps): JSX.Element { const t = useTranslations("grades") const router = useRouter() const searchParams = useSearchParams() @@ -37,8 +42,18 @@ export function GradeQueryFilters({ classes, subjects }: GradeQueryFiltersProps) const type = searchParams.get("type") ?? "all" const semester = searchParams.get("semester") ?? "all" + const hasFilters = Boolean( + (classId !== "all") || (subjectId !== "all") || (type !== "all") || (semester !== "all") + ) + return ( -
+ { + router.push("/teacher/grades") + }} + >
-
+
) } diff --git a/src/modules/attendance/components/attendance-stats-card.tsx b/src/modules/attendance/components/attendance-stats-card.tsx deleted file mode 100644 index 245cbb7..0000000 --- a/src/modules/attendance/components/attendance-stats-card.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useTranslations } from "next-intl" -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { EmptyState } from "@/shared/components/ui/empty-state" -import { StatItem } from "@/shared/components/ui/stat-item" -import { - Users, - CheckCircle2, - XCircle, - Clock, - LogOut, - FileText, - School, - TrendingUp, - BarChart3, -} from "lucide-react" -import type { AttendanceStats } from "../types" - -export function AttendanceStatsCard({ stats }: { stats: AttendanceStats | null }) { - const t = useTranslations("attendance") - - if (!stats || stats.total === 0) { - return ( - - ) - } - - return ( - - - {t("title.teacherStats")} - - -
- } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> -
-
-
- ) -} diff --git a/src/modules/attendance/components/attendance-stats-cards.tsx b/src/modules/attendance/components/attendance-stats-cards.tsx deleted file mode 100644 index 8634858..0000000 --- a/src/modules/attendance/components/attendance-stats-cards.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Users, CheckCircle2, XCircle, Clock, LogOut, FileText, School } from "lucide-react" -import { useTranslations } from "next-intl" - -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { cn } from "@/shared/lib/utils" -import type { AttendanceStats } from "../types" - -interface AttendanceStatsCardsProps { - stats: AttendanceStats -} - -export function AttendanceStatsCards({ stats }: AttendanceStatsCardsProps) { - const t = useTranslations("attendance") - const cards = [ - { - title: t("stats.totalRecords"), - value: stats.total, - icon: FileText, - color: "text-blue-500", - bgColor: "bg-blue-500/10", - }, - { - title: t("stats.present"), - value: stats.present, - icon: CheckCircle2, - color: "text-green-500", - bgColor: "bg-green-500/10", - }, - { - title: t("stats.absent"), - value: stats.absent, - icon: XCircle, - color: "text-red-500", - bgColor: "bg-red-500/10", - }, - { - title: t("stats.late"), - value: stats.late, - icon: Clock, - color: "text-yellow-500", - bgColor: "bg-yellow-500/10", - }, - { - title: t("stats.earlyLeave"), - value: stats.earlyLeave, - icon: LogOut, - color: "text-orange-500", - bgColor: "bg-orange-500/10", - }, - { - title: t("stats.schoolActivity"), - value: stats.schoolActivity, - icon: School, - color: "text-cyan-500", - bgColor: "bg-cyan-500/10", - }, - { - title: t("stats.attendanceRate"), - value: `${stats.presentRate.toFixed(1)}%`, - icon: Users, - color: "text-primary", - bgColor: "bg-primary/10", - }, - ] - - return ( -
- {cards.map((card) => ( - - - {card.title} -
- -
-
- -
{card.value}
-
-
- ))} -
- ) -} diff --git a/src/modules/attendance/components/student-attendance-view.tsx b/src/modules/attendance/components/student-attendance-view.tsx index d8c7ae9..d806faf 100644 --- a/src/modules/attendance/components/student-attendance-view.tsx +++ b/src/modules/attendance/components/student-attendance-view.tsx @@ -10,9 +10,20 @@ import { TableRow, } from "@/shared/components/ui/table" import { EmptyState } from "@/shared/components/ui/empty-state" -import { CalendarCheck } from "lucide-react" +import { StatsGrid } from "@/shared/components/ui/stats-grid" +import { + BarChart3, + CalendarCheck, + CheckCircle2, + Clock, + FileText, + LogOut, + School, + TrendingUp, + Users, + XCircle, +} from "lucide-react" -import { AttendanceStatsCard } from "./attendance-stats-card" import { ATTENDANCE_STATUS_BADGE_VARIANTS, ATTENDANCE_STATUS_LABEL_KEYS, @@ -62,7 +73,37 @@ export function StudentAttendanceView({
- + {summary.stats.total === 0 ? ( + + ) : ( + + )} {summary.recentRecords.length === 0 ? ( - {cards.map((card) => ( - - - {card.title} -
- -
-
- -
{card.value}
-
-
- ))} -
- ) -} diff --git a/src/modules/error-book/components/analytics-stats-cards.tsx b/src/modules/error-book/components/analytics-stats-cards.tsx deleted file mode 100644 index 9ad2a71..0000000 --- a/src/modules/error-book/components/analytics-stats-cards.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client" - -import { BookOpen, Brain, CheckCircle2, Clock, TrendingUp } from "lucide-react" -import { useTranslations } from "next-intl" -import { Card, CardContent } from "@/shared/components/ui/card" -import { cn } from "@/shared/lib/utils" - -interface AnalyticsStatsCardsProps { - totalStudents: number - studentsWithErrorBook: number - totalErrorItems: number - averageMasteryRate: number - dueReviewCount: number - /** 涉及的知识点数 */ - knowledgePointCount?: number - className?: string -} - -/** - * 错题分析统计卡片(教师/管理员视图) - * 5 个卡片:覆盖学生/错题总数/平均掌握率/待复习/知识点数 - */ -export function AnalyticsStatsCards({ - totalStudents, - studentsWithErrorBook, - totalErrorItems, - averageMasteryRate, - dueReviewCount, - knowledgePointCount, - className, -}: AnalyticsStatsCardsProps) { - const t = useTranslations("errorBook") - const avg = (totalStudents > 0 ? totalErrorItems / totalStudents : 0).toFixed(1) - - const cards = [ - { - label: t("analyticsStats.coverage"), - value: studentsWithErrorBook, - sub: t("analyticsStats.coverageSub", { total: totalStudents }), - icon: BookOpen, - color: "text-blue-600 dark:text-blue-400", - bg: "bg-blue-50 dark:bg-blue-950/30", - }, - { - label: t("analyticsStats.totalErrors"), - value: totalErrorItems, - sub: t("analyticsStats.totalErrorsSub", { avg }), - icon: TrendingUp, - color: "text-rose-600 dark:text-rose-400", - bg: "bg-rose-50 dark:bg-rose-950/30", - }, - { - label: t("analyticsStats.avgMastery"), - value: `${Math.round(averageMasteryRate * 100)}%`, - sub: averageMasteryRate >= 0.6 - ? t("analyticsStats.avgMasteryGood") - : t("analyticsStats.avgMasteryNeedImprove"), - icon: CheckCircle2, - color: "text-emerald-600 dark:text-emerald-400", - bg: "bg-emerald-50 dark:bg-emerald-950/30", - }, - { - label: t("analyticsStats.dueReview"), - value: dueReviewCount, - sub: dueReviewCount > 0 - ? t("analyticsStats.dueReviewNeedAttention") - : t("analyticsStats.dueReviewNone"), - icon: Clock, - color: "text-amber-600 dark:text-amber-400", - bg: "bg-amber-50 dark:bg-amber-950/30", - }, - { - label: t("analyticsStats.knowledgePoints"), - value: knowledgePointCount ?? 0, - sub: knowledgePointCount && knowledgePointCount > 5 - ? t("analyticsStats.knowledgePointsWide") - : t("analyticsStats.knowledgePointsFocused"), - icon: Brain, - color: "text-purple-600 dark:text-purple-400", - bg: "bg-purple-50 dark:bg-purple-950/30", - }, - ] - - return ( -
- {cards.map((card) => { - const Icon = card.icon - return ( - - -
-
-
{card.label}
-
- {card.value} -
-
{card.sub}
-
-
- -
-
-
-
- ) - })} -
- ) -} diff --git a/src/modules/error-book/components/error-book-stats-cards.tsx b/src/modules/error-book/components/error-book-stats-cards.tsx deleted file mode 100644 index 85507c2..0000000 --- a/src/modules/error-book/components/error-book-stats-cards.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client" - -import { BookX, Clock, GraduationCap, Repeat, Sparkles } from "lucide-react" -import { useTranslations } from "next-intl" - -import { StatCard } from "@/shared/components/ui/stat-card" -import type { ErrorBookStats } from "../types" - -interface ErrorBookStatsCardsProps { - stats: ErrorBookStats - isLoading?: boolean -} - -export function ErrorBookStatsCards({ stats, isLoading }: ErrorBookStatsCardsProps) { - const t = useTranslations("errorBook") - const masteredPercent = stats.totalCount > 0 - ? Math.round(stats.masteredRate * 100) - : 0 - - return ( -
- - - - - 0} - isLoading={isLoading} - /> -
- ) -} diff --git a/src/modules/grades/components/batch-grade-entry-selectors.tsx b/src/modules/grades/components/batch-grade-entry-selectors.tsx new file mode 100644 index 0000000..3f36123 --- /dev/null +++ b/src/modules/grades/components/batch-grade-entry-selectors.tsx @@ -0,0 +1,124 @@ +"use client" + +import type { JSX } from "react" +import { Info } from "lucide-react" +import { useTranslations } from "next-intl" + +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" +import { Label } from "@/shared/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" +import { Badge } from "@/shared/components/ui/badge" + +import type { ExamOptionForEntry, ExamForGradeEntry } from "@/modules/exams/types" + +type ClassOption = { id: string; name: string } + +interface Props { + exams: ExamOptionForEntry[] + classes: ClassOption[] + exam: ExamForGradeEntry | null + defaultExamId?: string + defaultClassId?: string + onExamChange: (examId: string) => void + onClassChange: (classId: string) => void +} + +/** + * 按试卷批量录入成绩的试卷/班级选择器(含未选试卷态)。 + * + * Phase 4.6 重构:从 batch-grade-entry.tsx 拆分。 + * - 未选试卷时显示单个试卷选择器 + 引导提示 + * - 已选试卷时显示试卷/班级双列选择器 + 试卷信息徽章 + */ +export function BatchGradeEntrySelectors({ + exams, + classes, + exam, + defaultExamId, + defaultClassId, + onExamChange, + onClassChange, +}: Props): JSX.Element { + const t = useTranslations("grades") + + // 未选试卷 + if (!exam) { + return ( + + + {t("batchByExam.selectExam")} + + +
+ + +
+
+ + {t("batchByExam.guideSelectExam")} +
+
+
+ ) + } + + return ( + + + {t("batchByExam.title")} + + +
+ + +
+
+ + +
+ {/* 试卷信息 */} +
+ + {t("batchByExam.questionCount", { count: exam.questions.length })} + + + {t("batchByExam.fullScore", { score: exam.totalScore })} + +
+
+
+ ) +} diff --git a/src/modules/grades/components/batch-grade-entry-toolbar.tsx b/src/modules/grades/components/batch-grade-entry-toolbar.tsx new file mode 100644 index 0000000..fb52e70 --- /dev/null +++ b/src/modules/grades/components/batch-grade-entry-toolbar.tsx @@ -0,0 +1,56 @@ +"use client" + +import type { JSX } from "react" +import { Info } from "lucide-react" +import { useTranslations } from "next-intl" + +import { Button } from "@/shared/components/ui/button" + +interface Props { + isSubmitting: boolean + hasInvalidScores: boolean + onClear: () => void + onSubmit: () => void +} + +/** + * 按试卷批量录入成绩的底部工具栏(清除/保存按钮 + 粘贴提示)。 + * + * Phase 4.6 重构:从 batch-grade-entry.tsx 拆分。 + */ +export function BatchGradeEntryToolbar({ + isSubmitting, + hasInvalidScores, + onClear, + onSubmit, +}: Props): JSX.Element { + const t = useTranslations("grades") + + return ( + <> +
+ + +
+ + {/* 提示 */} +
+ + {t("batchByExam.pasteHint")} +
+ + ) +} diff --git a/src/modules/grades/components/batch-grade-entry.tsx b/src/modules/grades/components/batch-grade-entry.tsx index e8bd06f..d6a1626 100644 --- a/src/modules/grades/components/batch-grade-entry.tsx +++ b/src/modules/grades/components/batch-grade-entry.tsx @@ -4,13 +4,8 @@ import { useState, useRef, useMemo, type JSX, type KeyboardEvent, type Clipboard import { toast } from "sonner" import { useRouter } from "next/navigation" import { useTranslations } from "next-intl" -import { Info } from "lucide-react" -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" -import { Button } from "@/shared/components/ui/button" -import { Label } from "@/shared/components/ui/label" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select" -import { Badge } from "@/shared/components/ui/badge" +import { Card, CardContent } from "@/shared/components/ui/card" import { safeActionCall } from "@/shared/lib/action-utils" import { batchCreateGradeRecordsByExamAction } from "../actions" @@ -18,6 +13,8 @@ import { useBatchGradeEntryUndo } from "../hooks/use-batch-grade-entry-undo" import { BatchGradeEntryStats } from "./batch-grade-entry-stats" import { BatchGradeEntryDialog, type PendingSwitch } from "./batch-grade-entry-dialog" import { BatchGradeEntryTable } from "./batch-grade-entry-table" +import { BatchGradeEntrySelectors } from "./batch-grade-entry-selectors" +import { BatchGradeEntryToolbar } from "./batch-grade-entry-toolbar" import type { ExamOptionForEntry, ExamForGradeEntry } from "@/modules/exams/types" type Student = { id: string; name: string; email: string } @@ -34,11 +31,13 @@ interface Props { } /** - * 按试卷批量录入成绩主组件。 + * 按试卷批量录入成绩主组件(容器)。 * * P1-6 重构:将表格、统计栏、确认对话框拆分为独立子组件。 + * Phase 4.6 重构:进一步将试卷/班级选择器(含未选态)与底部工具栏拆出, + * 容器仅负责状态编排与 Server Action 调用。 * - useBatchGradeEntryUndo Hook 封装撤销逻辑 + 类型守卫(P1-7 修复 `as` 断言) - * - BatchGradeEntryTable / BatchGradeEntryStats / BatchGradeEntryDialog 为组合子组件 + * - BatchGradeEntrySelectors / BatchGradeEntryTable / BatchGradeEntryStats / BatchGradeEntryToolbar / BatchGradeEntryDialog 为组合子组件 */ export function BatchGradeEntryByExam({ exams, @@ -301,87 +300,17 @@ export function BatchGradeEntryByExam({ } } - // 未选试卷 - if (!exam) { - return ( - - - {t("batchByExam.selectExam")} - - -
- - -
-
- - {t("batchByExam.guideSelectExam")} -
-
-
- ) - } - return (
- {/* 试卷 + 班级选择器 */} - - - {t("batchByExam.title")} - - -
- - -
-
- - -
- {/* 试卷信息 */} -
- - {t("batchByExam.questionCount", { count: exam.questions.length })} - - - {t("batchByExam.fullScore", { score: exam.totalScore })} - -
-
-
+ {/* 未选班级 */} {!defaultClassId ? ( @@ -390,6 +319,12 @@ export function BatchGradeEntryByExam({ {t("batchByExam.selectClassFirst")} + ) : !exam ? ( + + + {t("batchByExam.selectExam")} + + ) : students.length === 0 ? ( @@ -419,34 +354,17 @@ export function BatchGradeEntryByExam({ isScoreInvalid={isScoreInvalid} /> - {/* 提交按钮 */} -
- - -
- - {/* 提示 */} -
- - {t("batchByExam.pasteHint")} -
+ {/* 底部工具栏:保存/清除按钮 + 粘贴提示 */} + setScores({})} + onSubmit={handleSubmit} + /> )} - {/* 确认对话框 */} + {/* 切换试卷/班级确认对话框 */}
- + {stats.stats.count === 0 ? ( + + + {t("stats.title")} + + +

{t("stats.noData")}

+
+
+ ) : ( + + )}
) : ( void + onCancelEdit: () => void + onSaveEdit: () => void + onCancelDelete: () => void + onConfirmDelete: () => void + onCloseBulkDelete: () => void + onConfirmBulkDelete: () => void +} + +/** + * 成绩记录详情/编辑/删除对话框集合。 + * + * Phase 4.6 重构:从 grade-record-list.tsx 拆分。 + * 包含三个对话框: + * - 编辑对话框(标题/分数/满分/类型/学期/备注) + * - 单条删除确认对话框 + * - 批量删除确认对话框 + */ +export function GradeRecordDetail({ + editTarget, + deleteId, + isBulkDeleteDialogOpen, + isSaving, + isDeleting, + isBulkDeleting, + bulkDeleteCount, + onEditTargetChange, + onCancelEdit, + onSaveEdit, + onCancelDelete, + onConfirmDelete, + onCloseBulkDelete, + onConfirmBulkDelete, +}: DetailDialogsProps): JSX.Element { + const t = useTranslations("grades") + + return ( + <> + {/* 编辑对话框 */} + !open && onCancelEdit()}> + + + {t("edit.title")} + + {editTarget?.title} + + + {editTarget && ( +
+
+ + onEditTargetChange({ ...editTarget, title: e.target.value })} + /> +
+
+ + onEditTargetChange({ ...editTarget, score: e.target.value })} + /> +
+
+ + onEditTargetChange({ ...editTarget, fullScore: e.target.value })} + /> +
+
+ + +
+
+ + +
+
+ +