- 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
151 lines
4.8 KiB
TypeScript
151 lines
4.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|