引入二级大纲功能,新增父子节点关系及子节点排序,同时调整API与UI以支持嵌套大纲管理

This commit is contained in:
2026-03-22 16:53:33 +08:00
parent 20c759150a
commit 098ff4e16d
33 changed files with 2516 additions and 381 deletions

View File

@@ -32,24 +32,62 @@ def _get_outline_or_404(
return outline
def _build_outline_read(outline: Outline, children: list[Outline]) -> OutlineRead:
"""将 Outline ORM 对象转为 OutlineRead附带子节点列表"""
return OutlineRead(
id=outline.id,
novel_id=outline.novel_id,
parent_id=outline.parent_id,
sort_order=outline.sort_order,
summary=outline.summary,
detail=outline.detail,
children=[
OutlineRead(
id=c.id,
novel_id=c.novel_id,
parent_id=c.parent_id,
sort_order=c.sort_order,
summary=c.summary,
detail=c.detail,
children=[],
created_at=c.created_at,
updated_at=c.updated_at,
)
for c in children
],
created_at=outline.created_at,
updated_at=outline.updated_at,
)
@router.get("", response_model=OutlineList)
def list_outlines(
novel_id: int,
session: Session = Depends(get_session),
):
_get_novel_or_404(novel_id, session)
total = session.exec(
select(func.count(Outline.id)).where(Outline.novel_id == novel_id)
).one()
items = session.exec(
# 只查顶级节点parent_id IS NULL
top_items = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id)
.where(Outline.novel_id == novel_id, Outline.parent_id.is_(None)) # type: ignore
.order_by(Outline.sort_order)
).all()
return OutlineList(
items=[OutlineRead.model_validate(o, from_attributes=True) for o in items],
total=total,
)
# 查询所有子节点,按 parent_id 分组
all_children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id.isnot(None)) # type: ignore
.order_by(Outline.sort_order)
).all()
children_map: dict[int, list[Outline]] = {}
for child in all_children:
children_map.setdefault(child.parent_id, []).append(child)
items = [
_build_outline_read(o, children_map.get(o.id, []))
for o in top_items
]
return OutlineList(items=items, total=len(items))
@router.post("", response_model=OutlineRead, status_code=201)
@@ -59,17 +97,41 @@ def create_outline(
session: Session = Depends(get_session),
):
_get_novel_or_404(novel_id, session)
# 自动设置 sort_order 为当前最大值 + 1
max_order = session.exec(
select(func.max(Outline.sort_order)).where(Outline.novel_id == novel_id)
).one()
# 如果指定了 parent_id校验父节点存在且是顶级节点
if data.parent_id is not None:
parent = session.get(Outline, data.parent_id)
if not parent or parent.novel_id != novel_id:
raise HTTPException(status_code=404, detail="父节点不存在")
if parent.parent_id is not None:
raise HTTPException(status_code=400, detail="仅支持两级结构,不能在子节点下创建子节点")
# 同级内自增 sort_order同 parent_id 下的 max + 1
if data.parent_id is not None:
max_order = session.exec(
select(func.max(Outline.sort_order))
.where(Outline.novel_id == novel_id, Outline.parent_id == data.parent_id)
).one()
else:
max_order = session.exec(
select(func.max(Outline.sort_order))
.where(Outline.novel_id == novel_id, Outline.parent_id.is_(None)) # type: ignore
).one()
next_order = (max_order or 0) + 1
outline = Outline(novel_id=novel_id, sort_order=next_order, **data.model_dump())
outline = Outline(
novel_id=novel_id,
parent_id=data.parent_id,
sort_order=next_order,
summary=data.summary,
detail=data.detail,
)
session.add(outline)
session.commit()
session.refresh(outline)
return outline
# 返回时附带空 children
return _build_outline_read(outline, [])
@router.put("/reorder", response_model=OutlineList)
@@ -78,25 +140,53 @@ def reorder_outlines(
data: OutlineReorder,
session: Session = Depends(get_session),
):
"""仅处理顶级节点排序"""
_get_novel_or_404(novel_id, session)
now = datetime.now(timezone.utc)
for idx, oid in enumerate(data.ordered_ids):
outline = _get_outline_or_404(oid, novel_id, session)
if outline.parent_id is not None:
raise HTTPException(status_code=400, detail=f"节点 {oid} 不是顶级节点,不能在此排序")
outline.sort_order = idx + 1
outline.updated_at = now
session.add(outline)
session.commit()
items = session.exec(
# 重新查询并返回嵌套结构
return list_outlines(novel_id, session)
@router.put("/{outline_id}/children/reorder", response_model=OutlineRead)
def reorder_children(
novel_id: int,
outline_id: int,
data: OutlineReorder,
session: Session = Depends(get_session),
):
"""子节点排序"""
_get_novel_or_404(novel_id, session)
parent = _get_outline_or_404(outline_id, novel_id, session)
if parent.parent_id is not None:
raise HTTPException(status_code=400, detail="只有顶级节点可以排序子节点")
now = datetime.now(timezone.utc)
for idx, cid in enumerate(data.ordered_ids):
child = _get_outline_or_404(cid, novel_id, session)
if child.parent_id != outline_id:
raise HTTPException(status_code=400, detail=f"节点 {cid} 不是该父节点的子节点")
child.sort_order = idx + 1
child.updated_at = now
session.add(child)
session.commit()
# 查询更新后的子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
.order_by(Outline.sort_order)
).all()
total = len(items)
return OutlineList(
items=[OutlineRead.model_validate(o, from_attributes=True) for o in items],
total=total,
)
session.refresh(parent)
return _build_outline_read(parent, list(children))
@router.get("/{outline_id}", response_model=OutlineRead)
@@ -105,7 +195,14 @@ def get_outline(
outline_id: int,
session: Session = Depends(get_session),
):
return _get_outline_or_404(outline_id, novel_id, session)
outline = _get_outline_or_404(outline_id, novel_id, session)
# 查询子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
.order_by(Outline.sort_order)
).all()
return _build_outline_read(outline, list(children))
@router.patch("/{outline_id}", response_model=OutlineRead)
@@ -123,7 +220,14 @@ def update_outline(
session.add(outline)
session.commit()
session.refresh(outline)
return outline
# 查询子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
.order_by(Outline.sort_order)
).all()
return _build_outline_read(outline, list(children))
@router.delete("/{outline_id}", status_code=204)
@@ -133,5 +237,12 @@ def delete_outline(
session: Session = Depends(get_session),
):
outline = _get_outline_or_404(outline_id, novel_id, session)
# 删除父节点时先删除所有子节点
children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id == outline_id)
).all()
for child in children:
session.delete(child)
session.delete(outline)
session.commit()