feat(lesson-preparation): major update with AI features, schedules, and new components

- Add actions-schedules.ts and data-access-schedules.ts for schedule management

- Add AI differentiation, AI feedback, consistency check dialogs

- Add attachment-picker, curriculum-heatmap, print-view, version-diff-view

- Add lesson-plan-mobile-view and schedule-dialog components

- Add lib: ai-differentiation, ai-feedback, auto-layout, consistency-check,

  curriculum-coverage, export, version-diff

- Add history-slice hook for version history

- Update existing components, hooks, providers, services, types

- Add teacher lesson-plans heatmap and library pages
This commit is contained in:
SpecialX
2026-07-04 10:22:10 +08:00
parent 41fe8d8903
commit 25dca843be
45 changed files with 5295 additions and 210 deletions

View File

@@ -0,0 +1,150 @@
"use client";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import { Book } from "lucide-react";
import type {
LessonPlanDocument,
LessonPlanNode,
TeachingStage,
} from "../types";
import { TEACHING_STAGE_KEYS } from "../types";
import { getNodeColor, getNodeSummary } from "../lib/node-summary";
import { cn } from "@/shared/lib/utils";
interface Props {
doc: LessonPlanDocument;
textbookTitle?: string;
chapterTitle?: string;
}
/**
* V5-13 P3移动端只读视图。
*
* 在小屏设备上以线性卡片列表替代 React Flow 画布,
* 按教学阶段V5-15分组展示节点提升可读性。
* 仅展示,无编辑交互。
*/
export function LessonPlanMobileView({ doc, textbookTitle, chapterTitle }: Props) {
const t = useTranslations("lessonPreparation");
// 教学节点按 stage 分组(未归类的归入"其他"
const grouped = useMemo(() => {
const teachingNodes = doc.nodes.filter(
(n): n is LessonPlanNode => n.type !== "textbook_content",
);
const groups: Record<string, LessonPlanNode[]> = {};
const unstaged: LessonPlanNode[] = [];
for (const n of teachingNodes) {
if (n.stage) {
const key = n.stage as TeachingStage;
if (!groups[key]) groups[key] = [];
groups[key].push(n);
} else {
unstaged.push(n);
}
}
return { groups, unstaged };
}, [doc.nodes]);
return (
<div className="flex flex-col h-full overflow-y-auto bg-surface-container-low">
{/* 顶部信息条 */}
{(textbookTitle || chapterTitle) && (
<div className="sticky top-0 z-10 bg-surface/95 backdrop-blur border-b border-outline-variant px-4 py-2 text-sm">
{textbookTitle && (
<div className="flex items-center gap-1 text-on-surface-variant">
<Book className="w-3.5 h-3.5" aria-hidden="true" />
<span className="font-medium">{textbookTitle}</span>
</div>
)}
{chapterTitle && (
<div className="text-on-surface-variant text-xs mt-0.5">{chapterTitle}</div>
)}
</div>
)}
<div className="flex-1 px-3 py-3 space-y-4">
{/* 按阶段分组渲染 */}
{TEACHING_STAGE_KEYS.map((stage) => {
const nodes = grouped.groups[stage];
if (!nodes || nodes.length === 0) return null;
return (
<section key={stage} className="space-y-2">
<h3 className="text-xs font-medium text-on-surface-variant px-1">
{t(`editor.stage.${stage}`)}
</h3>
{nodes.map((n) => (
<MobileNodeCard key={n.id} node={n} t={t} />
))}
</section>
);
})}
{/* 未归类节点 */}
{grouped.unstaged.length > 0 && (
<section className="space-y-2">
{TEACHING_STAGE_KEYS.some((s) => grouped.groups[s]?.length > 0) && (
<h3 className="text-xs font-medium text-on-surface-variant px-1">
{t("editor.stageNone")}
</h3>
)}
{grouped.unstaged.map((n) => (
<MobileNodeCard key={n.id} node={n} t={t} />
))}
</section>
)}
</div>
</div>
);
}
function MobileNodeCard({
node,
t,
}: {
node: LessonPlanNode;
t: ReturnType<typeof useTranslations>;
}) {
const color = getNodeColor(node.type);
const summary = getNodeSummary(node, (key, values) => t(key, values));
const diff = node.differentiation;
return (
<article className="rounded-lg border border-outline-variant bg-surface p-3 shadow-sm">
<div className="flex items-start gap-2">
<span
className="inline-block w-2.5 h-2.5 rounded-full mt-1.5 flex-shrink-0"
style={{ backgroundColor: color }}
aria-hidden="true"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<h4 className="font-medium text-sm text-on-surface truncate flex-1">
{node.title || node.type}
</h4>
{diff && (
<span
className={cn(
"text-[10px] px-1.5 py-0.5 rounded font-medium flex-shrink-0",
diff === "basic"
? "bg-primary-container text-on-primary-container"
: diff === "intermediate"
? "bg-secondary-container text-on-secondary-container"
: "bg-tertiary-container text-on-tertiary-container",
)}
>
{t(`editor.differentiation.${diff}`)}
</span>
)}
</div>
{summary && (
<p className="text-xs text-on-surface-variant mt-1 line-clamp-3">
{summary}
</p>
)}
</div>
</div>
</article>
);
}