Files
NextEdu/src/modules/lesson-preparation/components/lesson-plan-mobile-view.tsx
SpecialX 25dca843be 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
2026-07-04 10:22:10 +08:00

151 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}