From 4db77b7d1e2824d36fa18b6fd2f3c990e59e9e4f Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:29:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(lesson-preparation):=20=E5=8F=B3=E9=94=AE?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=EF=BC=88=E8=8A=82=E7=82=B9=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=20+=20AI=20=E5=8D=8F=E5=8A=A9=20+=20=E9=94=9A=E5=AE=9A?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paper-editor/paper-context-menu.tsx | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx diff --git a/src/modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx b/src/modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx new file mode 100644 index 0000000..0375e6b --- /dev/null +++ b/src/modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import type { LessonPlanNode } from "../../types"; +import { useLessonPlanEditor } from "../../hooks/use-lesson-plan-editor"; + +export interface ContextMenuState { + visible: boolean; + x: number; + y: number; + /** 右键的节点(如果是 inline-node 右键) */ + nodeId: string | null; + /** 右键的选中文本范围(如果是正文右键) */ + selectionRange: { from: number; to: number } | null; +} + +interface Props { + state: ContextMenuState; + onClose: () => void; + /** AI 协助回调(V1: 仅 toast 提示"功能开发中",V2 接入 actions-ai.ts) */ + onAiAction?: (action: "generate" | "optimize" | "differentiation" | "layered", nodeId: string) => void; +} + +/** + * V4 右键菜单:根据 state.nodeId 区分节点菜单 vs 锚定菜单。 + * + * V1 范围:展开/收起、删除、锚定 已实现。 + * V1 占位:上下移动、复制节点、AI 协助 4 项(按钮显示,点击 toast 提示)。 + * 这些占位功能在 spec §15 YAGNI 边界内,V2 接入。 + */ +export function PaperContextMenu({ state, onClose, onAiAction }: Props) { + const t = useTranslations("lessonPreparation"); + const { + toggleExpand, + expandedNodeIds, + updateNode, + removeNode, + addAnchor, + doc, + } = useLessonPlanEditor(); + + // V1 占位实现:上下移动、复制(按 spec §15 YAGNI,V2 完整实现) + const moveNodeOrder = (nodeId: string, direction: "up" | "down") => { + const teachingNodes = doc.nodes + .filter((n): n is LessonPlanNode => n.type !== "textbook_content") + .sort((a, b) => a.order - b.order); + const idx = teachingNodes.findIndex((n) => n.id === nodeId); + if (idx < 0) return; + const newIdx = direction === "up" ? idx - 1 : idx + 1; + if (newIdx < 0 || newIdx >= teachingNodes.length) return; + const a = teachingNodes[idx]; + const b = teachingNodes[newIdx]; + updateNode(a.id, { order: b.order }); + updateNode(b.id, { order: a.order }); + }; + + const copyNode = (nodeId: string) => { + void nodeId; + // V1 占位:toast 提示 + void import("sonner").then(({ toast }) => toast.info("复制节点功能将在 V2 提供")); + }; + + // AI 协助默认实现(V1 占位) + const handleAi = (action: "generate" | "optimize" | "differentiation" | "layered", nodeId: string) => { + if (onAiAction) { + onAiAction(action, nodeId); + } else { + void import("sonner").then(({ toast }) => toast.info("AI 协助功能将在 V2 提供")); + } + }; + + if (!state.visible) return null; + + const isNodeMenu = state.nodeId !== null; + const isExpanded = state.nodeId ? expandedNodeIds.includes(state.nodeId) : false; + + const handleAction = (action: () => void) => { + action(); + onClose(); + }; + + const item = (label: string, action: () => void, opts?: { danger?: boolean; ai?: boolean; shortcut?: string }) => ( + + ); + + const section = (label: string) => ( +