作家大纲拖拽调整顺序
This commit is contained in:
1 parent
9526268f1f
commit
6cab62d140
2 files changed
+48
-3
No files matched your search
@@ -14,6 +14,8 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
|
|||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [editingTitle, setEditingTitle] = useState(false);
|
const [editingTitle, setEditingTitle] = useState(false);
|
||||||
const [titleDraft, setTitleDraft] = useState(outline.title);
|
const [titleDraft, setTitleDraft] = useState(outline.title);
|
||||||
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||||
|
const [overIndex, setOverIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const editable = !readOnly && !!onChange;
|
const editable = !readOnly && !!onChange;
|
||||||
|
|
||||||
@@ -37,6 +39,14 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
|
|||||||
onChange?.({ ...outline, sections: outline.sections.filter((_, idx) => idx !== i) });
|
onChange?.({ ...outline, sections: outline.sections.filter((_, idx) => idx !== i) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reorderSections(from: number, to: number) {
|
||||||
|
if (from === to) return;
|
||||||
|
const sections = [...outline.sections];
|
||||||
|
const [moved] = sections.splice(from, 1);
|
||||||
|
sections.splice(to, 0, moved);
|
||||||
|
onChange?.({ ...outline, sections });
|
||||||
|
}
|
||||||
|
|
||||||
function addSection() {
|
function addSection() {
|
||||||
onChange?.({
|
onChange?.({
|
||||||
...outline,
|
...outline,
|
||||||
@@ -96,6 +106,19 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
|
|||||||
onChange={(updated) => updateSection(i, updated)}
|
onChange={(updated) => updateSection(i, updated)}
|
||||||
onRemove={editable ? () => removeSection(i) : undefined}
|
onRemove={editable ? () => removeSection(i) : undefined}
|
||||||
readOnly={!editable}
|
readOnly={!editable}
|
||||||
|
isDragging={dragIndex === i}
|
||||||
|
isOver={overIndex === i && dragIndex !== null && dragIndex !== i}
|
||||||
|
onDragStart={() => setDragIndex(i)}
|
||||||
|
onDragEnter={() => setOverIndex(i)}
|
||||||
|
onDrop={() => {
|
||||||
|
if (dragIndex !== null && dragIndex !== i) reorderSections(dragIndex, i);
|
||||||
|
setDragIndex(null);
|
||||||
|
setOverIndex(null);
|
||||||
|
}}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDragIndex(null);
|
||||||
|
setOverIndex(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ interface OutlineSectionProps {
|
|||||||
onChange: (updated: OutlineSectionType) => void;
|
onChange: (updated: OutlineSectionType) => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
// Drag-and-drop
|
||||||
|
isDragging?: boolean;
|
||||||
|
isOver?: boolean;
|
||||||
|
onDragStart?: () => void;
|
||||||
|
onDragEnter?: () => void;
|
||||||
|
onDrop?: () => void;
|
||||||
|
onDragEnd?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OutlineSection({ section, index, onChange, onRemove, readOnly }: OutlineSectionProps) {
|
export function OutlineSection({
|
||||||
|
section, index, onChange, onRemove, readOnly,
|
||||||
|
isDragging, isOver, onDragStart, onDragEnter, onDrop, onDragEnd,
|
||||||
|
}: OutlineSectionProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [editingTitle, setEditingTitle] = useState(false);
|
const [editingTitle, setEditingTitle] = useState(false);
|
||||||
const [titleDraft, setTitleDraft] = useState(section.sectionTitle);
|
const [titleDraft, setTitleDraft] = useState(section.sectionTitle);
|
||||||
@@ -41,10 +51,22 @@ export function OutlineSection({ section, index, onChange, onRemove, readOnly }:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded border border-border/60 bg-background text-xs">
|
<div
|
||||||
|
draggable={!readOnly}
|
||||||
|
onDragStart={(e) => { e.stopPropagation(); onDragStart?.(); }}
|
||||||
|
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); onDragEnter?.(); }}
|
||||||
|
onDrop={(e) => { e.preventDefault(); e.stopPropagation(); onDrop?.(); }}
|
||||||
|
onDragEnd={(e) => { e.stopPropagation(); onDragEnd?.(); }}
|
||||||
|
className={cn(
|
||||||
|
'rounded border bg-background text-xs transition-all duration-150 select-none',
|
||||||
|
isDragging ? 'opacity-40 border-border/60' : isOver ? 'border-blue-400 ring-1 ring-blue-400/40' : 'border-border/60',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{/* 章节头 */}
|
{/* 章节头 */}
|
||||||
<div className="flex items-center gap-1.5 px-2.5 py-2">
|
<div className="flex items-center gap-1.5 px-2.5 py-2">
|
||||||
{!readOnly && <GripVertical className="size-3 text-muted-foreground/40 shrink-0 cursor-grab" />}
|
{!readOnly && (
|
||||||
|
<GripVertical className="size-3 text-muted-foreground/40 shrink-0 cursor-grab active:cursor-grabbing" />
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
|||||||
Reference in new issue
Block a user