feat(web): 用户工作台 - 产品化壳层、内容库布局与积分入口

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-16 22:59:45 +08:00
parent f52702b70f
commit 969e2831b2
46 changed files with 5355 additions and 944 deletions

View File

@@ -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 ? (

View File

@@ -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>
);

View File

@@ -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>