feat(canvas): 前端重构与渠道兼容 - 重构工作区、任务中心等多页面,修复智能体对话与节点生成
Co-authored-by: sikongyue <3155157671@qq.com> Original-PR: https://github.com/ddcat-ai/open-ai-canvas/pull/217
This commit is contained in:
1 parent
fb2a54b345
commit
cfdf08d05f
37 files changed
+4599
-912
No files matched your search
+64
-131
@@ -1,13 +1,13 @@
|
||||
import { useMemo, type CSSProperties, type ReactNode } from "react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { App, Button } from "antd";
|
||||
import { ArrowRight, BookOpenText, Bot, CheckCircle2, CircleAlert, Clapperboard, Clock3, FolderKanban, Images, LayoutGrid, ListChecks, Plus, Sparkles } from "lucide-react";
|
||||
import { ArrowRight, Bot, Clapperboard, FolderKanban, Images, LayoutGrid, ListChecks, Plus, Sparkles } from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
|
||||
import { CanvasProjectCard } from "@/components/canvas/canvas-project-card";
|
||||
import { WorkspaceErrorState, WorkspaceLoadingState } from "@/components/layout/workspace-state";
|
||||
import { WorkspaceSignalIcon } from "@/components/ui/aceternity/workspace-signal-icon";
|
||||
import { projectAttentionCount, projectContinueTarget, projectDetailStage, projectNextActions, projectSummaryCompletion } from "@/lib/project-workbench";
|
||||
import { projectDetailStage, projectSummaryCompletion } from "@/lib/project-workbench";
|
||||
import { getProject, listProjects, type ProjectSummary } from "@/services/api/projects";
|
||||
import { createCanvasProjectWithRemoteSync } from "@/services/user-data-sync";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
@@ -59,17 +59,15 @@ export default function IndexPage() {
|
||||
const loadingUserWorkspace = !userHydrated || (Boolean(user && shortDramaEnabled) && domainProjectsQuery.isLoading);
|
||||
return (
|
||||
<main className="app-user-content app-workspace-canvas app-workspace-scroll h-full overflow-y-auto text-foreground">
|
||||
<div className="app-home-workbench mx-auto w-full max-w-[1440px] px-4 pb-12 pt-5 sm:px-6 lg:px-8">
|
||||
<div className="app-home-workbench w-full px-4 pb-12 pt-5 sm:px-6 lg:px-8">
|
||||
{loadingUserWorkspace ? (
|
||||
<WorkspaceLoadingState className="mt-3 max-w-[980px]" label="正在恢复工作台" detail="读取项目、章节和最近画布" rows={5} />
|
||||
<WorkspaceLoadingState className="mt-3" label="正在恢复工作台" detail="读取项目、章节和最近画布" rows={5} />
|
||||
) : user && shortDramaEnabled && domainProjectsQuery.isError ? (
|
||||
<WorkspaceErrorState title="项目工作台加载失败" description={domainProjectsQuery.error instanceof Error ? domainProjectsQuery.error.message : "暂时无法读取项目列表。"} onRetry={() => void domainProjectsQuery.refetch()} />
|
||||
) : shortDramaEnabled && activeProject ? (
|
||||
<ReturningWorkspace
|
||||
summary={activeProject}
|
||||
detail={activeProjectQuery.data}
|
||||
detailLoading={activeProjectQuery.isLoading}
|
||||
detailError={activeProjectQuery.isError}
|
||||
recentProjects={domainProjects.slice(0, 5)}
|
||||
recentIndependentCanvases={recentIndependentCanvases}
|
||||
onCreateIndependentCanvas={createIndependentCanvas}
|
||||
@@ -88,91 +86,45 @@ export default function IndexPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function ReturningWorkspace({ summary, detail, detailLoading, detailError, recentProjects, recentIndependentCanvases, onCreateIndependentCanvas }: {
|
||||
function ReturningWorkspace({ summary, detail, recentProjects, recentIndependentCanvases, onCreateIndependentCanvas }: {
|
||||
summary: ProjectSummary;
|
||||
detail?: Awaited<ReturnType<typeof getProject>>;
|
||||
detailLoading: boolean;
|
||||
detailError: boolean;
|
||||
recentProjects: ProjectSummary[];
|
||||
recentIndependentCanvases: ReturnType<typeof useCanvasStore.getState>["projects"];
|
||||
onCreateIndependentCanvas: () => void;
|
||||
}) {
|
||||
const taskCenterEnabled = useUserStore((state) => state.features.taskCenterEnabled);
|
||||
const stage = detail ? projectDetailStage(detail) : { label: "进行中", detail: "读取项目进度" };
|
||||
const continueTarget = detail ? projectContinueTarget(detail) : { href: `/projects/${summary.project.id}/overview`, title: summary.project.name, context: "打开项目概览", updatedAt: summary.project.updatedAt };
|
||||
const nextActions = detail ? projectNextActions(detail, 4).filter((action) => taskCenterEnabled || !action.href.startsWith("/tasks")).slice(0, 3) : [];
|
||||
const completion = projectSummaryCompletion(summary);
|
||||
const attentionCount = detail ? projectAttentionCount(detail) : 0;
|
||||
return (
|
||||
<>
|
||||
<header className="app-home-header flex flex-col gap-4 border-b border-border/80 pb-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<WorkspaceSignalIcon variant="home" />
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-[var(--fs-title)] font-semibold leading-7">继续创作</h1>
|
||||
<p className="mt-1 text-xs leading-5 text-foreground/55">回到最近工作,或先处理阻塞制作的事项。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button icon={<LayoutGrid className="size-3.5" />} onClick={onCreateIndependentCanvas}>打开画布</Button>
|
||||
<Link className="inline-flex h-9 items-center gap-2 rounded-md bg-foreground px-3.5 text-sm font-medium text-background transition-opacity hover:opacity-85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/25" to="/projects?create=1"><Plus className="size-3.5" />创建项目</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="app-home-metrics grid grid-cols-2 gap-3 py-5 xl:grid-cols-4" aria-label="项目状态概览">
|
||||
<WorkspaceMetric icon={<BookOpenText />} label="剧情章节" value={summary.unitCount} detail={`${summary.completedUnitCount} 章已完成`} />
|
||||
<WorkspaceMetric icon={<LayoutGrid />} label="项目画布" value={summary.canvasCount} detail="已关联制作空间" />
|
||||
<WorkspaceMetric icon={<Images />} label="项目资产" value={summary.assetCount} detail="角色、场景与媒体" />
|
||||
<WorkspaceMetric icon={<CircleAlert />} label="需要处理" value={attentionCount} detail={attentionCount ? "建议优先处理" : "当前流程顺畅"} attention={attentionCount > 0} />
|
||||
</section>
|
||||
|
||||
<section className="app-home-focus-panel grid overflow-hidden rounded-lg border border-border/80 lg:grid-cols-[minmax(260px,.8fr)_minmax(320px,1fr)_minmax(280px,.72fr)]">
|
||||
<div className="min-w-0 p-5 sm:p-6 lg:p-7">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-foreground/50">
|
||||
<span className="font-medium text-[var(--workspace-accent)]">{stage.label}</span>
|
||||
<span aria-hidden>·</span>
|
||||
<span>{stage.detail}</span>
|
||||
</div>
|
||||
<h2 className="mt-3 truncate text-2xl font-semibold sm:text-3xl">{summary.project.name}</h2>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-xs text-foreground/52">
|
||||
<span>{summary.completedUnitCount}/{summary.unitCount} 章已完成</span>
|
||||
<span>{summary.canvasCount} 张项目画布</span>
|
||||
<span>{summary.assetCount} 项资产</span>
|
||||
<span>更新于 {formatRelativeTime(summary.project.updatedAt)}</span>
|
||||
</div>
|
||||
<div className="mt-4 h-1.5 w-full max-w-[620px] overflow-hidden rounded-full bg-foreground/[.08]" aria-label={`章节完成度 ${completion}%`}>
|
||||
<div className="h-full rounded-full bg-[var(--workspace-accent)] transition-[width]" style={{ width: `${completion}%` }} />
|
||||
</div>
|
||||
<Link to={continueTarget.href} className="mt-6 inline-flex min-h-10 max-w-full items-center gap-3 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-opacity hover:opacity-85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/25">
|
||||
<span className="min-w-0 truncate">{continueTarget.context} · {continueTarget.title}</span>
|
||||
<ArrowRight className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<SpatialChapterStack detail={detail} loading={detailLoading} projectId={summary.project.id} />
|
||||
|
||||
<div className="border-t border-border/75 p-5 sm:p-6 lg:border-l lg:border-t-0 lg:p-7">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold">下一步</h2>
|
||||
<Link to={`/projects/${summary.project.id}/overview`} className="text-xs text-foreground/48 hover:text-foreground">查看项目</Link>
|
||||
</div>
|
||||
{detailLoading ? <div className="mt-4 text-xs text-foreground/45">正在整理项目待办...</div> : null}
|
||||
{detailError ? <div className="mt-4 text-xs leading-5 text-foreground/48">项目详情暂时无法读取。可先打开项目概览或自由画布继续工作。</div> : null}
|
||||
{!detailLoading && nextActions.length ? (
|
||||
<div className="mt-3 divide-y divide-border/70">
|
||||
{nextActions.map((action) => <WorkbenchActionLink key={action.id} action={action} />)}
|
||||
<div className="studio-band">
|
||||
<header className="app-page-header flex min-h-14 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-[var(--fs-title)] font-semibold leading-7">继续创作</h1>
|
||||
<p className="mt-1 text-xs leading-5 text-foreground/55">回到最近工作,或先处理阻塞制作的事项。</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button icon={<LayoutGrid className="size-3.5" />} onClick={onCreateIndependentCanvas}>打开画布</Button>
|
||||
<Link className="inline-flex h-9 items-center gap-2 rounded-md bg-foreground px-3.5 text-sm font-medium text-background transition-opacity hover:opacity-85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/25" to="/projects?create=1"><Plus className="size-3.5" />创建项目</Link>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<section className="app-home-quick-create grid gap-3 py-6 sm:grid-cols-3" aria-label="快捷创建">
|
||||
<QuickCreateCard icon={<FolderKanban className="size-5" />} title="创建短剧项目" description="从空白、小说或文本开始建立章节流程。" action="创建" href="/projects?create=1" />
|
||||
<QuickCreateCard icon={<LayoutGrid className="size-5" />} title="打开自由画布" description="适合快速试图、提示词实验和自由创作。" action="打开" onClick={onCreateIndependentCanvas} />
|
||||
<QuickCreateCard icon={<Images className="size-5" />} title="进入素材库" description="整理角色、场景、画风和媒体资产。" action="进入" href="/assets" />
|
||||
</section>
|
||||
|
||||
<section className="grid gap-8 py-7 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,.8fr)]">
|
||||
<section className="grid gap-8 py-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,.8fr)]">
|
||||
<div className="min-w-0">
|
||||
<div className="mb-3 flex items-center justify-between gap-4">
|
||||
<div><h2 className="text-base font-semibold">最近项目</h2><p className="mt-1 text-xs text-foreground/45">按最近更新时间排列</p></div>
|
||||
<Link to="/projects" className="inline-flex items-center gap-1.5 text-xs text-foreground/50 hover:text-foreground">查看全部<ArrowRight className="size-3.5" /></Link>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-lg border border-border/80 bg-background/65">
|
||||
<div className="app-home-timeline overflow-hidden rounded-lg border border-border/80 bg-background/65">
|
||||
{recentProjects.map((project, index) => <RecentProjectRow key={project.project.id} summary={project} divided={index > 0} />)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,6 +145,25 @@ function ReturningWorkspace({ summary, detail, detailLoading, detailError, recen
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="app-home-template-rail border-t border-border/80 py-7" aria-label="工作流入口">
|
||||
<div className="mb-4 flex items-end justify-between gap-4">
|
||||
<div><h2 className="text-base font-semibold">从工作流开始</h2><p className="mt-1 text-xs leading-5 text-foreground/48">整理故事 → 确认设定 → 制作镜头 → 检查结果。</p></div>
|
||||
<span className="hidden text-[var(--fs-label)] text-foreground/38 sm:block">{stage.label} · {completion}%</span>
|
||||
</div>
|
||||
<div className="app-workflow-grid grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{workflow.map((item, index) => (
|
||||
<div key={item.title} className="app-workflow-card relative min-w-0 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="grid size-6 place-items-center rounded-md bg-foreground/[.06] text-[var(--fs-label)] font-semibold tabular-nums text-[var(--workspace-accent)]">0{index + 1}</span>
|
||||
{index < workflow.length - 1 ? <ArrowRight className="size-3.5 text-foreground/25" aria-hidden="true" /> : null}
|
||||
</div>
|
||||
<h3 className="mt-2 text-sm font-semibold">{item.title}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-foreground/48">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -219,10 +190,13 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen
|
||||
|
||||
<section className="border-b border-border/80 py-7">
|
||||
<div className="mb-5"><h2 className="text-lg font-semibold">从故事到结果</h2><p className="mt-1 text-xs leading-5 text-foreground/48">每一步都保留输入、版本和生成记录,可以随时返回调整。</p></div>
|
||||
<div className="app-workflow-rail grid border-t border-border/75 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="app-workflow-grid grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{workflow.map((item, index) => (
|
||||
<div key={item.title} className={`app-workflow-step min-w-0 border-b border-border/75 py-4 sm:px-4 xl:border-b-0 xl:border-r ${index % 2 === 0 ? "sm:pl-0" : "sm:border-l"} ${index === workflow.length - 1 ? "xl:border-r-0" : ""}`}>
|
||||
<span className="text-[var(--fs-label)] font-semibold tabular-nums text-[var(--workspace-accent)]">0{index + 1}</span>
|
||||
<div key={item.title} className="app-workflow-card relative min-w-0 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="grid size-6 place-items-center rounded-md bg-foreground/[.06] text-[var(--fs-label)] font-semibold tabular-nums text-[var(--workspace-accent)]">0{index + 1}</span>
|
||||
{index < workflow.length - 1 ? <ArrowRight className="size-3.5 text-foreground/25" aria-hidden="true" /> : null}
|
||||
</div>
|
||||
<h3 className="mt-2 text-sm font-semibold">{item.title}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-foreground/48">{item.description}</p>
|
||||
</div>
|
||||
@@ -251,74 +225,33 @@ function FirstProjectWorkspace({ authenticated, canvasHydrated, recentIndependen
|
||||
{recentIndependentCanvases.length ? (
|
||||
<section className="border-t border-border/80 pt-6">
|
||||
<div className="mb-4 flex items-center justify-between"><h2 className="text-base font-semibold">继续自由画布</h2><Link to="/canvas" className="text-xs text-foreground/50 hover:text-foreground">查看全部</Link></div>
|
||||
<div className="grid max-w-[940px] gap-4 sm:grid-cols-2 lg:grid-cols-3">{recentIndependentCanvases.map((project) => <CanvasProjectCard key={project.id} project={project} variant="recent" />)}</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{recentIndependentCanvases.map((project) => <CanvasProjectCard key={project.id} project={project} variant="recent" />)}</div>
|
||||
</section>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceMetric({ icon, label, value, detail, attention = false }: { icon: ReactNode; label: string; value: number; detail: string; attention?: boolean }) {
|
||||
return (
|
||||
<article className="app-home-metric min-w-0 rounded-lg border p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className={attention ? "text-foreground/75" : "text-[var(--workspace-accent)]"}>{icon}</span>
|
||||
<span className={`size-1.5 rounded-full ${attention ? "bg-foreground/75" : "bg-[var(--workspace-accent)]"}`} />
|
||||
</div>
|
||||
<div className="mt-5 text-[var(--fs-label)] text-foreground/45">{label}</div>
|
||||
<div className="mt-1 text-2xl font-semibold tabular-nums">{value}</div>
|
||||
<div className="mt-1 truncate text-[var(--fs-tiny)] text-foreground/38">{detail}</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function SpatialChapterStack({ detail, loading, projectId }: { detail?: Awaited<ReturnType<typeof getProject>>; loading: boolean; projectId: string }) {
|
||||
const units = detail?.units.slice().sort((left, right) => left.position - right.position).slice(0, 6) || [];
|
||||
const activeUnitId = units.find((unit) => unit.status !== "completed")?.id || units.at(-1)?.id;
|
||||
return (
|
||||
<div className="spatial-chapter-panel min-w-0 border-t border-border/75 p-5 sm:p-6 lg:border-l lg:border-t-0 lg:p-7">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div><div className="text-[var(--fs-tiny)] font-medium text-foreground/38">制作层级</div><h2 className="mt-1 text-sm font-semibold">章节轨道</h2></div>
|
||||
<span className="text-[var(--fs-tiny)] tabular-nums text-foreground/35">{units.length ? `${units.length} 章` : "待建立"}</span>
|
||||
</div>
|
||||
<div className="spatial-chapter-deck mt-4" aria-label="项目章节制作层级">
|
||||
{loading ? Array.from({ length: 4 }, (_, index) => <span key={index} className="spatial-chapter-card is-loading" style={{ "--deck-x": `${index * 12}px`, "--deck-y": `${index * 28}px`, "--deck-z": `${-index * 18}px`, zIndex: 8 - index } as CSSProperties} />) : null}
|
||||
{!loading && units.length ? units.map((unit, index) => (
|
||||
<Link
|
||||
key={unit.id}
|
||||
to={`/projects/${projectId}/chapters/${unit.id}`}
|
||||
className={`spatial-chapter-card ${unit.id === activeUnitId ? "is-active" : ""}`}
|
||||
style={{ "--deck-x": `${index * 12}px`, "--deck-y": `${index * 28}px`, "--deck-z": `${-index * 18}px`, zIndex: 8 - index } as CSSProperties}
|
||||
>
|
||||
<span className="text-[var(--fs-micro)] font-semibold tabular-nums opacity-55">{String(unit.position + 1).padStart(2, "0")}</span>
|
||||
<span className="mt-2 block truncate text-xs font-semibold">{unit.title}</span>
|
||||
<span className="mt-1 block text-[var(--fs-micro)] opacity-50">{unit.status === "completed" ? "已完成" : unit.id === activeUnitId ? "当前制作" : "等待推进"}</span>
|
||||
</Link>
|
||||
)) : null}
|
||||
{!loading && !units.length ? <div className="grid min-h-52 place-items-center text-center text-xs leading-5 text-foreground/42">创建剧情章节后<br />这里会形成制作层级</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkbenchActionLink({ action }: { action: ReturnType<typeof projectNextActions>[number] }) {
|
||||
const Icon = action.tone === "danger" ? CircleAlert : action.tone === "attention" ? Clock3 : CheckCircle2;
|
||||
return (
|
||||
<Link to={action.href} className="group grid grid-cols-[20px_minmax(0,1fr)_auto] gap-2 py-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20">
|
||||
<Icon className={`mt-0.5 size-4 ${action.tone === "danger" ? "text-foreground/80" : action.tone === "attention" ? "text-foreground/60" : "text-foreground/35"}`} />
|
||||
<span className="min-w-0"><span className="block text-xs font-medium">{action.title}</span><span className="mt-1 line-clamp-2 block text-[var(--fs-label)] leading-4 text-foreground/45">{action.description}</span></span>
|
||||
<span className="self-center text-[var(--fs-label)] font-medium text-foreground/45 transition-colors group-hover:text-foreground">{action.actionLabel}</span>
|
||||
</Link>
|
||||
function QuickCreateCard({ icon, title, description, action, href, onClick }: { icon: ReactNode; title: string; description: string; action: string; href?: string; onClick?: () => void }) {
|
||||
const content = (
|
||||
<>
|
||||
<span className="grid size-10 shrink-0 place-items-center rounded-md border border-border/70 bg-foreground/[.04] text-[var(--workspace-accent)]">{icon}</span>
|
||||
<span className="mt-4 block text-sm font-semibold">{title}</span>
|
||||
<span className="mt-1 block text-xs leading-5 text-foreground/48">{description}</span>
|
||||
<span className="mt-4 inline-flex items-center gap-1 text-xs font-medium text-foreground/55 transition-colors group-hover:text-foreground">{action}<ArrowRight className="size-3.5" /></span>
|
||||
</>
|
||||
);
|
||||
const className = "app-home-quick-create-card group flex min-h-[148px] flex-col items-start rounded-lg border border-border/80 bg-card p-4 text-left transition hover:-translate-y-0.5 hover:border-foreground/20 hover:shadow-[var(--card-elevation-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
||||
return href ? <Link to={href} className={className}>{content}</Link> : <button type="button" className={className} onClick={onClick}>{content}</button>;
|
||||
}
|
||||
|
||||
function RecentProjectRow({ summary, divided }: { summary: ProjectSummary; divided: boolean }) {
|
||||
const completion = projectSummaryCompletion(summary);
|
||||
return (
|
||||
<Link to={`/projects/${summary.project.id}/overview`} className={`group grid min-h-[68px] grid-cols-[minmax(0,1fr)_80px_20px] items-center gap-3 px-3 py-2.5 hover:bg-foreground/[.025] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-foreground/20 sm:grid-cols-[minmax(0,1fr)_100px_120px_20px] ${divided ? "border-t border-border/65" : ""}`}>
|
||||
<Link to={`/projects/${summary.project.id}/overview`} className={`group grid min-h-[68px] grid-cols-[minmax(0,1fr)_80px_20px] items-center gap-3 px-3 py-2.5 hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-foreground/20 sm:grid-cols-[minmax(0,1fr)_100px_120px_20px] ${divided ? "border-t border-border/65" : ""}`}>
|
||||
<span className="min-w-0"><span className="block truncate text-sm font-medium">{summary.project.name}</span><span className="mt-1 block truncate text-[var(--fs-label)] text-foreground/42">{summary.unitCount} 章 · {summary.canvasCount} 张项目画布 · {summary.assetCount} 项资产</span></span>
|
||||
<span className="hidden text-[var(--fs-label)] text-foreground/45 sm:block">更新于<br />{formatRelativeTime(summary.project.updatedAt)}</span>
|
||||
<span className="min-w-0"><span className="flex items-center justify-between text-[var(--fs-tiny)] text-foreground/42"><span>章节</span><span>{completion}%</span></span><span className="mt-1.5 block h-1 overflow-hidden rounded-full bg-foreground/[.08]"><span className="block h-full rounded-full bg-foreground/65" style={{ width: `${completion}%` }} /></span></span>
|
||||
<span className="min-w-0"><span className="flex items-center justify-between text-[var(--fs-tiny)] text-foreground/42"><span>章节</span><span>{completion}%</span></span><span className="mt-1.5 block h-1 overflow-hidden rounded-full bg-surface-active"><span className="block h-full rounded-full bg-foreground/65" style={{ width: `${completion}%` }} /></span></span>
|
||||
<ArrowRight className="size-4 text-foreground/25 transition-transform group-hover:translate-x-0.5 group-hover:text-foreground/60" />
|
||||
</Link>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user