From e26f53608bf9904df5afae450201eb7354baed11 Mon Sep 17 00:00:00 2001 From: yangqianqian <5845211314@qq.com> Date: Wed, 1 Jul 2026 14:48:18 +0800 Subject: [PATCH] feat: show fission records in history --- backend/app/api/v1/tasks.py | 95 +++++++++++++++++-- frontend/src/app/history/components.tsx | 40 +++++++- frontend/src/app/history/page.tsx | 4 +- .../src/components/tasks/RecentTasksBar.tsx | 5 +- frontend/src/types/dto.tasks.ts | 3 +- 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/backend/app/api/v1/tasks.py b/backend/app/api/v1/tasks.py index 9a0b685..a635160 100644 --- a/backend/app/api/v1/tasks.py +++ b/backend/app/api/v1/tasks.py @@ -183,10 +183,13 @@ def list_tasks( ): from datetime import datetime, timedelta from app.models.product import Product + include_fission = bool(status and "fission" in status) + task_statuses = [s for s in (status or []) if s != "fission"] + q = db.query(GenerationTask).filter(GenerationTask.workspace_id == current_user.workspace_id) - if status: + if task_statuses: # 支持多状态(?status=approved&status=rejected),单值也兼容 - q = q.filter(GenerationTask.status.in_(status)) + q = q.filter(GenerationTask.status.in_(task_statuses)) if product_id is not None: q = q.filter(GenerationTask.product_id == product_id) # 日期筛选:date 形参非法 → 40001(不静默吞成全量,防运营误判) @@ -200,19 +203,99 @@ def list_tasks( except ValueError: from app.core.response import raise_param_error raise_param_error("date_from/date_to 需为 YYYY-MM-DD 格式") - total = q.count() - items = q.order_by(GenerationTask.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + if not include_fission: + total = q.count() + items = q.order_by(GenerationTask.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all() + # product_name 一次性批量查(防 N+1):收集本页 product_id → id→name dict + pids = {t.product_id for t in items if t.product_id} + name_map: dict[int, str] = {} + if pids: + for p in db.query(Product.id, Product.name).filter(Product.id.in_(pids)).all(): + name_map[p.id] = p.name + rows = [] + for t in items: + d = _fmt_task(t) + d["item_type"] = "task" + d["product_name"] = name_map.get(t.product_id) + rows.append(d) + return ok(paginate(rows, total, page, page_size)) + + from app.models.fission import FissionTask, FissionNote + + task_items = q.all() if task_statuses else [] + fission_q = db.query(FissionTask).filter(FissionTask.workspace_id == current_user.workspace_id) + try: + if date_from: + fission_q = fission_q.filter(FissionTask.created_at >= datetime.fromisoformat(date_from)) + if date_to: + _end = datetime.fromisoformat(date_to) + timedelta(days=1) + fission_q = fission_q.filter(FissionTask.created_at < _end) + except ValueError: + from app.core.response import raise_param_error + raise_param_error("date_from/date_to 需为 YYYY-MM-DD 格式") + fission_items = fission_q.all() + + source_ids = {f.source_task_id for f in fission_items if f.source_task_id} + source_tasks: dict[int, GenerationTask] = {} + if source_ids: + for t in db.query(GenerationTask).filter(GenerationTask.id.in_(source_ids)).all(): + if t.workspace_id == current_user.workspace_id: + source_tasks[t.id] = t + if product_id is not None: + fission_items = [ + f for f in fission_items + if f.source_task_id and source_tasks.get(f.source_task_id) + and source_tasks[f.source_task_id].product_id == product_id + ] + # product_name 一次性批量查(防 N+1):收集本页 product_id → id→name dict - pids = {t.product_id for t in items if t.product_id} + pids = {t.product_id for t in task_items if t.product_id} + pids.update( + t.product_id for t in source_tasks.values() + if t.product_id and (product_id is None or t.product_id == product_id) + ) name_map: dict[int, str] = {} if pids: for p in db.query(Product.id, Product.name).filter(Product.id.in_(pids)).all(): name_map[p.id] = p.name rows = [] - for t in items: + for t in task_items: d = _fmt_task(t) + d["item_type"] = "task" d["product_name"] = name_map.get(t.product_id) rows.append(d) + for f in fission_items: + source = source_tasks.get(f.source_task_id) if f.source_task_id else None + notes = db.query(FissionNote).filter( + FissionNote.fission_id == f.id, + FissionNote.workspace_id == current_user.workspace_id, + ).all() + image_count = 0 + for n in notes: + import json + try: + images = json.loads(n.images_json) if n.images_json else [] + except (TypeError, ValueError): + images = [] + image_count += sum(1 for im in images if isinstance(im, dict) and im.get("url") and not im.get("error")) + rows.append({ + "id": f.id, + "item_type": "fission", + "product_id": source.product_id if source else 0, + "product_name": name_map.get(source.product_id) if source else None, + "theme": (source.theme if source and source.theme else f"裂变笔记 #{f.id}"), + "status": "fission", + "created_at": f.created_at.isoformat(), + "text_count": len(notes), + "image_count": image_count, + "review_status": None, + "reject_reason": None, + "reviewed_at": None, + }) + rows.sort(key=lambda x: x["created_at"], reverse=True) + total = len(rows) + start = (page - 1) * page_size + rows = rows[start:start + page_size] return ok(paginate(rows, total, page, page_size)) diff --git a/frontend/src/app/history/components.tsx b/frontend/src/app/history/components.tsx index db49403..d78069d 100644 --- a/frontend/src/app/history/components.tsx +++ b/frontend/src/app/history/components.tsx @@ -18,6 +18,7 @@ const STATUS_LABEL: Record = { archived: { text: '已归档', color: 'text-gray-500 bg-gray-100' }, rejected: { text: '被打回', color: 'text-red-500 bg-red-50' }, failed: { text: '生成失败', color: 'text-red-600 bg-red-50' }, + fission: { text: '裂变', color: 'text-orange-600 bg-orange-50' }, }; // --- DownloadButton --- 触发打包→轮询→下载 zip @@ -73,6 +74,40 @@ function DownloadButton({ taskId }: { taskId: number }) { ); } +function FissionDownloadButton({ fissionId }: { fissionId: number }) { + const [state, setState] = useState<'idle' | 'downloading' | 'done' | 'error'>('idle'); + + async function handleDownload() { + setState('downloading'); + try { + const blob = await api.getBlob(`/api/v1/fission/${fissionId}/download`); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `fission-${fissionId}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + setState('done'); + } catch { + setState('error'); + } + } + + if (state === 'done') return ✓ 已下载; + if (state === 'error') return 失败,刷新重试; + return ( + + ); +} + // --- HistoryTable --- export function HistoryTable({ tasks }: { tasks: TaskListItem[] }) { return ( @@ -120,12 +155,12 @@ export function HistoryTable({ tasks }: { tasks: TaskListItem[] }) {
查看 - {task.status !== 'failed' && ( + {task.item_type !== 'fission' && task.status !== 'failed' && ( )} {task.status === 'approved' && } + {task.item_type === 'fission' && }
diff --git a/frontend/src/app/history/page.tsx b/frontend/src/app/history/page.tsx index b8ae00d..7fac340 100644 --- a/frontend/src/app/history/page.tsx +++ b/frontend/src/app/history/page.tsx @@ -13,9 +13,9 @@ import { clsx } from 'clsx'; import { HistoryTable, HistorySkeleton, Pagination } from './components'; import { HistoryFilters } from './HistoryFilters'; -const HISTORY_STATUSES = ['approved', 'archived', 'rejected', 'failed']; +const HISTORY_STATUSES = ['approved', 'archived', 'rejected', 'failed', 'fission']; const STATUS_LABELS: Record = { - approved: '已通过', archived: '已归档', rejected: '被打回', failed: '生成失败', + approved: '已通过', archived: '已归档', rejected: '被打回', failed: '生成失败', fission: '裂变', }; const PAGE_SIZE = 20; diff --git a/frontend/src/components/tasks/RecentTasksBar.tsx b/frontend/src/components/tasks/RecentTasksBar.tsx index eecf661..218608c 100644 --- a/frontend/src/components/tasks/RecentTasksBar.tsx +++ b/frontend/src/components/tasks/RecentTasksBar.tsx @@ -20,6 +20,7 @@ const STATUS_LABELS: Record = { rejected: { label: '已打回', color: 'text-status-error' }, archived: { label: '已归档', color: 'text-text-tertiary' }, failed: { label: '生成失败', color: 'text-status-error' }, + fission: { label: '裂变', color: 'text-brand-orange' }, }; export function RecentTasksBar() { @@ -47,7 +48,9 @@ export function RecentTasksBar() { {recentTasks.slice(0, 4).map((task) => { const statusInfo = STATUS_LABELS[task.status]; return ( -

{task.theme}

diff --git a/frontend/src/types/dto.tasks.ts b/frontend/src/types/dto.tasks.ts index 2d67318..762b7d9 100644 --- a/frontend/src/types/dto.tasks.ts +++ b/frontend/src/types/dto.tasks.ts @@ -3,10 +3,11 @@ // 任务 export type TaskStatus = | 'pending' | 'generating' | 'pending_selection' - | 'pending_review' | 'approved' | 'rejected' | 'archived' | 'failed'; + | 'pending_review' | 'approved' | 'rejected' | 'archived' | 'failed' | 'fission'; export interface TaskListItem { id: number; + item_type?: 'task' | 'fission'; product_id: number; product_name?: string | null; // M4: 历史页"产品"列,list_tasks 批量 JOIN 填充 theme: string;