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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user