refactor(*): 用户工作台 - 统一画布、任务中心与设置页产品化交互
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
29
web/src/pages/tasks/task-status-filter.tsx
Normal file
29
web/src/pages/tasks/task-status-filter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user