refactor(*): 用户工作台 - 统一画布、任务中心与设置页产品化交互

This commit is contained in:
ddCat
2026-08-14 23:44:55 +08:00
parent 4f28847543
commit d89a5a5b7b
29 changed files with 1662 additions and 2067 deletions

View File

@@ -1,4 +1,4 @@
import { App, Button, Drawer, Form, Input, Modal, Segmented, Select, Switch, Tooltip, Typography } from "antd";
import { App, Button, Drawer, Form, Input, Modal, Select, Switch, Tooltip, Typography } from "antd";
import { LayoutGrid, List, Plus, RefreshCw, Search } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "react-router";
@@ -19,7 +19,7 @@ import { TaskGridCard } from "./task-grid-card";
import { TaskGroupHeader, type TaskGroup } from "./task-group-header";
import { TaskListRow } from "./task-list-row";
import { formatModelName, getTaskCanvasContext, isTaskFailed, providerCancelStatusLabel, taskMediaKind } from "./task-shared";
import { TaskStatPills, type TaskStatusFilter } from "./task-stat-pills";
import { TaskStatusFilterBar, type TaskStatusFilter } from "./task-status-filter";
type TaskKindFilter = "all" | "text" | "image" | "video";
type TaskViewMode = "list" | "grid";
@@ -125,7 +125,7 @@ export default function TasksPage() {
else if (task.status === "succeeded") succeeded += 1;
else if (task.status === "failed" || task.status === "cancelled") failed += 1;
}
return { today, active, succeeded, failed };
return { total: tasks.length, today, active, succeeded, failed };
}, [tasks]);
const groupingActive = viewMode === "list" && groupEnabled;
const visibleTaskGroups = useMemo(
@@ -397,7 +397,6 @@ export default function TasksPage() {
</>
)}
/>
<TaskStatPills stats={taskStats} statusFilter={statusFilter} onFilterChange={setStatusFilter} />
<ListToolbar
className="library-toolbar task-library-toolbar"
active={Boolean(keyword || projectFilter !== "all" || kindFilter !== "all" || modelFilter !== "all" || statusFilter !== "all")}
@@ -421,21 +420,11 @@ export default function TasksPage() {
</div>
)}
>
<TaskStatusFilterBar stats={taskStats} value={statusFilter} onChange={(value) => { setStatusFilter(value); setPage(1); }} />
<Input id="task-search" name="taskSearch" allowClear className="app-list-search" prefix={<Search className="size-4 text-foreground/40" />} value={keyword} placeholder="搜索任务、模型或画布" onChange={(event) => { setKeyword(event.target.value); setPage(1); }} />
<Select className="w-full sm:w-48" value={projectFilter} onChange={(value) => { setProjectFilter(value); setPage(1); }} options={[{ label: "全部画布", value: "all" }, ...projectOptions]} />
<Select className="w-full sm:w-32" value={kindFilter} onChange={(value) => { setKindFilter(value as TaskKindFilter); setPage(1); }} options={[{ label: "全部类型", value: "all" }, { label: "文本", value: "text" }, { label: "图片", value: "image" }, { label: "视频", value: "video" }]} />
<Select className="w-full sm:w-44" value={modelFilter} onChange={(value) => { setModelFilter(value); setPage(1); }} options={[{ label: "全部模型", value: "all" }, ...modelOptions.map((model) => ({ label: model, value: model }))]} />
<Segmented
size="small"
value={statusFilter}
onChange={(value) => { setStatusFilter(value as typeof statusFilter); setPage(1); }}
options={[
{ label: "全部", value: "all" },
{ label: "失败/取消", value: "failed" },
{ label: "运行中", value: "active" },
{ label: "已完成", value: "succeeded" },
]}
/>
</ListToolbar>
</div>
@@ -492,7 +481,7 @@ export default function TasksPage() {
<Drawer className="library-drawer" title="任务详情" open={Boolean(detailTask)} onClose={() => setDetailTask(null)} size="large" destroyOnHidden>
{detailTask ? (
<div className="space-y-5">
<div className="grid border-y border-border text-sm sm:grid-cols-2">
<div className="task-detail-facts grid text-sm sm:grid-cols-2">
<InfoItem label="状态" value={statusLabel[detailTask.status]} />
<InfoItem label="画布名称" value={getTaskCanvasContext(detailTask, canvasById, domainProjectNameById).canvasName} />
<InfoItem label="任务类型" value={formatTaskKind(detailTask)} />
@@ -503,7 +492,7 @@ export default function TasksPage() {
{detailTask.providerCancelRequestedAt ? <InfoItem label="请求取消时间" value={formatDate(detailTask.providerCancelRequestedAt)} /> : null}
</div>
{canQueryProviderTask(detailTask) ? <div className="flex justify-end"><Button icon={<RefreshCw className="size-4" />} loading={actingId === detailTask.id} onClick={() => void queryProviderTask(detailTask)}>手动查询任务</Button></div> : null}
{detailTask.error ? <pre className="max-h-28 overflow-auto whitespace-pre-wrap border-l-2 border-red-500 bg-red-50 px-3 py-2 text-xs text-red-700 dark:bg-red-950/30 dark:text-red-300">{generationErrorMessage(detailTask.error)}</pre> : null}
{detailTask.error ? <pre className="task-detail-error max-h-28 overflow-auto whitespace-pre-wrap px-3 py-2 text-xs">{generationErrorMessage(detailTask.error)}</pre> : null}
<TaskResultMedia value={detailTask.resultJson} taskType={detailTask.type} />
<DetailBlock title="输入" value={detailLoading ? "详情加载中..." : formatTaskJson(detailTask.inputJson)} />
<DetailBlock title="结果" value={detailLoading ? "详情加载中..." : formatTaskJson(detailTask.resultJson)} />
@@ -626,7 +615,7 @@ function formatDate(value?: string) {
function InfoItem({ label, value }: { label: string; value: string }) {
return (
<div className="min-w-0 border-b border-border px-0 py-2.5">
<div className="task-detail-fact min-w-0 px-3 py-2.5">
<Typography.Text type="secondary" className="block text-xs">
{label}
</Typography.Text>

View File

@@ -1,36 +0,0 @@
export type TaskStatusFilter = "all" | "failed" | "active" | "succeeded";
export type TaskStats = { today: number; active: number; succeeded: number; failed: number };
export function TaskStatPills({ stats, statusFilter, onFilterChange }: { stats: TaskStats; statusFilter: TaskStatusFilter; onFilterChange: (value: TaskStatusFilter) => void }) {
return (
<div className="task-stat-pills">
<span className="task-stat-pill">
<span className="task-stat-dot" />
今日生成<b>{stats.today}</b>
</span>
<button type="button" aria-pressed={statusFilter === "active"} className={`task-stat-pill is-clickable${statusFilter === "active" ? " is-active-filter" : ""}`} onClick={() => onFilterChange(statusFilter === "active" ? "all" : "active")}>
<span className="task-stat-dot is-run" />
运行中<b>{stats.active}</b>
</button>
<button
type="button"
aria-pressed={statusFilter === "succeeded"}
className={`task-stat-pill is-clickable${statusFilter === "succeeded" ? " is-active-filter" : ""}`}
onClick={() => onFilterChange(statusFilter === "succeeded" ? "all" : "succeeded")}
>
<span className="task-stat-dot is-ok" />
已完成<b>{stats.succeeded}</b>
</button>
<button
type="button"
aria-pressed={statusFilter === "failed"}
className={`task-stat-pill is-clickable is-fail${statusFilter === "failed" ? " is-active-filter" : ""}`}
onClick={() => onFilterChange(statusFilter === "failed" ? "all" : "failed")}
>
<span className="task-stat-dot is-bad" />
失败<b>{stats.failed}</b>
</button>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { Segmented } from "antd";
export type TaskStatusFilter = "all" | "failed" | "active" | "succeeded";
export type TaskStats = { total: number; today: number; active: number; succeeded: number; failed: number };
export function TaskStatusFilterBar({ stats, value, onChange }: { stats: TaskStats; value: TaskStatusFilter; onChange: (value: TaskStatusFilter) => void }) {
const options = [
{ value: "all", label: "全部", count: stats.total },
{ value: "active", label: "运行中", count: stats.active },
{ value: "succeeded", label: "已完成", count: stats.succeeded },
{ value: "failed", label: "失败/取消", count: stats.failed },
] satisfies Array<{ value: TaskStatusFilter; label: string; count: number }>;
return (
<div className="task-status-filter">
<span className="task-status-today">今日生成 <strong>{stats.today}</strong></span>
<Segmented<TaskStatusFilter>
size="small"
value={value}
options={options.map((option) => ({
value: option.value,
label: <span>{option.label}<b>{option.count}</b></span>,
}))}
onChange={onChange}
/>
</div>
);
}