feat(web): 用户工作台 - 产品化壳层、内容库布局与积分入口
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { CollectionToolbar } from "@/components/layout/collection-toolbar";
|
||||
import { App, Button, Drawer, Form, Input, Modal, Select, Typography } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { SegmentedControl } from "@/components/ui/base/segmented-control";
|
||||
@@ -6,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
|
||||
import { MediaPreview } from "@/components/media-preview";
|
||||
import { ListToolbar, PageHeader, PaginationBar, WorkspacePage } from "@/components/layout/workspace-page";
|
||||
import { PageHeader, PaginationBar, WorkspacePage } from "@/components/layout/workspace-page";
|
||||
import { WorkspaceState } from "@/components/layout/workspace-state";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error";
|
||||
import { formatTaskKind, operationOptions, statusLabel } from "@/lib/generation-task-display";
|
||||
@@ -383,8 +384,17 @@ export default function TasksPage() {
|
||||
<>
|
||||
<WorkspacePage grid className="library-page task-library-page">
|
||||
<div className="studio-band">
|
||||
<ListToolbar
|
||||
className="library-toolbar task-library-toolbar"
|
||||
<PageHeader
|
||||
title="创作历史"
|
||||
description="查看文本、图片和视频生成任务,跟踪进度并处理失败任务。"
|
||||
meta={<span className="app-projects-header-meta">{taskStats.total} 个任务</span>}
|
||||
actions={
|
||||
<Button type="primary" icon={<Plus className="size-3.5" />} onClick={() => setCreateOpen(true)}>
|
||||
新建任务
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CollectionToolbar
|
||||
active={Boolean(keyword || projectFilter !== "all" || kindFilter !== "all" || modelFilter !== "all" || statusFilter !== "all")}
|
||||
onReset={() => { setKeyword(""); setProjectFilter("all"); setKindFilter("all"); setModelFilter("all"); setStatusFilter("all"); setPage(1); }}
|
||||
trailing={(
|
||||
@@ -415,10 +425,10 @@ export default function TasksPage() {
|
||||
<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 }))]} />
|
||||
</ListToolbar>
|
||||
</CollectionToolbar>
|
||||
</div>
|
||||
|
||||
<div className="canvas-library-frame task-library-frame">
|
||||
<div className="collection-content task-collection-content">
|
||||
{loading && !tasks.length ? <div className="library-loading-grid" aria-label="正在加载任务">{Array.from({ length: 8 }, (_, index) => <div key={index} className="library-skeleton" />)}</div> : null}
|
||||
{!loading || tasks.length ? (
|
||||
visibleTasks.length ? (
|
||||
|
||||
@@ -7,18 +7,19 @@ import { MediaPreview } from "@/components/media-preview";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||
import { statusLabel } from "@/lib/generation-task-display";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { isTaskFailed, statusDotClassName, TaskDate } from "./task-shared";
|
||||
import { isTaskFailed, statusDotClassName, taskAttentionReason, TaskDate } from "./task-shared";
|
||||
import { TaskVideoThumbnail } from "./task-video-thumbnail";
|
||||
|
||||
export function TaskGridCard({ task, actingId, onOpen, onRetry }: { task: GenerationTask; actingId: string; onOpen: () => void; onRetry: () => void }) {
|
||||
const isActive = task.status === "queued" || task.status === "running";
|
||||
const isFailed = isTaskFailed(task);
|
||||
const retryDisabled = task.errorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(task.error);
|
||||
const isVideo = task.previewKind === "video";
|
||||
const thumbnailUrl = isVideo ? task.previewPosterUrl : task.previewUrl;
|
||||
const fallbackVideo = task.type.includes("video");
|
||||
const Icon = fallbackVideo ? Video : task.type.includes("image") ? ImageIcon : FileText;
|
||||
return (
|
||||
<article className={`task-grid-card${isFailed ? " is-attention" : ""}`}>
|
||||
<article className={`product-collection-card task-grid-card${isFailed ? " is-attention" : ""}`}>
|
||||
<div className="task-grid-thumb">
|
||||
{thumbnailUrl ? (
|
||||
<MediaPreview src={thumbnailUrl} kind="image" loading="lazy" className="h-full w-full object-cover" />
|
||||
@@ -32,14 +33,14 @@ export function TaskGridCard({ task, actingId, onOpen, onRetry }: { task: Genera
|
||||
<IconButton size="sm" variant="ghost" icon={Eye} aria-label="查看详情" onClick={onOpen} />
|
||||
</Tooltip>
|
||||
{isFailed ? (
|
||||
<Tooltip title="重试任务">
|
||||
<Tooltip title={retryDisabled ? "内容审核失败,无法自动重试" : "重试任务"}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<RotateCcw className="size-3.5" />}
|
||||
aria-label="重试任务"
|
||||
loading={actingId === task.id}
|
||||
disabled={task.errorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(task.error)}
|
||||
disabled={retryDisabled}
|
||||
onClick={onRetry}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -59,6 +60,14 @@ export function TaskGridCard({ task, actingId, onOpen, onRetry }: { task: Genera
|
||||
<TaskDate value={task.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
{isActive ? (
|
||||
<div className="task-grid-progress" role="progressbar" aria-label={task.stage || "任务生成进度"} aria-valuemin={0} aria-valuemax={100} aria-valuenow={task.progress || 0}>
|
||||
<span>{task.stage || "正在生成"}</span>
|
||||
<strong>{task.progress || 0}%</strong>
|
||||
<i><b style={{ width: `${task.progress || 0}%` }} /></i>
|
||||
</div>
|
||||
) : null}
|
||||
{isFailed ? <p className="task-grid-error" title={task.error || undefined}>{taskAttentionReason(task)}</p> : null}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -36,8 +36,9 @@ export function TaskListRow({
|
||||
const context = getTaskCanvasContext(task, canvasById, projectNameById);
|
||||
const isActive = task.status === "queued" || task.status === "running";
|
||||
const isFailed = isTaskFailed(task);
|
||||
const retryDisabled = task.errorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(task.error);
|
||||
return (
|
||||
<article className={`task-record-row group${isFailed ? " is-attention" : ""}`}>
|
||||
<article className={`product-collection-card task-record-row group${isFailed ? " is-attention" : ""}`}>
|
||||
<TaskPreviewThumbnail task={task} onOpen={onPreview} />
|
||||
<div className="task-record-main">
|
||||
<div className="task-record-heading">
|
||||
@@ -60,7 +61,7 @@ export function TaskListRow({
|
||||
</span>
|
||||
</div>
|
||||
{isActive ? (
|
||||
<div className="task-record-progress">
|
||||
<div className="task-record-progress" role="progressbar" aria-label={task.stage || "任务生成进度"} aria-valuemin={0} aria-valuemax={100} aria-valuenow={task.progress || 0}>
|
||||
<span>{task.stage || "正在生成"}</span>
|
||||
<span>{task.progress || 0}%</span>
|
||||
<i>
|
||||
@@ -83,14 +84,14 @@ export function TaskListRow({
|
||||
<IconButton size="sm" variant="ghost" icon={Eye} aria-label="查看详情" onClick={onOpen} />
|
||||
</Tooltip>
|
||||
{isFailed ? (
|
||||
<Tooltip title="重试任务">
|
||||
<Tooltip title={retryDisabled ? "内容审核失败,无法自动重试" : "重试任务"}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<RotateCcw className="size-3.5" />}
|
||||
aria-label="重试任务"
|
||||
loading={actingId === task.id}
|
||||
disabled={task.errorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(task.error)}
|
||||
disabled={retryDisabled}
|
||||
onClick={onRetry}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
Reference in New Issue
Block a user