refactor(platform): 用户工作区 - 收紧素材卡片与分页布局并统一设置页表面

This commit is contained in:
ddCat
2026-08-04 21:23:16 +08:00
parent e84fbb8a76
commit a03eb9ed0a
6 changed files with 45 additions and 52 deletions

View File

@@ -7,7 +7,7 @@ import { cn } from "@/lib/utils";
export function WorkspacePage({ children, className, grid = false, fluid = false }: { children: ReactNode; className?: string; grid?: boolean; fluid?: boolean }) {
return (
<main className={cn("app-user-content thin-scrollbar h-full overflow-y-auto text-foreground", grid && "app-workspace-grid", className)}>
<main className={cn("app-user-content app-workspace-scroll h-full overflow-y-auto text-foreground", grid && "app-workspace-grid", className)}>
<div className={fluid ? "h-full w-full" : "w-full px-3 py-3 sm:px-4 sm:py-4 xl:px-5"}>{children}</div>
</main>
);
@@ -54,8 +54,9 @@ export function CollectionGrid({ children, className }: { children: ReactNode; c
export function PaginationBar({ current, pageSize, total, onChange, pageSizeOptions = [20, 50, 100] }: { current: number; pageSize: number; total: number; onChange: (page: number, pageSize: number) => void; pageSizeOptions?: number[] }) {
if (total <= pageSize && current === 1) return null;
return (
<div className="app-pagination-bar sticky bottom-0 z-10 flex min-w-0 justify-end border-t border-border bg-background/95 px-4 py-3 backdrop-blur">
<div className="app-pagination-bar mt-4 flex min-h-10 min-w-0 items-center justify-end border-t border-border/70 px-2 py-1.5">
<Pagination
size="small"
current={current}
pageSize={pageSize}
total={total}

View File

@@ -287,7 +287,7 @@ export default function AssetsPage() {
<Button size="small" danger icon={<Trash2 className="size-3.5" />} onClick={() => setBatchDeleteOpen(true)}>删除</Button>
</div>
) : null}
<CollectionGrid className="sm:grid-cols-[repeat(auto-fill,minmax(250px,1fr))] xl:grid-cols-[repeat(auto-fill,minmax(270px,1fr))]">
<CollectionGrid>
{visibleAssets.map((asset) => (
<AssetCard key={asset.id} asset={asset} selected={selectedIds.includes(asset.id)} onSelect={(selected) => setSelectedIds((current) => selected ? [...new Set([...current, asset.id])] : current.filter((id) => id !== asset.id))} onOpen={() => setPreviewAsset(asset)} onEdit={() => openEdit(asset)} onCopy={copyAssetText} onDownload={downloadImage} onDelete={() => setDeletingAsset(asset)} />
))}
@@ -427,43 +427,12 @@ export default function AssetsPage() {
function AssetCard({ asset, selected, onSelect, onOpen, onEdit, onCopy, onDownload, onDelete }: { asset: LibraryAsset; selected: boolean; onSelect: (selected: boolean) => void; onOpen: () => void; onEdit: () => void; onCopy: (asset: LibraryAsset) => void; onDownload: (asset: LibraryAsset) => void; onDelete: () => void }) {
const summary = assetSummary(asset);
return (
<article className={`app-collection-card group h-full overflow-hidden ${selected ? "border-foreground/45" : ""}`}>
<div className="relative">
<button type="button" className="block w-full text-left" onClick={onOpen}>
<AssetMediaPreview asset={asset} alt={asset.title} className="aspect-[16/10] w-full bg-black object-cover" fallback={<div className="flex aspect-[16/10] items-center justify-center bg-stone-100 p-4 text-center text-xs leading-5 text-stone-600 dark:bg-stone-900 dark:text-stone-300">{asset.kind === "text" ? asset.data.content : "暂无封面"}</div>} />
<article className={`group overflow-hidden rounded-[var(--r-md)] border bg-background/55 transition-[border-color,background-color,box-shadow] duration-[var(--motion-dur-fast-calc)] ${selected ? "border-[var(--workspace-accent)] bg-[var(--workspace-accent-soft)] shadow-[0_0_0_var(--stroke-2)_var(--workspace-accent-soft)]" : "border-border/80 hover:border-foreground/25 hover:bg-background/75 focus-within:border-foreground/25"}`}>
<div className="relative overflow-hidden bg-foreground/[.045]">
<button type="button" className="block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--workspace-accent)]" onClick={onOpen} aria-label={`查看素材:${asset.title}`}>
<AssetMediaPreview asset={asset} alt={asset.title} className="aspect-[4/3] w-full bg-black object-cover" fallback={<div className="flex aspect-[4/3] items-center justify-center bg-foreground/[.035] p-4 text-center text-xs leading-5 text-foreground/55">{asset.kind === "text" ? asset.data.content : "暂无封面"}</div>} />
</button>
<input type="checkbox" checked={selected} onClick={(event) => event.stopPropagation()} onChange={(event) => onSelect(event.target.checked)} className={`absolute left-2 top-2 size-4 accent-[var(--workspace-accent)] ${selected ? "opacity-100" : "opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"}`} aria-label={`选择 ${asset.title}`} />
</div>
<button type="button" className="block w-full text-left" onClick={onOpen}>
<div className="p-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h2 className="line-clamp-1 text-sm font-semibold text-stone-950 dark:text-stone-100">{asset.title}</h2>
<Typography.Text type="secondary" className="mt-1 block text-xs">
{asset.source || "未标注来源"}
</Typography.Text>
</div>
<span className="flex shrink-0 flex-wrap justify-end gap-1">
<span className="rounded bg-foreground/[.055] px-1.5 py-0.5 text-[var(--fs-tiny)] text-foreground/55">{assetKindLabel(asset.kind)}</span>
<span className="rounded bg-foreground/[.055] px-1.5 py-0.5 text-[var(--fs-tiny)] text-foreground/55">{assetCategoryLabel(asset.category)}</span>
</span>
</div>
<Typography.Paragraph type="secondary" ellipsis={{ rows: 2 }} className="!mb-0 !mt-2 !text-xs !leading-5">
{summary}
</Typography.Paragraph>
<div className="mt-2 flex items-center justify-between gap-2 text-[var(--fs-tiny)] text-foreground/42"><span className="truncate">{assetProjectLabel(asset)}</span><span className="shrink-0">{formatAssetTime(asset.updatedAt)}</span></div>
<div className="mt-3 flex flex-wrap gap-1.5">
{(asset.tags || []).slice(0, 3).map((tag) => (
<span key={tag} className="rounded bg-foreground/[.055] px-1.5 py-0.5 text-[var(--fs-tiny)] text-foreground/55">{tag}</span>
))}
{!asset.tags?.length ? <span className="text-[var(--fs-tiny)] text-foreground/35">无标签</span> : null}
</div>
</div>
</button>
<div className="flex items-center justify-between gap-2 border-t border-border/70 px-3 py-2.5">
<Button size="small" onClick={onOpen}>
查看
</Button>
<input type="checkbox" checked={selected} onClick={(event) => event.stopPropagation()} onChange={(event) => onSelect(event.target.checked)} className={`absolute left-2 top-2 size-4 accent-[var(--workspace-accent)] drop-shadow-md transition-opacity ${selected ? "opacity-100" : "opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"}`} aria-label={`选择 ${asset.title}`} />
<Dropdown
trigger={["click"]}
menu={{ items: [
@@ -474,9 +443,27 @@ function AssetCard({ asset, selected, onSelect, onOpen, onEdit, onCopy, onDownlo
{ key: "delete", danger: true, icon: <Trash2 className="size-3.5" />, label: "删除", onClick: onDelete },
] }}
>
<Button size="small" aria-label="更多素材操作" icon={<MoreHorizontal className="size-4" />} />
<button type="button" className="absolute right-2 top-2 grid size-8 place-items-center rounded-[var(--r-sm)] border border-white/20 bg-black/55 text-white/85 opacity-100 shadow-sm backdrop-blur transition-[opacity,background-color] hover:bg-black/75 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100" aria-label="更多素材操作" title="更多操作">
<MoreHorizontal className="size-4" />
</button>
</Dropdown>
<div className="pointer-events-none absolute inset-x-2 bottom-2 flex items-center gap-1">
<span className="rounded-[var(--r-xs)] bg-black/60 px-1.5 py-0.5 text-[var(--fs-tiny)] font-medium text-white/90 backdrop-blur">{assetKindLabel(asset.kind)}</span>
<span className="rounded-[var(--r-xs)] bg-black/60 px-1.5 py-0.5 text-[var(--fs-tiny)] text-white/75 backdrop-blur">{assetCategoryLabel(asset.category)}</span>
</div>
</div>
<button type="button" className="block w-full px-2.5 py-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--workspace-accent)]" onClick={onOpen}>
<div className="flex min-w-0 items-center justify-between gap-2">
<h2 className="truncate text-[var(--fs-body)] font-semibold text-foreground" title={asset.title}>{asset.title}</h2>
<span className="shrink-0 text-[var(--fs-tiny)] tabular-nums text-foreground/38">{formatAssetTime(asset.updatedAt)}</span>
</div>
<div className="mt-1 truncate text-[var(--fs-label)] text-foreground/52" title={summary}>{summary}</div>
<div className="mt-1 flex min-w-0 items-center gap-1.5 text-[var(--fs-tiny)] text-foreground/38">
<span className="truncate">{asset.source || "未标注来源"}</span>
<span aria-hidden="true">·</span>
<span className="truncate">{assetProjectLabel(asset)}</span>
</div>
</button>
</article>
);
}

View File

@@ -58,7 +58,7 @@ export default function IndexPage() {
const loadingUserWorkspace = !userHydrated || (Boolean(user && shortDramaEnabled) && domainProjectsQuery.isLoading);
return (
<main className="app-user-content app-workspace-canvas h-full overflow-y-auto text-foreground">
<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">
{loadingUserWorkspace ? (
<WorkspaceLoadingState className="mt-3 max-w-[980px]" label="正在恢复工作台" detail="读取项目、章节和最近画布" rows={5} />

View File

@@ -253,7 +253,7 @@ export default function SettingsPage() {
};
return (
<main className="flex h-full min-h-0 flex-col bg-background text-foreground">
<main className="flex h-full min-h-0 flex-col bg-[var(--workspace-canvas)] text-foreground">
<header className="flex min-h-16 shrink-0 items-center justify-between gap-4 border-b border-border/70 px-4 py-3 sm:px-5">
<div className="flex min-w-0 items-center gap-3">
{shouldPromptContinue ? (
@@ -268,7 +268,7 @@ export default function SettingsPage() {
{shouldPromptContinue ? <Button type="primary" onClick={finishConfig}>保存并返回创作</Button> : null}
</header>
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
<aside className="w-full shrink-0 border-b border-border/70 bg-muted/[.12] p-2 md:w-[224px] md:border-b-0 md:border-r md:p-3">
<aside className="w-full shrink-0 border-b border-border/70 bg-transparent p-2 md:w-[224px] md:border-b-0 md:border-r md:p-3">
<nav className="thin-scrollbar flex gap-1 overflow-x-auto md:block md:space-y-1" aria-label="配置分类">
{configSections.map((item) => {
const selected = item.key === activeTab;
@@ -288,8 +288,8 @@ export default function SettingsPage() {
</nav>
</aside>
<section className="flex min-h-0 min-w-0 flex-1 flex-col bg-background">
<div className="thin-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-4 md:px-6 md:py-5">
<section className="flex min-h-0 min-w-0 flex-1 flex-col bg-transparent">
<div className="app-workspace-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-4 md:px-6 md:py-5">
<div className={activeTab === "prompts" ? "h-full w-full" : "mx-auto w-full max-w-[1180px]"}>
{([
{

View File

@@ -118,7 +118,7 @@ export default function WalletPage() {
];
return (
<main className="app-user-content thin-scrollbar relative h-full overflow-y-auto text-foreground">
<main className="app-user-content app-workspace-scroll relative h-full overflow-y-auto text-foreground">
<div className="relative mx-auto max-w-[1440px] px-4 py-6 sm:px-6 lg:px-8">
<motion.header initial={reducedMotion ? false : { opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="app-page-header flex flex-wrap items-start justify-between gap-4 pb-6">
<div className="flex items-center gap-3">

View File

@@ -840,8 +840,13 @@
background: color-mix(in srgb, var(--workspace-canvas) 92%, transparent);
}
.app-spatial-workspace .app-user-content {
scrollbar-gutter: stable;
/* 工作区保留滚轮与触控板滚动,只隐藏会形成额外竖向分割的浏览器轨道。 */
.app-spatial-workspace .app-workspace-scroll {
scrollbar-width: none;
}
.app-spatial-workspace .app-workspace-scroll::-webkit-scrollbar {
display: none;
}
.app-spatial-workspace .app-page-header {
@@ -889,7 +894,7 @@
.app-spatial-workspace .app-pagination-bar {
border-color: var(--workspace-border);
background: color-mix(in srgb, var(--workspace-surface-strong) 92%, transparent);
background: transparent;
}
.app-spatial-workspace .app-home-header {
@@ -3093,10 +3098,10 @@
.canvas-library-page .app-pagination-bar {
position: static;
margin-top: 28px;
margin-top: var(--space-4);
border-color: var(--workspace-border);
background: transparent;
padding: 16px 0 8px;
padding: var(--space-1-half) var(--space-2);
backdrop-filter: none;
}
@@ -3287,7 +3292,7 @@
min-width: 0;
flex-wrap: wrap;
justify-content: flex-end;
gap: 4px 0;
gap: var(--space-1) 0;
}
:where(.ant-modal-content, .ant-drawer-content) {