feat(lesson-preparation): 师生交互编辑器(对话轮次增删改)

This commit is contained in:
SpecialX
2026-07-04 11:32:54 +08:00
parent 40cba54ed4
commit 43f30f4013

View File

@@ -0,0 +1,250 @@
"use client";
import { useTranslations } from "next-intl";
import { createId } from "@paralleldrive/cuid2";
import type { InteractionBlockData, QATurn } from "../../types";
import { useLessonPlanEditor } from "../../hooks/use-lesson-plan-editor";
interface Props {
nodeId: string;
data: InteractionBlockData;
}
export function QaEditor({ nodeId, data }: Props) {
const t = useTranslations("lessonPreparation");
const updateNode = useLessonPlanEditor((s) => s.updateNode);
const updateData = (next: InteractionBlockData) => {
updateNode(nodeId, { data: next });
};
const addTurn = () => {
const newTurn: QATurn = {
id: createId(),
role: "teacher",
content: "",
expectedAnswer: "",
order: data.turns.length,
};
updateData({ ...data, turns: [...data.turns, newTurn] });
};
const updateTurn = (id: string, patch: Partial<QATurn>) => {
updateData({
...data,
turns: data.turns.map((t) => (t.id === id ? { ...t, ...patch } : t)),
});
};
const removeTurn = (id: string) => {
updateData({
...data,
turns: data.turns
.filter((t) => t.id !== id)
.map((t, i) => ({ ...t, order: i })),
});
};
const moveTurn = (id: string, direction: "up" | "down") => {
const idx = data.turns.findIndex((t) => t.id === id);
if (idx < 0) return;
const newIdx = direction === "up" ? idx - 1 : idx + 1;
if (newIdx < 0 || newIdx >= data.turns.length) return;
const turns = [...data.turns];
[turns[idx], turns[newIdx]] = [turns[newIdx], turns[idx]];
updateData({
...data,
turns: turns.map((t, i) => ({ ...t, order: i })),
});
};
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div>
<label
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--muted-foreground)",
display: "block",
marginBottom: 6,
}}
>
{t("v4.detail.designIntent")}
</label>
<textarea
value={data.designIntent}
onChange={(e) => updateData({ ...data, designIntent: e.target.value })}
rows={2}
style={{
width: "100%",
border: "1px solid var(--border)",
borderRadius: 5,
padding: "8px 10px",
background: "var(--background)",
fontFamily: "Inter, sans-serif",
fontSize: 13,
lineHeight: 1.6,
color: "var(--foreground)",
resize: "vertical",
}}
/>
</div>
<div>
<label
style={{
fontSize: 10,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--muted-foreground)",
display: "block",
marginBottom: 8,
}}
>
{t("v4.detail.qaDialog")}
</label>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{data.turns.map((turn, idx) => (
<QaTurnEditor
key={turn.id}
turn={turn}
index={idx}
total={data.turns.length}
onUpdate={(patch) => updateTurn(turn.id, patch)}
onRemove={() => removeTurn(turn.id)}
onMoveUp={() => moveTurn(turn.id, "up")}
onMoveDown={() => moveTurn(turn.id, "down")}
/>
))}
<button
type="button"
onClick={addTurn}
style={{
border: "1px dashed var(--border)",
background: "transparent",
padding: 8,
borderRadius: 5,
textAlign: "center",
fontSize: 12,
color: "var(--muted-foreground)",
cursor: "pointer",
}}
>
+ {t("v4.detail.addTurn")}
</button>
</div>
</div>
</div>
);
}
interface TurnProps {
turn: QATurn;
index: number;
total: number;
onUpdate: (patch: Partial<QATurn>) => void;
onRemove: () => void;
onMoveUp: () => void;
onMoveDown: () => void;
}
function QaTurnEditor({ turn, index, total, onUpdate, onRemove, onMoveUp, onMoveDown }: TurnProps) {
const t = useTranslations("lessonPreparation");
const isTeacher = turn.role === "teacher";
return (
<div
style={{
border: "1px solid var(--border)",
borderRadius: 5,
padding: "8px 10px",
background: "var(--background)",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
fontSize: 10,
}}
>
<select
value={turn.role}
onChange={(e) => onUpdate({ role: e.target.value as "teacher" | "student" })}
className={isTeacher ? "teacher" : "student"}
style={{
fontFamily: "JetBrains Mono, monospace",
fontSize: 9,
fontWeight: 600,
padding: "2px 6px",
borderRadius: 2,
border: "1px solid var(--border)",
background: "var(--background)",
cursor: "pointer",
color: isTeacher ? "#1c1917" : "#6b7280",
}}
>
<option value="teacher">{t("v4.detail.turnTeacher")}</option>
<option value="student">{t("v4.detail.turnStudent")}</option>
</select>
<span style={{ color: "var(--muted-foreground)", fontSize: 10 }}>
{t("v4.interaction.round", { n: index + 1 })}
</span>
<div style={{ marginLeft: "auto" }}>
<button type="button" onClick={onMoveUp} disabled={index === 0} style={{ border: "none", background: "transparent", color: "var(--muted-foreground)", cursor: index === 0 ? "default" : "pointer", padding: "2px 4px", fontSize: 11 }}></button>
<button type="button" onClick={onMoveDown} disabled={index === total - 1} style={{ border: "none", background: "transparent", color: "var(--muted-foreground)", cursor: index === total - 1 ? "default" : "pointer", padding: "2px 4px", fontSize: 11 }}></button>
<button type="button" onClick={onRemove} style={{ border: "none", background: "transparent", color: "var(--muted-foreground)", cursor: "pointer", padding: "2px 4px", fontSize: 11 }}>×</button>
</div>
</div>
<textarea
value={turn.content}
onChange={(e) => onUpdate({ content: e.target.value })}
rows={2}
placeholder="内容"
style={{
width: "100%",
border: "none",
outline: "none",
background: "transparent",
resize: "vertical",
fontFamily: "Inter, sans-serif",
fontSize: 12.5,
lineHeight: 1.5,
color: "var(--foreground)",
minHeight: 36,
}}
/>
{isTeacher && (
<textarea
value={turn.expectedAnswer ?? ""}
onChange={(e) => onUpdate({ expectedAnswer: e.target.value })}
rows={1}
placeholder={t("v4.detail.expectedAnswer")}
style={{
width: "100%",
border: "none",
outline: "none",
background: "var(--muted)",
resize: "vertical",
fontFamily: "Inter, sans-serif",
fontSize: 11,
fontStyle: "italic",
color: "var(--muted-foreground)",
padding: "4px 8px",
borderRadius: 3,
marginTop: 4,
minHeight: 24,
}}
/>
)}
</div>
);
}