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