feat: 文件浏览页支持多选、批量下载/删除与分页
- 后端: - UploadService.delete_file 下沉删除逻辑(DB 行 + 磁盘文件),单删/批删复用。 - POST /api/admin/files/batch-delete:批量硬删除,返回 deleted 数与 not_found 列表。 - list 接口加 Query 约束(limit 1-10000、offset>=0),非法返回 422。 - 前端(file_browser): - 复选框列 + 全选/反选,选中行高亮,批量操作栏(已选计数/下载/删除/取消)。 - 分页控件:每页 20/50/100/全部 切换,页码导航(首页/末页/当前±1/省略号)。 - 批量下载逐个触发(间隔 150ms 防浏览器拦截);批量删除走 batch-delete 接口。 - 删除后重载当前页,当前页空则回退一页。
This commit is contained in:
@@ -8,11 +8,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
@@ -32,15 +30,24 @@ class DeleteResult(BaseModel):
|
||||
deleted: bool
|
||||
|
||||
|
||||
class BatchDeleteRequest(BaseModel):
|
||||
ids: list[int] = Field(..., description="要删除的文件 id 列表")
|
||||
|
||||
|
||||
class BatchDeleteResult(BaseModel):
|
||||
deleted: int = Field(..., description="实际删除的条数")
|
||||
not_found: list[int] = Field(default_factory=list, description="未找到的 id")
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=FileListResponse,
|
||||
summary="列出已上传文件(需鉴权)",
|
||||
description="供文件浏览页使用;字段同公开 /api/files,但要求 docs Basic Auth。",
|
||||
description="供文件浏览页使用;字段同公开 /api/files,但要求 docs Basic Auth。支持分页。",
|
||||
)
|
||||
def list_files(
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
limit: int = Query(100, ge=1, le=10000, description="每页条数,1-10000"),
|
||||
offset: int = Query(0, ge=0, description="偏移量"),
|
||||
service: UploadService = Depends(_service),
|
||||
_: str = Depends(require_docs_auth),
|
||||
) -> FileListResponse:
|
||||
@@ -48,6 +55,30 @@ def list_files(
|
||||
return FileListResponse(total=total, items=items)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/batch-delete",
|
||||
response_model=BatchDeleteResult,
|
||||
summary="批量硬删除文件(需鉴权)",
|
||||
description="一次删除多个文件;返回实际删除条数与未命中的 id 列表。",
|
||||
)
|
||||
def batch_delete_files(
|
||||
body: BatchDeleteRequest,
|
||||
service: UploadService = Depends(_service),
|
||||
_: str = Depends(require_docs_auth),
|
||||
) -> BatchDeleteResult:
|
||||
if not body.ids:
|
||||
raise HTTPException(400, "ids 不能为空")
|
||||
deleted = 0
|
||||
not_found: list[int] = []
|
||||
for fid in body.ids:
|
||||
ok = service.delete_file(fid)
|
||||
if ok:
|
||||
deleted += 1
|
||||
else:
|
||||
not_found.append(fid)
|
||||
return BatchDeleteResult(deleted=deleted, not_found=not_found)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{file_id}",
|
||||
response_model=UploadedFileOut,
|
||||
@@ -97,15 +128,4 @@ def delete_file(
|
||||
service: UploadService = Depends(_service),
|
||||
_: str = Depends(require_docs_auth),
|
||||
) -> DeleteResult:
|
||||
out, path = service.get_out_with_disk_path(file_id)
|
||||
if out is None:
|
||||
return DeleteResult(deleted=False)
|
||||
# 先删磁盘文件,再删 DB 行;磁盘文件缺失不阻断 DB 清理
|
||||
if path is not None:
|
||||
try:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
# 即使磁盘删除失败也继续清 DB 行,保证列表不再显示
|
||||
pass
|
||||
service.dao.delete(file_id)
|
||||
return DeleteResult(deleted=True)
|
||||
return DeleteResult(deleted=service.delete_file(file_id))
|
||||
|
||||
Reference in New Issue
Block a user