feat(lesson-preparation): add readonly view, anchor node selector, and type guards

- Add lesson-plan-readonly-view for viewing published plans

- Add anchor-node-selector and textbook-segments for canvas anchor positioning

- Add i18n-errors and type-guards lib utilities

- Add lesson-plan-provider-setup for provider initialization

- Update actions, data-access (knowledge, versions, main), publish-service

- Update blocks (blackboard, exercise, homework, import, key-point, objective, reflection)

- Update editor, node-editor, node-edit-panel, pickers, and providers
This commit is contained in:
SpecialX
2026-06-24 12:02:42 +08:00
parent a48e7d0e27
commit 6bc113eaff
39 changed files with 2129 additions and 571 deletions

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import { useTranslations } from "next-intl";
import { Tag } from "lucide-react";
import type { BlackboardBlockData } from "../../types";
import { isBlackboardLayout } from "../../lib/type-guards";
import { KnowledgePointPicker } from "../knowledge-point-picker";
interface Props {
@@ -30,12 +31,12 @@ export function BlackboardBlock({ data, textbookId, chapterId, onUpdate }: Props
</label>
<select
value={data.layout}
onChange={(e) =>
onUpdate({
...data,
layout: e.target.value as BlackboardBlockData["layout"],
})
}
onChange={(e) => {
const value = e.target.value;
if (isBlackboardLayout(value)) {
onUpdate({ ...data, layout: value });
}
}}
className="w-full text-sm border border-outline-variant rounded px-2 py-1 bg-surface"
>
{LAYOUTS.map((l) => (

View File

@@ -12,8 +12,8 @@ import { Plus, Trash2 } from "lucide-react";
import type {
ExerciseBlockData,
ExerciseItem,
ExercisePurpose,
} from "../../types";
import { isExercisePurpose } from "../../lib/type-guards";
interface Props {
blockId: string;
@@ -59,9 +59,12 @@ export function ExerciseBlock({ blockId, data, classes, textbookId, chapterId }:
<select
id={`exercise-purpose-${blockId}`}
value={data.purpose}
onChange={(e) =>
update({ purpose: e.target.value as ExercisePurpose })
}
onChange={(e) => {
const value = e.target.value;
if (isExercisePurpose(value)) {
update({ purpose: value });
}
}}
className="border rounded px-2 py-1 text-sm"
>
<option value="class_practice">{t("exercise.purpose.class_practice")}</option>

View File

@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import { Plus, Trash2 } from "lucide-react";
import type { HomeworkAssignment, HomeworkBlockData } from "../../types";
import { isHomeworkType } from "../../lib/type-guards";
import { Button } from "@/shared/components/ui/button";
interface Props {
@@ -45,11 +46,12 @@ export function HomeworkBlock({ data, onUpdate }: Props) {
<div key={idx} className="flex items-start gap-2">
<select
value={item.type}
onChange={(e) =>
updateItem(idx, {
type: e.target.value as HomeworkAssignment["type"],
})
}
onChange={(e) => {
const value = e.target.value;
if (isHomeworkType(value)) {
updateItem(idx, { type: value });
}
}}
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
>
{TYPES.map((tp) => (

View File

@@ -2,6 +2,7 @@
import { useTranslations } from "next-intl";
import type { ImportBlockData } from "../../types";
import { isImportMethod } from "../../lib/type-guards";
interface Props {
data: ImportBlockData;
@@ -24,9 +25,12 @@ export function ImportBlock({ data, onUpdate }: Props) {
</label>
<select
value={data.method}
onChange={(e) =>
onUpdate({ ...data, method: e.target.value as ImportBlockData["method"] })
}
onChange={(e) => {
const value = e.target.value;
if (isImportMethod(value)) {
onUpdate({ ...data, method: value });
}
}}
className="w-full text-sm border border-outline-variant rounded px-2 py-1 bg-surface"
>
{METHODS.map((m) => (

View File

@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import { Plus, Trash2 } from "lucide-react";
import type { KeyPointBlockData, KeyPointItem } from "../../types";
import { isKeyPointType } from "../../lib/type-guards";
import { Button } from "@/shared/components/ui/button";
interface Props {
@@ -45,9 +46,12 @@ export function KeyPointBlock({ data, onUpdate }: Props) {
<div key={idx} className="flex items-start gap-2">
<select
value={item.type}
onChange={(e) =>
updateItem(idx, { type: e.target.value as KeyPointItem["type"] })
}
onChange={(e) => {
const value = e.target.value;
if (isKeyPointType(value)) {
updateItem(idx, { type: value });
}
}}
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
>
{TYPES.map((tp) => (

View File

@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import { Plus, Trash2 } from "lucide-react";
import type { ObjectiveBlockData, ObjectiveItem } from "../../types";
import { isObjectiveDimension } from "../../lib/type-guards";
import { Button } from "@/shared/components/ui/button";
interface Props {
@@ -48,11 +49,12 @@ export function ObjectiveBlock({ data, onUpdate }: Props) {
<div key={idx} className="flex items-start gap-2">
<select
value={item.dimension}
onChange={(e) =>
updateItem(idx, {
dimension: e.target.value as ObjectiveItem["dimension"],
})
}
onChange={(e) => {
const value = e.target.value;
if (isObjectiveDimension(value)) {
updateItem(idx, { dimension: value });
}
}}
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
>
{DIMENSIONS.map((d) => (

View File

@@ -3,6 +3,7 @@
import { useTranslations } from "next-intl";
import { Plus, Trash2 } from "lucide-react";
import type { ReflectionBlockData, ReflectionItem } from "../../types";
import { isReflectionAspect } from "../../lib/type-guards";
import { Button } from "@/shared/components/ui/button";
interface Props {
@@ -45,11 +46,12 @@ export function ReflectionBlock({ data, onUpdate }: Props) {
<div key={idx} className="flex items-start gap-2">
<select
value={item.aspect}
onChange={(e) =>
updateItem(idx, {
aspect: e.target.value as ReflectionItem["aspect"],
})
}
onChange={(e) => {
const value = e.target.value;
if (isReflectionAspect(value)) {
updateItem(idx, { aspect: value });
}
}}
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
>
{ASPECTS.map((a) => (