From 203f001c1d82a3bdd2ae2395b42c0e42b77a0cae Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 26 Jul 2026 18:10:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(task-history):=20=E4=B8=89?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E9=87=8D=E6=9E=84=20TaskHistory=EF=BC=88302?= =?UTF-8?q?=E2=86=9290=E8=A1=8C,=20-70%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Phase 1: 抽 constants(TaskStatus/TABS/PAGE_SIZE)+ utils(normalizeStatus/formatDate) - Phase 2: 抽 2 个 UI 组件(TaskList含HistoryTabs/TaskItem/Pagination + States含4种状态组件) - Phase 3: 抽 useTaskHistory 业务 Hook(查询/筛选/分页/重试) - 主文件 302→90 行,职责分离清晰 --- apps/web/src/pages/history/TaskHistory.tsx | 304 +++--------------- .../src/pages/history/components/States.tsx | 52 +++ .../src/pages/history/components/TaskList.tsx | 142 ++++++++ apps/web/src/pages/history/constants.ts | 28 ++ .../src/pages/history/hooks/useTaskHistory.ts | 131 ++++++++ apps/web/src/pages/history/utils.ts | 26 ++ 6 files changed, 425 insertions(+), 258 deletions(-) mode change 100644 => 100755 apps/web/src/pages/history/TaskHistory.tsx create mode 100644 apps/web/src/pages/history/components/States.tsx create mode 100644 apps/web/src/pages/history/components/TaskList.tsx create mode 100644 apps/web/src/pages/history/constants.ts create mode 100644 apps/web/src/pages/history/hooks/useTaskHistory.ts create mode 100644 apps/web/src/pages/history/utils.ts diff --git a/apps/web/src/pages/history/TaskHistory.tsx b/apps/web/src/pages/history/TaskHistory.tsx old mode 100644 new mode 100755 index def38c9b8..93dd65e73 --- a/apps/web/src/pages/history/TaskHistory.tsx +++ b/apps/web/src/pages/history/TaskHistory.tsx @@ -1,300 +1,88 @@ /** * 任务历史页面 — V21 设计系统 * 页面头部 + 圆角胶囊 Tab 筛选(含计数)+ 卡片式任务列表 + 分页 + 空状态 - * 使用 useQuery 对接后端真实 API(api/tasks.ts) */ -import React, { useState } from "react" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { Button } from "@/components/ui" -import { getUserTasks, retryTask, type TaskItem } from "@/api/tasks" +import React from "react" +import { PageHeader, LoadingState, ErrorState, EmptyState } from "./components/States" +import { HistoryTabs, TaskItem, Pagination } from "./components/TaskList" +import { useTaskHistory } from "./hooks/useTaskHistory" import "./history.css" -/* ============================================================ - * 类型 & 常量 - * ============================================================ */ -type TaskStatus = "completed" | "processing" | "pending" | "failed" - -const statusLabel: Record = { - completed: "已完成", - processing: "进行中", - pending: "排队中", - failed: "失败", -} - -/** 将后端 status 字符串映射为前端 TaskStatus */ -const normalizeStatus = (s: string): TaskStatus => { - const map: Record = { - completed: "completed", - succeeded: "completed", - success: "completed", - processing: "processing", - running: "processing", - pending: "pending", - queued: "pending", - failed: "failed", - error: "failed", - } - return map[s] ?? "pending" -} - -/** 格式化日期 */ -const formatDate = (iso?: string | null): string => { - if (!iso) return "—" - const d = new Date(iso) - if (isNaN(d.getTime())) return "—" - const pad = (n: number) => String(n).padStart(2, "0") - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` -} - -/* ============================================================ - * Tab 配置 - * ============================================================ */ -interface TabConfig { - key: string - label: string - statusFilter?: TaskStatus -} - -const tabs: TabConfig[] = [ - { key: "all", label: "全部" }, - { key: "processing", label: "进行中", statusFilter: "processing" }, - { key: "completed", label: "已完成", statusFilter: "completed" }, - { key: "failed", label: "失败", statusFilter: "failed" }, -] - -/* ============================================================ - * 分页配置 - * ============================================================ */ -const PAGE_SIZE = 10 - -/* ============================================================ - * 组件 - * ============================================================ */ const TaskHistory: React.FC = () => { - const [activeTab, setActiveTab] = useState("all") - const [currentPage, setCurrentPage] = useState(1) - const queryClient = useQueryClient() - - // ── 获取任务列表 ── const { - data: tasks = [], + activeTab, + currentPage, + totalPages, + tabs, + tabCounts, + paginatedTasks, isLoading, isError, error, + retryLoading, + setCurrentPage, + handleTabChange, + handleRetry, + handleView, refetch, - } = useQuery({ - queryKey: ["tasks"], - queryFn: getUserTasks, - staleTime: 30_000, - }) + } = useTaskHistory() - // ── 重试任务 mutation ── - const retryMutation = useMutation({ - mutationFn: retryTask, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["tasks"] }) - }, - }) - - // 将后端数据映射为页面展示用的结构 - const mappedTasks = tasks.map((t) => ({ - id: t.id, - name: t.user_message || t.task_type, - type: t.task_type, - template: t.template_id, - status: normalizeStatus(t.status), - date: formatDate(t.created_at), - duration: undefined as string | undefined, - progress: t.progress, - retryable: t.retryable, - errorMessage: t.error_message, - })) - - // 获取当前 Tab 的筛选状态 - const currentTab = tabs.find((t) => t.key === activeTab) - const statusFilter = currentTab?.statusFilter - - // 过滤任务 - const filteredTasks = statusFilter - ? mappedTasks.filter((t) => t.status === statusFilter) - : mappedTasks - - // 计算各 Tab 的数量 - const tabCounts: Record = { - all: mappedTasks.length, - processing: mappedTasks.filter((t) => t.status === "processing").length, - completed: mappedTasks.filter((t) => t.status === "completed").length, - failed: mappedTasks.filter((t) => t.status === "failed").length, - } - - // 分页 - const totalPages = Math.ceil(filteredTasks.length / PAGE_SIZE) - const paginatedTasks = filteredTasks.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE) - - // 切换 Tab 时重置页码 - const handleTabChange = (key: string) => { - setActiveTab(key) - setCurrentPage(1) - } - - // 重试任务 - const handleRetry = (taskId: string) => { - retryMutation.mutate(taskId) - } - - // 查看任务详情 - const handleView = (taskId: string) => { - // TODO: 跳转到任务详情页(待路由实现) - console.log("查看任务:", taskId) - } - - // ── Loading 状态 ── + // Loading 状态 if (isLoading) { return (
-
-

任务历史

-

查看和管理所有生成任务

-
-
-
-

加载中...

-
+ +
) } - // ── Error 状态 ── + // Error 状态 if (isError) { return (
-
-

任务历史

-

查看和管理所有生成任务

-
-
-
-

加载失败

-

{error?.message || "网络异常,请稍后重试"}

- -
+ + refetch()} />
) } + const totalCount = tabCounts[activeTab] ?? paginatedTasks.length + return (
- {/* ── 页面头部 ──────────────────────────────────────────── */} -
-

任务历史

-

查看和管理所有生成任务

-
+ - {/* ── Tab 切换 ──────────────────────────────────────────── */} -
- {tabs.map((tab) => ( - - ))} -
+ - {/* ── 任务列表 ──────────────────────────────────────────── */} + {/* 任务列表 */} {paginatedTasks.length === 0 ? ( -
-
📭
-

暂无任务记录

-

{activeTab === "all" ? "点击上方按钮开始创建任务" : "当前分类下没有任务"}

-
+ ) : (
{paginatedTasks.map((task) => ( -
- {/* 任务信息 */} -
-

{task.name}

- - {task.type} · 模板:{task.template} - -
- - {/* 状态标签 */} - - {statusLabel[task.status]} - - - {/* 时间区 */} -
- {task.date} - {task.status === "completed" ? ( - 完成 - ) : task.status === "processing" ? ( - 进度 {task.progress}% - ) : task.status === "failed" ? ( - {task.errorMessage || "请重试"} - ) : ( - 等待中 - )} -
- - {/* 操作按钮 */} -
- {task.status === "failed" && task.retryable ? ( - - ) : ( - - )} -
-
+ ))}
)} - {/* ── 分页 ──────────────────────────────────────────────── */} - {totalPages > 1 && ( -
- - {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( - - ))} - - 共 {filteredTasks.length} 条 -
- )} +
) } diff --git a/apps/web/src/pages/history/components/States.tsx b/apps/web/src/pages/history/components/States.tsx new file mode 100644 index 000000000..ae4251de7 --- /dev/null +++ b/apps/web/src/pages/history/components/States.tsx @@ -0,0 +1,52 @@ +import React from "react" +import { Button } from "@/components/ui" + +interface LoadingStateProps { + title?: string +} + +/** 加载状态 */ +export const LoadingState: React.FC = ({ title = "加载中..." }) => ( +
+
+

{title}

+
+) + +interface ErrorStateProps { + message?: string + onRetry: () => void +} + +/** 错误状态 */ +export const ErrorState: React.FC = ({ message, onRetry }) => ( +
+
+

加载失败

+

{message || "网络异常,请稍后重试"}

+ +
+) + +interface EmptyStateProps { + activeTab?: string +} + +/** 空状态 */ +export const EmptyState: React.FC = ({ activeTab = "all" }) => ( +
+
📭
+

暂无任务记录

+

{activeTab === "all" ? "点击上方按钮开始创建任务" : "当前分类下没有任务"}

+
+) + +/** 页面头部 */ +export const PageHeader: React.FC = () => ( +
+

任务历史

+

查看和管理所有生成任务

+
+) diff --git a/apps/web/src/pages/history/components/TaskList.tsx b/apps/web/src/pages/history/components/TaskList.tsx new file mode 100644 index 000000000..12102d15a --- /dev/null +++ b/apps/web/src/pages/history/components/TaskList.tsx @@ -0,0 +1,142 @@ +import React from "react" +import { Button } from "@/components/ui" +import type { TabConfig } from "../constants" +import type { MappedTask } from "../hooks/useTaskHistory" +import { statusLabel } from "../constants" + +interface HistoryTabsProps { + tabs: TabConfig[] + activeTab: string + tabCounts: Record + onChange: (key: string) => void +} + +/** Tab 切换栏 */ +export const HistoryTabs: React.FC = ({ + tabs, + activeTab, + tabCounts, + onChange, +}) => ( +
+ {tabs.map((tab) => ( + + ))} +
+) + +interface TaskItemProps { + task: MappedTask + onRetry: (id: string) => void + onView: (id: string) => void + retryLoading?: boolean +} + +/** 单个任务卡片 */ +export const TaskItem: React.FC = ({ task, onRetry, onView, retryLoading }) => { + const getSubText = () => { + switch (task.status) { + case "completed": + return "完成" + case "processing": + return `进度 ${task.progress}%` + case "failed": + return task.errorMessage || "请重试" + default: + return "等待中" + } + } + + return ( +
+ {/* 任务信息 */} +
+

{task.name}

+ + {task.type} · 模板:{task.template} + +
+ + {/* 状态标签 */} + + {statusLabel[task.status]} + + + {/* 时间区 */} +
+ {task.date} + {getSubText()} +
+ + {/* 操作按钮 */} +
+ {task.status === "failed" && task.retryable ? ( + + ) : ( + + )} +
+
+ ) +} + +interface PaginationProps { + currentPage: number + totalPages: number + total: number + onChange: (page: number) => void +} + +/** 分页组件 */ +export const Pagination: React.FC = ({ + currentPage, + totalPages, + total, + onChange, +}) => { + if (totalPages <= 1) return null + return ( +
+ + {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + ))} + + 共 {total} 条 +
+ ) +} diff --git a/apps/web/src/pages/history/constants.ts b/apps/web/src/pages/history/constants.ts new file mode 100644 index 000000000..e8f765596 --- /dev/null +++ b/apps/web/src/pages/history/constants.ts @@ -0,0 +1,28 @@ +/** 任务状态 */ +export type TaskStatus = "completed" | "processing" | "pending" | "failed" + +/** 状态标签 */ +export const statusLabel: Record = { + completed: "已完成", + processing: "进行中", + pending: "排队中", + failed: "失败", +} + +/** Tab 配置 */ +export interface TabConfig { + key: string + label: string + statusFilter?: TaskStatus +} + +/** 默认 Tab 列表 */ +export const TABS: TabConfig[] = [ + { key: "all", label: "全部" }, + { key: "processing", label: "进行中", statusFilter: "processing" }, + { key: "completed", label: "已完成", statusFilter: "completed" }, + { key: "failed", label: "失败", statusFilter: "failed" }, +] + +/** 每页数量 */ +export const PAGE_SIZE = 10 diff --git a/apps/web/src/pages/history/hooks/useTaskHistory.ts b/apps/web/src/pages/history/hooks/useTaskHistory.ts new file mode 100644 index 000000000..b7fcd1327 --- /dev/null +++ b/apps/web/src/pages/history/hooks/useTaskHistory.ts @@ -0,0 +1,131 @@ +import { useState, useMemo, useCallback } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { getUserTasks, retryTask, type TaskItem } from "@/api/tasks" +import { TABS, PAGE_SIZE, type TabConfig } from "../constants" +import { normalizeStatus, formatDate } from "../utils" + +/** 映射后的任务列表项 */ +export interface MappedTask { + id: string + name: string + type: string + template: string + status: "completed" | "processing" | "pending" | "failed" + date: string + progress?: number + retryable?: boolean + errorMessage?: string +} + +/** + * 任务历史业务 Hook + */ +export const useTaskHistory = () => { + const [activeTab, setActiveTab] = useState("all") + const [currentPage, setCurrentPage] = useState(1) + const queryClient = useQueryClient() + + // 获取任务列表 + const { + data: tasks = [], + isLoading, + isError, + error, + refetch, + } = useQuery({ + queryKey: ["tasks"], + queryFn: getUserTasks, + staleTime: 30_000, + }) + + // 重试 mutation + const retryMutation = useMutation({ + mutationFn: retryTask, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tasks"] }) + }, + }) + + // 映射后端数据 + const mappedTasks: MappedTask[] = useMemo( + () => + tasks.map((t) => ({ + id: t.id, + name: t.user_message || t.task_type, + type: t.task_type, + template: t.template_id, + status: normalizeStatus(t.status), + date: formatDate(t.created_at), + progress: t.progress, + retryable: t.retryable, + errorMessage: t.error_message, + })), + [tasks], + ) + + // 当前 Tab 筛选 + const currentTabConfig: TabConfig | undefined = TABS.find((t) => t.key === activeTab) + const statusFilter = currentTabConfig?.statusFilter + + // 过滤任务 + const filteredTasks = useMemo( + () => (statusFilter ? mappedTasks.filter((t) => t.status === statusFilter) : mappedTasks), + [mappedTasks, statusFilter], + ) + + // 各 Tab 计数 + const tabCounts: Record = useMemo( + () => ({ + all: mappedTasks.length, + processing: mappedTasks.filter((t) => t.status === "processing").length, + completed: mappedTasks.filter((t) => t.status === "completed").length, + failed: mappedTasks.filter((t) => t.status === "failed").length, + }), + [mappedTasks], + ) + + // 分页 + const totalPages = Math.ceil(filteredTasks.length / PAGE_SIZE) + const paginatedTasks = filteredTasks.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE) + + // 切换 Tab + const handleTabChange = useCallback((key: string) => { + setActiveTab(key) + setCurrentPage(1) + }, []) + + // 重试 + const handleRetry = useCallback( + (taskId: string) => { + retryMutation.mutate(taskId) + }, + [retryMutation], + ) + + // 查看详情 + const handleView = useCallback((taskId: string) => { + // TODO: 跳转到任务详情页(待路由实现) + console.log("查看任务:", taskId) + }, []) + + return { + // 状态 + activeTab, + currentPage, + totalPages, + // 数据 + tabs: TABS, + tabCounts, + paginatedTasks, + isLoading, + isError, + error, + retryLoading: retryMutation.isPending, + // 操作 + setCurrentPage, + handleTabChange, + handleRetry, + handleView, + refetch, + } +} diff --git a/apps/web/src/pages/history/utils.ts b/apps/web/src/pages/history/utils.ts new file mode 100644 index 000000000..025aa80ca --- /dev/null +++ b/apps/web/src/pages/history/utils.ts @@ -0,0 +1,26 @@ +import type { TaskStatus } from "./constants" + +/** 将后端 status 字符串映射为前端 TaskStatus */ +export const normalizeStatus = (s: string): TaskStatus => { + const map: Record = { + completed: "completed", + succeeded: "completed", + success: "completed", + processing: "processing", + running: "processing", + pending: "pending", + queued: "pending", + failed: "failed", + error: "failed", + } + return map[s] ?? "pending" +} + +/** 格式化日期 */ +export const formatDate = (iso?: string | null): string => { + if (!iso) return "—" + const d = new Date(iso) + if (isNaN(d.getTime())) return "—" + const pad = (n: number) => String(n).padStart(2, "0") + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` +} -- 2.54.0 From fc4997874a754c47bc93c04cc1f728952cabe89a Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 15:27:06 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20MappedTask.template=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=8F=AF=E9=80=89=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/history/hooks/useTaskHistory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/history/hooks/useTaskHistory.ts b/apps/web/src/pages/history/hooks/useTaskHistory.ts index b7fcd1327..c50d28927 100644 --- a/apps/web/src/pages/history/hooks/useTaskHistory.ts +++ b/apps/web/src/pages/history/hooks/useTaskHistory.ts @@ -9,7 +9,7 @@ export interface MappedTask { id: string name: string type: string - template: string + template?: string status: "completed" | "processing" | "pending" | "failed" date: string progress?: number -- 2.54.0