feat: refine asset media and project setup flows
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
This commit is contained in:
parent
e06a16e200
commit
2ba1058329
@ -1566,7 +1566,7 @@ submitBtn.addEventListener('click', () => {
|
||||
card.dataset.source = '手动上传';
|
||||
card.dataset.used = '0';
|
||||
card.dataset.added = stamp;
|
||||
card.setAttribute('onclick', `Shell.toast('查看资产', ${JSON.stringify(name)})`);
|
||||
card.setAttribute('onclick', `if(!document.body.classList.contains('edit-mode')&&window.LibraryAssetDetailOpen){window.LibraryAssetDetailOpen(this)}else if(!document.body.classList.contains('edit-mode')){Shell.toast('查看资产', ${JSON.stringify(name)})}`);
|
||||
|
||||
let metaText = '';
|
||||
if (kind === 'people') {
|
||||
@ -1979,6 +1979,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
|
||||
.asset-modal-bg.show { display: flex; }
|
||||
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
|
||||
.asset-modal.product-mode { width: min(1180px, 100%); }
|
||||
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
|
||||
.asset-modal-h h2 { font-size: 15px; font-weight: 600; }
|
||||
.asset-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
@ -1997,9 +1998,11 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
.asset-detail-tri-row .placeholder:hover .ad-zoom-btn { opacity: 1; }
|
||||
.asset-detail-tri-row .placeholder { position: relative; }
|
||||
.asset-detail-lead .ad-thumbs { display: flex; gap: 8px; }
|
||||
.asset-detail-lead .ad-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); cursor: pointer; overflow: hidden; }
|
||||
.asset-detail-lead .ad-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); cursor: pointer; overflow: hidden; background-size: cover; background-position: center; display: grid; place-items: center; }
|
||||
.asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }
|
||||
.asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }
|
||||
.asset-detail-lead .ad-thumbs .thumb.thumb-wide { aspect-ratio: 16/9; }
|
||||
.asset-detail-lead .ad-thumbs .thumb .ph-frame { font-size: 10px; }
|
||||
.asset-detail-right .ad-section + .ad-section { margin-top: 18px; }
|
||||
.asset-detail-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }
|
||||
.asset-detail-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }
|
||||
@ -2022,6 +2025,91 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
.ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }
|
||||
.ad-props .ad-prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }
|
||||
.ad-props .ad-prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }
|
||||
.asset-detail-product[hidden], .asset-detail-right[hidden] { display: none !important; }
|
||||
.asset-detail-product { min-width: 0; }
|
||||
.asset-modal.product-mode .asset-modal-body { padding: 0; }
|
||||
.asset-modal.product-mode .asset-detail-grid { grid-template-columns: 280px minmax(0, 1fr); gap: 0; min-height: 640px; }
|
||||
.asset-modal.product-mode .asset-detail-lead { padding: 20px; border-right: 1px solid var(--border-faint); background: var(--background-lighter); gap: 16px; }
|
||||
.asset-modal.product-mode .asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 1; background-color: var(--surface); }
|
||||
.asset-modal.product-mode .asset-detail-lead .placeholder.ad-lead-img.tri-previewing { aspect-ratio: 16/9; background-color: var(--background-lighter); border-style: dashed; color: var(--black-alpha-64); }
|
||||
.asset-modal.product-mode .asset-detail-lead .placeholder.ad-lead-img.tri-previewing .ph-frame { max-width: 92%; white-space: normal; line-height: 1.5; }
|
||||
.asset-modal.product-mode .ad-thumbs { display: block; }
|
||||
.product-side-info { display: grid; gap: 6px; }
|
||||
.product-side-info .psi-name { font-size: 14px; font-weight: 600; color: var(--accent-black); line-height: 1.45; }
|
||||
.product-side-info .psi-meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.6; }
|
||||
.product-side-nav { display: grid; gap: 6px; padding-top: 10px; border-top: 1px solid var(--border-faint); }
|
||||
.product-side-nav .psn-item { width: 100%; height: 34px; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 10px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--black-alpha-64); font: inherit; font-size: 12.5px; cursor: pointer; text-align: left; }
|
||||
.product-side-nav .psn-item:hover { background: var(--black-alpha-4); color: var(--accent-black); }
|
||||
.product-side-nav .psn-item.active { background: var(--heat-12); color: var(--heat); }
|
||||
.product-side-nav .psn-item .ct { font-family: var(--font-mono); font-size: 10.5px; color: currentColor; opacity: .72; }
|
||||
.product-gallery { padding: 22px 24px 28px; }
|
||||
.product-gallery-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 20px; }
|
||||
.product-gallery-head .pgh-title { font-size: 14px; font-weight: 600; color: var(--accent-black); line-height: 1.45; }
|
||||
.product-gallery-head .pgh-sub { margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.product-gallery-head .pgh-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||||
.product-gallery-head .pgh-actions .btn { height: 32px; }
|
||||
.product-source-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: -6px 0 20px; }
|
||||
.product-source-filter .chip { height: 30px; padding: 0 12px; font-size: 12px; }
|
||||
.product-block + .product-block { margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--border-faint); }
|
||||
.product-block-h { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
|
||||
.product-block-h .t { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||||
.product-block-h .m { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||||
.product-block-h .spacer { flex: 1; }
|
||||
.product-block-h .mini-action { height: 26px; padding: 0 10px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); color: var(--black-alpha-72); font-size: 11.5px; font-family: inherit; cursor: pointer; }
|
||||
.product-block-h .mini-action:hover { background: var(--black-alpha-4); color: var(--accent-black); border-color: var(--black-alpha-24); }
|
||||
.tri-header-actions { display: inline-flex; align-items: center; gap: 8px; }
|
||||
.tri-header-actions[hidden] { display: none; }
|
||||
.tri-header-actions .btn { height: 26px; padding: 0 10px; font-size: 11.5px; }
|
||||
.product-media-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
|
||||
.product-media-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; background: var(--surface); cursor: pointer; transition: background var(--t-base), border-color var(--t-base); }
|
||||
.product-media-card[hidden], .product-triview-card[hidden], .product-block[hidden] { display: none; }
|
||||
.product-media-card:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); }
|
||||
.product-media-card.active { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; }
|
||||
.product-media-card .pm-img { aspect-ratio: 1; background: var(--background-lighter); background-size: cover; background-position: center; position: relative; display: grid; place-items: center; }
|
||||
.product-media-card .pm-img.wide { aspect-ratio: 16/9; }
|
||||
.product-media-card .pm-img .asset-badge { top: 6px; left: 6px; }
|
||||
.product-media-card .pm-b { padding: 9px 10px 10px; min-width: 0; }
|
||||
.product-media-card .pm-t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.product-media-card .pm-m { margin-top: 3px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.product-triview-card { display: grid; grid-template-columns: minmax(0, 1fr) 220px; gap: 12px; align-items: stretch; border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px; background: var(--surface); cursor: pointer; }
|
||||
.product-triview-card:hover { border-color: var(--black-alpha-24); background: var(--black-alpha-4); }
|
||||
.product-triview-card.active { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; }
|
||||
.product-triview-card.tri-pending { border-color: var(--heat-40); background: var(--heat-12); }
|
||||
.product-triview-card .tri-preview { min-height: 132px; aspect-ratio: 16/9; border-radius: var(--r-md); background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; position: relative; overflow: hidden; }
|
||||
.product-triview-card .tri-preview .ph-frame { font-size: 11px; }
|
||||
.tri-meta-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.tri-meta-list .tm-row { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; padding-bottom: 8px; border-bottom: 1px solid var(--border-faint); }
|
||||
.tri-meta-list .tm-row:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
.tri-meta-list .k { color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; }
|
||||
.tri-meta-list .v { color: var(--accent-black); font-weight: 500; text-align: right; }
|
||||
.tri-version-panel { margin-top: 10px; padding: 9px 10px 10px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--background-lighter); overflow: hidden; }
|
||||
.tri-version-panel + .product-triview-card { margin-top: 10px; }
|
||||
.tri-version-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
||||
.tri-version-head .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
|
||||
.tri-version-head .m { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; color: var(--black-alpha-48); }
|
||||
.tri-version-list { display: flex; gap: 6px; overflow-x: auto; overflow-y: hidden; padding: 0 0 4px; scrollbar-width: thin; scrollbar-color: var(--black-alpha-24) transparent; }
|
||||
.tri-version-list::-webkit-scrollbar { height: 6px; }
|
||||
.tri-version-list::-webkit-scrollbar-track { background: transparent; }
|
||||
.tri-version-list::-webkit-scrollbar-thumb { background: var(--black-alpha-12); border-radius: var(--r-pill); }
|
||||
.tri-version-list:hover::-webkit-scrollbar-thumb { background: var(--black-alpha-24); }
|
||||
.tri-version-item { flex: 0 0 132px; min-height: 44px; padding: 7px 8px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); color: var(--black-alpha-64); font: inherit; cursor: pointer; text-align: left; display: grid; gap: 3px; transition: background var(--t-base), border-color var(--t-base); }
|
||||
.tri-version-item:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); color: var(--accent-black); }
|
||||
.tri-version-item.active { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; color: var(--accent-black); }
|
||||
.tri-version-item.current:not(.active) { border-color: var(--forest-bd); background: var(--forest-bg); }
|
||||
.tri-version-item.pending { background: var(--heat-12); border-color: var(--heat-20); }
|
||||
.tri-version-item .tv-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.tri-version-item .tv-v { font-size: 12px; font-weight: 600; color: var(--accent-black); }
|
||||
.tri-version-item .tv-tag { height: 17px; display: inline-flex; align-items: center; padding: 0 6px; border: 1px solid var(--border-faint); border-radius: var(--r-pill); font-size: 10px; white-space: nowrap; }
|
||||
.tri-version-item .tv-tag.info { color: var(--heat); background: var(--heat-12); border-color: var(--heat-20); }
|
||||
.tri-version-item .tv-tag.ok { color: var(--accent-forest); background: var(--forest-bg); border-color: var(--forest-bd); }
|
||||
.tri-version-item .tv-tag.neutral { color: var(--black-alpha-56); background: var(--black-alpha-4); border-color: var(--border-faint); }
|
||||
.tri-version-item .tv-meta { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
@media (max-width: 1100px) {
|
||||
.asset-detail-grid { grid-template-columns: 280px 1fr; }
|
||||
.product-media-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.product-triview-card { grid-template-columns: 1fr; }
|
||||
.tri-version-item { flex-basis: 124px; }
|
||||
}
|
||||
.asset-detail-tip { margin-top: 10px; padding: 10px 12px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); display: flex; align-items: center; gap: 8px; line-height: 1.5; }
|
||||
.asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
|
||||
.asset-detail-tip .ai-gen-btn { margin-left: auto; height: 26px; padding: 0 10px; background: var(--heat); color: #fff; border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; flex-shrink: 0; display: inline-flex; align-items: center; }
|
||||
@ -2080,7 +2168,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
</div>
|
||||
<div class="ad-thumbs" id="lib-detail-thumbs"></div>
|
||||
</div>
|
||||
<div class="asset-detail-right">
|
||||
<div class="asset-detail-right" id="lib-detail-generic-pane">
|
||||
<div class="ad-section" id="lib-detail-tri-section">
|
||||
<div class="asset-detail-section-h">
|
||||
<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="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
|
||||
@ -2114,6 +2202,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
</div>
|
||||
<div class="ad-props" id="lib-detail-props"></div>
|
||||
</div>
|
||||
<div class="asset-detail-product" id="lib-detail-product-pane" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-modal-f">
|
||||
@ -2149,10 +2238,13 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
|
||||
const bg = document.getElementById('lib-detail-bg');
|
||||
const modalEl = bg.querySelector('.asset-modal');
|
||||
const titleEl = document.getElementById('lib-detail-title');
|
||||
const kindEl = document.getElementById('lib-detail-kind');
|
||||
const leadImg = document.getElementById('lib-detail-lead-img');
|
||||
const thumbsEl = document.getElementById('lib-detail-thumbs');
|
||||
const genericPane = document.getElementById('lib-detail-generic-pane');
|
||||
const productPane = document.getElementById('lib-detail-product-pane');
|
||||
const triSection = document.getElementById('lib-detail-tri-section');
|
||||
const triEl = document.getElementById('lib-detail-tri');
|
||||
const ratioChip = document.getElementById('lib-detail-ratio');
|
||||
@ -2178,6 +2270,406 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
let _generating = false;
|
||||
let _allowGen = false; // 是否启用生成入口(missing tri-view 才启用)
|
||||
|
||||
function _esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
function _imgStyle(src) {
|
||||
return src ? ' style="background-image:url(' + _esc(src) + ')"' : '';
|
||||
}
|
||||
|
||||
function _productInfo(productName, assetName) {
|
||||
const key = productName + ' ' + assetName;
|
||||
const map = [
|
||||
{ test: /面膜|补水/i, product: '透真玻尿酸补水面膜', cat: '美妆个护', sku: 'SKU-MASK-0515', hero: 'assets/mock/product-mask.png', tone: '敏感肌 · 熬夜修护' },
|
||||
{ test: /防晒/i, product: '透真清透物理防晒霜', cat: '美妆个护', sku: 'SKU-SUN-0508', hero: 'assets/mock/product-sunscreen.png', tone: '通勤防晒 · 物理防护' },
|
||||
{ test: /耳机|南卡/i, product: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', sku: 'SKU-EAR-0512', hero: 'assets/mock/product-earbuds.png', tone: '通勤降噪 · 长续航' },
|
||||
{ test: /速食|牛肉面|面/i, product: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', sku: 'SKU-NOODLE-0510', hero: 'assets/mock/product-noodle.png', tone: '加班夜宵 · 热汤治愈' },
|
||||
{ test: /咖啡|三顿半/i, product: '三顿半同款冻干咖啡粉', cat: '食品饮料', sku: 'SKU-COFFEE-0505', hero: 'assets/mock/product-coffee.png', tone: '早八提神 · 冷萃口感' },
|
||||
{ test: /炸锅|小熊/i, product: '小熊 4L 可视空气炸锅', cat: '家居家电', sku: 'SKU-FRYER-0503', hero: 'assets/mock/product-air-fryer.png', tone: '小户型 · 低脂快手' },
|
||||
{ test: /瑜伽|露露/i, product: '露露同款裸感瑜伽裤', cat: '运动户外', sku: 'SKU-YOGA-0430', hero: 'assets/mock/product-yoga-pants.png', tone: '通勤运动 · 裸感高弹' },
|
||||
];
|
||||
return map.find(x => x.test.test(key)) || { product: productName || assetName || '未绑定商品', cat: '商品图', sku: 'SKU-ASSET-' + (_hash(assetName || productName) % 1000), hero: '', tone: '商品素材 · 待补充标签' };
|
||||
}
|
||||
|
||||
function _assetKind(name, source) {
|
||||
if (/三视图|正.?侧.?背/.test(name)) return '商品三视图';
|
||||
if (/AI|生成|优化/.test(source) || /AI|优化|套图|场景/.test(name)) return 'AI 生成图';
|
||||
if (/上传|官方/.test(source) || /官方|实拍|原图/.test(name)) return '上传图';
|
||||
return '商品详情图';
|
||||
}
|
||||
|
||||
function _productMediaCard(item, idx) {
|
||||
return `<div class="product-media-card" data-product-media="${idx}" data-product-source="${_esc(item.sourceKey || 'detail')}">
|
||||
<div class="pm-img${item.wide ? ' wide' : ''}"${_imgStyle(item.img)}>
|
||||
<span class="asset-badge">${_esc(item.badge)}</span>
|
||||
${item.img ? '' : '<span class="ph-frame">' + _esc(item.frame || item.title) + '</span>'}
|
||||
</div>
|
||||
<div class="pm-b">
|
||||
<div class="pm-t">${_esc(item.title)}</div>
|
||||
<div class="pm-m">${_esc(item.meta)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _renderProductDetail(card, name, source, used) {
|
||||
const productName = card.dataset.product || name.split('·')[0].trim() || name;
|
||||
const info = _productInfo(productName, name);
|
||||
const currentKind = _assetKind(name, source);
|
||||
const hero = info.hero;
|
||||
const detailImages = [
|
||||
{ sourceKey: 'detail', badge: '主图', title: '官方主图', meta: '商品详情页 · 1:1', img: hero, frame: info.product },
|
||||
{ sourceKey: 'detail', badge: '细节', title: '包装细节', meta: '商品详情页 · 4:3', img: hero, frame: '包装细节' },
|
||||
{ sourceKey: 'detail', badge: '场景', title: '使用场景', meta: '商品详情页 · 3:4', img: hero, frame: '使用场景' },
|
||||
{ sourceKey: 'detail', badge: '当前', title: name, meta: source + ' · 当前查看', img: hero, frame: name },
|
||||
];
|
||||
const triImage = { sourceKey: 'tri', badge: '三视图', title: '正 / 侧 / 背合图', meta: '16:9 · 当前采用 v2', img: '', wide: true, frame: info.product + ' · 商品三视图' };
|
||||
const aiImages = [
|
||||
{ sourceKey: 'tryon', badge: '模特上身', title: 'Ava · 试用图', meta: '图片生成 · 已入库', img: hero, frame: '模特上身图' },
|
||||
{ sourceKey: 'platform', badge: '平台头图', title: '小红书首图', meta: '图片生成 · 3:4', img: hero, frame: '平台头图' },
|
||||
{ sourceKey: 'creation', badge: '图片创作', title: '卖点海报', meta: '图片生成 · 1:1', img: hero, frame: '卖点海报' },
|
||||
];
|
||||
const allMedia = [...detailImages, triImage, ...aiImages];
|
||||
const triIndex = detailImages.length;
|
||||
let triVersion = 2;
|
||||
let triVersionSeq = triVersion;
|
||||
let triGeneratingVersion = null;
|
||||
let triViewingVersion = triVersion;
|
||||
let triHistory = [
|
||||
{ version: 2, state: 'current', date: '2026-05-27', source: 'image-2 · 商品库生成' },
|
||||
{ version: 1, state: 'history', date: '2026-05-19', source: '商品详情页补录' },
|
||||
];
|
||||
|
||||
leadImg.style.backgroundImage = hero ? `url("${hero}")` : '';
|
||||
leadImg.style.backgroundSize = 'cover';
|
||||
leadImg.style.backgroundPosition = 'center';
|
||||
leadImg.innerHTML = hero ? '' : '<span class="ph-frame">' + _esc(name) + '</span>';
|
||||
thumbsEl.innerHTML = `
|
||||
<div class="product-side-info">
|
||||
<div class="psi-name">${_esc(info.product)}</div>
|
||||
<div class="psi-meta">// ${_esc(info.cat)} · ${_esc(info.tone)}</div>
|
||||
<div class="psi-meta">// 共 ${allMedia.length} 张 · 商品详情图 ${detailImages.length} · 三视图 1 · AI 生成图 ${aiImages.length}</div>
|
||||
</div>
|
||||
<div class="product-side-nav">
|
||||
<button class="psn-item active" type="button" data-product-jump="product-section-detail"><span>商品详情图</span><span class="ct">${detailImages.length}</span></button>
|
||||
<button class="psn-item" type="button" data-product-jump="product-section-triview"><span>商品三视图</span><span class="ct">1</span></button>
|
||||
<button class="psn-item" type="button" data-product-jump="product-section-ai"><span>AI 生成图</span><span class="ct">${aiImages.length}</span></button>
|
||||
</div>`;
|
||||
|
||||
titleEl.textContent = info.product + ' · 商品图片';
|
||||
kindEl.textContent = '/ 商品图';
|
||||
productPane.innerHTML = `
|
||||
<div class="product-gallery">
|
||||
<div class="product-gallery-head">
|
||||
<div>
|
||||
<div class="pgh-title">按来源整理商品详情页中的全部图片</div>
|
||||
<div class="pgh-sub">// 当前查看: ${_esc(currentKind)} · 用过 ${_esc(used)} 次</div>
|
||||
</div>
|
||||
<div class="pgh-actions">
|
||||
<button class="btn" type="button" data-product-action="open-product">打开商品详情</button>
|
||||
<button class="btn" type="button" data-product-action="upload-more">上传图片</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-source-filter" aria-label="图片来源筛选">
|
||||
<button class="chip active" type="button" data-product-filter="all">全部 ${allMedia.length}</button>
|
||||
<button class="chip" type="button" data-product-filter="detail">商品详情图 ${detailImages.length}</button>
|
||||
<button class="chip" type="button" data-product-filter="tri">商品三视图 1</button>
|
||||
<button class="chip" type="button" data-product-filter="tryon">模特上身 1</button>
|
||||
<button class="chip" type="button" data-product-filter="platform">平台头图 1</button>
|
||||
<button class="chip" type="button" data-product-filter="creation">图片创作 1</button>
|
||||
</div>
|
||||
|
||||
<div class="product-block" id="product-section-detail">
|
||||
<div class="product-block-h">
|
||||
<span class="t">商品详情图</span>
|
||||
<span class="m">// 商品详情页已绑定图片,不是单独的平台头图</span>
|
||||
</div>
|
||||
<div class="product-media-grid">${detailImages.map((item, i) => _productMediaCard(item, i)).join('')}</div>
|
||||
</div>
|
||||
|
||||
<div class="product-block" id="product-section-triview">
|
||||
<div class="product-block-h">
|
||||
<span class="t">商品三视图</span>
|
||||
<span class="m">// 单张 16:9 · 正 / 侧 / 背 合一</span>
|
||||
<span class="spacer"></span>
|
||||
<span class="tri-header-actions" hidden>
|
||||
<button class="btn btn-sm btn-primary" type="button" data-product-action="adopt-tri">采用此版</button>
|
||||
</span>
|
||||
<button class="mini-action" type="button" data-product-action="regen-tri">重新生成</button>
|
||||
</div>
|
||||
<div class="tri-version-panel">
|
||||
<div class="tri-version-head">
|
||||
<span class="t">版本记录</span>
|
||||
<span class="m">// 横向滚动查看,当前采用已标记</span>
|
||||
</div>
|
||||
<div class="tri-version-list" data-tri-history></div>
|
||||
</div>
|
||||
<div class="product-triview-card" data-product-media="${triIndex}" data-product-source="tri">
|
||||
<div class="tri-preview">
|
||||
<span class="ph-frame">${_esc(info.product)} · 三视图(正 / 侧 / 背) · v2</span>
|
||||
</div>
|
||||
<div class="tri-meta-list">
|
||||
<div class="tm-row"><span class="k">状态</span><span class="v">当前采用</span></div>
|
||||
<div class="tm-row"><span class="k">版本</span><span class="v">v2 · 2026-05-27</span></div>
|
||||
<div class="tm-row"><span class="k">用途</span><span class="v">视频镜头一致性 / 商品角度参考</span></div>
|
||||
<div class="tm-row"><span class="k">来源</span><span class="v">image-2 · 商品库生成</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-block" id="product-section-ai">
|
||||
<div class="product-block-h">
|
||||
<span class="t">AI 生成图</span>
|
||||
<span class="m">// 模特上身 / 平台头图 / 图片创作</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="mini-action" type="button" data-product-action="go-generate">继续生成</button>
|
||||
</div>
|
||||
<div class="product-media-grid">${aiImages.map((item, i) => _productMediaCard(item, detailImages.length + 1 + i)).join('')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function triVersionInfo(version) {
|
||||
const row = triHistory.find(v => v.version === version) || triHistory.find(v => v.state === 'current') || triHistory[0];
|
||||
const isGenerating = row.state === 'generating';
|
||||
const isCurrent = row.state === 'current' || row.version === triVersion;
|
||||
const stateText = isGenerating ? '生成中' : (isCurrent ? '当前采用' : '可切换采用');
|
||||
const tag = isGenerating ? 'info' : (isCurrent ? 'ok' : '');
|
||||
const suffix = isGenerating ? ' 生成中' : (isCurrent ? ' 当前采用' : '');
|
||||
return {
|
||||
...row,
|
||||
isGenerating,
|
||||
isCurrent,
|
||||
isAdoptable: !isGenerating && !isCurrent,
|
||||
stateText,
|
||||
tag,
|
||||
versionText: `v${row.version} · ${row.date}`,
|
||||
frame: `${info.product} · 三视图(正 / 侧 / 背) · v${row.version}${suffix}`,
|
||||
};
|
||||
}
|
||||
|
||||
function renderTriVersionHistory() {
|
||||
const historyEl = productPane.querySelector('[data-tri-history]');
|
||||
if (!historyEl) return;
|
||||
const rows = [...triHistory].sort((a, b) => b.version - a.version).map(row => triVersionInfo(row.version));
|
||||
historyEl.innerHTML = rows.map(row => `
|
||||
<button class="tri-version-item ${row.isCurrent ? 'current' : ''} ${row.isGenerating ? 'pending' : ''} ${triViewingVersion === row.version ? 'active' : ''}" type="button" data-tri-version="${row.version}">
|
||||
<span class="tv-top">
|
||||
<span class="tv-v">v${row.version}</span>
|
||||
${row.tag ? `<span class="tv-tag ${row.tag}">${_esc(row.stateText)}</span>` : ''}
|
||||
</span>
|
||||
<span class="tv-meta">${_esc(row.date)} · ${_esc(row.source)}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
historyEl.querySelectorAll('[data-tri-version]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
viewTriVersion(Number(btn.dataset.triVersion));
|
||||
});
|
||||
});
|
||||
historyEl.querySelector('.tri-version-item.active')?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
}
|
||||
|
||||
function renderTriPreview() {
|
||||
const triCard = productPane.querySelector('.product-triview-card');
|
||||
const headerActions = productPane.querySelector('.tri-header-actions');
|
||||
if (!triCard) return;
|
||||
const preview = triCard.querySelector('.tri-preview');
|
||||
const meta = triCard.querySelector('.tri-meta-list');
|
||||
const view = triVersionInfo(triViewingVersion);
|
||||
triCard.classList.toggle('tri-pending', view.isGenerating);
|
||||
if (headerActions) headerActions.hidden = !view.isAdoptable;
|
||||
preview.innerHTML = view.isGenerating
|
||||
? '<div style="display:grid;place-items:center;gap:8px;"><div class="spinner"></div><span class="ph-frame">三视图生成中 · 约 12s</span></div>'
|
||||
: `<span class="ph-frame">${_esc(view.frame)}</span>`;
|
||||
meta.innerHTML = `
|
||||
<div class="tm-row"><span class="k">状态</span><span class="v">${_esc(view.stateText)}</span></div>
|
||||
<div class="tm-row"><span class="k">版本</span><span class="v">${_esc(view.versionText)}</span></div>
|
||||
<div class="tm-row"><span class="k">用途</span><span class="v">视频镜头一致性 / 商品角度参考</span></div>
|
||||
<div class="tm-row"><span class="k">来源</span><span class="v">${_esc(view.source)}</span></div>`;
|
||||
}
|
||||
|
||||
function viewTriVersion(version) {
|
||||
triViewingVersion = version;
|
||||
renderTriPreview();
|
||||
renderTriVersionHistory();
|
||||
selectProductMedia(triIndex);
|
||||
}
|
||||
|
||||
function renderTriState(mode) {
|
||||
const triCard = productPane.querySelector('.product-triview-card');
|
||||
const regenBtn = productPane.querySelector('[data-product-action="regen-tri"]');
|
||||
if (!triCard) return;
|
||||
if (regenBtn) {
|
||||
const hasExtraVersion = triHistory.some(row => row.state === 'candidate' || row.state === 'generating');
|
||||
regenBtn.disabled = Boolean(triGeneratingVersion);
|
||||
regenBtn.textContent = triGeneratingVersion ? '生成中...' : (hasExtraVersion ? '再次生成' : '重新生成');
|
||||
}
|
||||
renderTriPreview();
|
||||
renderTriVersionHistory();
|
||||
}
|
||||
|
||||
function selectProductMedia(idx) {
|
||||
const item = allMedia[idx] || allMedia[0];
|
||||
const isTri = item.sourceKey === 'tri';
|
||||
const triView = triVersionInfo(triViewingVersion);
|
||||
const triLabel = triView.isGenerating
|
||||
? info.product + ' · 三视图生成中'
|
||||
: info.product + ' · 三视图 · v' + triView.version + (triView.isCurrent ? ' 当前采用' : '');
|
||||
leadImg.classList.toggle('tri-previewing', isTri && !item.img);
|
||||
if (!item.img) {
|
||||
leadImg.classList.remove('has-mock-media', 'mock-label');
|
||||
leadImg.style.removeProperty('--mock-media-url');
|
||||
leadImg.dataset.mockMediaApplied = '1';
|
||||
}
|
||||
leadImg.style.backgroundImage = item.img ? `url("${item.img}")` : '';
|
||||
leadImg.innerHTML = item.img ? '' : '<span class="ph-frame">' + _esc(isTri ? triLabel : (item.frame || item.title)) + '</span>';
|
||||
productPane.querySelectorAll('[data-product-media]').forEach(x => x.classList.toggle('active', Number(x.dataset.productMedia) === idx));
|
||||
}
|
||||
|
||||
function openProductUpload() {
|
||||
resetUploadModal();
|
||||
uploadState.kind = 'products';
|
||||
kindSel.value = 'products';
|
||||
syncKindFields();
|
||||
const productSelect = $('upload-product');
|
||||
const matchedOption = [...productSelect.options].find(opt => {
|
||||
const text = opt.textContent.trim();
|
||||
return text && (info.product.includes(text) || text.includes(info.product.replace(/^透真玻尿酸/, '透真').replace(/ ·.*/, '')));
|
||||
});
|
||||
if (matchedOption) productSelect.value = matchedOption.value;
|
||||
nameInput.value = info.product + ' · 补充图';
|
||||
syncSubmit();
|
||||
modalBg.style.zIndex = '1300';
|
||||
Shell.openModal('upload-modal-bg');
|
||||
setTimeout(() => fileInput.click(), 120);
|
||||
}
|
||||
|
||||
function startTriRegenerate() {
|
||||
if (triGeneratingVersion) return;
|
||||
const nextVersion = ++triVersionSeq;
|
||||
triGeneratingVersion = nextVersion;
|
||||
triViewingVersion = nextVersion;
|
||||
triHistory = [
|
||||
{ version: nextVersion, state: 'generating', date: '生成中', source: 'image-2 · 资产库重跑' },
|
||||
...triHistory.filter(row => row.version !== nextVersion),
|
||||
];
|
||||
renderTriState('loading');
|
||||
selectProductMedia(triIndex);
|
||||
setTimeout(() => {
|
||||
const generated = triHistory.find(row => row.version === nextVersion);
|
||||
if (!generated) return;
|
||||
generated.state = 'candidate';
|
||||
generated.date = '刚刚生成';
|
||||
triGeneratingVersion = null;
|
||||
triViewingVersion = nextVersion;
|
||||
triImage.title = '正 / 侧 / 背合图 v' + nextVersion;
|
||||
triImage.meta = '16:9 · v' + nextVersion;
|
||||
triImage.frame = info.product + ' · 商品三视图 · v' + nextVersion;
|
||||
renderTriState('pending');
|
||||
selectProductMedia(triIndex);
|
||||
Shell.toast('三视图已生成', 'v' + nextVersion + ' 已加入版本记录,可切换采用');
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function adoptTriVersion() {
|
||||
const selected = triHistory.find(row => row.version === triViewingVersion);
|
||||
if (!selected || selected.state === 'generating' || selected.version === triVersion) return;
|
||||
triHistory = triHistory.map(row => {
|
||||
if (row.version === selected.version) return { ...row, state: 'current' };
|
||||
if (row.state === 'generating') return row;
|
||||
return { ...row, state: 'candidate' };
|
||||
});
|
||||
triVersion = selected.version;
|
||||
triVersionSeq = Math.max(triVersionSeq, triVersion);
|
||||
triViewingVersion = triVersion;
|
||||
triImage.title = '正 / 侧 / 背合图 v' + triVersion;
|
||||
triImage.meta = '16:9 · 当前采用 v' + triVersion;
|
||||
triImage.frame = info.product + ' · 商品三视图 · v' + triVersion;
|
||||
renderTriState('current');
|
||||
selectProductMedia(triIndex);
|
||||
Shell.toast('已采用此版本三视图', info.product + ' · v' + triVersion);
|
||||
}
|
||||
|
||||
function applyProductSourceFilter(sourceKey) {
|
||||
const source = sourceKey || 'all';
|
||||
productPane.querySelectorAll('[data-product-media]').forEach(el => {
|
||||
el.hidden = source !== 'all' && el.dataset.productSource !== source;
|
||||
});
|
||||
productPane.querySelectorAll('.product-block').forEach(block => {
|
||||
const media = [...block.querySelectorAll('[data-product-media]')];
|
||||
block.hidden = media.length > 0 && media.every(el => el.hidden);
|
||||
});
|
||||
const firstVisible = productPane.querySelector('[data-product-media]:not([hidden])');
|
||||
if (firstVisible) selectProductMedia(Number(firstVisible.dataset.productMedia));
|
||||
}
|
||||
|
||||
thumbsEl.querySelectorAll('[data-product-jump]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const target = productPane.querySelector('#' + el.dataset.productJump);
|
||||
thumbsEl.querySelectorAll('.psn-item').forEach(x => x.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
target?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
productPane.querySelectorAll('[data-product-media]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = Number(el.dataset.productMedia);
|
||||
selectProductMedia(idx);
|
||||
const item = allMedia[idx];
|
||||
Shell.toast('已选中图片', item ? item.title : name);
|
||||
});
|
||||
});
|
||||
renderTriState('current');
|
||||
selectProductMedia(0);
|
||||
|
||||
productPane.querySelectorAll('[data-product-filter]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
productPane.querySelectorAll('[data-product-filter]').forEach(x => x.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
applyProductSourceFilter(btn.dataset.productFilter);
|
||||
});
|
||||
});
|
||||
|
||||
function bindProductActions() {
|
||||
productPane.querySelectorAll('[data-product-action]').forEach(btn => {
|
||||
if (btn.dataset.bound === '1') return;
|
||||
btn.dataset.bound = '1';
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const act = btn.dataset.productAction;
|
||||
if (act === 'open-product') {
|
||||
location.href = 'product-detail.html?product=' + encodeURIComponent(info.product);
|
||||
return;
|
||||
}
|
||||
if (act === 'upload-more') {
|
||||
openProductUpload();
|
||||
return;
|
||||
}
|
||||
if (act === 'new-video') {
|
||||
location.href = 'projects-new.html?product=' + encodeURIComponent(info.product);
|
||||
return;
|
||||
}
|
||||
if (act === 'go-generate') {
|
||||
location.href = 'image-optimize.html?product=' + encodeURIComponent(info.product);
|
||||
return;
|
||||
}
|
||||
if (act === 'regen-tri') {
|
||||
startTriRegenerate();
|
||||
return;
|
||||
}
|
||||
if (act === 'adopt-tri') {
|
||||
adoptTriVersion();
|
||||
return;
|
||||
}
|
||||
Shell.toast('已记录', info.product);
|
||||
});
|
||||
});
|
||||
}
|
||||
bindProductActions();
|
||||
}
|
||||
|
||||
function _renderHistory() {
|
||||
if (!_versions.length) { historyEl.style.display = 'none'; return; }
|
||||
historyEl.style.display = 'block';
|
||||
@ -2258,13 +2750,32 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
historyEl.style.display = 'none';
|
||||
tipEl.classList.remove('is-loading');
|
||||
_setAigenLabel('AI 生成三视图', false);
|
||||
modalEl.classList.remove('product-mode');
|
||||
genericPane.hidden = false;
|
||||
productPane.hidden = true;
|
||||
leadImg.style.backgroundImage = '';
|
||||
leadImg.style.backgroundSize = '';
|
||||
leadImg.style.backgroundPosition = '';
|
||||
applyBtn.textContent = '保存';
|
||||
|
||||
const name = card.dataset.name || '资产';
|
||||
_curName = name;
|
||||
const used = card.dataset.used || '0';
|
||||
const source = card.dataset.source || '平台预设';
|
||||
const isProductAsset = !!card.dataset.product || card.dataset.kind === '商品';
|
||||
let tagText = 'AI 素材', intro = '', tags = [], props = [], hasTri = false, isActor = false;
|
||||
|
||||
if (isProductAsset) {
|
||||
titleEl.textContent = name;
|
||||
modalEl.classList.add('product-mode');
|
||||
genericPane.hidden = true;
|
||||
productPane.hidden = false;
|
||||
applyBtn.textContent = '完成';
|
||||
_renderProductDetail(card, name, source, used);
|
||||
bg.classList.add('show');
|
||||
return;
|
||||
}
|
||||
|
||||
if (card.dataset.gender) {
|
||||
tagText = '人物 · 模特';
|
||||
isActor = true; hasTri = true;
|
||||
@ -2309,6 +2820,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
|
||||
titleEl.textContent = name;
|
||||
kindEl.textContent = '/ ' + tagText;
|
||||
leadImg.style.backgroundImage = '';
|
||||
leadImg.innerHTML = '<span class="ph-frame">' + name + '</span>';
|
||||
// 立绘 zoom 按钮(单次绑定 · 通过 name 闭包始终读最新 _curName)
|
||||
const _leadZoomBtn = document.getElementById('lib-detail-lead-zoom');
|
||||
@ -2385,6 +2897,7 @@ document.querySelectorAll('.asset-card').forEach(card => {
|
||||
|
||||
bg.classList.add('show');
|
||||
}
|
||||
window.LibraryAssetDetailOpen = open;
|
||||
// ── 二次确认弹窗 ──
|
||||
const confirmBg = document.getElementById('lib-confirm-bg');
|
||||
const confirmCountEl = document.getElementById('lib-confirm-count');
|
||||
|
||||
@ -442,8 +442,16 @@
|
||||
/* ── shared field styles ── */
|
||||
.field { display: block; margin-bottom: 16px; }
|
||||
.config-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: end; margin-bottom: 16px; }
|
||||
.config-row.single { grid-template-columns: minmax(0, 1fr); }
|
||||
.config-row .field { margin-bottom: 0; }
|
||||
.duration-select { cursor: pointer; }
|
||||
.duration-card-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.duration-card { width: 100%; min-height: 108px; text-align: left; font-family: inherit; }
|
||||
.duration-card .duration-title { font-size: 14px; font-weight: 600; color: var(--accent-black); }
|
||||
.duration-card .duration-meta { margin-top: 5px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.duration-card .duration-note { margin-top: auto; padding-top: 10px; font-size: 11.5px; color: var(--black-alpha-56); line-height: 1.45; }
|
||||
.duration-card.selected .duration-title,
|
||||
.duration-card.selected .duration-meta { color: var(--heat); }
|
||||
@media (max-width: 1100px) { .duration-card-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 900px) { .config-row { grid-template-columns: 1fr; } }
|
||||
.field-label { display: block; font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; margin-bottom: 6px; }
|
||||
.field-label .req { color: var(--heat); margin-left: 2px; }
|
||||
@ -654,10 +662,10 @@
|
||||
];
|
||||
|
||||
const DURATIONS = [
|
||||
{ id: '0-10', label: '0-10 秒', shots: [3, 4], tag: '黄金完播', completion: 52, conversion: 1.6 },
|
||||
{ id: '0-15', label: '0-15 秒', shots: [4, 5], tag: '完播率最佳', completion: 42, conversion: 1.8 },
|
||||
{ id: '0-30', label: '0-30 秒', shots: [6, 8], tag: '卖点详解', completion: 32, conversion: 2.1 },
|
||||
{ id: '0-60', label: '0-60 秒', shots: [10, 12], tag: '故事化', completion: 26, conversion: 2.4 },
|
||||
{ id: '0-10', label: '0-10 秒', shots: [3, 4], tag: '快速种草' },
|
||||
{ id: '0-15', label: '0-15 秒', shots: [4, 5], tag: '标准短片' },
|
||||
{ id: '0-30', label: '0-30 秒', shots: [6, 8], tag: '卖点展开' },
|
||||
{ id: '0-60', label: '0-60 秒', shots: [10, 12], tag: '故事化' },
|
||||
];
|
||||
|
||||
const STYLES = [
|
||||
@ -775,7 +783,7 @@
|
||||
if (s.id === 'manual') return state.manualScript.trim().length >= 20;
|
||||
return true;
|
||||
}
|
||||
function canPass3() { return state.projectName.trim().length >= 2; }
|
||||
function canPass3() { return state.projectName.trim().length >= 2 && !!state.duration; }
|
||||
function canFinish() { return canPass1() && canPass2() && canPass3() && state.agreed && balanceAfter() >= 5; }
|
||||
|
||||
/* ---------- icons ---------- */
|
||||
@ -839,8 +847,8 @@
|
||||
}
|
||||
|
||||
function startGenerate() {
|
||||
const p = getProduct(), d = getDuration(), st = getStyle();
|
||||
if (!p || !d || !st || state.projectName.trim().length < 2) return;
|
||||
const p = getProduct(), d = getDuration();
|
||||
if (!p || !d || state.projectName.trim().length < 2) return;
|
||||
// 持久化项目, 让 projects.html 下次加载时自动 prepend 到列表
|
||||
try {
|
||||
const seconds = (d.id.split('-')[1] || '15');
|
||||
@ -1001,11 +1009,11 @@
|
||||
============================================================ */
|
||||
|
||||
function railConfig() {
|
||||
const p = getProduct(), du = getDuration(), st = getStyle();
|
||||
const cfgReady = !!(du && st);
|
||||
const p = getProduct(), du = getDuration();
|
||||
const cfgReady = !!du && canPass3();
|
||||
return [
|
||||
{ n: 1, label: '选择商品', desc: p ? p.name : '未选择', done: canPass1() },
|
||||
{ n: 2, label: '项目配置', desc: cfgReady ? (du.label + ' · ' + st.name) : '时长 · 风格 · 人物', done: cfgReady && canPass3() },
|
||||
{ n: 2, label: '项目配置', desc: cfgReady ? du.label : '项目名 · 视频时长', done: cfgReady },
|
||||
];
|
||||
}
|
||||
|
||||
@ -1221,67 +1229,31 @@
|
||||
}
|
||||
|
||||
function renderStep3() {
|
||||
const personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
|
||||
let showReco = false, recoDur = null, recoStyle = null;
|
||||
if (personaObj && state.duration && state.scriptStyle) {
|
||||
const recoMismatch = personaObj.defaults.duration !== state.duration || personaObj.defaults.style !== state.scriptStyle;
|
||||
showReco = recoMismatch && !state.recoDismissed;
|
||||
recoDur = DURATIONS.find(d => d.id === personaObj.defaults.duration);
|
||||
recoStyle = STYLES.find(s => s.id === personaObj.defaults.style);
|
||||
}
|
||||
|
||||
return `<div class="wiz-pane active" data-step="2">
|
||||
<div class="wiz-step-h">
|
||||
<h2>第 2 步 · 项目配置</h2>
|
||||
<p>这些设置会影响 LLM 生成脚本的方向,确认后会进入流水线第 1 步(脚本生成)。</p>
|
||||
<p>基础配置只保留项目名和成片时长。脚本来源、风格和人物设定已移到流水线第 1 步由脚本助手引导。</p>
|
||||
</div>
|
||||
|
||||
<div class="config-row">
|
||||
<div class="config-row single">
|
||||
<div class="field">
|
||||
<label class="field-label">项目名称<span class="req">*</span></label>
|
||||
<input class="input" value="${esc(state.projectName)}" oninput="_wiz.setName(this.value)">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">视频时长<span class="req">*</span></label>
|
||||
<select class="input duration-select" onchange="_wiz.setDur(this.value)">
|
||||
<option value="" disabled ${state.duration ? '' : 'selected'}>选择时长</option>
|
||||
${DURATIONS.map(d => `<option value="${esc(d.id)}" ${state.duration === d.id ? 'selected' : ''}>${esc(d.label)} · ${d.shots[0]}-${d.shots[1]} 镜</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">脚本风格</label>
|
||||
<div class="opt-row cols-4">
|
||||
${STYLES.map(s => `<div class="opt-card${state.scriptStyle === s.id ? ' selected' : ''}" onclick="_wiz.setStyle('${s.id}')">
|
||||
<h4>${esc(s.name)}</h4>
|
||||
<div class="note">${esc(s.note)}</div>
|
||||
${s.tag ? `<span class="badge">[ ${esc(s.tag)} ]</span>` : ''}
|
||||
</div>`).join('')}
|
||||
<label class="field-label">视频时长<span class="req">*</span></label>
|
||||
<div class="opt-row duration-card-row">
|
||||
${DURATIONS.map(d => `<button class="opt-card duration-card${state.duration === d.id ? ' selected' : ''}" type="button" data-duration="${esc(d.id)}" aria-pressed="${state.duration === d.id ? 'true' : 'false'}" onclick="_wiz.setDur('${esc(d.id)}')">
|
||||
<span class="badge">[ ${esc(d.tag)} ]</span>
|
||||
<span class="duration-title">${esc(d.label)}</span>
|
||||
<span class="duration-meta">${d.shots[0]}-${d.shots[1]} 镜 · 9:16</span>
|
||||
<span class="duration-note">适合${d.id === '0-10' ? '快速种草' : d.id === '0-15' ? '短平快投放' : d.id === '0-30' ? '卖点展开' : '故事化表达'}</span>
|
||||
</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">人物设定</label>
|
||||
<div class="opt-row cols-6">
|
||||
${PERSONAS.map(p => `<div class="opt-card${state.persona === p.id ? ' selected' : ''}" onclick="_wiz.setPersona('${p.id}')">
|
||||
<h4>${esc(p.name)}</h4>
|
||||
<div class="sub">${esc(p.sub)}</div>
|
||||
<div class="metric"><span class="val">${esc(p.metric)}</span></div>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
|
||||
${showReco ? `<div class="reco-bubble">
|
||||
<span class="ic">${ICONS.bulb}</span>
|
||||
<div class="txt">
|
||||
<span>抖音同人设 TOP 视频更常用 <strong>${esc(recoDur.label)}</strong> + <strong>${esc(recoStyle.name)}</strong></span>
|
||||
<span class="meta">当前 ${esc(durObj.label)} · ${esc(styleObj.name)} → 推荐换为同人设最优组合</span>
|
||||
</div>
|
||||
<button class="btn-apply" onclick="_wiz.applyPreset()">一键套用</button>
|
||||
<button class="dismiss" onclick="_wiz.dismissReco()" aria-label="忽略">${ICONS.x}</button>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
|
||||
${Object.keys(state.points).length > 0 ? `<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">关键卖点(可勾选要重点突出的)</label>
|
||||
<div class="theme-pill-row">
|
||||
@ -1292,7 +1264,7 @@
|
||||
}
|
||||
|
||||
function renderStep4() {
|
||||
const p = getProduct(), s = getSource(), personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
|
||||
const p = getProduct(), s = getSource(), durObj = getDuration();
|
||||
const c = getCost();
|
||||
const ba = balanceAfter();
|
||||
const low = ba < 5;
|
||||
@ -1331,7 +1303,7 @@
|
||||
<div class="cc-h"><span>// 项目配置</span><button class="cc-edit" onclick="_wiz.jumpTo(3)">修改</button></div>
|
||||
<div class="cc-body">
|
||||
<div style="font-weight:600; font-size:13px;">${esc(state.projectName)}</div>
|
||||
<div class="ln"><b>${esc(styleObj.name)}</b> · ${esc(personaObj.name)} · ${esc(personaObj.sub)}</div>
|
||||
<div class="ln"><b>${esc(durObj.label)}</b> · ${durObj.shots[0]}-${durObj.shots[1]} 镜 · 9:16</div>
|
||||
<div class="ln" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">卖点:${esc(pointsList)}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1340,8 +1312,7 @@
|
||||
<div class="cc-h"><span>// 输出参数</span></div>
|
||||
<div class="cc-body">
|
||||
<div class="ln"><b>${esc(durObj.label)}</b> · <b>${durObj.shots[0]}-${durObj.shots[1]} 镜</b> · 9:16</div>
|
||||
<div class="ln">预估完播 <b>${durObj.completion}%</b> · 预估转化 <b>${durObj.conversion}%</b></div>
|
||||
<div class="ln" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">// 数据来源:抖音同品类 TOP 均值</div>
|
||||
<div class="ln" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">// 进入 Stage 1 后由脚本助手确认风格与人物设定</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1516,8 +1487,8 @@
|
||||
<div class="pv-title">${esc(title)}</div>
|
||||
<div class="pv-metrics">
|
||||
<div class="pv-metric"><div class="l">镜头</div><div class="v">${shots}<small>镜</small></div></div>
|
||||
<div class="pv-metric accent"><div class="l">预估完播</div><div class="v">${durObj.completion}<small>%</small></div></div>
|
||||
<div class="pv-metric"><div class="l">预估转化</div><div class="v">${durObj.conversion}<small>%</small></div></div>
|
||||
<div class="pv-metric accent"><div class="l">时长</div><div class="v">${esc(durObj.label)}</div></div>
|
||||
<div class="pv-metric"><div class="l">比例</div><div class="v">9:16</div></div>
|
||||
<div class="pv-metric"><div class="l">预估成本</div><div class="v">¥${c.total.toFixed(2)}</div></div>
|
||||
</div>
|
||||
${summaryBlock}
|
||||
@ -1573,8 +1544,8 @@
|
||||
renderRail();
|
||||
const body = $('#wiz-body');
|
||||
// 单页式: 商品 (step1) + 项目配置 (原 step3, 现 step2),底部「开始」CTA
|
||||
const p = getProduct(), du = getDuration(), st = getStyle();
|
||||
const canStart = !!(p && du && st && state.projectName.trim().length >= 2);
|
||||
const p = getProduct(), du = getDuration();
|
||||
const canStart = !!(p && du && state.projectName.trim().length >= 2);
|
||||
let html = '';
|
||||
html += '<section id="step-pane-1" class="step-pane-wrap">' + renderStep1() + '</section>';
|
||||
html += '<section id="step-pane-2" class="step-pane-wrap">' + renderStep3() + '</section>';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user