模特上身图提示词重构 + 图片创作页 UI 调整
后端(模特上身图提示词): - build_model_tryon_prompt_refs 重写:穿戴/非穿戴分流(穿戴=真实穿身替换原衣, 非穿戴=手持/佩戴/使用不动原衣)、每张按 index 变化动作/场景/镜头、负面词尾接、 多图参考序号自适应(参考图1~N=商品,参考图N+1=模特) - 新增 _product_reference_urls:商品参考图真实上传图优先、排除 AI 生成图、可多张(≤3), 无真实图回落 cover - worker run_standalone_image_task 模特分支改用多图取图 + 传 index/n_product 前端(图片创作/工作室): - 生成数量改 1/2/4;图片比例新增「手动输入」(宽:高 两输入框) - 临时隐藏「商品库」按钮 - 模特卡:去掉 // 真人模特,标题改图片底部白字遮罩层 + 单行省略 - 工作室壳负边距对齐 .content padding,修复上下被遮挡/裁切 其他:并入此前未提交的商品页改动、脚本 Agent/格式实测文档与 demo
This commit is contained in:
@@ -170,14 +170,20 @@
|
||||
在 .content 内铺满可用高度(shell 由 App.tsx 渲染,这里只占正文)。
|
||||
============================================================ */
|
||||
.image-workbench {
|
||||
/* 抵消 .content 的 48/28/72 padding,让工作室壳贴边铺满(同旧 .tool-shell 思路) */
|
||||
margin: -48px -28px -72px;
|
||||
/* 抵消 .content 的 24/28/60 padding,让工作室壳贴边铺满(同旧 .tool-shell 思路)。
|
||||
必须与 design-restraint.css .content 的 padding 严格一致:多减则顶部钻进 sticky
|
||||
topbar(被遮)、底部溢出视口(被裁)。 */
|
||||
margin: -24px -28px -60px;
|
||||
height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background-base);
|
||||
overflow: hidden;
|
||||
}
|
||||
/* <1100px 时 .content 改 28/24/48,负边距需同步,否则窄屏又会上下被遮/裁 */
|
||||
@media (max-width: 1100px) {
|
||||
.image-workbench { margin: -28px -24px -48px; }
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════
|
||||
通用:返回 pill(图片创作侧栏头 / 模特·平台侧栏头共用)
|
||||
@@ -488,6 +494,18 @@
|
||||
border-color: var(--heat-40);
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 手动输入比例:宽 : 高 两个数字框 */
|
||||
.image-workbench .iw-ratio-manual {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.image-workbench .iw-ratio-manual .input {
|
||||
flex: 1; min-width: 0; height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.image-workbench .iw-ratio-manual .sep {
|
||||
color: var(--black-alpha-48); font-size: 13px; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── 模特选择 · 3:4 矩形卡多选(基线 .model-card)── */
|
||||
.image-workbench .model-grid {
|
||||
@@ -522,11 +540,14 @@
|
||||
object-fit: cover; display: block;
|
||||
background: var(--black-alpha-4);
|
||||
}
|
||||
.image-workbench .model-card .m-name { font-size: 13px; font-weight: 500; color: var(--accent-black); }
|
||||
.image-workbench .model-card.selected .m-name { color: var(--heat); }
|
||||
.image-workbench .model-card .m-tag {
|
||||
font-family: var(--font-mono); font-size: 12px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
/* 标题:遮罩层贴图片底部,白字单行省略,节省一行纵向空间 */
|
||||
.image-workbench .model-card .m-name {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
padding: 14px 8px 6px;
|
||||
font-size: 13px; font-weight: 500; color: #fff;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, .62), rgba(0, 0, 0, 0));
|
||||
pointer-events: none; z-index: 1;
|
||||
}
|
||||
.image-workbench .model-card .m-check {
|
||||
position: absolute; top: 14px; right: 14px;
|
||||
|
||||
@@ -432,7 +432,7 @@ const MODE_META: Record<
|
||||
const RATIO_OPTIONS = ["1:1", "3:4", "4:5", "9:16", "16:9"];
|
||||
const COUNT_OPTIONS = ["1", "2", "4"];
|
||||
const MODEL_RATIO_OPTIONS = ["1:1", "3:4", "9:16"];
|
||||
const MODEL_COUNT_OPTIONS = ["4", "8", "12"];
|
||||
const MODEL_COUNT_OPTIONS = ["1", "2", "4"];
|
||||
const COVER_COUNT_OPTIONS = ["4", "8", "12"];
|
||||
|
||||
/* 图片创作 · 空态提示词建议 chip(基线 image-optimize EXAMPLES) */
|
||||
@@ -528,6 +528,10 @@ export function ImageWorkbenchPage({
|
||||
const product = products.find((item) => item.id === productId) || products[0];
|
||||
const [prompt, setPrompt] = useState(meta.promptTemplate(products[0]?.title || "商品"));
|
||||
const [ratio, setRatio] = useState(meta.ratio);
|
||||
// 手动输入比例:开启后用 W:H 两个输入框自定义,关闭则用预设 pill
|
||||
const [ratioManual, setRatioManual] = useState(false);
|
||||
const [ratioW, setRatioW] = useState("");
|
||||
const [ratioH, setRatioH] = useState("");
|
||||
const [style, setStyle] = useState("auto");
|
||||
const [count, setCount] = useState(mode === "image" ? "4" : "4");
|
||||
// 模特单选(长度恒 0/1);平台改回多选(P0③:可选多个平台,各出一组结果)
|
||||
@@ -613,6 +617,9 @@ export function ImageWorkbenchPage({
|
||||
if (product) setPrompt(meta.promptTemplate(product.title));
|
||||
// mode 或商品切换都重置 prompt 与默认比例
|
||||
setRatio(meta.ratio);
|
||||
setRatioManual(false);
|
||||
setRatioW("");
|
||||
setRatioH("");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [productId, mode]);
|
||||
|
||||
@@ -1323,11 +1330,11 @@ export function ImageWorkbenchPage({
|
||||
{product?.title || "未选择 · 请在左侧商品空间选一个"}
|
||||
</span>
|
||||
</div>
|
||||
{/* P0④:商品库全屏选择器入口(多选商品,各起一批) */}
|
||||
<button className="iw-pl-btn" type="button" onClick={() => { setPlDraft(productId ? [productId] : []); setPlQuery(""); setPlCat(""); setPlOpen(true); }} title="从商品库选择">
|
||||
{/* P0④:商品库全屏选择器入口(多选商品,各起一批) — 临时屏蔽 */}
|
||||
{/* <button className="iw-pl-btn" type="button" onClick={() => { setPlDraft(productId ? [productId] : []); setPlQuery(""); setPlCat(""); setPlOpen(true); }} title="从商品库选择">
|
||||
<LayoutGrid size={13} />
|
||||
商品库
|
||||
</button>
|
||||
</button> */}
|
||||
<span className="spacer" />
|
||||
<div className="tb-search-wrap">
|
||||
{searchOpen && (
|
||||
@@ -1402,9 +1409,8 @@ export function ImageWorkbenchPage({
|
||||
<span className="ph-frame">{item.name.slice(0, 4)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="m-name">{item.name}</div>
|
||||
</div>
|
||||
<div className="m-name">{item.name}</div>
|
||||
<div className="m-tag">// 真人模特</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
@@ -1423,9 +1429,8 @@ export function ImageWorkbenchPage({
|
||||
</span>
|
||||
<div className="placeholder m-thumb">
|
||||
<span className="ph-frame">{item.name}</span>
|
||||
<div className="m-name">{item.name}</div>
|
||||
</div>
|
||||
<div className="m-name">{item.name}</div>
|
||||
<div className="m-tag">{item.tag}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1481,13 +1486,52 @@ export function ImageWorkbenchPage({
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
className={`opt ${ratio === value ? "active" : ""}`}
|
||||
onClick={() => setRatio(value)}
|
||||
className={`opt ${!ratioManual && ratio === value ? "active" : ""}`}
|
||||
onClick={() => { setRatioManual(false); setRatio(value); }}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`opt ${ratioManual ? "active" : ""}`}
|
||||
onClick={() => {
|
||||
setRatioManual(true);
|
||||
if (ratioW && ratioH) setRatio(`${ratioW}:${ratioH}`);
|
||||
}}
|
||||
>
|
||||
手动输入
|
||||
</button>
|
||||
</div>
|
||||
{ratioManual && (
|
||||
<div className="iw-ratio-manual">
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="宽"
|
||||
value={ratioW}
|
||||
onChange={(event) => {
|
||||
const w = event.target.value;
|
||||
setRatioW(w);
|
||||
if (w && ratioH) setRatio(`${w}:${ratioH}`);
|
||||
}}
|
||||
/>
|
||||
<span className="sep">:</span>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="高"
|
||||
value={ratioH}
|
||||
onChange={(event) => {
|
||||
const h = event.target.value;
|
||||
setRatioH(h);
|
||||
if (ratioW && h) setRatio(`${ratioW}:${h}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="iw-sub">
|
||||
|
||||
@@ -10,6 +10,8 @@ import { SkeletonGrid } from "../components/loading";
|
||||
import { api } from "../api";
|
||||
|
||||
const PROD_PAGE_SIZE = 10;
|
||||
// 商品详情「AI 素材」grid 每页条数(4 列 → 3 整行)
|
||||
const MAT_PAGE_SIZE = 12;
|
||||
import type { Asset, Product, ProductMaterials, Project } from "../types";
|
||||
import type { NavigateFn, Page } from "./route-config";
|
||||
import "../product-create-page.css";
|
||||
@@ -621,13 +623,8 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
const [triAdoptedId, setTriAdoptedId] = useState<string>("");
|
||||
const triViewing = triVersions.find((v) => v.id === triViewingId) || triVersions[0];
|
||||
const triUrl = triViewing?.url || "";
|
||||
// 素材 tab 筛选 / 排序 / 分页 / 视图
|
||||
const [openFilter, setOpenFilter] = useState<"" | "type" | "status" | "sort">("");
|
||||
const [typeFilter, setTypeFilter] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<"" | "pass" | "fail" | "pending">("");
|
||||
const [assetView, setAssetView] = useState<"grid" | "list">("grid");
|
||||
const [assetSortDesc, setAssetSortDesc] = useState(true);
|
||||
const [assetLimit, setAssetLimit] = useState(12);
|
||||
// AI 素材 grid 当前页(与全站列表一致:整体翻页)
|
||||
const [matPage, setMatPage] = useState(1);
|
||||
const [videoSortDesc, setVideoSortDesc] = useState(true);
|
||||
// 素材审核状态为只读(直接反映 review_status),不再有用户点击覆盖
|
||||
// 采用三视图后,本地 prepend 的新素材卡(供「采用版本联动 + prepend 新卡」即时呈现,不等服务端回灌)
|
||||
@@ -665,19 +662,11 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
if (!alive) return;
|
||||
setProductAssets(assetsRes?.results ?? []);
|
||||
setMaterials(matRes);
|
||||
setMatPage(1); // 切商品 / 刷新素材后回到第 1 页
|
||||
}).finally(() => { if (alive) setAssetsLoading(false); });
|
||||
return () => { alive = false; };
|
||||
}, [product?.id, assetReload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!openFilter) return;
|
||||
const close = (event: MouseEvent) => {
|
||||
if (!(event.target as HTMLElement).closest(".chip-wrap")) setOpenFilter("");
|
||||
};
|
||||
document.addEventListener("click", close);
|
||||
return () => document.removeEventListener("click", close);
|
||||
}, [openFilter]);
|
||||
|
||||
async function onPickProductImage(event: ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
@@ -757,29 +746,27 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
// 采用三视图后本地 prepend 的新卡:置顶且与服务端列表去重(服务端回灌同 id 后即不再重复)
|
||||
const serverIds = new Set(serverImageAssets.map((a) => a.id));
|
||||
const allImageAssets = [...adoptedAssets.filter((a) => !serverIds.has(a.id)), ...serverImageAssets];
|
||||
// 联动采用状态前的「全部素材基线」(供 adoptTriView 找出该商品所有三视图)
|
||||
const allAssetsBase = allImageAssets;
|
||||
// 单个素材当前显示状态:本地覆盖(点击循环 / 采用联动)优先,否则回退真实 review_status 映射
|
||||
const statusOf = (asset: Asset): PdAssetStatus => pdBaseStatus(asset);
|
||||
// 类型筛选选项(当前素材里真实存在的 category)
|
||||
const typeOptions = Array.from(new Set(allImageAssets.map((a) => a.category).filter(Boolean)));
|
||||
// 全集计数(给状态筛选下拉做计数前缀,不随当前筛选变化)
|
||||
const filteredAssets = allImageAssets
|
||||
.filter((asset) => !typeFilter || asset.category === typeFilter)
|
||||
.filter((asset) => !statusFilter || statusOf(asset) === statusFilter)
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const cmp = (b.created_at || "").localeCompare(a.created_at || "");
|
||||
return assetSortDesc ? cmp : -cmp;
|
||||
});
|
||||
const assetCount = filteredAssets.length;
|
||||
const imageAssets = filteredAssets.slice(0, assetLimit);
|
||||
// 结构化素材总数(角色 + 商品图 + 图片成品 + 项目包内素材),用于头部计数 / 空态判断
|
||||
const matTotal = materials
|
||||
? materials.roles.length + materials.product_images.length + materials.image_products.length + materials.project_packs.reduce((s, p) => s + p.items.length, 0)
|
||||
: 0;
|
||||
const hasActiveAssetFilter = Boolean(typeFilter || statusFilter);
|
||||
const resetAssetFilters = () => { setTypeFilter(""); setStatusFilter(""); setAssetLimit(12); setOpenFilter(""); };
|
||||
// 拍平 4 类素材为单序列(角色 → 商品图 → 图片成品 → 视频项目包),整体翻页(与全站列表统一 Pager 一致)
|
||||
type MatItem =
|
||||
| { kind: "role"; role: ProductMaterials["roles"][number] }
|
||||
| { kind: "product_image"; asset: Asset }
|
||||
| { kind: "image_product"; asset: Asset }
|
||||
| { kind: "pack"; pack: ProductMaterials["project_packs"][number] };
|
||||
const matItems: MatItem[] = materials
|
||||
? [
|
||||
...materials.roles.map((role) => ({ kind: "role", role } as MatItem)),
|
||||
...materials.product_images.map((asset) => ({ kind: "product_image", asset } as MatItem)),
|
||||
...materials.image_products.map((asset) => ({ kind: "image_product", asset } as MatItem)),
|
||||
...materials.project_packs.map((pack) => ({ kind: "pack", pack } as MatItem)),
|
||||
]
|
||||
: [];
|
||||
const matTotalPages = Math.max(1, Math.ceil(matItems.length / MAT_PAGE_SIZE));
|
||||
const matCurPage = Math.min(matPage, matTotalPages);
|
||||
const matPageItems = matItems.slice((matCurPage - 1) * MAT_PAGE_SIZE, matCurPage * MAT_PAGE_SIZE);
|
||||
// 视频项目 · 用传入的该商品 projects 渲染真实项目名 / 状态 / 阶段(按更新时间排序)
|
||||
const videoProjects = [...projects].sort((a, b) => {
|
||||
const cmp = (b.updated_at || "").localeCompare(a.updated_at || "");
|
||||
@@ -1075,68 +1062,62 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
<button className="btn btn-primary" type="button" onClick={() => navigate("imageOptimize", { productId: product.id })}>去生成素材</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="asset-grid pd-mat-grid">
|
||||
{/* 角色:逐个(形象图+三视图绑一起);左上标签、左下「N 个项目引用」;点开看这套 */}
|
||||
{materials!.roles.map((role) => {
|
||||
const cover = pdAssetPreview(role.portrait);
|
||||
return (
|
||||
<div className="asset-card pd-role-card" key={`role-${role.id}`} role="button" tabIndex={0} title={role.name}
|
||||
onClick={() => setOpenDetail({ title: role.name, sub: role.ref_count > 0 ? `角色 · ${role.ref_count} 个项目引用` : "角色 · 暂未引用", assets: [role.portrait, role.triview].filter(Boolean) as Asset[] })}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpenDetail({ title: role.name, sub: role.ref_count > 0 ? `角色 · ${role.ref_count} 个项目引用` : "角色 · 暂未引用", assets: [role.portrait, role.triview].filter(Boolean) as Asset[] }); } }}>
|
||||
<div className="thumb placeholder">
|
||||
{cover ? <img src={cover} alt={role.name} loading="lazy" /> : <span className="ph-frame">角色</span>}
|
||||
<span className="type-pill">角色</span>
|
||||
{role.triview && <span className="pd-role-tri mono">含三视图</span>}
|
||||
{matPageItems.map((item) => {
|
||||
// 角色:逐个(形象图+三视图绑一起);左上标签、左下「N 个项目引用」;点开看这套
|
||||
if (item.kind === "role") {
|
||||
const role = item.role;
|
||||
const cover = pdAssetPreview(role.portrait);
|
||||
return (
|
||||
<div className="asset-card pd-role-card" key={`role-${role.id}`} role="button" tabIndex={0} title={role.name}
|
||||
onClick={() => setOpenDetail({ title: role.name, sub: role.ref_count > 0 ? `角色 · ${role.ref_count} 个项目引用` : "角色 · 暂未引用", assets: [role.portrait, role.triview].filter(Boolean) as Asset[] })}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpenDetail({ title: role.name, sub: role.ref_count > 0 ? `角色 · ${role.ref_count} 个项目引用` : "角色 · 暂未引用", assets: [role.portrait, role.triview].filter(Boolean) as Asset[] }); } }}>
|
||||
<div className="thumb placeholder">
|
||||
{cover ? <img src={cover} alt={role.name} loading="lazy" /> : <span className="ph-frame">角色</span>}
|
||||
<span className="type-pill">角色</span>
|
||||
{role.triview && <span className="pd-role-tri mono">含三视图</span>}
|
||||
</div>
|
||||
<div className="meta pd-role-foot"><span className="mono">{role.ref_count > 0 ? `${role.ref_count} 个项目引用` : "// 暂未引用"}</span></div>
|
||||
</div>
|
||||
<div className="meta pd-role-foot"><span className="mono">{role.ref_count > 0 ? `${role.ref_count} 个项目引用` : "// 暂未引用"}</span></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 商品图:平铺 */}
|
||||
{materials!.product_images.map((a) => {
|
||||
);
|
||||
}
|
||||
// 视频项目包:场景+分镜+视频素材按项目打包,点开看整组
|
||||
if (item.kind === "pack") {
|
||||
const pack = item.pack;
|
||||
const first = pack.items[0];
|
||||
const cover = pdAssetPreview(first);
|
||||
const isVid = first?.asset_type === "video";
|
||||
return (
|
||||
<div className="asset-card pd-pack-card" key={`pack-${pack.project_id || pack.project_name}`} role="button" tabIndex={0} title={pack.project_name}
|
||||
onClick={() => setOpenDetail({ title: pack.project_name, sub: `视频项目 · ${pack.items.length} 个素材`, assets: pack.items })}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpenDetail({ title: pack.project_name, sub: `视频项目 · ${pack.items.length} 个素材`, assets: pack.items }); } }}>
|
||||
<div className="thumb placeholder">
|
||||
{isVid && cover ? <video src={cover} muted playsInline preload="metadata" /> : cover ? <img src={cover} alt={pack.project_name} loading="lazy" /> : <span className="ph-frame">视频项目</span>}
|
||||
<span className="type-pill">视频项目</span>
|
||||
<span className="pd-group-count mono">{pack.items.length}</span>
|
||||
</div>
|
||||
<div className="meta"><span className="date">{pack.project_name}</span></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 商品图 / 图片成品(模特上身图/平台套图/自由创作):平铺,各带类型标签
|
||||
const a = item.asset;
|
||||
const u = pdAssetPreview(a);
|
||||
const label = item.kind === "product_image" ? "商品图" : pdAssetTypeLabel(a);
|
||||
return (
|
||||
<div className="asset-card" key={a.id}>
|
||||
<div className="thumb placeholder" role={u ? "button" : undefined} style={u ? { cursor: "zoom-in" } : undefined} onClick={u ? () => setPreview({ src: u, name: a.name }) : undefined}>
|
||||
{u ? <img src={u} alt={a.name} loading="lazy" /> : <span className="ph-frame">商品图</span>}
|
||||
<span className="type-pill">商品图</span>
|
||||
{u ? <img src={u} alt={a.name} loading="lazy" /> : <span className="ph-frame">{label}</span>}
|
||||
<span className="type-pill">{label}</span>
|
||||
</div>
|
||||
<div className="meta"><span className="date">{(a.created_at || "").slice(0, 10)}</span></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 图片成品(模特上身图/平台套图/自由创作):平铺,各带类型标签 */}
|
||||
{materials!.image_products.map((a) => {
|
||||
const u = pdAssetPreview(a);
|
||||
return (
|
||||
<div className="asset-card" key={a.id}>
|
||||
<div className="thumb placeholder" role={u ? "button" : undefined} style={u ? { cursor: "zoom-in" } : undefined} onClick={u ? () => setPreview({ src: u, name: a.name }) : undefined}>
|
||||
{u ? <img src={u} alt={a.name} loading="lazy" /> : <span className="ph-frame">{pdAssetTypeLabel(a)}</span>}
|
||||
<span className="type-pill">{pdAssetTypeLabel(a)}</span>
|
||||
</div>
|
||||
<div className="meta"><span className="date">{(a.created_at || "").slice(0, 10)}</span></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* 视频项目包:场景+分镜+视频素材按项目打包,点开看整组 */}
|
||||
{materials!.project_packs.map((pack) => {
|
||||
const first = pack.items[0];
|
||||
const cover = pdAssetPreview(first);
|
||||
const isVid = first?.asset_type === "video";
|
||||
return (
|
||||
<div className="asset-card pd-pack-card" key={`pack-${pack.project_id || pack.project_name}`} role="button" tabIndex={0} title={pack.project_name}
|
||||
onClick={() => setOpenDetail({ title: pack.project_name, sub: `视频项目 · ${pack.items.length} 个素材`, assets: pack.items })}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpenDetail({ title: pack.project_name, sub: `视频项目 · ${pack.items.length} 个素材`, assets: pack.items }); } }}>
|
||||
<div className="thumb placeholder">
|
||||
{isVid && cover ? <video src={cover} muted playsInline preload="metadata" /> : cover ? <img src={cover} alt={pack.project_name} loading="lazy" /> : <span className="ph-frame">视频项目</span>}
|
||||
<span className="type-pill">视频项目</span>
|
||||
<span className="pd-group-count mono">{pack.items.length}</span>
|
||||
</div>
|
||||
<div className="meta"><span className="date">{pack.project_name}</span></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Pager page={matCurPage} total={matItems.length} pageSize={MAT_PAGE_SIZE} onChange={setMatPage} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user