AirShelf/电商AI平台/product-detail.html
iye 086d92991e
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
统一 Airshelf 界面组件与图标
2026-05-27 12:29:41 +08:00

2225 lines
91 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>商品详情 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<style>
/* ─── 顶部 标题 + 状态 ─── */
.pd-title {
display: flex; align-items: center; gap: 12px;
margin-bottom: 22px;
}
.pd-title h1 {
font-size: 24px; font-weight: 600;
letter-spacing: -.015em;
color: var(--accent-black);
line-height: 1.25;
}
.pd-title .status {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px;
background: var(--accent-emerald-bg, #e6f4ec);
color: var(--accent-emerald, #1f8a51);
border: 1px solid var(--accent-emerald-bd, #c4e3d1);
border-radius: var(--r-sm);
font-size: 11.5px;
font-weight: 500;
}
/* ─── 商品信息(含图片) + 快速操作(辅助) · 3 : 2 两栏 · 高度对齐 ─── */
.pd-overview {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 16px;
margin-bottom: 24px;
align-items: stretch;
}
.pd-overview .ov-card { height: 100%; box-sizing: border-box; }
.pd-overview .ov-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 20px 22px;
min-width: 0;
position: relative;
}
/* 编辑按钮 · 放在 .ov-h 标题行右侧 (flex item, 不再 absolute) */
.pd-overview .ov-h { align-items: center; }
.ov-edit {
display: inline-flex; align-items: center; gap: 5px;
height: 28px;
padding: 0 12px;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 12px;
font-family: inherit;
cursor: pointer;
white-space: nowrap;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.ov-edit-single { margin-left: auto; }
.ov-edit:hover {
border-color: var(--heat-40);
color: var(--heat);
background: var(--heat-12);
}
.ov-edit svg { width: 12px; height: 12px; }
.ov-edit.primary {
background: var(--heat);
color: var(--accent-white);
border-color: var(--heat);
white-space: nowrap;
}
.ov-edit.primary:hover { filter: brightness(1.05); background: var(--heat); color: var(--accent-white); }
.ov-edit:disabled { cursor: not-allowed; color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); opacity: 1; }
.ov-edit:disabled:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }
.ov-edit:disabled svg { color: var(--heat); }
/* 编辑模式按钮组 (重置 + 取消 + 保存) */
.ov-edit-group {
display: none;
align-items: center;
gap: 6px;
margin-left: auto;
}
.ov-card.editing .ov-edit-single { display: none; }
.ov-card.editing .ov-edit-group { display: inline-flex; }
/* AI 生成三视图 · 按钮 + 弹出 panel(布局复刻 pipeline.html stage 2 三视图预览) */
.ov-tri-wrap { position: relative; margin-left: auto; }
/* 当 AI 入口存在时,编辑信息按钮不再独占 ml-auto,与 AI 按钮紧贴 */
.ov-tri-wrap + .ov-edit-single { margin-left: 0; }
.ov-tri-trigger { white-space: nowrap; }
.ov-tri-trigger.is-open { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.ov-card.editing .ov-tri-wrap { display: none; }
.ov-tri-pop {
position: absolute;
top: calc(100% + 6px); right: 0;
width: 360px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 8px 24px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.06);
padding: 14px 14px 12px;
display: none;
flex-direction: column;
gap: 10px;
z-index: 40;
}
.ov-tri-pop.show { display: flex; }
.ov-tri-pop::before {
content: ''; position: absolute;
top: -5px; right: 36px;
width: 9px; height: 9px;
background: var(--surface);
border-left: 1px solid var(--border-faint);
border-top: 1px solid var(--border-faint);
transform: rotate(45deg);
}
.ov-tri-close {
position: absolute;
top: 8px; right: 8px;
width: 22px; height: 22px;
display: grid; place-items: center;
background: transparent;
border: 1px solid transparent;
border-radius: var(--r-sm);
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
z-index: 2;
}
.ov-tri-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--border-faint); }
.ov-tri-close svg { width: 12px; height: 12px; }
/* 复刻 pipeline.html .prod-preview-* 内部样式 */
.ov-tri-pop .prod-preview-h { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase; padding-right: 28px; }
.ov-tri-pop .prod-preview-img { aspect-ratio: 16/9; }
.ov-tri-pop .prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }
.ov-tri-pop .prod-preview-history { display: none; flex-direction: column; gap: 6px; }
.ov-tri-pop .prod-preview-history.show { display: flex; }
.ov-tri-pop .prod-preview-history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.ov-tri-pop .prod-preview-history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }
.ov-tri-pop .prod-preview-history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; scrollbar-width: thin; }
.ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar { height: 4px; }
.ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
.ov-tri-pop .prod-preview-history .h-thumb {
flex: 0 0 auto;
width: 72px; aspect-ratio: 16/9;
background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);
position: relative; cursor: pointer; transition: border-color var(--t-base);
display: grid; place-items: center; overflow: hidden;
}
.ov-tri-pop .prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }
/* 已采用版本:主橙描边 + 「已采用」徽标 */
.ov-tri-pop .prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
/* 仅预览(未采用):黑色描边,无徽标 */
.ov-tri-pop .prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }
.ov-tri-pop .prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
.ov-tri-pop .prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }
.ov-tri-pop .prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }
.ov-tri-pop .prod-preview-history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
.ov-tri-pop .prod-preview-history .h-thumb.adopted .badge { display: block; }
/* 主图可点击放大 */
.ov-tri-pop .prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }
.ov-tri-pop .prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }
.ov-tri-pop .prod-preview-img.is-zoomable::after {
content: '';
position: absolute; top: 8px; right: 8px;
width: 22px; height: 22px;
background: rgba(21,20,15,.72) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='M21 21l-4.3-4.3M8 11h6M11 8v6'/></svg>") center/14px no-repeat;
border-radius: var(--r-sm);
opacity: 0; transition: opacity var(--t-base);
pointer-events: none;
}
.ov-tri-pop .prod-preview-img.is-zoomable:hover::after { opacity: 1; }
/* 三视图放大查看 lightbox */
#ov-tri-lightbox-bg { z-index: 80; }
#ov-tri-lightbox-bg .tri-lightbox {
position: relative;
width: min(1100px, 92vw);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px 20px 20px;
display: flex; flex-direction: column; gap: 12px;
box-shadow: 0 24px 64px rgba(0,0,0,.24);
}
.tri-lightbox-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--font-mono);
font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
color: var(--black-alpha-56);
padding-right: 32px;
}
.tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }
.tri-lightbox-head .lb-tag {
margin-left: 6px;
padding: 2px 6px;
background: var(--heat-12); color: var(--heat);
border-radius: 3px;
font-size: 10px;
}
.tri-lightbox-close {
position: absolute;
top: 12px; right: 12px;
width: 28px; height: 28px;
display: grid; place-items: center;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-56);
cursor: pointer;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
z-index: 2;
}
.tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }
.tri-lightbox-close svg { width: 14px; height: 14px; }
.tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }
.tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
.tri-lightbox-foot .spc { flex: 1; }
.tri-lightbox-foot kbd {
display: inline-block;
padding: 1px 5px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-bottom-width: 2px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--black-alpha-72);
}
/* 字段 view ↔ edit 状态切换 */
.v-edit { display: none; }
.ov-card.editing .v-static { display: none; }
.ov-card.editing .v-edit { display: block; }
/* 输入控件 · 对齐新建表单 V2.1 规范 */
.v-input,
.v-select {
width: 100%;
max-width: 100%;
height: 38px;
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 0 14px;
font-size: 13.5px;
color: var(--accent-black);
background: var(--background-lighter);
font-family: inherit;
outline: none;
transition: border-color var(--t-base), box-shadow var(--t-base);
}
.v-input:focus,
.v-select:focus {
border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
/* 编辑模式下 · 核心卖点 bullet-list (与新建表单完全一致) */
.v-bullet-list {
list-style: none;
padding: 0; margin: 0;
}
.v-bullet-list .bl-item,
.v-bullet-list .bl-add {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13.5px;
}
.v-bullet-list .bl-add { background: transparent; border-style: dashed; }
.v-bullet-list .bl-add:focus-within { border-color: var(--heat-40); background: var(--surface); }
.v-bullet-list .num {
width: 22px; height: 22px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 11px;
color: var(--heat);
font-weight: 700;
display: grid; place-items: center;
flex-shrink: 0;
}
.v-bullet-list .bl-add .num {
background: transparent;
color: var(--heat);
border-color: var(--heat-40);
}
.v-bullet-list .bl-text { flex: 1; color: var(--accent-black); }
.v-bullet-list .bl-input {
flex: 1;
background: transparent; border: 0; outline: none;
font-size: 13.5px;
color: var(--accent-black);
font-family: inherit;
}
.v-bullet-list .bl-input::placeholder { color: var(--black-alpha-48); }
.v-bullet-list .bl-x {
width: 22px; height: 22px;
color: var(--black-alpha-48);
cursor: pointer;
display: grid; place-items: center;
border-radius: var(--r-sm);
transition: color var(--t-base), background var(--t-base);
}
.v-bullet-list .bl-x:hover { color: var(--accent-crimson, #c43d3d); background: var(--crimson-bg, #fdebea); }
.v-bullet-list .bl-x svg { width: 11px; height: 11px; }
/* 编辑模式下,商品图片显示一个 [+ 上传] 占位 */
.img-upload {
display: none;
aspect-ratio: 1;
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-sm);
cursor: pointer;
place-items: center;
color: var(--black-alpha-48);
background: var(--background-lighter);
transition: border-color var(--t-base), color var(--t-base);
}
.img-upload:hover { border-color: var(--heat); color: var(--heat); }
.img-upload svg { width: 18px; height: 18px; }
.ov-card.editing .img-upload { display: grid; }
.ov-card.editing .ov-images-sub .thumb { cursor: pointer; }
.ov-card.editing .ov-images-sub .thumb::after {
content: '×';
position: absolute;
top: 4px; right: 4px;
width: 18px; height: 18px;
background: rgba(0,0,0,.7);
color: var(--accent-white);
border-radius: 50%;
display: grid; place-items: center;
font-size: 13px;
line-height: 1;
}
.ov-images-sub .thumb { position: relative; }
.pd-overview .ov-h {
display: flex; align-items: baseline; gap: 8px;
margin-bottom: 14px;
}
.pd-overview .ov-h .ti {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
}
.pd-overview .ov-h .ct {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.pd-overview .ov-h .more {
margin-left: auto;
font-size: 12px;
color: var(--heat);
cursor: pointer;
}
.pd-overview .ov-h .more:hover { text-decoration: underline; }
/* 商品信息卡片内 · 上信息 / 下图片 (堆叠, 图片铺满卡片) */
.ov-main-grid {
display: flex;
flex-direction: column;
gap: 18px;
}
.ov-main-grid > .ov-images-sub {
padding-top: 18px;
border-top: 1px solid var(--border-faint);
}
.ov-info .row {
display: flex; gap: 12px;
margin-bottom: 10px;
font-size: 13px;
}
.ov-info .row:last-child { margin-bottom: 0; }
.ov-info .k {
width: 64px;
flex-shrink: 0;
color: var(--black-alpha-48);
font-size: 12.5px;
}
.ov-info .v {
flex: 1; min-width: 0;
color: var(--accent-black);
line-height: 1.6;
}
.ov-info .v .bullet { display: block; }
.ov-info .v .bullet::before {
content: '·';
color: var(--heat);
margin-right: 6px;
font-weight: 700;
}
/* 商品图片 · 卡片内子 section */
.ov-images-sub .sub-h {
display: flex; align-items: baseline; gap: 6px;
margin-bottom: 10px;
}
.ov-images-sub .sub-h .ti {
font-size: 12.5px; font-weight: 500;
color: var(--black-alpha-72);
}
.ov-images-sub .sub-h .ct {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.ov-images-sub .sub-h .more {
margin-left: auto;
font-size: 11.5px;
color: var(--heat);
cursor: pointer;
}
.ov-images-sub .sub-h .more:hover { text-decoration: underline; }
.ov-images-sub .grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 8px;
}
.ov-images-sub .thumb {
aspect-ratio: 1;
border-radius: var(--r-sm);
overflow: hidden;
cursor: pointer;
}
.ov-images-sub .thumb img { width: 100%; height: 100%; object-fit: cover; }
/* 快速操作 · 2 段:图片生成(3 等比 CTA)+ 视频生成(1 CTA) · 两段等高填充容器 */
.ov-actions { display: flex; flex-direction: column; }
.ov-actions .qa-section {
margin-bottom: 14px;
display: flex; flex-direction: column;
flex: 1 1 0; min-height: 0;
}
.ov-actions .qa-section:last-child { margin-bottom: 0; }
.ov-actions .qa-section-h {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
text-transform: uppercase;
margin-bottom: 8px;
}
.ov-actions .qa-row-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
flex: 1 1 0; min-height: 0;
}
.ov-actions .qa-row-1 {
display: flex;
flex: 1 1 0; min-height: 0;
}
.ov-actions .qa-row-1 .qa-item { width: 100%; }
.qa-item {
display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 8px;
padding: 14px 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
font-size: 12.5px;
color: var(--accent-black);
text-align: center;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.qa-item:hover { border-color: var(--heat); background: var(--heat-12); color: var(--heat); }
.qa-item .ic {
width: 32px; height: 32px;
display: grid; place-items: center;
color: var(--heat);
flex-shrink: 0;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
}
.qa-item:hover .ic { border-color: var(--heat-20); background: var(--surface); }
.qa-item .ic svg { width: 16px; height: 16px; }
.qa-item.primary {
background: var(--heat);
color: var(--accent-white);
border-color: var(--heat);
}
.qa-item.primary .ic { background: rgba(255,255,255,.16); color: var(--accent-white); border-color: rgba(255,255,255,.24); }
.qa-item.primary:hover { color: var(--accent-white); box-shadow: var(--shadow-cta-hover); }
/* 状态 pill 三态(通过/不通过/归档) */
.asset-card .meta .pill.pass {
background: var(--accent-emerald-bg, #e6f4ec);
color: var(--accent-emerald, #1f8a51);
border: 1px solid var(--accent-emerald-bd, #c4e3d1);
cursor: pointer;
}
.asset-card .meta .pill.fail {
background: var(--crimson-bg, #fdebea);
color: var(--accent-crimson, #c43d3d);
border: 1px solid var(--crimson-bd, #f5c2bf);
cursor: pointer;
}
.asset-card .meta .pill.archive {
background: var(--background-lighter);
color: var(--black-alpha-56);
border: 1px solid var(--border-faint);
cursor: pointer;
}
/* ─── Tabs ─── */
.pd-tabs {
display: flex; gap: 4px;
border-bottom: 1px solid var(--border-faint);
margin-bottom: 18px;
}
.pd-tabs .tab {
padding: 10px 14px;
font-size: 13.5px;
color: var(--black-alpha-56);
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
cursor: pointer;
font-family: inherit;
font-weight: 500;
transition: color var(--t-base), border-color var(--t-base);
}
.pd-tabs .tab:hover { color: var(--accent-black); }
.pd-tabs .tab.active {
color: var(--accent-black);
border-bottom-color: var(--heat);
font-weight: 600;
}
.tab-pane { display: none; }
.tab-pane.active { display: block; }
/* ─── AI 素材 工具栏 ─── */
.pd-toolbar {
display: flex; align-items: center; gap: 10px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.pd-toolbar .total {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
}
.pd-toolbar .total .ct {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
margin-left: 4px;
font-weight: 500;
}
.pd-toolbar .filter {
display: inline-flex; align-items: center; gap: 4px;
height: 30px;
padding: 0 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
cursor: pointer;
font-size: 12.5px;
color: var(--black-alpha-72);
font-family: inherit;
transition: border-color var(--t-base), color var(--t-base);
}
.pd-toolbar .filter:hover { border-color: var(--black-alpha-24); }
.pd-toolbar .filter.open, .pd-toolbar .filter.filtered { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.pd-toolbar .filter.open svg, .pd-toolbar .filter.filtered svg { opacity: 1; }
.pd-toolbar .filter svg { width: 10px; height: 10px; opacity: .6; transition: transform var(--t-base); }
.pd-toolbar .filter.open svg { transform: rotate(180deg); }
/* 筛选下拉 · 挂在 body 上避免被祖先 overflow 裁切 */
.filter-pop {
position: fixed;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 4px;
box-shadow: 0 6px 20px var(--black-alpha-12);
z-index: 1500;
min-width: 130px;
display: none;
flex-direction: column;
}
.filter-pop.show { display: flex; }
.filter-pop button {
background: transparent; border: 0;
padding: 8px 12px;
text-align: left;
font-size: 12.5px;
color: var(--accent-black);
cursor: pointer;
border-radius: var(--r-sm);
font-family: inherit;
white-space: nowrap;
transition: background var(--t-base);
}
.filter-pop button:hover { background: var(--background-lighter); }
.filter-pop button.selected { background: var(--heat-12); color: var(--heat); font-weight: 600; }
.filter-pop button.selected::before { content: '✓ '; }
.pd-toolbar .right { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }
.pd-toolbar .view-tog {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 2px;
}
.pd-toolbar .view-tog button {
width: 28px; height: 26px;
display: grid; place-items: center;
border: 0;
background: transparent;
color: var(--black-alpha-48);
cursor: pointer;
border-radius: 4px;
}
.pd-toolbar .view-tog button.active {
background: var(--accent-black);
color: var(--accent-white);
}
.pd-toolbar .view-tog button svg { width: 13px; height: 13px; }
/* ─── AI 素材 网格 ─── */
.asset-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
/* 列表视图:卡片横排,缩略图缩到 88px */
.asset-grid.list-view {
display: flex;
flex-direction: column;
gap: 6px;
}
.asset-grid.list-view .asset-card {
display: grid;
grid-template-columns: 88px minmax(0, 1fr);
gap: 0;
align-items: center;
}
.asset-grid.list-view .asset-card .thumb {
aspect-ratio: 1;
width: 88px;
border-right: 1px solid var(--border-faint);
}
.asset-grid.list-view .asset-card .thumb .type-pill {
font-size: 9.5px;
padding: 2px 6px;
top: 4px;
left: 4px;
}
.asset-grid.list-view .asset-card .thumb .ph-frame { font-size: 10px; }
.asset-grid.list-view .asset-card .meta {
padding: 10px 14px;
}
/* 空筛选结果 */
.empty-filter {
padding: 56px 24px;
text-align: center;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 12.5px;
letter-spacing: .02em;
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
background: var(--background-lighter);
}
.empty-filter .reset {
display: inline-block; margin-top: 12px;
color: var(--heat); cursor: pointer; text-decoration: underline;
}
.asset-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
overflow: hidden;
cursor: pointer;
transition: border-color var(--t-base), transform var(--t-fast);
}
.asset-card:hover { border-color: var(--black-alpha-24); transform: translateY(-1px); }
.asset-card .thumb {
aspect-ratio: 3/4;
position: relative;
overflow: hidden;
}
.asset-card .thumb .type-pill {
position: absolute; top: 8px; left: 8px;
padding: 3px 8px;
background: rgba(0,0,0,.65);
color: var(--accent-white);
border-radius: var(--r-sm);
font-size: 11px;
font-weight: 500;
backdrop-filter: blur(4px);
}
.asset-card .meta {
padding: 10px 12px;
display: flex; align-items: center; gap: 8px;
}
.asset-card .meta .pill {
padding: 2px 8px;
border-radius: var(--r-sm);
font-size: 10.5px;
font-weight: 500;
}
.asset-card .meta .date {
margin-left: auto;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.pd-more {
text-align: center;
padding: 18px 0 32px;
}
.pd-more button {
height: 32px;
padding: 0 18px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
color: var(--black-alpha-72);
font-size: 12.5px;
font-family: inherit;
cursor: pointer;
}
.pd-more button:hover { border-color: var(--heat-40); color: var(--heat); }
/* ─── 任务记录 · 表格 ─── */
.task-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 18px;
}
.task-stat {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 18px;
}
.task-stat .lbl {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
margin-bottom: 6px;
}
.task-stat .v {
font-size: 22px; font-weight: 600;
color: var(--accent-black);
letter-spacing: -.01em;
}
.task-stat .v small {
font-size: 13px;
color: var(--black-alpha-48);
font-weight: 400;
margin-left: 4px;
}
.task-stat.ok .v { color: var(--accent-emerald, #1f8a51); }
.task-stat.gen .v { color: var(--heat); }
.task-stat.err .v { color: var(--accent-crimson, #c43d3d); }
.task-table {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
overflow: hidden;
}
.task-row {
display: grid;
grid-template-columns: 36px 1.8fr 0.7fr 1fr 1.1fr 1.1fr 0.7fr 100px;
align-items: center;
gap: 12px;
padding: 12px 18px;
border-bottom: 1px solid var(--border-faint);
font-size: 13px;
}
.task-row:last-child { border-bottom: 0; }
.task-row.head {
background: var(--background-lighter);
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
font-weight: 500;
padding: 10px 18px;
}
.task-row .ph {
width: 36px; height: 36px;
border-radius: var(--r-sm);
flex-shrink: 0;
}
.task-row .nm {
color: var(--accent-black);
font-weight: 500;
display: flex; align-items: center; gap: 8px;
}
.task-row .nm .id-mono {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
font-weight: 400;
}
.task-row .qty { color: var(--black-alpha-72); font-family: var(--font-mono); }
.task-row .time {
color: var(--black-alpha-72);
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: .01em;
}
.task-row .dur {
color: var(--black-alpha-56);
font-family: var(--font-mono);
font-size: 12px;
}
.task-row .pill {
display: inline-flex; align-items: center; gap: 5px;
padding: 3px 9px;
border-radius: var(--r-sm);
font-size: 11.5px;
font-weight: 500;
width: fit-content;
}
.task-row .pill .dot {
width: 6px; height: 6px;
border-radius: 50%;
}
.task-row .pill.ok {
background: var(--accent-emerald-bg, #e6f4ec);
color: var(--accent-emerald, #1f8a51);
border: 1px solid var(--accent-emerald-bd, #c4e3d1);
}
.task-row .pill.ok .dot { background: var(--accent-emerald, #1f8a51); }
.task-row .pill.gen {
background: var(--heat-12);
color: var(--heat);
border: 1px solid var(--heat-20);
}
.task-row .pill.gen .dot { background: var(--heat); animation: pulse 1.6s ease-in-out infinite; }
.task-row .pill.err {
background: var(--crimson-bg, #fdebea);
color: var(--accent-crimson, #c43d3d);
border: 1px solid var(--crimson-bd, #f5c2bf);
}
.task-row .pill.err .dot { background: var(--accent-crimson, #c43d3d); }
.task-row .pill.wait {
background: var(--background-lighter);
color: var(--black-alpha-56);
border: 1px solid var(--border-faint);
}
.task-row .pill.wait .dot { background: var(--black-alpha-32); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .3; }
}
.task-row .status-cell { display: flex; flex-direction: column; gap: 4px; }
.task-row .progress {
width: 100%; height: 3px;
background: var(--black-alpha-12);
border-radius: 2px;
overflow: hidden;
}
.task-row .progress > span {
display: block;
height: 100%;
background: var(--heat);
}
.task-row .ops {
display: inline-flex; gap: 4px;
justify-self: end;
}
.task-row .ops button {
padding: 4px 10px;
height: 26px;
background: transparent;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 11.5px;
font-family: inherit;
cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.task-row .ops button:hover { border-color: var(--heat-40); color: var(--heat); }
.task-row .ops button.danger:hover { border-color: var(--crimson-bd, #f5c2bf); color: var(--accent-crimson, #c43d3d); }
@media (max-width: 1100px) {
.pd-overview { grid-template-columns: 1fr; }
.ov-actions .qa-grid {
display: grid !important;
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.ov-actions .qa-grid { grid-template-columns: 1fr 1fr; }
.task-stats { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div id="page">
<!-- 顶部 标题 + 状态 -->
<div class="pd-title">
<h1 id="pd-name">补水保湿精华液</h1>
</div>
<!-- 商品信息(含图片) + 快速操作 · 主辅两栏 -->
<div class="pd-overview">
<div class="ov-card ov-main" id="ov-main-card">
<div class="ov-h">
<span class="ti">商品信息</span>
<!-- AI 生成三视图 · 按钮 + 弹出 panel(view 模式可见) -->
<div class="ov-tri-wrap">
<button class="ov-edit ov-tri-trigger" type="button" id="ov-tri-btn" title="AI 生成商品三视图" aria-haspopup="dialog" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/><path d="M19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9L19 14z"/></svg>
AI 生成三视图
</button>
<div class="ov-tri-pop" id="ov-tri-pop" role="dialog" aria-label="AI 生成三视图">
<button class="ov-tri-close" type="button" id="ov-tri-close" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<div class="prod-preview-h">// 三视图预览 · <span id="ov-tri-status">待生成</span></div>
<div class="placeholder prod-preview-img" id="ov-tri-img"><span class="ph-frame">// 尚未生成 · 点击下方按钮开始</span></div>
<div class="prod-preview-foot" id="ov-tri-foot">
<button class="ov-edit primary" type="button" id="ov-tri-start" style="height:28px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/></svg>
生成
</button>
<span style="flex:1;"></span>
<span class="mono" style="font-size:11px; color: var(--black-alpha-56);">~¥0.30 / 次</span>
</div>
<div class="prod-preview-history" id="ov-tri-history">
<div class="h-lbl">// 历史版本 · <span class="ct" id="ov-tri-history-count">0</span></div>
<div class="h-row" id="ov-tri-history-row"></div>
</div>
</div>
</div>
<!-- view 模式: 单个 [编辑信息] -->
<button class="ov-edit ov-edit-single" type="button" id="ov-edit-btn" title="编辑商品信息">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
编辑信息
</button>
<!-- edit 模式: [重置] [取消] [保存] -->
<div class="ov-edit-group">
<button class="ov-edit" type="button" id="ov-reset-btn" title="重置为修改前">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
重置
</button>
<button class="ov-edit" type="button" id="ov-cancel-btn">取消</button>
<button class="ov-edit primary" type="button" id="ov-save-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存
</button>
</div>
</div>
<div class="ov-main-grid">
<div class="ov-info">
<div class="row" data-field="name">
<div class="k">商品名称</div>
<div class="v">
<span class="v-static">补水保湿精华液</span>
<input class="v-edit v-input" type="text" value="补水保湿精华液" maxlength="100">
</div>
</div>
<div class="row" data-field="cat">
<div class="k">品类</div>
<div class="v">
<span class="v-static">美妆个护 / 精华液</span>
<select class="v-edit v-select">
<option>美妆个护 / 精华液</option>
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
</div>
<div class="row" data-field="target">
<div class="k">目标人群</div>
<div class="v">
<span class="v-static">22-32 岁女性、敏感肌、办公室通勤</span>
<input class="v-edit v-input" type="text" value="22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="row" data-field="bullets">
<div class="k">核心卖点</div>
<div class="v">
<div class="v-static">
<span class="bullet">透明质酸 + B5,敷完不黏不闷</span>
<span class="bullet">30g 大容量精华液</span>
<span class="bullet">0 香精 0 酒精,敏感肌可用</span>
</div>
<ul class="v-edit v-bullet-list" id="v-bullets-list">
<!-- li.bl-item × N + li.bl-add 由 JS 在进入编辑模式时渲染 -->
</ul>
</div>
</div>
</div>
<div class="ov-images-sub">
<div class="sub-h">
<span class="ti">商品图片</span>
<span class="ct">(6)</span>
</div>
<div class="grid" id="ov-images-grid">
<div class="thumb placeholder"><span class="ph-frame">1:1</span></div>
<div class="thumb placeholder"><span class="ph-frame">1:1</span></div>
<div class="thumb placeholder"><span class="ph-frame">1:1</span></div>
<div class="thumb placeholder"><span class="ph-frame">1:1</span></div>
<div class="thumb placeholder"><span class="ph-frame">1:1</span></div>
<div class="thumb placeholder"><span class="ph-frame">1:1</span></div>
<div class="img-upload" id="ov-img-add" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</div>
</div>
</div>
</div>
</div>
<div class="ov-card ov-actions">
<div class="ov-h"><span class="ti">快速操作</span></div>
<div class="qa-section">
<div class="qa-section-h">// 图片生成</div>
<div class="qa-row-3">
<div class="qa-item" data-go="model-photo" role="button" tabindex="0">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a4 4 0 014-4h8a4 4 0 014 4v2"/></svg></span>
模特上身图
</div>
<div class="qa-item" data-go="platform-cover" role="button" tabindex="0">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg></span>
平台套图
</div>
<div class="qa-item" data-go="image-optimize" role="button" tabindex="0">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M3 12h18M5 5l14 14M5 19l14-14"/></svg></span>
图片创作
</div>
</div>
</div>
<div class="qa-section">
<div class="qa-section-h">// 视频生成</div>
<div class="qa-row-1">
<div class="qa-item primary" data-go="projects-new" role="button" tabindex="0">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg></span>
生成视频
</div>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="pd-tabs">
<button class="tab active" type="button" data-tab="assets">AI 生成素材</button>
<button class="tab" type="button" data-tab="videos">视频项目</button>
<button class="tab" type="button" data-tab="tasks" hidden>任务记录</button>
</div>
<!-- ===== AI 生成素材 ===== -->
<div class="tab-pane active" data-pane="assets">
<div class="pd-toolbar">
<div class="total">全部 AI 素材 <span class="ct">(32)</span></div>
<button class="filter" type="button" data-key="type">
全部类型
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="filter" type="button" data-key="status">
通过
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="right">
<div class="view-tog">
<button type="button" class="active" title="网格视图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
</button>
<button type="button" title="列表视图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
<button class="filter" type="button" data-key="sort">
最新生成
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
</div>
</div>
<div class="asset-grid">
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">模特上身图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">模特上身图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">模特上身图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill fail" data-status="fail" title="点击切换状态">不通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">模特上身图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">模特上身图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill archive" data-status="archive" title="点击切换状态">归档</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">平台套图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">平台套图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">平台套图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill fail" data-status="fail" title="点击切换状态">不通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">平台套图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill archive" data-status="archive" title="点击切换状态">归档</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">平台套图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">三视图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill pass" data-status="pass" title="点击切换状态">通过</span><span class="date">2026-05-19 15:30</span></div></div>
<div class="asset-card"><div class="thumb placeholder"><span class="type-pill">三视图</span><span class="ph-frame">3:4</span></div><div class="meta"><span class="pill archive" data-status="archive" title="点击切换状态">归档</span><span class="date">2026-05-19 15:30</span></div></div>
</div>
<div class="pd-more"><button type="button">加载更多</button></div>
</div>
<!-- ===== 视频项目 ===== -->
<div class="tab-pane" data-pane="videos">
<div class="pd-toolbar">
<div class="total">该商品视频项目 <span class="ct">(4)</span></div>
<div class="right">
<button class="filter" type="button" data-key="sort">
最新导出
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
</div>
</div>
<div class="asset-grid">
<div class="asset-card" data-proj-status="done"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v3</span></div><div class="meta"><span class="pill ok"><span class="dot"></span>已完成</span><span class="date">2026-05-20 12:08</span></div></div>
<div class="asset-card" data-proj-status="wip"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v2</span></div><div class="meta"><span class="pill info"><span class="dot"></span>视频生成 4/6</span><span class="date">2026-05-19 10:24</span></div></div>
<div class="asset-card" data-proj-status="archived"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">熬夜急救 · v1</span></div><div class="meta"><span class="pill neutral"><span class="dot"></span>已归档</span><span class="date">2026-05-18 21:42</span></div></div>
<div class="asset-card" data-proj-status="fail"><div class="thumb placeholder" style="aspect-ratio: 9/16;"><span class="type-pill">视频 · 9:16</span><span class="ph-frame">补水面膜 · v1</span></div><div class="meta"><span class="pill err"><span class="dot"></span>故事板失败</span><span class="date">2026-05-17 16:00</span></div></div>
</div>
<div class="pd-more"><button type="button">加载更多</button></div>
</div>
<!-- ===== 任务记录 ===== -->
<div class="tab-pane" data-pane="tasks">
<!-- 顶部统计概览 -->
<div class="task-stats">
<div class="task-stat">
<div class="lbl">// TOTAL</div>
<div class="v">12 <small>个任务</small></div>
</div>
<div class="task-stat ok">
<div class="lbl">// SUCCESS</div>
<div class="v">9</div>
</div>
<div class="task-stat gen">
<div class="lbl">// RUNNING</div>
<div class="v">2</div>
</div>
<div class="task-stat err">
<div class="lbl">// FAILED</div>
<div class="v">1</div>
</div>
</div>
<!-- 工具栏 -->
<div class="pd-toolbar">
<div class="total">任务记录 <span class="ct">(12)</span></div>
<button class="filter" type="button" data-key="type">
全部类型
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="filter" type="button" data-key="status">
全部状态
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<button class="filter" type="button" data-key="date">
近 7 天
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="right">
<button class="filter" type="button" data-key="sort">
提交时间倒序
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
</div>
</div>
<!-- 任务表格 -->
<div class="task-table">
<div class="task-row head">
<span></span>
<span>任务 / 编号</span>
<span>数量</span>
<span>状态</span>
<span>提交时间</span>
<span>完成时间</span>
<span>耗时</span>
<span style="justify-self:end">操作</span>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">视频素材 <span class="id-mono">// T-2026-0519-0007</span></div>
<div class="qty">1 个</div>
<div class="status-cell">
<span class="pill gen"><span class="dot"></span>生成中 60%</span>
<span class="progress"><span style="width:60%"></span></span>
</div>
<div class="time">2026-05-19 16:00</div>
<div class="time"></div>
<div class="dur"></div>
<div class="ops">
<button type="button">查看</button>
<button type="button" class="danger">取消</button>
</div>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">模特上身图 <span class="id-mono">// T-2026-0519-0006</span></div>
<div class="qty">3 张</div>
<div class="status-cell">
<span class="pill wait"><span class="dot"></span>排队中</span>
</div>
<div class="time">2026-05-19 15:58</div>
<div class="time"></div>
<div class="dur"></div>
<div class="ops">
<button type="button">查看</button>
<button type="button" class="danger">取消</button>
</div>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">模特上身图 <span class="id-mono">// T-2026-0519-0005</span></div>
<div class="qty">5 张</div>
<div class="status-cell">
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
<div class="time">2026-05-19 15:30</div>
<div class="time">2026-05-19 15:32</div>
<div class="dur">2m 14s</div>
<div class="ops">
<button type="button">查看</button>
</div>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">平台套图 <span class="id-mono">// T-2026-0519-0004</span></div>
<div class="qty">4 张</div>
<div class="status-cell">
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
<div class="time">2026-05-19 14:20</div>
<div class="time">2026-05-19 14:23</div>
<div class="dur">3m 02s</div>
<div class="ops">
<button type="button">查看</button>
</div>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">模特上身图 <span class="id-mono">// T-2026-0519-0003</span></div>
<div class="qty">4 张</div>
<div class="status-cell">
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
<div class="time">2026-05-19 13:10</div>
<div class="time">2026-05-19 13:13</div>
<div class="dur">2m 50s</div>
<div class="ops">
<button type="button">查看</button>
</div>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">三视图 <span class="id-mono">// T-2026-0519-0002</span></div>
<div class="qty">3 张</div>
<div class="status-cell">
<span class="pill err"><span class="dot"></span>失败</span>
</div>
<div class="time">2026-05-19 12:00</div>
<div class="time">2026-05-19 12:01</div>
<div class="dur">30s</div>
<div class="ops">
<button type="button">重试</button>
<button type="button">日志</button>
</div>
</div>
<div class="task-row">
<div class="ph placeholder"></div>
<div class="nm">平台套图 <span class="id-mono">// T-2026-0518-0001</span></div>
<div class="qty">6 张</div>
<div class="status-cell">
<span class="pill ok"><span class="dot"></span>已完成</span>
</div>
<div class="time">2026-05-18 18:42</div>
<div class="time">2026-05-18 18:46</div>
<div class="dur">4m 10s</div>
<div class="ops">
<button type="button">查看</button>
</div>
</div>
</div>
<div class="pd-more"><button type="button">加载更多</button></div>
</div>
</div>
<!-- 三视图 · 放大查看 lightbox -->
<div class="modal-bg" id="ov-tri-lightbox-bg" onclick="if(event.target===this)Shell.closeModal('ov-tri-lightbox-bg')">
<div class="tri-lightbox" role="dialog" aria-label="三视图放大查看">
<button class="tri-lightbox-close" type="button" onclick="Shell.closeModal('ov-tri-lightbox-bg')" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
<div class="tri-lightbox-head">
// 三视图(正/侧/背) · <span class="lb-ver" id="ov-tri-lightbox-label">v1</span>
<span class="lb-tag" id="ov-tri-lightbox-tag" hidden>已采用</span>
</div>
<div class="placeholder tri-lightbox-img" id="ov-tri-lightbox-img"></div>
<div class="tri-lightbox-foot">
<span id="ov-tri-lightbox-meta">// 生成于 --:--</span>
<span class="spc"></span>
<span><kbd>Esc</kbd> 关闭</span>
</div>
</div>
</div>
<script src="assets/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script>
// 从 URL ?product= 读出商品名,注入 crumb / h1 / 商品名字段
const _urlProductName = (function () {
try {
const q = new URLSearchParams(location.search);
const v = q.get('product') || q.get('name');
return v ? decodeURIComponent(v) : '';
} catch (e) { return ''; }
})();
const _productDisplayName = _urlProductName || '补水保湿精华液';
Shell.render({
active: 'products',
crumbs: [
{ label: '工作台', href: 'index.html' },
{ label: '商品库', href: 'products.html' },
{ label: _productDisplayName }
]
});
if (_urlProductName) {
const h1 = document.getElementById('pd-name');
if (h1) h1.textContent = _urlProductName;
const nameRow = document.querySelector('[data-field="name"] .v-static');
if (nameRow) nameRow.textContent = _urlProductName;
const nameInput = document.querySelector('[data-field="name"] .v-input');
if (nameInput) nameInput.value = _urlProductName;
}
// 快速操作 · 跳转至对应工作台/wizard 并携带商品名
(function bindQuickActions() {
const productName = (document.querySelector('.pd-title h1, .pd-title, .ov-h .ti, h1') || {}).textContent || '';
const crumbName = (document.querySelector('.crumb-current') || {}).textContent
|| (document.querySelectorAll('.crumb-item').length ? document.querySelectorAll('.crumb-item')[document.querySelectorAll('.crumb-item').length-1].textContent : '')
|| '补水保湿精华液';
const name = (crumbName || productName || '').trim();
document.querySelectorAll('.qa-item[data-go]').forEach(item => {
item.style.cursor = 'pointer';
let go = item.dataset.go;
// 图片创作 → 独立工作台 image-optimize.html(自由创作),带 product 作为提示词种子
let url;
if (go === 'image-optimize') {
url = 'image-optimize.html?t=' + Date.now() + '&prompt=' + encodeURIComponent(name);
} else {
url = go + '.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
}
item.addEventListener('click', () => { location.href = url; });
item.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); location.href = url; }
});
});
})();
// 任务记录 · 「查看」按钮 / 行点击 → 跳对应工作台
(function bindTaskRowJump() {
const TYPE_MAP = {
'模特上身图': 'model-photo.html',
'平台套图': 'platform-cover.html',
'视频素材': 'projects-new.html',
'视频': 'projects-new.html',
'三视图': 'model-photo.html?mode=tri',
};
const productName = (document.getElementById('pd-name')?.textContent || '').trim();
document.querySelectorAll('.task-table .task-row:not(.head)').forEach(row => {
const nameCell = row.querySelector('.nm');
if (!nameCell) return;
// 任务类型 = nm 的第一段文本(去掉 id-mono)
const type = nameCell.firstChild?.nodeValue?.trim() || '';
let url = TYPE_MAP[type];
if (!url) return;
url += (url.includes('?') ? '&' : '?') + 't=' + Date.now()
+ (productName ? '&product=' + encodeURIComponent(productName) : '');
row.style.cursor = 'pointer';
row.addEventListener('click', e => {
if (e.target.closest('.ops button')) return;
location.href = url;
});
// 「查看」按钮
row.querySelectorAll('.ops button').forEach(btn => {
if (btn.textContent.trim() === '查看') {
btn.addEventListener('click', e => {
e.stopPropagation();
location.href = url;
});
}
});
});
})();
// 状态 pill · 三态循环(通过 → 不通过 → 归档 → 通过)
(function bindStatusPills() {
const labels = { pass: '通过', fail: '不通过', archive: '归档' };
const order = ['pass', 'fail', 'archive'];
document.addEventListener('click', e => {
const pill = e.target.closest('.asset-card .meta .pill[data-status]');
if (!pill) return;
e.stopPropagation();
const cur = pill.dataset.status;
const next = order[(order.indexOf(cur) + 1) % order.length];
pill.dataset.status = next;
pill.classList.remove('pass', 'fail', 'archive');
pill.classList.add(next);
pill.textContent = labels[next];
Shell.toast('状态已更新', labels[next]);
});
})();
// Tab 切换
document.querySelectorAll('.pd-tabs .tab').forEach(t => {
t.onclick = () => {
document.querySelectorAll('.pd-tabs .tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
const target = t.dataset.tab;
document.querySelectorAll('.tab-pane').forEach(p => {
p.classList.toggle('active', p.dataset.pane === target);
});
};
});
// ─── 工具栏:筛选 / 排序 / 视图 / 加载更多(per pane)───
(function setupToolbars() {
// 每个 (pane, key) 的可选项 + 默认值(数组首项)
const OPTIONS = {
'assets:type': ['全部类型', '模特上身图', '平台套图', '三视图'],
// 状态:默认 通过 · 没有「全部状态」选项,只能切换 通过 / 不通过 / 归档
'assets:status': ['通过', '不通过', '归档'],
'assets:sort': ['最新生成', '最早生成'],
'videos:sort': ['最新导出', '最早导出'],
'tasks:type': ['全部类型', '模特上身图', '平台套图', '视频素材', '三视图'],
'tasks:status': ['全部状态', '已完成', '生成中', '排队中', '失败'],
'tasks:date': ['全部', '今天', '近 7 天', '近 30 天'],
'tasks:sort': ['提交时间倒序', '提交时间正序'],
};
// 「状态」永远视为已生效筛选(没有"全部"选项),即便选了默认值也走过滤逻辑
const ALWAYS_APPLY_KEYS = new Set(['status']);
// 任务行 status pill 的 class → 中文标签
const TASK_STATUS_MAP = { ok: '已完成', gen: '生成中', wait: '排队中', err: '失败', fail: '失败' };
// assets 卡片 data-status → 中文标签
const ASSET_STATUS_MAP = { pass: '通过', fail: '不通过', archive: '归档' };
let openPop = null;
function closeOpenPop() {
if (!openPop) return;
openPop.pop.classList.remove('show');
openPop.btn.classList.remove('open');
openPop = null;
}
document.addEventListener('click', closeOpenPop);
window.addEventListener('resize', closeOpenPop);
// .content 区域滚动时也关
(document.querySelector('.content') || document).addEventListener('scroll', closeOpenPop, true);
document.querySelectorAll('.tab-pane').forEach(paneEl => {
const paneId = paneEl.dataset.pane;
paneEl.querySelectorAll('.pd-toolbar .filter[data-key]').forEach(btn => {
const key = btn.dataset.key;
const opts = OPTIONS[paneId + ':' + key];
if (!opts) return;
btn.dataset.value = opts[0];
btn.dataset.default = opts[0];
const pop = document.createElement('div');
pop.className = 'filter-pop';
pop.innerHTML = opts.map(o =>
`<button type="button" data-val="${o}">${o}</button>`
).join('');
document.body.appendChild(pop);
btn.addEventListener('click', e => {
e.stopPropagation();
if (openPop && openPop.btn === btn) { closeOpenPop(); return; }
closeOpenPop();
const r = btn.getBoundingClientRect();
pop.style.top = (r.bottom + 4) + 'px';
pop.style.left = r.left + 'px';
pop.style.minWidth = r.width + 'px';
pop.classList.add('show');
btn.classList.add('open');
pop.querySelectorAll('button').forEach(b =>
b.classList.toggle('selected', b.dataset.val === btn.dataset.value));
openPop = { btn, pop };
});
pop.addEventListener('click', e => {
e.stopPropagation();
const opt = e.target.closest('button[data-val]');
if (!opt) return;
setFilterValue(btn, opt.dataset.val);
closeOpenPop();
applyFilters(paneEl);
});
});
// 视图切换:网格 / 列表
const viewTog = paneEl.querySelector('.view-tog');
if (viewTog) {
const btns = viewTog.querySelectorAll('button');
btns.forEach((b, i) => {
b.onclick = () => {
btns.forEach(x => x.classList.remove('active'));
b.classList.add('active');
const grid = paneEl.querySelector('.asset-grid');
if (grid) grid.classList.toggle('list-view', i === 1);
};
});
}
// 加载更多:复用前 N 个卡片克隆,演示用
const moreBtn = paneEl.querySelector('.pd-more button');
if (moreBtn) {
let loaded = 0;
moreBtn.onclick = () => {
loaded++;
if (loaded > 2) {
moreBtn.textContent = '已加载全部';
moreBtn.disabled = true;
moreBtn.style.opacity = '.5';
moreBtn.style.cursor = 'not-allowed';
Shell.toast('已到末尾', '没有更多素材了');
return;
}
const grid = paneEl.querySelector('.asset-grid');
if (!grid) return;
const src = [...grid.querySelectorAll('.asset-card')].slice(0, 4);
src.forEach(c => grid.appendChild(c.cloneNode(true)));
applyFilters(paneEl);
Shell.toast('已加载更多', '+' + src.length + ' 个');
};
}
// 首次 apply 一遍(同步 count)
applyFilters(paneEl);
});
function setFilterValue(btn, val) {
btn.dataset.value = val;
[...btn.childNodes].forEach(n => { if (n.nodeType === 3) n.remove(); });
btn.insertBefore(document.createTextNode(val + ' '), btn.firstChild);
// 仅在切到非默认值时才高亮(状态 chip 即便永远过滤,视觉也保持中性,与「全部类型」一致)
btn.classList.toggle('filtered', val !== btn.dataset.default);
}
function applyFilters(paneEl) {
const paneId = paneEl.dataset.pane;
const f = {};
paneEl.querySelectorAll('.pd-toolbar .filter[data-key]').forEach(b => {
f[b.dataset.key] = b.dataset.value;
});
const isDefault = (key) => {
const btn = paneEl.querySelector('.pd-toolbar .filter[data-key="' + key + '"]');
if (!btn) return true; // 该 pane 没这个 filter → 等同默认,不过滤
if (ALWAYS_APPLY_KEYS.has(key)) return false; // 状态有按钮时,永远走过滤
return btn.dataset.value === btn.dataset.default;
};
if (paneId === 'assets' || paneId === 'videos') {
const cards = [...paneEl.querySelectorAll('.asset-card')];
let visible = 0;
cards.forEach(c => {
let show = true;
if (paneId === 'assets' && !isDefault('type')) {
const t = c.querySelector('.type-pill')?.textContent?.trim() || '';
if (t !== f.type) show = false;
}
if (!isDefault('status')) {
const s = c.querySelector('.pill[data-status]')?.dataset.status || '';
if ((ASSET_STATUS_MAP[s] || '') !== f.status) show = false;
}
c.style.display = show ? '' : 'none';
if (show) visible++;
});
if (!isDefault('sort')) {
const grid = paneEl.querySelector('.asset-grid');
const asc = (f.sort || '').includes('最早');
[...grid.querySelectorAll('.asset-card')]
.filter(c => c.style.display !== 'none')
.sort((a, b) => {
const ad = a.querySelector('.date')?.textContent || '';
const bd = b.querySelector('.date')?.textContent || '';
return asc ? ad.localeCompare(bd) : bd.localeCompare(ad);
})
.forEach(c => grid.appendChild(c));
}
updateCount(paneEl, visible);
toggleEmpty(paneEl, visible === 0);
// 不足 2 行时不显示「加载更多」按钮(布局后用 offsetTop 统计行数)
const moreEl = paneEl.querySelector('.pd-more');
if (moreEl) {
requestAnimationFrame(() => {
const visCards = [...paneEl.querySelectorAll('.asset-card')].filter(c => c.style.display !== 'none');
const rows = new Set(visCards.map(c => c.offsetTop)).size;
moreEl.style.display = rows >= 2 ? '' : 'none';
});
}
} else if (paneId === 'tasks') {
const rows = [...paneEl.querySelectorAll('.task-table .task-row:not(.head)')];
let visible = 0;
rows.forEach(row => {
let show = true;
if (!isDefault('type')) {
const t = row.querySelector('.nm')?.firstChild?.nodeValue?.trim() || '';
if (t !== f.type) show = false;
}
if (!isDefault('status')) {
const pill = row.querySelector('.status-cell .pill');
const cls = pill?.className || '';
const matched = Object.entries(TASK_STATUS_MAP)
.some(([k, v]) => cls.split(/\s+/).includes(k) && v === f.status);
if (!matched) show = false;
}
// date 过滤 · mock 数据都是 5/19,简单按天数差近似
if (!isDefault('date')) {
const submitTxt = row.querySelectorAll('.time')[0]?.textContent || '';
const m = submitTxt.match(/(\d{4})-(\d{2})-(\d{2})/);
if (m) {
const d = new Date(m[1] + '-' + m[2] + '-' + m[3]);
const now = new Date();
const days = Math.floor((now - d) / 86400000);
if (f.date === '今天' && days > 0) show = false;
if (f.date === '近 7 天' && days > 7) show = false;
if (f.date === '近 30 天' && days > 30) show = false;
}
}
row.style.display = show ? '' : 'none';
if (show) visible++;
});
if (!isDefault('sort')) {
const table = paneEl.querySelector('.task-table');
const asc = (f.sort || '').includes('正序');
[...table.querySelectorAll('.task-row:not(.head)')]
.filter(r => r.style.display !== 'none')
.sort((a, b) => {
const ad = a.querySelectorAll('.time')[0]?.textContent || '';
const bd = b.querySelectorAll('.time')[0]?.textContent || '';
return asc ? ad.localeCompare(bd) : bd.localeCompare(ad);
})
.forEach(r => table.appendChild(r));
}
updateCount(paneEl, visible);
toggleEmpty(paneEl, visible === 0);
}
}
function updateCount(paneEl, n) {
const ct = paneEl.querySelector('.pd-toolbar .total .ct');
if (ct) ct.textContent = '(' + n + ')';
}
function toggleEmpty(paneEl, isEmpty) {
let empty = paneEl.querySelector('.empty-filter');
const container = paneEl.querySelector('.asset-grid, .task-table');
const more = paneEl.querySelector('.pd-more');
if (isEmpty) {
if (!empty) {
empty = document.createElement('div');
empty.className = 'empty-filter';
empty.innerHTML = '// 当前筛选下没有结果<br><a class="reset">点这里重置筛选</a>';
container?.after(empty);
empty.querySelector('.reset').addEventListener('click', () => {
paneEl.querySelectorAll('.pd-toolbar .filter[data-key]').forEach(b => {
setFilterValue(b, b.dataset.default);
});
applyFilters(paneEl);
});
}
empty.style.display = '';
if (more) more.style.display = 'none';
} else if (empty) {
empty.style.display = 'none';
if (more) more.style.display = '';
}
}
})();
// 编辑商品信息 · 在卡片内 inline 切换 view ↔ edit
(function initEdit() {
const card = document.getElementById('ov-main-card');
const editBtn = document.getElementById('ov-edit-btn');
const cancelBtn = document.getElementById('ov-cancel-btn');
const saveBtn = document.getElementById('ov-save-btn');
if (!card || !editBtn || !cancelBtn || !saveBtn) return;
// 同时同步顶部 h1 标题 — 商品名称改动后,顶部大标题也跟着更新
const pdName = document.getElementById('pd-name');
function escapeHtml(s) {
return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
const X_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
// bullet-list 编辑模式 · 操作 helpers
function renumberBullets(list) {
[...list.querySelectorAll('.bl-item .num')].forEach((n, i) => n.textContent = i + 1);
}
function bindBulletX(x, list) {
x.addEventListener('click', () => {
x.closest('.bl-item').remove();
renumberBullets(list);
});
}
function makeBulletLi(text) {
const li = document.createElement('li');
li.className = 'bl-item';
const safe = (text || '').replace(/"/g, '&quot;');
li.innerHTML = `<span class="num"></span><input class="bl-input bl-edit" type="text" value="${safe}" placeholder="卖点内容"><span class="bl-x" title="删除">${X_SVG}</span>`;
return li;
}
function addBulletItem(list, text) {
const li = makeBulletLi(text);
list.querySelector('.bl-add').before(li);
bindBulletX(li.querySelector('.bl-x'), list);
renumberBullets(list);
}
function renderBulletEditor(list, items) {
list.innerHTML = '';
// 已有项 (每条都是 input 可直接修改)
items.forEach(text => list.appendChild(makeBulletLi(text)));
// 添加行
const addLi = document.createElement('li');
addLi.className = 'bl-add';
addLi.innerHTML = `<span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认">`;
list.appendChild(addLi);
// 绑定已有项的删除
list.querySelectorAll('.bl-item .bl-x').forEach(x => bindBulletX(x, list));
// 绑定回车追加
const addInput = addLi.querySelector('.bl-input');
addInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
const v = addInput.value.trim();
if (!v) return;
addBulletItem(list, v);
addInput.value = '';
addInput.focus();
}
});
renumberBullets(list);
}
// 进入编辑模式: 把当前静态值灌进 input
function enterEdit() {
card.querySelectorAll('[data-field]').forEach(row => {
const stat = row.querySelector('.v-static');
const inp = row.querySelector('.v-edit');
if (!stat || !inp) return;
if (row.dataset.field === 'bullets') {
const items = [...stat.querySelectorAll('.bullet')].map(b => b.textContent.trim());
renderBulletEditor(inp, items);
} else if (inp.tagName === 'SELECT') {
const cur = stat.textContent.trim();
[...inp.options].forEach((o, i) => { if (o.textContent.trim() === cur) inp.selectedIndex = i; });
} else {
inp.value = stat.textContent.trim();
}
});
card.classList.add('editing');
}
function exitEdit() {
card.classList.remove('editing');
}
function save() {
card.querySelectorAll('[data-field]').forEach(row => {
const stat = row.querySelector('.v-static');
const inp = row.querySelector('.v-edit');
if (!stat || !inp) return;
if (row.dataset.field === 'bullets') {
// 从 .bl-item .bl-edit input 读值 (允许修改已有条目)
const items = [...inp.querySelectorAll('.bl-item .bl-edit')]
.map(t => t.value.trim()).filter(Boolean);
stat.innerHTML = items.map(s => `<span class="bullet">${escapeHtml(s)}</span>`).join('');
} else {
const val = (inp.tagName === 'SELECT') ? inp.options[inp.selectedIndex].textContent : inp.value;
stat.textContent = val.trim();
if (row.dataset.field === 'name' && pdName) {
pdName.textContent = val.trim() || pdName.textContent;
}
}
});
card.classList.remove('editing');
Shell.toast('已保存', '商品信息已更新');
}
// 重置 · 在编辑模式下,把所有输入框回退到当前静态值 (相当于重新进入 edit)
function resetEdit() {
card.querySelectorAll('[data-field]').forEach(row => {
const stat = row.querySelector('.v-static');
const inp = row.querySelector('.v-edit');
if (!stat || !inp) return;
if (row.dataset.field === 'bullets') {
const items = [...stat.querySelectorAll('.bullet')].map(b => b.textContent.trim());
renderBulletEditor(inp, items);
} else if (inp.tagName === 'SELECT') {
const cur = stat.textContent.trim();
[...inp.options].forEach((o, i) => { if (o.textContent.trim() === cur) inp.selectedIndex = i; });
} else {
inp.value = stat.textContent.trim();
}
});
Shell.toast('已重置');
}
const resetBtn = document.getElementById('ov-reset-btn');
// 防御性: 先清空可能存在的 inline onclick, 再用 addEventListener 绑定
editBtn.onclick = null;
cancelBtn.onclick = null;
saveBtn.onclick = null;
if (resetBtn) resetBtn.onclick = null;
editBtn.addEventListener('click', (e) => { e.preventDefault(); enterEdit(); });
cancelBtn.addEventListener('click', (e) => { e.preventDefault(); exitEdit(); });
saveBtn.addEventListener('click', (e) => { e.preventDefault(); save(); });
if (resetBtn) resetBtn.addEventListener('click', (e) => { e.preventDefault(); resetEdit(); });
// 编辑模式下,点缩略图 → 删除
const grid = document.getElementById('ov-images-grid');
if (grid) {
grid.addEventListener('click', e => {
if (!card.classList.contains('editing')) return;
const thumb = e.target.closest('.thumb');
if (thumb && !thumb.classList.contains('img-upload')) {
thumb.remove();
}
});
// [+] 上传占位
const addBtn = document.getElementById('ov-img-add');
if (addBtn) addBtn.onclick = () => Shell.toast('上传图片', '请选择本地图片 (占位)');
}
})();
// AI 生成三视图 · 按钮悬浮 panel(可重复打开 / X 关闭 / 点击外部关闭 / Esc 关闭)
(function initTriView() {
const btn = document.getElementById('ov-tri-btn');
const pop = document.getElementById('ov-tri-pop');
const closeBtn = document.getElementById('ov-tri-close');
const startBtn = document.getElementById('ov-tri-start');
const img = document.getElementById('ov-tri-img');
const statusEl = document.getElementById('ov-tri-status');
const foot = document.getElementById('ov-tri-foot');
const history = document.getElementById('ov-tri-history');
const historyRow = document.getElementById('ov-tri-history-row');
const historyCount = document.getElementById('ov-tri-history-count');
if (!btn || !pop || !closeBtn || !startBtn) return;
const versions = []; // [{ ts, label }]
let previewIdx = -1; // 主图当前正在「预览」哪一版(浏览态)
let adoptedIdx = -1; // 真正被「采用」的那一版 · 与素材库通过状态联动
let generating = false;
function prodName() {
return (document.getElementById('pd-name')?.textContent || '商品').trim();
}
function open() {
pop.classList.add('show');
btn.classList.add('is-open');
btn.setAttribute('aria-expanded', 'true');
}
function close() {
pop.classList.remove('show');
btn.classList.remove('is-open');
btn.setAttribute('aria-expanded', 'false');
}
function toggle() {
if (pop.classList.contains('show')) close(); else open();
}
function renderHistory() {
if (versions.length === 0) { history.classList.remove('show'); return; }
history.classList.add('show');
historyCount.textContent = versions.length;
historyRow.innerHTML = versions.map((ver, i) => {
const isAdopted = i === adoptedIdx;
const isPreview = i === previewIdx;
const cls = [
isAdopted ? 'adopted' : '',
isPreview && !isAdopted ? 'previewing' : '',
].filter(Boolean).join(' ');
const titleParts = [ver.label, ver.ts];
if (isAdopted) titleParts.push('已采用');
else if (isPreview) titleParts.push('预览中');
return `
<div class="h-thumb ${cls}" data-idx="${i}" title="${titleParts.join(' · ')}">
<span class="badge">已采用</span>
<span class="v">${ver.label}</span>
</div>
`;
}).join('');
historyRow.querySelectorAll('.h-thumb').forEach(el => {
el.addEventListener('click', () => {
const idx = Number(el.dataset.idx);
if (idx === previewIdx) return;
setPreview(idx);
});
});
}
function renderMain() {
if (previewIdx < 0) return;
const ver = versions[previewIdx];
const isAdopted = previewIdx === adoptedIdx;
img.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
img.classList.add('is-zoomable');
img.title = '点击放大查看';
statusEl.textContent = isAdopted
? `${ver.label} · 已采用,不满意可重跑`
: `${ver.label} · 预览中(未采用)`;
foot.innerHTML = `
<button class="ov-edit" type="button" id="ov-tri-rerun" style="height:28px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
重跑
</button>
<button class="ov-edit ${isAdopted ? '' : 'primary'}" type="button" id="ov-tri-adopt" style="height:28px;" ${isAdopted ? 'disabled title="此版本已采用"' : 'title="将此版本设为唯一通过版本,其他版本变为不通过"'}>
${isAdopted
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg> 已采用'
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg> 采用此版本'}
</button>
<span style="flex:1;"></span>
<span class="mono" style="font-size:11px; color: var(--black-alpha-56);">~¥0.30 / 次</span>
`;
document.getElementById('ov-tri-rerun')?.addEventListener('click', start);
document.getElementById('ov-tri-adopt')?.addEventListener('click', adoptPreview);
}
function syncLibraryStatus() {
const grid = document.querySelector('.tab-pane[data-pane="assets"] .asset-grid');
if (!grid) return;
const adoptedLabel = versions[adoptedIdx]?.label;
grid.querySelectorAll('.asset-card[data-tri-version]').forEach(c => {
const pill = c.querySelector('.pill');
if (!pill) return;
const isAdopted = c.dataset.triVersion === adoptedLabel;
pill.className = 'pill ' + (isAdopted ? 'pass' : 'fail');
pill.textContent = isAdopted ? '通过' : '不通过';
pill.setAttribute('data-status', isAdopted ? 'pass' : 'fail');
pill.setAttribute('title', isAdopted ? '当前采用版本' : '未被采用');
});
}
function appendLibraryCard(ver) {
const grid = document.querySelector('.tab-pane[data-pane="assets"] .asset-grid');
if (!grid) return;
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const dateStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
const card = document.createElement('div');
card.className = 'asset-card';
card.dataset.triVersion = ver.label;
// 新生成的版本默认 `不通过`,等用户点「采用此版本」才转为通过
card.innerHTML = `
<div class="thumb placeholder"><span class="type-pill">三视图</span><span class="ph-frame">${prodName()} · ${ver.label}</span></div>
<div class="meta"><span class="pill fail" data-status="fail" title="未被采用">不通过</span><span class="date">${dateStr}</span></div>
`;
grid.prepend(card);
// 更新「全部 AI 素材 (N)」计数
const ct = document.querySelector('.pd-toolbar .total .ct');
if (ct) {
const m = ct.textContent.match(/(\d+)/);
const n = m ? Number(m[1]) + 1 : 1;
ct.textContent = `(${n})`;
}
}
// 切换主图预览(不动采用状态、不动素材库)
function setPreview(idx) {
previewIdx = idx;
renderHistory();
renderMain();
}
// 显式「采用」当前预览版本 · 同步素材库通过/不通过
function adoptPreview() {
if (previewIdx < 0) return;
if (previewIdx === adoptedIdx) return;
adoptedIdx = previewIdx;
renderHistory();
renderMain();
syncLibraryStatus();
if (window.Shell?.toast) {
Shell.toast('已采用 ' + versions[adoptedIdx].label,
prodName() + ' · 该版本通过,其余版本转为不通过');
}
}
function renderLoading() {
img.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px;align-items:center;"><div class="spinner"></div><span class="ph-frame" style="font-size:10.5px;">生成中</span></div>`;
img.classList.remove('is-zoomable');
img.removeAttribute('title');
statusEl.textContent = '生成中 · 约 12s';
foot.innerHTML = '<span class="mono" style="font-size:11px; color: var(--black-alpha-48);">// POST /assets/tri-view</span>';
}
function openLightbox() {
if (previewIdx < 0) return;
const ver = versions[previewIdx];
const isAdopted = previewIdx === adoptedIdx;
const lbImg = document.getElementById('ov-tri-lightbox-img');
const lbLabel = document.getElementById('ov-tri-lightbox-label');
const lbTag = document.getElementById('ov-tri-lightbox-tag');
const lbMeta = document.getElementById('ov-tri-lightbox-meta');
if (lbImg) lbImg.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
if (lbLabel) lbLabel.textContent = ver.label;
if (lbTag) {
lbTag.hidden = !isAdopted;
lbTag.textContent = '已采用';
}
if (lbMeta) lbMeta.textContent = `// 生成于 ${ver.ts}`;
window.Shell?.openModal?.('ov-tri-lightbox-bg');
}
function start() {
if (generating) return;
generating = true;
open();
renderLoading();
setTimeout(() => {
generating = false;
const now = new Date();
const ts = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
const newVer = { ts, label: 'v' + (versions.length + 1) };
versions.push(newVer);
appendLibraryCard(newVer);
const newIdx = versions.length - 1;
previewIdx = newIdx;
// 第一次生成 · 自动采用新版本(无选择可言);之后只切预览,不动采用
if (adoptedIdx === -1) {
adoptedIdx = newIdx;
syncLibraryStatus();
}
renderHistory();
renderMain();
if (window.Shell?.toast) {
const tip = (adoptedIdx === newIdx)
? `${newVer.label} · 已采用并同步到素材库`
: `${newVer.label} · 预览中 · 满意请点「采用此版本」`;
Shell.toast('三视图已生成', `${prodName()} · ${tip}`);
}
}, 1800);
}
btn.addEventListener('click', (e) => { e.stopPropagation(); toggle(); });
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); close(); });
startBtn.addEventListener('click', (e) => { e.stopPropagation(); start(); });
pop.addEventListener('click', (e) => e.stopPropagation());
img.addEventListener('click', (e) => {
if (!img.classList.contains('is-zoomable')) return;
e.stopPropagation();
openLightbox();
});
document.addEventListener('click', (e) => {
if (!pop.classList.contains('show')) return;
if (pop.contains(e.target) || btn.contains(e.target)) return;
close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && pop.classList.contains('show')) close();
});
})();
// 从 create 跳来时显示 toast + 清空三个 tab 数据(新商品没有素材/项目/任务)
if (location.search.includes('id=new')) {
setTimeout(() => Shell.toast('商品已创建', '开始创建 AI 资产'), 200);
// 从 sessionStorage 读出 drawer 刚保存的完整 product,注入到「商品信息」卡
(function injectFromSession() {
let p = null;
try {
const raw = sessionStorage.getItem('npd-last-created');
if (raw) p = JSON.parse(raw);
sessionStorage.removeItem('npd-last-created'); // 读完即清,避免污染
} catch (e) { /* ignore */ }
if (!p) return;
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// 商品名称(URL 已经设过,这里兜底)
if (p.name) {
const nm = document.querySelector('[data-field="name"] .v-static');
if (nm) nm.textContent = p.name;
const nmI = document.querySelector('[data-field="name"] .v-input');
if (nmI) nmI.value = p.name;
}
// 品类
if (p.cat) {
const catRow = document.querySelector('[data-field="cat"] .v-static');
if (catRow) catRow.textContent = p.cat;
const catSel = document.querySelector('[data-field="cat"] .v-select');
if (catSel) {
// 找到匹配 option 并 select;若没有则插入到首位
let matched = false;
[...catSel.options].forEach(o => { if (o.value === p.cat || o.textContent === p.cat) { o.selected = true; matched = true; } });
if (!matched) {
const opt = document.createElement('option');
opt.value = p.cat; opt.textContent = p.cat; opt.selected = true;
catSel.insertBefore(opt, catSel.firstChild);
}
}
}
// 目标人群(可空)
const tgtRow = document.querySelector('[data-field="target"] .v-static');
const tgtIn = document.querySelector('[data-field="target"] .v-input');
if (tgtRow) tgtRow.textContent = p.target || '—';
if (tgtIn) tgtIn.value = p.target || '';
// 卖点
if (Array.isArray(p.points)) {
const blStatic = document.querySelector('[data-field="bullets"] .v-static');
if (blStatic) {
blStatic.innerHTML = p.points.length
? p.points.map(t => `<span class="bullet">${esc(t)}</span>`).join('')
: '<span class="bullet" style="color:var(--black-alpha-48)">—</span>';
}
}
// 商品图片 — 替换 6 张占位为真实 dataUrl;数量按上传数定
const grid = document.getElementById('ov-images-grid');
const addBtn = document.getElementById('ov-img-add');
if (grid) {
// 移除现有所有 .thumb 占位(保留末尾 #ov-img-add)
[...grid.querySelectorAll('.thumb')].forEach(t => t.remove());
if (Array.isArray(p.images) && p.images.length) {
p.images.forEach(img => {
const t = document.createElement('div');
t.className = 'thumb';
t.style.cssText = 'background-image:url(' + img.dataUrl + ');background-size:cover;background-position:center;';
if (addBtn) grid.insertBefore(t, addBtn);
else grid.appendChild(t);
});
}
// 同步计数
const ct = document.querySelector('.ov-images-sub .sub-h .ct');
if (ct) ct.textContent = '(' + ((p.images || []).length) + ')';
}
})();
const EMPTY_HTML = (txt) => `<div class="empty-filter">// NO DATA<br><span style="margin-top:6px;display:inline-block">${txt}</span></div>`;
// AI 生成素材
const assetsPane = document.querySelector('.tab-pane[data-pane="assets"]');
if (assetsPane) {
const grid = assetsPane.querySelector('.asset-grid');
if (grid) grid.outerHTML = EMPTY_HTML('还没有 AI 素材,使用右上角「图片生成」开始创建');
const ct = assetsPane.querySelector('.total .ct');
if (ct) ct.textContent = '(0)';
const more = assetsPane.querySelector('.pd-more');
if (more) more.remove();
}
// 视频项目
const videosPane = document.querySelector('.tab-pane[data-pane="videos"]');
if (videosPane) {
const grid = videosPane.querySelector('.asset-grid');
if (grid) grid.outerHTML = EMPTY_HTML('还没有视频项目,前往工作台「新建项目」开始');
const ct = videosPane.querySelector('.total .ct');
if (ct) ct.textContent = '(0)';
const more = videosPane.querySelector('.pd-more');
if (more) more.remove();
}
// 任务记录
const tasksPane = document.querySelector('.tab-pane[data-pane="tasks"]');
if (tasksPane) {
tasksPane.querySelectorAll('.task-stat .v').forEach(el => {
const small = el.querySelector('small');
el.textContent = '0';
if (small) el.appendChild(small);
});
const tbl = tasksPane.querySelector('.task-table');
if (tbl) tbl.outerHTML = EMPTY_HTML('暂无任务记录');
const ct = tasksPane.querySelector('.total .ct');
if (ct) ct.textContent = '(0)';
const more = tasksPane.querySelector('.pd-more');
if (more) more.remove();
}
}
</script>
</body>
</html>