refactor(platform): 用户工作台 - 重构画布、任务与资源页面的浏览体验
This commit is contained in:
@@ -83,6 +83,7 @@ export default function AssetsPage() {
|
||||
const selectedAssets = useMemo(() => validAssets.filter((asset) => selectedIds.includes(asset.id)), [selectedIds, validAssets]);
|
||||
const kindCounts = useMemo(() => new Map(kindOptions.map((option) => [option.value, option.value === "all" ? validAssets.length : validAssets.filter((asset) => asset.kind === option.value).length])), [validAssets]);
|
||||
const categoryCounts = useMemo(() => new Map(categoryOptions.map((option) => [option.value, option.value === "all" ? validAssets.length : validAssets.filter((asset) => (asset.category || "other") === option.value).length])), [validAssets]);
|
||||
const canCreateAsset = !keyword.trim() && kindFilter === "all" && categoryFilter === "all";
|
||||
|
||||
const filteredAssets = useMemo(() => {
|
||||
const query = keyword.trim().toLowerCase();
|
||||
@@ -255,7 +256,7 @@ export default function AssetsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkspacePage grid>
|
||||
<WorkspacePage grid className="library-page assets-library-page">
|
||||
<PageHeader
|
||||
icon="assets"
|
||||
title="素材库"
|
||||
@@ -265,11 +266,10 @@ export default function AssetsPage() {
|
||||
<>
|
||||
<Button icon={<Download className="size-4" />} onClick={() => void exportAllAssets()}>导出全部</Button>
|
||||
<Dropdown trigger={["click"]} menu={{ items: [{ key: "package", icon: <FileUp className="size-4" />, label: "导入素材包", onClick: () => assetInputRef.current?.click() }, { key: "model", icon: <Upload className="size-4" />, label: "上传 3D 模型", onClick: () => modelInputRef.current?.click() }] }}><Button icon={<FileUp className="size-4" />}>导入</Button></Dropdown>
|
||||
<Button type="primary" icon={<Plus className="size-4" />} onClick={openCreate}>新增素材</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<ListToolbar active={Boolean(keyword || kindFilter !== "all" || categoryFilter !== "all")} onReset={() => { setKeyword(""); setKindFilter("all"); setCategoryFilter("all"); setPage(1); }}>
|
||||
<ListToolbar className="library-toolbar" active={Boolean(keyword || kindFilter !== "all" || categoryFilter !== "all")} onReset={() => { setKeyword(""); setKindFilter("all"); setCategoryFilter("all"); setPage(1); }}>
|
||||
<Input allowClear className="w-full sm:w-80" prefix={<Search className="size-4 text-foreground/40" />} value={keyword} placeholder="搜索标题、内容、标签或来源" onChange={(event) => { setPage(1); setKeyword(event.target.value); }} />
|
||||
</ListToolbar>
|
||||
|
||||
@@ -287,20 +287,25 @@ export default function AssetsPage() {
|
||||
<Button size="small" danger icon={<Trash2 className="size-3.5" />} onClick={() => setBatchDeleteOpen(true)}>删除</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<CollectionGrid>
|
||||
<CollectionGrid className="library-grid assets-library-grid">
|
||||
{canCreateAsset ? <button type="button" className="library-create-card" onClick={openCreate}>
|
||||
<span className="library-create-cover"><Plus className="size-8" /></span>
|
||||
<span className="library-create-title">新增素材</span>
|
||||
<span className="library-create-meta">文本、图片、音视频或模型</span>
|
||||
</button> : null}
|
||||
{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)} />
|
||||
))}
|
||||
</CollectionGrid>
|
||||
|
||||
{!visibleAssets.length ? <WorkspaceState icon="assets" compact title={validAssets.length ? "没有匹配的素材" : "素材库还是空的"} description={validAssets.length ? "调整关键词或左侧分类后再试。" : "从画布保存结果,或导入已有素材包开始整理。"} action={!validAssets.length ? <Button type="primary" icon={<Plus className="size-4" />} onClick={openCreate}>新增素材</Button> : undefined} /> : null}
|
||||
{!visibleAssets.length && (validAssets.length > 0 || !canCreateAsset) ? <WorkspaceState icon="assets" compact title={validAssets.length ? "没有匹配的素材" : "素材库还是空的"} description={validAssets.length ? "调整关键词或左侧分类后再试。" : "从画布保存结果,或导入已有素材包开始整理。"} action={!validAssets.length ? <Button type="primary" icon={<Plus className="size-4" />} onClick={openCreate}>新增素材</Button> : undefined} /> : null}
|
||||
|
||||
<PaginationBar current={page} pageSize={pageSize} total={filteredAssets.length} pageSizeOptions={[20, 40, 80]} onChange={(nextPage, nextPageSize) => { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} />
|
||||
</section>
|
||||
</div>
|
||||
</WorkspacePage>
|
||||
|
||||
<Modal title={editingAsset ? "编辑素材" : "新增素材"} open={isAssetOpen} width={980} onCancel={() => setIsAssetOpen(false)} onOk={() => void saveAsset()} okText="保存" cancelText="取消" destroyOnHidden>
|
||||
<Modal className="library-modal" title={editingAsset ? "编辑素材" : "新增素材"} open={isAssetOpen} width={980} onCancel={() => setIsAssetOpen(false)} onOk={() => void saveAsset()} okText="保存" cancelText="取消" destroyOnHidden>
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
|
||||
<Form form={form} layout="vertical" requiredMark={false} initialValues={{ kind: "text", category: "other", tags: [] }}>
|
||||
<Form.Item name="kind" label="类型">
|
||||
@@ -414,10 +419,10 @@ export default function AssetsPage() {
|
||||
<input ref={assetInputRef} type="file" accept="application/zip,.zip" className="hidden" onChange={(event) => void importAssetZip(event.target.files?.[0])} />
|
||||
<input ref={modelInputRef} type="file" accept=".glb,.gltf,model/gltf-binary,model/gltf+json" className="hidden" onChange={(event) => { void readModelFile(event.target.files?.[0]); event.currentTarget.value = ""; }} />
|
||||
|
||||
<Modal title="删除素材" open={Boolean(deletingAsset)} onCancel={() => setDeletingAsset(null)} onOk={() => void confirmDelete()} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
<Modal className="library-modal library-confirm-modal" title="删除素材" open={Boolean(deletingAsset)} onCancel={() => setDeletingAsset(null)} onOk={() => void confirmDelete()} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
确定删除「{deletingAsset?.title}」吗?删除后会从我的素材中移除。
|
||||
</Modal>
|
||||
<Modal title="批量删除素材" open={batchDeleteOpen} onCancel={() => setBatchDeleteOpen(false)} onOk={() => void confirmBatchDelete()} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
<Modal className="library-modal library-confirm-modal" title="批量删除素材" open={batchDeleteOpen} onCancel={() => setBatchDeleteOpen(false)} onOk={() => void confirmBatchDelete()} okText="删除" okButtonProps={{ danger: true }} cancelText="取消">
|
||||
确定删除已选择的 {selectedAssets.length} 个素材吗?此操作会同步移除远端记录。
|
||||
</Modal>
|
||||
</>
|
||||
@@ -427,7 +432,7 @@ 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={`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"}`}>
|
||||
<article className={`library-card asset-library-card group ${selected ? "is-selected" : ""}`}>
|
||||
<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>} />
|
||||
@@ -485,7 +490,7 @@ function AssetFilterGroup({ title, options, value, counts, onChange, className =
|
||||
|
||||
function AssetDrawer({ asset, onClose, onCopy, onDownload }: { asset: LibraryAsset | null; onClose: () => void; onCopy: (asset: LibraryAsset) => void; onDownload: (asset: LibraryAsset) => void }) {
|
||||
return (
|
||||
<Drawer title="素材详情" open={Boolean(asset)} size="large" onClose={onClose}>
|
||||
<Drawer className="library-drawer" title="素材详情" open={Boolean(asset)} size="large" onClose={onClose}>
|
||||
{asset ? (
|
||||
<div className="space-y-5">
|
||||
<AssetMediaPreview asset={asset} alt={asset.title} className="max-h-[420px] w-full rounded-lg bg-black object-contain" fallback={<div className="rounded-lg border border-stone-200 bg-stone-50 p-5 text-sm leading-6 text-stone-600 dark:border-stone-800 dark:bg-stone-900 dark:text-stone-300">{asset.kind === "text" ? asset.data.content : "暂无封面"}</div>} />
|
||||
@@ -502,7 +507,7 @@ function AssetDrawer({ asset, onClose, onCopy, onDownload }: { asset: LibraryAss
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
<div className="rounded-lg border border-stone-200 p-4 dark:border-stone-800">
|
||||
<div className="asset-drawer-content rounded-lg border border-stone-200 p-4 dark:border-stone-800">
|
||||
<Typography.Text type="secondary" className="block text-xs">
|
||||
内容
|
||||
</Typography.Text>
|
||||
|
||||
Reference in New Issue
Block a user