import { useState, useMemo } from 'react';
import { ChevronRight, Plus, X, GripVertical } from 'lucide-react';
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { cn } from '@/lib/utils';
import type { KeyPoint, OutlineSection as OutlineSectionType } from '../../types/ai-writing';
import { normalizeKeyPoint } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
interface OutlineSectionProps {
section: OutlineSectionType;
index: number;
/** 章节在 outline.sections 中的下标,用于生成稳定 dnd id */
sectionIndex: number;
onChange: (updated: OutlineSectionType) => void;
onRemove?: () => void;
readOnly?: boolean;
}
// 各级 grip 标题提示文案
const DRAG_HINT = '按住拖动:项的顶/底边 = 同级重排;中间区域 = 嵌入为子项';
const DRAG_HINT_LEAF = '按住拖动重排(顶半区 = 上插入,底半区 = 下插入)';
// ─── 章节 (Level 1) ───────────────────────────────────────────────────────────
export function OutlineSection({
section, index, sectionIndex, onChange, onRemove, readOnly,
}: OutlineSectionProps) {
const [expanded, setExpanded] = useState(true);
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState(section.sectionTitle);
const id = `s${sectionIndex}`;
const { attributes, listeners, setNodeRef: setDragRef, isDragging } = useDraggable({
id, disabled: readOnly,
});
// 三个 drop 区:before / into / after
const beforeDrop = useDroppable({ id: `before:${id}`, disabled: readOnly });
const intoDrop = useDroppable({ id: `into:${id}`, disabled: readOnly });
const afterDrop = useDroppable({ id: `after:${id}`, disabled: readOnly });
const keyPoints: KeyPoint[] = useMemo(
() => (section.keyPoints ?? []).map(normalizeKeyPoint),
[section.keyPoints],
);
function commitTitle() {
setEditingTitle(false);
const trimmed = titleDraft.trim();
if (trimmed && trimmed !== section.sectionTitle) {
onChange({ ...section, sectionTitle: trimmed });
} else {
setTitleDraft(section.sectionTitle);
}
}
function updateKeyPoint(i: number, updated: KeyPoint) {
const next = [...keyPoints];
next[i] = updated;
onChange({ ...section, keyPoints: next });
}
function removeKeyPoint(i: number) {
onChange({ ...section, keyPoints: keyPoints.filter((_, idx) => idx !== i) });
}
function addKeyPoint() {
onChange({ ...section, keyPoints: [...keyPoints, { pointTitle: '', subPoints: [] }] });
}
const totalSubPoints = keyPoints.reduce((sum, kp) => sum + (kp.subPoints?.length || 0), 0);
return (
{/* before:顶部 12px 命中区,命中时顶边显示蓝色线 */}
{beforeDrop.isOver &&
}
{/* into:包住头部 + 折叠体;本身就是 drop 容器 */}
{/* 章节头 */}
{!readOnly && (
)}
{index + 1}.
{editingTitle && !readOnly ? (
setTitleDraft(e.target.value)}
onBlur={commitTitle}
onKeyDown={(e) => {
if (e.key === 'Enter') commitTitle();
if (e.key === 'Escape') { setTitleDraft(section.sectionTitle); setEditingTitle(false); }
}}
className="flex-1 bg-transparent border-b border-primary outline-none font-medium"
/>
) : (
!readOnly && setEditingTitle(true)}
>
{section.sectionTitle || (未命名)}
)}
{keyPoints.length} 小节{totalSubPoints > 0 ? ` · ${totalSubPoints} 子要点` : ''}
{!readOnly && onRemove && (
)}
{/* 小节列表 (Level 2) */}
{keyPoints.map((kp, i) => (
updateKeyPoint(i, updated)}
onRemove={readOnly ? undefined : () => removeKeyPoint(i)}
readOnly={readOnly}
/>
))}
{!readOnly && (
)}
{/* after:底部 12px 命中区 */}
{afterDrop.isOver &&
}
);
}
// ─── 小节 (Level 2) ───────────────────────────────────────────────────────────
interface KeyPointRowProps {
keyPoint: KeyPoint;
sectionIndex: number;
keyPointIndex: number;
onChange: (updated: KeyPoint) => void;
onRemove?: () => void;
readOnly?: boolean;
}
function KeyPointRow({ keyPoint, sectionIndex, keyPointIndex, onChange, onRemove, readOnly }: KeyPointRowProps) {
const [expanded, setExpanded] = useState(true);
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState(keyPoint.pointTitle);
const id = `s${sectionIndex}-k${keyPointIndex}`;
const { attributes, listeners, setNodeRef: setDragRef, isDragging } = useDraggable({
id, disabled: readOnly,
});
const beforeDrop = useDroppable({ id: `before:${id}`, disabled: readOnly });
const intoDrop = useDroppable({ id: `into:${id}`, disabled: readOnly });
const afterDrop = useDroppable({ id: `after:${id}`, disabled: readOnly });
function commitTitle() {
setEditingTitle(false);
const trimmed = titleDraft.trim();
if (trimmed !== keyPoint.pointTitle) {
onChange({ ...keyPoint, pointTitle: trimmed });
}
}
function updateSubPoint(i: number, value: string) {
const next = [...keyPoint.subPoints];
next[i] = value;
onChange({ ...keyPoint, subPoints: next });
}
function removeSubPoint(i: number) {
onChange({ ...keyPoint, subPoints: keyPoint.subPoints.filter((_, idx) => idx !== i) });
}
function addSubPoint() {
onChange({ ...keyPoint, subPoints: [...keyPoint.subPoints, ''] });
setExpanded(true);
}
return (
{beforeDrop.isOver &&
}
{/* 小节头 */}
{!readOnly && (
)}
###
{editingTitle && !readOnly ? (
setTitleDraft(e.target.value)}
onBlur={commitTitle}
onKeyDown={(e) => {
if (e.key === 'Enter') commitTitle();
if (e.key === 'Escape') { setTitleDraft(keyPoint.pointTitle); setEditingTitle(false); }
}}
className="flex-1 bg-transparent border-b border-primary outline-none"
placeholder="小节标题"
/>
) : (
!readOnly && setEditingTitle(true)}
>
{keyPoint.pointTitle || (未命名小节)}
)}
{keyPoint.subPoints.length > 0 ? `${keyPoint.subPoints.length} 子要点` : '无子要点'}
{!readOnly && onRemove && (
)}
{/* 子要点 (Level 3) */}
{keyPoint.subPoints.map((sp, i) => (
updateSubPoint(i, value)}
onRemove={readOnly ? undefined : () => removeSubPoint(i)}
readOnly={readOnly}
/>
))}
{!readOnly && (
)}
{afterDrop.isOver &&
}
);
}
// ─── 子要点 (Level 3,叶节点,无 into) ────────────────────────────────────────
interface SubPointRowProps {
value: string;
sectionIndex: number;
keyPointIndex: number;
subPointIndex: number;
onChange: (value: string) => void;
onRemove?: () => void;
readOnly?: boolean;
}
function SubPointRow({
value, sectionIndex, keyPointIndex, subPointIndex, onChange, onRemove, readOnly,
}: SubPointRowProps) {
const id = `s${sectionIndex}-k${keyPointIndex}-p${subPointIndex}`;
const { attributes, listeners, setNodeRef: setDragRef, isDragging } = useDraggable({
id, disabled: readOnly,
});
// 叶节点:没有 into;顶半区 = before、底半区 = after,把整行高度铺满
const beforeDrop = useDroppable({ id: `before:${id}`, disabled: readOnly });
const afterDrop = useDroppable({ id: `after:${id}`, disabled: readOnly });
return (
{beforeDrop.isOver &&
}
{!readOnly && (
)}
####
{readOnly ? (
{value}
) : (
onChange(e.target.value)}
placeholder="子要点标题(会展开成一段正文)"
className="flex-1 bg-transparent border-b border-transparent hover:border-border focus:border-primary outline-none text-muted-foreground"
/>
)}
{!readOnly && onRemove && (
)}
{afterDrop.isOver &&
}
);
}
// ─── 蓝色插入指示线 ───────────────────────────────────────────────────────────
function DropLine({ position, thin }: { position: 'top' | 'bottom'; thin?: boolean }) {
return (
);
}