feat(ui): 自研 Celadon 组件族入库与 AntD 消费全量替换 - base(Kbd/StatusBadge/IconButton/ToolButton)+product(EmptyState/Callout/SegmentedControl/Checkbox/Select/Tooltip/Switch), composio 视觉基底, globals.css 覆盖清理 (ADR-0008)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Alert, App, Tooltip } from "antd";
|
||||
import { Alert, App } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { ArrowLeft, BookOpenText, Clapperboard, Images, LayoutDashboard, LayoutGrid, Plus, Scissors, Settings2, type LucideIcon } from "lucide-react";
|
||||
import { Link, Navigate, useNavigate, useParams } from "react-router";
|
||||
|
||||
@@ -173,7 +174,7 @@ function ProjectWorkspaceTopBar({ detail, projectId, activeView, unitId, stage,
|
||||
const active = item.key === activeView;
|
||||
const href = item.key === "chapters" ? chapterHref : item.key === "workflow" ? workflowHref : `/projects/${projectId}/${item.key}`;
|
||||
return (
|
||||
<Tooltip key={item.key} title={item.label} mouseEnterDelay={0.15}>
|
||||
<Tooltip key={item.key} title={item.label} delay={150}>
|
||||
<Link
|
||||
to={href}
|
||||
aria-label={item.label}
|
||||
@@ -190,7 +191,7 @@ function ProjectWorkspaceTopBar({ detail, projectId, activeView, unitId, stage,
|
||||
{activeView === "workflow" ? (
|
||||
<WorkflowChapterNavigator projectId={projectId} units={detail.units} unitId={unitId} stage={stage} />
|
||||
) : (
|
||||
<Tooltip title={createCanvasLabel} mouseEnterDelay={0.15}><button type="button" onClick={onCreateCanvas} className="project-workspace-create" aria-label={createCanvasLabel}><Plus /></button></Tooltip>
|
||||
<Tooltip title={createCanvasLabel} delay={150}><button type="button" onClick={onCreateCanvas} className="grid size-8 shrink-0 place-items-center rounded-md text-foreground/42 transition-colors hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label={createCanvasLabel}><Plus className="size-4" /></button></Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { App, Button, Popconfirm, Select } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { App, Button, Popconfirm, Select, Tooltip } from "antd";
|
||||
|
||||
import { Link2, Unlink, X } from "lucide-react";
|
||||
|
||||
import { CanvasProjectCard } from "@/components/canvas/canvas-project-card";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { App, Button, Dropdown, Form, Input, InputNumber, Modal, Popconfirm } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useDeferredValue, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent, type ReactNode } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Alert, App, Button, Dropdown, Form, Input, InputNumber, Modal, Popconfirm, Tooltip } from "antd";
|
||||
|
||||
import { Callout } from "@/components/ui/product/callout";
|
||||
import CharacterCount from "@tiptap/extension-character-count";
|
||||
import Color from "@tiptap/extension-color";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
@@ -630,7 +633,7 @@ export default function ProjectChaptersView({ detail, refreshProject }: ProjectD
|
||||
<div className="mt-1 truncate text-sm font-medium text-foreground/85">{selectedUnit?.title}</div>
|
||||
<div className="mt-1 text-[var(--fs-tiny)] text-foreground/38">正文将作为分镜依据,生成结果会直接写入“分镜制作”。</div>
|
||||
</div>
|
||||
{storyboardImpact.shotCount ? <Alert type="warning" showIcon message={`本章已有 ${storyboardImpact.shotCount} 个分镜`} description="继续后会先生成新分镜;生成成功后,再按确认内容整体替换旧镜头及其关联数据。" /> : null}
|
||||
{storyboardImpact.shotCount ? <Callout tone="warning" title={`本章已有 ${storyboardImpact.shotCount} 个分镜`}>继续后会先生成新分镜;生成成功后,再按确认内容整体替换旧镜头及其关联数据。</Callout> : null}
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-xs font-medium text-foreground/68">文本模型</span>
|
||||
<ModelPicker config={effectiveConfig} capability="text" value={selectedTextModel} onChange={setSelectedTextModel} fullWidth placeholder="选择用于生成分镜的文本模型" showSelectedPrice={false} onMissingConfig={() => navigateToSettings({ continueCreation: true })} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { App, Button, Input, Modal, Select } from "antd";
|
||||
import { App, Button, Input, Modal } from "antd";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
import { Archive, Check, Eye, FolderOpen, Image as ImageIcon, Palette, Pencil, Save, ShieldAlert, Trash2 } from "lucide-react";
|
||||
|
||||
import { AssetLibraryPickerModal, type AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tag } from "antd";
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { ProjectDetail, ProjectUnit } from "@/services/api/projects";
|
||||
@@ -73,8 +73,8 @@ export function sourceTypeLabel(value: string) {
|
||||
}
|
||||
|
||||
export function StatusPill({ status }: { status: string }) {
|
||||
const color = status === "completed" || status === "confirmed" || status === "succeeded" ? "success" : status === "failed" ? "error" : status === "running" || status === "active" ? "processing" : status === "review" || status === "pending_confirmation" ? "warning" : "default";
|
||||
return <Tag color={color} className="m-0 !rounded-full !px-2 !text-[var(--fs-label)]">{statusLabel(status)}</Tag>;
|
||||
const tone = status === "completed" || status === "confirmed" || status === "succeeded" ? "success" : status === "failed" ? "error" : status === "running" || status === "active" ? "loading" : status === "review" || status === "pending_confirmation" ? "warning" : "neutral";
|
||||
return <StatusBadge tone={tone} label={statusLabel(status)} className="m-0" />;
|
||||
}
|
||||
|
||||
export function SectionTitle({ eyebrow, title, description, action }: { eyebrow?: string; title: string; description?: string; action?: ReactNode }) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { App, Button, Empty, Form, Image, Input, InputNumber, Modal, Segmented, Select, Tag } from "antd";
|
||||
import { App, Button, Form, Image, Input, InputNumber, Modal, Select } from "antd";
|
||||
import { SegmentedControl } from "@/components/ui/base/segmented-control";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
import { Box, ChevronDown, ChevronLeft, ChevronRight, Download, Film, Image as ImageIcon, Layers3, List, Maximize2, Play, Plus, RefreshCcw, Save, Search, SlidersHorizontal, Trash2, UsersRound, WandSparkles, X } from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
|
||||
@@ -388,7 +391,7 @@ export default function WorkflowProductionWorkbench(props: Props) {
|
||||
const selectedShotSubmitting = submittingShotIds.has(selectedShot?.id || "");
|
||||
|
||||
if (!selectedShot) {
|
||||
return <div className="workflow-empty-shot"><Empty description="当前章节还没有分镜"><Button type="primary" icon={<Plus className="size-4" />} loading={addingShot} onClick={onAddShot}>新增第一个分镜</Button></Empty></div>;
|
||||
return <div className="workflow-empty-shot"><EmptyState size="compact" title="当前章节还没有分镜" action={<Button type="primary" icon={<Plus className="size-4" />} loading={addingShot} onClick={onAddShot}>新增第一个分镜</Button>} /></div>;
|
||||
}
|
||||
|
||||
const requestShotSelection = (nextShotId: string) => {
|
||||
@@ -444,9 +447,9 @@ export default function WorkflowProductionWorkbench(props: Props) {
|
||||
<div className="workflow-production-shell">
|
||||
<div className="workflow-production-main">
|
||||
<aside className="workflow-library-panel">
|
||||
<Segmented
|
||||
<SegmentedControl
|
||||
block
|
||||
size="small"
|
||||
size="sm"
|
||||
value={leftTab}
|
||||
onChange={(value) => setLeftTab(value as typeof leftTab)}
|
||||
options={[{ value: "episodes", label: "章节" }, { value: "shots", label: "镜头" }, { value: "assets", label: "资产" }]}
|
||||
@@ -463,7 +466,7 @@ export default function WorkflowProductionWorkbench(props: Props) {
|
||||
<div className="workflow-shot-heading">
|
||||
<span className="workflow-shot-number">{formatShotOrdinal(shotIndex)}</span>
|
||||
{customShotTitle(watchedTitle || selectedShot.title, shotIndex) ? <h2>{customShotTitle(watchedTitle || selectedShot.title, shotIndex)}</h2> : null}
|
||||
<Tag className="!m-0" color={saveShot.isPending ? "blue" : editorDirty ? "orange" : revision ? "green" : undefined}>{saveShot.isPending ? "保存中" : editorDirty ? "有未保存修改" : revision ? "已保存" : "草稿"}</Tag>
|
||||
<StatusBadge tone={saveShot.isPending ? "loading" : editorDirty ? "warning" : revision ? "success" : "neutral"} label={saveShot.isPending ? "保存中" : editorDirty ? "有未保存修改" : revision ? "已保存" : "草稿"} className="m-0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1"><span className="mr-1 text-[var(--fs-micro)] text-foreground/45">{shotIndex + 1} / {shots.length}</span><Button type="text" size="small" icon={<ChevronLeft className="size-4" />} disabled={shotIndex <= 0} onClick={() => selectRelativeShot(-1)} aria-label="上一个镜头" /><Button type="text" size="small" icon={<ChevronRight className="size-4" />} disabled={shotIndex >= shots.length - 1} onClick={() => selectRelativeShot(1)} aria-label="下一个镜头" /></div>
|
||||
</header>
|
||||
@@ -557,11 +560,11 @@ export default function WorkflowProductionWorkbench(props: Props) {
|
||||
<header className="workflow-preview-header">
|
||||
<div className="workflow-preview-header-row">
|
||||
<div className="workflow-preview-title"><Film className="size-4 shrink-0" /><span>产物预览</span></div>
|
||||
<Segmented size="small" value={previewTab} onChange={(value) => setPreviewTab(value as typeof previewTab)} options={[{ value: "latest", label: "最新" }, { value: "history", label: `历史 ${artifacts.length}` }]} />
|
||||
<SegmentedControl size="sm" value={previewTab} onChange={(value) => setPreviewTab(value as typeof previewTab)} options={[{ value: "latest", label: "最新" }, { value: "history", label: `历史 ${artifacts.length}` }]} />
|
||||
</div>
|
||||
<Segmented
|
||||
<SegmentedControl
|
||||
block
|
||||
size="small"
|
||||
size="sm"
|
||||
className="workflow-preview-stage-switch"
|
||||
value={activeStage}
|
||||
options={[{ value: "storyboard", label: "分镜图" }, { value: "previz", label: "动作预演" }, { value: "video", label: "镜头视频" }]}
|
||||
@@ -631,7 +634,7 @@ function AssetLibrary({ detail, referenceByVersionId, changing, onToggle }: { de
|
||||
}, [assetsPage]);
|
||||
const total = assetsQuery.data?.total || 0;
|
||||
const pages = Math.max(1, Math.ceil(total / pageSize));
|
||||
return <div className="workflow-asset-groups"><Input allowClear size="small" className="mb-2 w-full" value={keyword} onChange={(event) => setKeyword(event.target.value)} prefix={<Search className="size-3.5 text-foreground/35" />} placeholder="搜索资产名称" aria-label="搜索镜头资产" /><Select size="small" className="mb-2 w-full" value={category} options={[{ value: "all", label: `全部资产(${Object.values(assetsQuery.data?.categoryCounts || {}).reduce((sum, count) => sum + count, 0)})` }, ...Object.entries(assetsQuery.data?.categoryCounts || {}).filter(([, count]) => count > 0).map(([value, count]) => ({ value, label: `${assetCategoryLabel(value)}(${count})` }))]} onChange={(value) => { setCategory(value); setPage(1); }} />{assetsQuery.isLoading ? <div className="py-6 text-center text-xs text-foreground/45">正在读取资产…</div> : groups.length ? groups.map(([groupCategory, assets]) => <section key={groupCategory}><h3>{assetCategoryLabel(groupCategory)} <span>({assets.length})</span></h3><div className="workflow-asset-list">{assets.map((asset) => { const reference = asset.primaryVersionId ? referenceByVersionId.get(asset.primaryVersionId) : undefined; const active = Boolean(reference); const previewUrl = assetPreviewUrl(asset); return <button key={asset.id} type="button" className={`workflow-asset-row ${active ? "is-active" : ""}`} disabled={changing || !asset.primaryVersionId} aria-pressed={active} onClick={() => onToggle(asset, reference)}><span className="workflow-asset-thumb">{previewUrl ? <img src={previewUrl} alt="" loading="lazy" /> : asset.category === "character" ? <UsersRound /> : asset.mediaType === "image" ? <ImageIcon /> : <Box />}</span><span className="min-w-0 flex-1"><strong>{asset.title}</strong><small>{active ? "已绑定 · 点击取消" : `${assetCategoryLabel(asset.category)} · v${Math.max(1, asset.versionCount)}`}</small></span>{active ? <span className="workflow-bound-dot" /> : null}</button>; })}</div></section>) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={debouncedKeyword ? "没有找到匹配资产" : "项目还没有资产"} />}{total > pageSize ? <div className="mt-3 flex items-center justify-between border-t border-border/60 pt-2 text-[var(--fs-micro)] text-foreground/45"><span>{page}/{pages} · 共 {total} 项</span><span className="flex gap-1"><Button type="text" size="small" icon={<ChevronLeft className="size-3.5" />} disabled={page <= 1} onClick={() => setPage((value) => Math.max(1, value - 1))} /><Button type="text" size="small" icon={<ChevronRight className="size-3.5" />} disabled={page >= pages} onClick={() => setPage((value) => Math.min(pages, value + 1))} /></span></div> : null}</div>;
|
||||
return <div className="workflow-asset-groups"><Input allowClear size="small" className="mb-2 w-full" value={keyword} onChange={(event) => setKeyword(event.target.value)} prefix={<Search className="size-3.5 text-foreground/35" />} placeholder="搜索资产名称" aria-label="搜索镜头资产" /><Select size="small" className="mb-2 w-full" value={category} options={[{ value: "all", label: `全部资产(${Object.values(assetsQuery.data?.categoryCounts || {}).reduce((sum, count) => sum + count, 0)})` }, ...Object.entries(assetsQuery.data?.categoryCounts || {}).filter(([, count]) => count > 0).map(([value, count]) => ({ value, label: `${assetCategoryLabel(value)}(${count})` }))]} onChange={(value) => { setCategory(value); setPage(1); }} />{assetsQuery.isLoading ? <div className="py-6 text-center text-xs text-foreground/45">正在读取资产…</div> : groups.length ? groups.map(([groupCategory, assets]) => <section key={groupCategory}><h3>{assetCategoryLabel(groupCategory)} <span>({assets.length})</span></h3><div className="workflow-asset-list">{assets.map((asset) => { const reference = asset.primaryVersionId ? referenceByVersionId.get(asset.primaryVersionId) : undefined; const active = Boolean(reference); const previewUrl = assetPreviewUrl(asset); return <button key={asset.id} type="button" className={`workflow-asset-row ${active ? "is-active" : ""}`} disabled={changing || !asset.primaryVersionId} aria-pressed={active} onClick={() => onToggle(asset, reference)}><span className="workflow-asset-thumb">{previewUrl ? <img src={previewUrl} alt="" loading="lazy" /> : asset.category === "character" ? <UsersRound /> : asset.mediaType === "image" ? <ImageIcon /> : <Box />}</span><span className="min-w-0 flex-1"><strong>{asset.title}</strong><small>{active ? "已绑定 · 点击取消" : `${assetCategoryLabel(asset.category)} · v${Math.max(1, asset.versionCount)}`}</small></span>{active ? <span className="workflow-bound-dot" /> : null}</button>; })}</div></section>) : <EmptyState size="compact" title={debouncedKeyword ? "没有找到匹配资产" : "项目还没有资产"} />}{total > pageSize ? <div className="mt-3 flex items-center justify-between border-t border-border/60 pt-2 text-[var(--fs-micro)] text-foreground/45"><span>{page}/{pages} · 共 {total} 项</span><span className="flex gap-1"><Button type="text" size="small" icon={<ChevronLeft className="size-3.5" />} disabled={page <= 1} onClick={() => setPage((value) => Math.max(1, value - 1))} /><Button type="text" size="small" icon={<ChevronRight className="size-3.5" />} disabled={page >= pages} onClick={() => setPage((value) => Math.min(pages, value + 1))} /></span></div> : null}</div>;
|
||||
}
|
||||
|
||||
function ShotAssetMentionTextarea({ value = "", onChange = () => undefined, references, variant = "motion" }: { value?: string; onChange?: (value: string) => void; references: ReturnType<typeof buildShotAssetReferenceContext>["mentionReferences"]; variant?: "scene" | "motion" }) {
|
||||
@@ -709,8 +712,8 @@ function VideoArtifactPreview({ src, title }: { src: string; title: string }) {
|
||||
}
|
||||
|
||||
function ArtifactHistory({ artifacts, activeId, onSelect, compact = false }: { artifacts: ShotArtifact[]; activeId?: string; onSelect: (artifact: ShotArtifact) => void; compact?: boolean }) {
|
||||
if (!artifacts.length) return compact ? null : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无历史版本" />;
|
||||
return <section className={`workflow-history ${compact ? "is-compact" : ""}`}><div className="workflow-history-title">历史版本</div>{artifacts.map((artifact) => <button key={artifact.id} type="button" className={artifact.id === activeId ? "is-active" : ""} onClick={() => onSelect(artifact)}>{artifact.resourceId ? artifact.type === "video" ? <span className="workflow-history-placeholder"><Film /></span> : <img src={resourceFileUrl(artifact.resourceId)} alt="" loading="lazy" /> : <span className="workflow-history-placeholder"><Layers3 /></span>}<span className="min-w-0 flex-1"><strong>v{artifact.version}{artifact.selected ? " · 当前" : ""}</strong><small>{new Date(artifact.createdAt).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" })}</small></span><ArtifactStatus artifact={artifact} compact /></button>)}</section>;
|
||||
if (!artifacts.length) return compact ? null : <EmptyState size="compact" title="暂无历史版本" />;
|
||||
return <section className={`workflow-history ${compact ? "is-compact" : ""}`}><div className="workflow-history-title">历史版本</div>{artifacts.map((artifact) => <button key={artifact.id} type="button" className={artifact.id === activeId ? "is-active" : ""} onClick={() => onSelect(artifact)}>{artifact.resourceId ? artifact.type === "video" ? <video src={resourceFileUrl(artifact.resourceId)} muted preload="metadata" /> : <img src={resourceFileUrl(artifact.resourceId)} alt="" loading="lazy" /> : <span className="workflow-history-placeholder"><Layers3 /></span>}<span className="min-w-0 flex-1"><strong>v{artifact.version}{artifact.selected ? " · 当前" : ""}</strong><small>{new Date(artifact.createdAt).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" })}</small></span><ArtifactStatus artifact={artifact} compact /></button>)}</section>;
|
||||
}
|
||||
|
||||
function ShotTimeline({ activeStage, detail, shots, selectedShotId, submittingShotIds, onSelectShot, onAddShot, addingShot }: { activeStage: ShortDramaWorkflowStage; detail: ProjectDetail; shots: ProjectShot[]; selectedShotId: string; submittingShotIds: Set<string>; onSelectShot: (id: string) => void; onAddShot: () => void; addingShot: boolean }) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Tag } from "antd";
|
||||
import { Check, CircleAlert } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
import { assetCategoryLabel as sharedAssetCategoryLabel } from "@/lib/asset-category";
|
||||
import type { ProjectDetail, ProjectShot, ShotArtifact, ShotRevision, WorkflowStep } from "@/services/api/projects";
|
||||
import type { TaskStatus } from "@/services/api/task-center";
|
||||
@@ -40,17 +40,15 @@ export function MetricCard({ icon, label, value }: { icon: ReactNode; label: str
|
||||
return <div className="rounded-xl border border-border/70 bg-surface p-5"><div className="text-foreground/35">{icon}</div><div className="mt-4 text-2xl font-semibold">{value}</div><div className="mt-1 text-xs text-foreground/45">{label}</div></div>;
|
||||
}
|
||||
|
||||
export function ArtifactStatus({ artifact, taskStatus, compact = false }: { artifact?: ShotArtifact; taskStatus?: TaskStatus; compact?: boolean }) {
|
||||
const className = `artifact-status-tag ${compact ? "!m-0" : ""}`;
|
||||
export function ArtifactStatus({ artifact, taskStatus }: { artifact?: ShotArtifact; taskStatus?: TaskStatus; compact?: boolean }) {
|
||||
if (taskStatus === "queued" || taskStatus === "running" || (taskStatus === "succeeded" && !artifact)) {
|
||||
return <Tag className={`${className} is-running`} color="processing">生成中</Tag>;
|
||||
return <StatusBadge variant="filled" tone="loading" label="生成中" />;
|
||||
}
|
||||
if (taskStatus === "failed") return <Tag className={`${className} is-failed`} color="error">生成失败</Tag>;
|
||||
if (!artifact) return <Tag className={`${className} is-pending`}>待生成</Tag>;
|
||||
const color = artifact.status === "ready" ? "success" : artifact.status === "failed" ? "error" : artifact.status === "stale" ? "warning" : "processing";
|
||||
if (taskStatus === "failed") return <StatusBadge variant="filled" tone="error" label="生成失败" />;
|
||||
if (!artifact) return <StatusBadge variant="filled" tone="neutral" label="待生成" />;
|
||||
const tone = artifact.status === "ready" ? "success" : artifact.status === "failed" ? "error" : artifact.status === "stale" ? "warning" : "loading";
|
||||
const label = artifact.status === "ready" ? "已生成" : artifact.status === "failed" ? "生成失败" : artifact.status === "stale" ? "已过期" : "生成中";
|
||||
const tone = artifact.status === "ready" ? "ready" : artifact.status === "failed" ? "failed" : artifact.status === "stale" ? "stale" : "running";
|
||||
return <Tag className={`${className} is-${tone}`} color={color}>{label}</Tag>;
|
||||
return <StatusBadge variant="filled" tone={tone} label={label} />;
|
||||
}
|
||||
|
||||
export function currentRevision(detail: ProjectDetail, shot?: ProjectShot): ShotRevision | undefined {
|
||||
|
||||
@@ -830,12 +830,6 @@
|
||||
.workflow-timeline-meta { display: block; overflow: hidden; margin-top: 4px; color: color-mix(in srgb, var(--foreground) 38%, transparent); font-size: var(--fs-micro); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.workflow-timeline-status { position: absolute; right: 10px; bottom: 8px; left: 10px; display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 6px; }
|
||||
.workflow-timeline-status > span:first-child { min-width: 0; overflow: hidden; color: color-mix(in srgb, var(--foreground) 45%, transparent); font-size: var(--fs-micro); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.artifact-status-tag.ant-tag { display: inline-flex; min-height: 20px; flex: 0 0 auto; align-items: center; border-radius: var(--r-full); padding-inline: 7px; font-size: var(--fs-micro); font-weight: 600; line-height: 18px; white-space: nowrap; }
|
||||
.artifact-status-tag.is-pending.ant-tag { border-color: color-mix(in srgb, var(--foreground) 18%, transparent) !important; background: color-mix(in srgb, var(--foreground) 7%, transparent) !important; color: color-mix(in srgb, var(--foreground) 62%, transparent) !important; }
|
||||
.artifact-status-tag.is-running.ant-tag { border-color: color-mix(in srgb, var(--palette-status-loading) 45%, transparent) !important; background: color-mix(in srgb, var(--palette-status-loading) 13%, transparent) !important; color: color-mix(in srgb, var(--palette-status-loading) 82%, var(--foreground)) !important; }
|
||||
.artifact-status-tag.is-ready.ant-tag { border-color: color-mix(in srgb, var(--palette-status-success) 45%, transparent) !important; background: color-mix(in srgb, var(--palette-status-success) 13%, transparent) !important; color: color-mix(in srgb, var(--palette-status-success) 82%, var(--foreground)) !important; }
|
||||
.artifact-status-tag.is-failed.ant-tag { border-color: color-mix(in srgb, var(--palette-status-error) 45%, transparent) !important; background: color-mix(in srgb, var(--palette-status-error) 13%, transparent) !important; color: color-mix(in srgb, var(--palette-status-error) 82%, var(--foreground)) !important; }
|
||||
.artifact-status-tag.is-stale.ant-tag { border-color: color-mix(in srgb, var(--palette-status-warning) 42%, transparent) !important; background: color-mix(in srgb, var(--palette-status-warning) 12%, transparent) !important; color: color-mix(in srgb, var(--palette-status-warning) 80%, var(--foreground)) !important; }
|
||||
|
||||
.workflow-add-shot-card {
|
||||
display: grid;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { App, Button, Empty } from "antd";
|
||||
import { App, Button } from "antd";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { formatShotOrdinal } from "@/lib/shot-label";
|
||||
@@ -66,7 +67,7 @@ export default function ProjectWorkflowView({ detail, projectId, unitId, stage }
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : "新增分镜失败"),
|
||||
});
|
||||
if (!unit) {
|
||||
return <div className="grid h-full place-items-center"><Empty description="先添加一个章节,再进入分镜制作"><Link to={`/projects/${projectId}/chapters`}><Button type="primary">添加章节</Button></Link></Empty></div>;
|
||||
return <div className="grid h-full place-items-center"><EmptyState title="先添加一个章节,再进入分镜制作" action={<Link to={`/projects/${projectId}/chapters`}><Button type="primary">添加章节</Button></Link>} /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user