/* 独立报告视图:以分析 JSON 为唯一数据源,完整展示分析与证据。 */
const esc = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
const arr = value => Array.isArray(value) ? value : [];
const compact = value => arr(value).map(item => typeof item === 'object' ? (item.value || item.topic || item.name || item.label || item.action || item.insight || '') : item).filter(Boolean).join(' · ');
const percent = value => `${(Number(value || 0) * 100).toFixed(1).replace('.0','')}%`;
const modelLabel = item => item?.modelLabel || item?.model || '默认生图模型';
const modelQuality = item => item?.quality || '服务商默认';
const IMAGE_QUALITY_OPTIONS = ['auto','low','medium','high','xhigh','max'];
const resultSortKey = item => {
const type = item?.assetType === 'main' ? 0 : 1;
const index = Number(item?.slotIndex || 0);
return [type, index, String(item?.modelLabel || item?.model || ''), String(item?.url || '')];
};
const orderedResults = items => arr(items).slice().sort((a,b) => {
const aa = resultSortKey(a), bb = resultSortKey(b);
return aa[0] - bb[0] || aa[1] - bb[1] || aa[2].localeCompare(bb[2]);
});
async function readJsonResponse(response, endpoint='接口') {
const body=await response.text();
try { return JSON.parse(body); }
catch (_) {
const returnedHtml=/^\s* item?.type === 'image' && item?.meta?.group === group);
}
function imageUrl(item) { return item?.meta?.localUrl || item?.value || ''; }
function figure(item, caption='', className='') {
if (!item || !imageUrl(item)) return '';
const source = esc(imageUrl(item));
return `
`;
}
function section(id, no, title, deck, body) {
if (!hasContent(body)) return '';
return `${esc(no)}${esc(title)}
${esc(deck)}
${body}
`;
}
function decisionCards(items) {
const valid=arr(items).filter(item=>hasContent(item?.headline)||hasContent(item?.name)||hasContent(item?.note)||hasContent(item?.bullets));
return valid.length ? `
${valid.map(item => `
${hasContent(item.label||item.type)?`${esc(item.label||item.type)}`:''}${hasContent(item.headline||item.name)?`${esc(item.headline||item.name)}
`:''}${hasContent(item.note)?`${esc(item.note)}
`:''}${hasContent(item.bullets)?`${arr(item.bullets).filter(hasContent).map(x=>`- ${esc(x)}
`).join('')}
`:''}`).join('')}
` : '';
}
function planCards(items) {
const tone = item => String(item?.sourceType || '').includes('探索') ? 'explore' : String(item?.sourceType || '').includes('反馈') ? 'upgrade' : 'evidence';
const themeCopy = item => {
const value=String(item?.sourceType||item?.type||'');
if (value.includes('反馈')) return [['升级动作',item.productAction],['体验证明',item.pageAction]];
if (value.includes('探索')) return [['探索假设',item.productAction],['验证表达',item.pageAction]];
return [['证据动作',item.productAction],['证据表达',item.pageAction]];
};
const valid=arr(items).map((item,index)=>({item,index})).filter(({item})=>hasContent(item));
return valid.length ? `${valid.map(({item,index}) => {const rows=themeCopy(item).filter(([,value])=>hasContent(value));return `
${hasContent(item.sourceType||item.type)?`${esc(item.sourceType||item.type)}`:''}${rows.length?`${rows.map(([label,value])=>`- ${label}
- ${esc(value)}
`).join('')}
`:''}`}).join('')}
` : '';
}
function titleAnalysis(block) {
const expressed = compact(block.currentExpression);
const reinforce = arr(block.reinforce).map(x=>x?.topic || x?.value || x).filter(Boolean).join(' · ');
const compare=[['已经表达',expressed],['建议强化',reinforce||compact(block.recommendedActions)]].filter(([,value])=>hasContent(value));
return `${hasContent(block.professionalOpinion)?`${esc(block.professionalOpinion)}
`:''}${hasContent(block.originalTitle)?`当前标题${esc(block.originalTitle)}
`:''}${compare.length?`${compare.map(([label,value])=>`
${label}${esc(value)}`).join('')}
`:''}${hasContent(block.recommendedTitles)?`查看推荐标题
${arr(block.recommendedTitles).filter(hasContent).map(x=>`- ${esc(x)}
`).join('')}
`:''}`;
}
function mainImages(data, block) {
const entries = new Map(imageEntries(data,'main').map(item=>[item.id,item]));
const items=arr(block.items).filter(item=>hasContent(item)&&entries.get(item.imageEvidenceId));
return `${hasContent(block.professionalOpinion)?`${esc(block.professionalOpinion)}
`:''}${items.length?`${items.map(item=>`
${figure(entries.get(item.imageEvidenceId), item.imageRole || '商品主图')}主图分析${hasContent(item.imageRole)?`
${esc(item.imageRole)}
`:''}${hasContent(item.visualSignals)?`
${esc(compact(item.visualSignals))}`:''}${hasContent(item.businessMeaning)?`
${esc(item.businessMeaning)}
`:''}${hasContent(item.nextAction)?`
${esc(item.nextAction)}`:''}
`).join('')}
`:''}`;
}
function classifyDetailGroup(group) {
const keyed = {
scene_problem: [10, '场景证明', '先回答为什么需要'],
material_proof: [20, '材质证明', '证明产品基础'],
structure_proof: [30, '结构工艺', '证明结构如何实现'],
function_proof: [40, '功能表现', '证明使用表现'],
spec_adaptation: [50, '规格适配', '降低选择门槛'],
care_durability: [60, '使用维护', '说明洗护与长期使用']
};
if (keyed[group?.contentKey]) {
const [sequencePriority, sequenceLabel, sequenceHint] = keyed[group.contentKey];
return {...group, sequencePriority, sequenceLabel, sequenceHint};
}
const source = `${group?.type || ''} ${group?.stage || ''}`;
const rules = [
[/场景|人群|体验|需求/,10,'使用场景','先回答为什么需要'],
[/卖点|利益|价值/,15,'核心利益','再说明能带来什么'],
[/材质|面料|成分|填充/,20,'材质证明','证明产品基础'],
[/工艺|细节|结构|做工/,30,'工艺细节','证明品质如何实现'],
[/耐用|性能|功能|洗涤|防护|透气/,40,'性能验证','证明长期使用表现'],
[/规格|尺寸|SKU|选择|适配/,50,'规格选择','降低选择门槛'],
[/洗护|护理|清洁|耐用/,60,'使用维护','说明洗护与长期使用']
];
const found = rules.find(([pattern])=>pattern.test(source));
return found ? {...group,sequencePriority:found[1],sequenceLabel:found[2],sequenceHint:found[3]} : {...group,sequencePriority:70,sequenceLabel:'补充证据',sequenceHint:'补充未覆盖的信息'};
}
function detailProof(data, block) {
const allImages = arr(data.evidenceLedger).filter(item=>item?.type === 'image');
const byId = new Map(allImages.map(item=>[item.id,item]));
const detailImages = imageEntries(data,'detail');
const groups = arr(block.contentGroups).filter(hasContent).slice(0,6).map(classifyDetailGroup).sort((a,b)=>a.sequencePriority-b.sequencePriority);
const used = new Set();
const rows = groups.map((group,index) => {
const image = byId.get(group.representativeImageId || group.imageEvidenceId);
if (image) used.add(image.id);
const imageGroup = image?.meta?.group;
const badge = imageGroup === 'detail' ? '详情代表图' : imageGroup === 'main' ? '主图补充证据' : '图片证据';
return `${image?`${figure(image, group.type || '详情任务')}
`:''}${hasContent(group.sequenceLabel)?`
${esc(group.sequenceLabel)}`:''}${hasContent(group.type)?`
${esc(group.type)}
`:''}${hasContent(group.visualSignals)||hasContent(group.businessMeaning)?`
${esc(compact(group.visualSignals)||group.businessMeaning)}`:''}${hasContent(group.businessMeaning)?`
${esc(group.businessMeaning)}
`:''}${hasContent(group.nextAction)?`
下一步动作${esc(group.nextAction)}
`:''}
`;
}).join('');
return `${hasContent(block.professionalOpinion)?`${esc(block.professionalOpinion)}
`:''}${groups.length?``:''}${rows?`${rows}
`:''}${detailImages.length?`查看全部详情图证据 ${detailImages.length} 张
默认展示信息任务代表图,其余图片作为完整证据保留。
${detailImages.map(item=>figure(item,'详情图')).join('')}
`:''}`;
}
function skuAnalysis(block) {
const dimensions=arr(block.selectionDimensions).filter(item=>hasContent(item?.name)||hasContent(item?.values));
return `${hasContent(block.professionalOpinion)?`${esc(block.professionalOpinion)}
`:''}${dimensions.length?`${dimensions.map(item=>`
${hasContent(item.name)?`${esc(item.name)}`:''}${hasContent(item.values)?`${esc(compact(item.values))}
`:''}`).join('')}
`:''}${hasContent(block.recommendedAction)?`推荐动作${esc(block.recommendedAction)}
`:''}`;
}
function topicBars(base) {
const topics = arr(base.topics).slice(0,8);
if (!topics.length) return '';
return `评论关注主题
用户实际在讨论什么
主题可重叠,比例不应相加为100%${topics.map(item=>{const rate = Number(item.sampleRate ?? (base.reviewCount ? item.mentions/base.reviewCount : 0)); return `
${esc(item.topic)}
${Number(item.mentions || 0)}次${percent(rate)} `}).join('')}
`;
}
function buyerGallery(data, block) {
const items = arr(block?.buyerEvidence?.items);
const byId = new Map(arr(data.evidenceLedger).map(item=>[item.id,item]));
const grouped = new Map();
items.forEach(item=>{
const key = item.reviewEvidenceId || item.imageEvidenceId;
if (!grouped.has(key)) grouped.set(key,{reviewId:item.reviewEvidenceId,quote:item.quote,date:item.date,sku:item.sku,scenario:item.scenario || item.context,insight:item.insight || item.conclusion || item.businessMeaning,images:[]});
grouped.get(key).images.push(byId.get(item.imageEvidenceId));
});
if (!grouped.size) {
const loose = imageEntries(data,'buyerShow');
return loose.length ? `${loose.map(item=>figure(item,item.id)).join('')}
买家秀已采集,尚未关联到具体评论。
` : '';
}
return [...grouped.values()].filter(group=>group.images.some(Boolean)||hasContent(group.quote)||hasContent(group.sku)).map((group,index)=>`案例 ${String(index+1).padStart(2,'0')}${hasContent(group.reviewId)?` · ${esc(group.reviewId)}`:''}${group.images.filter(Boolean).length} 张实拍图${hasContent(group.date)?` · ${esc(group.date)}`:''}
${group.images.some(Boolean)?`${group.images.filter(Boolean).map(item=>figure(item,'')).join('')}
`:''}${hasContent(group.scenario)?`使用场景${esc(group.scenario)}
`:''}${hasContent(group.quote)?`“${esc(group.quote)}”
`:''}${hasContent(group.insight)?`案例结论${esc(group.insight)}
`:''}${hasContent(group.sku)?`SKU ${esc(group.sku)}
`:''}`).join('');
}
function reviews(data, block) {
const base = data.baseline || {};
const quality = data.dataQuality || {};
const signals = arr(block?.consumerInsights?.experienceSignals).filter(item=>hasContent(item?.insight)||hasContent(item?.statement));
const research = data.consumerResearch || {};
const batchFindings = arr(data.reviewBatchAnalysis).flatMap(batch => arr(batch?.batchFindings));
const issues = [
...arr(research.painPoints).map(item => ({label:'用户痛点', title:item.problem, impact:item.businessImpact, action:item.productAction, evidence:item.evidenceIds, tone:'risk'})),
...arr(research.expectationGaps).map(item => ({label:'预期落差', title:`${item.expected} → ${item.actual}`, impact:item.commercialRisk, action:item.productOpportunity, evidence:item.evidenceIds, tone:'gap'})),
...arr(research.prePurchaseConcerns).map(item => ({label:'购前疑问', title:item.concern, impact:item.conversionImpact, action:item.pageAction, evidence:item.evidenceIds, tone:'question'}))
];
const fallbackIssues = batchFindings.filter(item=>['pain','expectation_gap','prepurchase_concern'].includes(item.type)).map(item=>({label:item.type==='pain'?'用户痛点':item.type==='expectation_gap'?'预期落差':'购前疑问',title:item.finding,impact:item.businessMeaning,action:'转为产品、页面或履约验证动作',evidence:item.evidenceIds,tone:item.type==='pain'?'risk':'gap'}));
const issueCards = (issues.length ? issues : fallbackIssues).filter(item=>hasContent(item.title)||hasContent(item.impact)||hasContent(item.action)).slice(0,4).map(item=>`${hasContent(item.label)?`${esc(item.label)}`:''}${hasContent(item.title)?`${esc(item.title)}
`:''}${hasContent(item.impact)?`影响 ${esc(item.impact)}
`:''}${hasContent(item.action)?`动作 ${esc(item.action)}
`:''}${arr(item.evidence).length?`${arr(item.evidence).length} 条对应证据`:''}`).join('');
const completed = Boolean(base.reviewCollectionComplete);
const stopText = completed ? '已到达可访问末页' : base.reviewStopReason === 'user_stopped' ? '用户主动停止,当前为样本分析' : '采集未确认完成';
const reviewCount = base.reviewCount ?? quality.reviewSample;
const publicCount = base.publicReviewCount ?? quality.publicReviewCount;
const stats = [['本次有效评论',reviewCount],['页面公开数量',publicCount],['访问页数',base.reviewPagesVisited],['样本状态',stopText,completed?'complete':'limited']].filter(([,value])=>hasContent(value));
const topics = topicBars(base);
const signalHtml = signals.length ? `${signals.map(item=>`
${hasContent(item.type)?`${esc(item.type)}`:''}${esc(item.insight||item.statement)}
${arr(item.evidenceIds).length?`${arr(item.evidenceIds).length} 条对应证据`:''}`).join('')}
` : '';
const buyers = buyerGallery(data,block);
return `${hasContent(block.professionalOpinion)?`${esc(block.professionalOpinion)}
`:''}${stats.length?`${stats.map(([label,value,tone])=>`
${label}${esc(value)}`).join('')}
`:''}${topics||signalHtml?`${topics}${signalHtml}
`:''}${issueCards?`问题洞察用户讨论暴露了哪些需要解决的问题
从痛点、预期落差和购前疑问转成可执行动作
${issueCards}
`:''}${buyers?`买家秀验证图片按评论合并,不把多张图重复计算为多人反馈
${buyers}
`:''}`;
}
const ROLE_DENSE_FIELDS = new Set(['baseline','buyerEvidence','signals','stages','evidenceDetail']);
function displayValue(value) {
if (typeof value === 'boolean') return value ? '是' : '否';
const status = {
passed:'已通过',confirmed:'已确认',missing:'缺失',partial:'部分满足',unknown:'待确认',high:'高风险',medium:'中风险',low:'低风险',
consumer_supported:'评论支持',conflicted:'存在冲突',page_claim_only:'仅页面宣称',not_enough_evidence:'证据不足',user_stopped:'用户主动停止',
expressed:'主图已表达',proven:'详情已证明',validated:'评论已验证',main_missing:'主图未表达',detail_missing:'详情未证明',review_pending:'等待评论验证',closed_loop:'证据闭环',conflict:'存在冲突',
product_risk:'产品风险',information_gap:'信息缺口',purchase_driver:'购买驱动',satisfaction:'满意因素',pain:'用户痛点',expectation_gap:'预期差异',prepurchase_concern:'购前顾虑',sku_signal:'规格反馈',usage_scene:'使用场景',
complete:'采集完成',limited:'样本有限',collected:'已采集',ready:'已就绪',normal:'一般',good:'良好',risk:'风险',true:'是',false:'否'
};
return status[value] || value;
}
function itemTitle(item) {
return item?.headline || item?.name || item?.type || item?.asset || item?.signal || item?.claim || item?.risk || item?.gate || item?.topic || item?.field || '';
}
function evidenceType(item, id='') {
if (item?.type === 'image') return ({main:'商品主图',detail:'详情图片',buyerShow:'买家秀'})[item?.meta?.group] || '图片证据';
if (item?.type === 'review' || id.startsWith('REV_')) return '用户评论';
if (item?.type === 'question' || id.startsWith('QA_')) return '用户问答';
if (item?.type === 'attribute' || id.startsWith('ATTR_')) return '商品属性';
if (item?.type === 'promotion' || id.startsWith('PROMO_')) return '促销信息';
if (item?.type === 'sales' || id.startsWith('S_')) return '页面数据';
if (item?.type === 'product' || id.startsWith('P_')) return '商品信息';
return '证据';
}
function evidenceText(item, id='') {
const value = item?.value;
if (item?.type === 'image') return ({main:'商品主图采集',detail:'详情页图片采集',buyerShow:'评论买家秀采集'})[item?.meta?.group] || '页面图片采集';
if (item?.type === 'attribute') return `${value?.name || '属性'}:${value?.value ?? ''}`;
if (item?.type === 'review') return value?.content || value?.appendContent || '评论内容未采集';
if (item?.type === 'question') return value?.question || '问答内容未采集';
if (item?.type === 'promotion') return value?.text || '促销内容未采集';
const labels = {P_TITLE:'商品标题',P_SHOP:'店铺信息',P_ITEMID:'商品ID',P_SKUID:'SKU ID',S_CURRENTPRICE:'当前价格',S_SOLD:'页面销量',S_RANKING:'榜单排名'};
return `${labels[id] ? `${labels[id]}:` : ''}${typeof value === 'object' ? compact(Object.values(value)) : (value ?? '未采集')}`;
}
function readableEvidence(id) {
const item = evidenceIndex.get(id);
if (!item || !hasContent(item.value)) return '';
const text = evidenceText(item,id);
const preview = text.length > 120 ? `${text.slice(0,120)}…` : text;
const image = item?.type === 'image' && imageUrl(item) ? `
` : '';
return `${image}${esc(evidenceType(item,id))}${esc(preview)}
${esc(id)} `;
}
function renderStructured(value, depth=0) {
if (!hasContent(value)) return '';
if (typeof value !== 'object') return `${esc(displayValue(value))}
`;
if (Array.isArray(value)) {
const valid=value.filter(hasContent);
if (!valid.length) return '';
if (valid.every(item=>typeof item !== 'object')) return `${valid.map(item=>`${esc(displayValue(item))}`).join('')}
`;
return `${valid.map(item=>{const title=itemTitle(item);const inner=renderStructured(Object.fromEntries(Object.entries(item || {}).filter(([key])=>!title || !['headline','name','type','asset','signal','claim','risk','gate','topic','field'].includes(key))),depth+1);return title||inner?`
${title?`${esc(displayValue(title))}`:''}${inner}`:''}).join('')}
`;
}
return `${Object.entries(value).map(([key,item])=>{
if (!hasContent(item)) return '';
const label = FIELD_LABELS[key] || key;
if (key === 'confidence') return `
${esc(label)}${percent(item)}
`;
if (key === 'evidenceIds') { const ids=arr(item).filter(id=>evidenceIndex.has(id)&&hasContent(evidenceIndex.get(id)?.value)); if(!ids.length)return ''; const sources=[...new Set(ids.map(id=>evidenceType(evidenceIndex.get(id),id)))]; return `
查看证据 · ${ids.length} 条${sources.length?` · ${esc(sources.join(' / '))}`:''}
${ids.map(readableEvidence).join('')}
`; }
const dense = depth > 0 && (key === 'examples' || key === 'skuBreakdown');
if (dense) return `
${esc(label)} · ${arr(item).length} 项
${renderStructured(item,depth+1)}`;
return `
${esc(label)}${renderStructured(item,depth+1)}
`;
}).join('')}
`;
}
function roleConclusion(key, content) {
if (content?.professionalOpinion) return content.professionalOpinion;
if (key === 'productStrategy') return content?.positioning?.statement || '';
if (key === 'conversion') return [content?.title?.professionalOpinion,content?.visual?.professionalOpinion,content?.detail?.professionalOpinion].filter(Boolean).join(' ');
if (key === 'productSupply') return content?.sku?.professionalOpinion || content?.product?.gates?.[0]?.gate || '';
if (key === 'visualDesign') return content?.fashionThesis?.statement || '';
if (key === 'executiveDecision') return arr(content?.cards).map(item=>item.headline).filter(Boolean).slice(0,3).join(';');
if (key === 'editorReview') return content?.status === 'passed' ? '本次报告已通过事实、证据、对象匹配和表达一致性审核。' : '报告仍有需要复核的项目。';
return '';
}
function roleOutputs(roles) {
const visible=ROLE_META.map(([key,no,title,deck])=>{const content=roles?.[key];if(!hasContent(content))return '';const sections=Object.entries(content).filter(([field,value])=>field!=='professionalOpinion'&&hasContent(value)&&hasContent(renderStructured(value)));const conclusion=roleConclusion(key,content);if(!hasContent(conclusion)&&!sections.length)return '';return `${no}${esc(title)}
${esc(deck)}
${sections.length} 个分析模块
${hasContent(conclusion)?`
核心判断${esc(conclusion)}
`:''}${sections.length?`
${sections.map(([field,value],index)=>`
${esc(FIELD_LABELS[field]||field)}${Array.isArray(value)?`${value.filter(hasContent).length} 项`:'查看分析'}
${renderStructured(value)}
`).join('')}
`:''}
`}).filter(Boolean);
return visible.length ? `${visible.join('')}
` : '';
}
function supportAnalysis(data) {
const exp=data.experienceSolution||{};
const blocks=[
['reportPlan','报告分析计划',data.reportPlan],
['dataQuality','数据质量与样本边界',data.dataQuality],
['reviewBatchAnalysis','评论批次分析',data.reviewBatchAnalysis],
['productExperience','产品与供应链核验',exp.productExperience],
['validationLoop','验证闭环',exp.validationLoop],
['expressionContinuity','卖点承接检查',exp.expressionContinuity],
['consumerResearch','消费者研究明细',data.consumerResearch],
['questionResearch','购前问答研究',data.questionResearch],
['commercialDecision','商业决策依据',data.commercialDecision],
['competitionDecision','单品边界与竞品数据',data.competitionDecision],
['merchandisingDecision','经营表达决策明细',data.merchandisingDecision],
['productDecision','产品决策明细',data.productDecision],
['visualDecision','视觉决策明细',data.visualDecision]
].filter(([, ,value])=>hasContent(value));
if(!blocks.length)return '';
return `${blocks.map(([key,title,value])=>`
${esc(title)}查看内容
${renderStructured(value)}
`).join('')}
`;
}
function initHeroCarousel() {
const root = document.querySelector('.hero-carousel');
if (!root) return;
const slides = [...root.querySelectorAll('.hero-slide')], dots = [...root.querySelectorAll('.hero-dot')], cards = [...root.querySelectorAll('.hero-deck-card')];
if (slides.length < 2) return;
let current = 0, timer;
const show = index => {
current = (index + slides.length) % slides.length;
slides.forEach((slide,i)=>{slide.classList.toggle('active',i===current); slide.setAttribute('aria-hidden',i===current?'false':'true')});
dots.forEach((dot,i)=>{dot.classList.toggle('active',i===current); dot.setAttribute('aria-current',i===current?'true':'false')});
const step = window.innerWidth <= 560 ? 26 : window.innerWidth <= 800 ? 34 : 58;
const angle = window.innerWidth <= 560 ? 5 : 6;
const center = (cards.length - 1) / 2;
cards.forEach((card,i)=>{const slot=i-center;card.classList.toggle('active',i===current);card.setAttribute('aria-current',i===current?'true':'false');card.style.setProperty('--deck-x',`${slot*step}px`);card.style.setProperty('--deck-r',`${slot*angle}deg`)});
const count = root.querySelector('.hero-carousel-count');
if (count) count.textContent = `${String(current+1).padStart(2,'0')} / ${String(slides.length).padStart(2,'0')}`;
};
const stop = () => clearInterval(timer);
const start = () => { stop(); if (!matchMedia('(prefers-reduced-motion: reduce)').matches) timer=setInterval(()=>show(current+1),5000); };
root.querySelector('[data-carousel-prev]')?.addEventListener('click',()=>{show(current-1);start()});
root.querySelector('[data-carousel-next]')?.addEventListener('click',()=>{show(current+1);start()});
dots.forEach((dot,index)=>dot.addEventListener('click',()=>{show(index);start()}));
cards.forEach((card,index)=>card.addEventListener('click',()=>{show(index);start()}));
root.addEventListener('mouseenter',stop); root.addEventListener('mouseleave',start);
root.addEventListener('focusin',stop); root.addEventListener('focusout',start);
root.addEventListener('keydown',event=>{if(event.key==='ArrowLeft')show(current-1);if(event.key==='ArrowRight')show(current+1)});
window.addEventListener('resize',()=>show(current),{passive:true});
show(0); start();
}
function initPlanActions(plans, context={}) {
const dialog = document.querySelector('#quick-plan-dialog');
if (!dialog) return;
const title = dialog.querySelector('[data-plan-title]'), reason = dialog.querySelector('[data-plan-reason]');
const product = dialog.querySelector('[data-plan-product]'), page = dialog.querySelector('[data-plan-page]'), validation = dialog.querySelector('[data-plan-validation]');
const generateButton=dialog.querySelector('[data-generate-images]');
const generateStatus=dialog.querySelector('[data-generate-status]');
const generateResults=dialog.querySelector('[data-generate-results]');
const generationControls=dialog.querySelector('[data-generation-controls]');
const userDirectionInput=dialog.querySelector('[data-user-direction]');
const fissionWrap=dialog.querySelector('[data-fission-wrap]');
const fissionInput=dialog.querySelector('[data-fission-pattern]');
const matchReferenceWrap=dialog.querySelector('[data-match-reference-wrap]');
const matchReferenceInput=dialog.querySelector('[data-match-reference-shooting]');
const matchReferenceNote=dialog.querySelector('[data-match-reference-note]');
const referenceInput=dialog.querySelector('[data-reference-input]');
const referenceList=dialog.querySelector('[data-reference-list]');
const referenceHint=dialog.querySelector('[data-reference-hint]');
let uploadedReferences=[];
if (generateButton) { generateButton.textContent='正在计算生成数量…'; generateButton.disabled=true; }
let activePlanIndex=0, previewSlots=[], selectedSlotKeys=new Set(), promptOverrides={};
let retainedResults=[], generatedTaskSlots=[];
let imageModelOptions=[], selectedImageModels=new Set(), selectedImageQualities=new Map(), imageModelPromise=null;
let submittedModelSpecs=[];
const slotKey=slot=>`${slot.assetType}:${slot.index}`;
const mergeResults=(older,newer)=>{
const resultKey=item=>`${item.modelKey||item.model||'default'}:${item.assetType}:${item.slotIndex}`;
const merged=new Map(arr(older).map(item=>[resultKey(item),item]));
arr(newer).forEach(item=>merged.set(resultKey(item),item));
return orderedResults([...merged.values()]);
};
const selectedTypes=()=>[...new Set(previewSlots.filter(slot=>selectedSlotKeys.has(slotKey(slot))).map(slot=>slot.assetType))];
const generationCounts=()=>({
main:previewSlots.filter(x=>x.assetType==='main'&&selectedSlotKeys.has(slotKey(x))).length,
detail:previewSlots.filter(x=>x.assetType==='detail'&&selectedSlotKeys.has(slotKey(x))).length
});
const countText=counts=>`${counts.main} 张主图 + ${counts.detail} 张详情图`;
const selectedModelOptions=()=>imageModelOptions
.filter(item=>selectedImageModels.has(item.id))
.map(item=>({...item,quality:selectedImageQualities.get(item.id) ?? item.quality ?? ''}));
const comparisonMode=()=>selectedImageModels.size>1;
const modelKey=item=>item?.modelKey||item?.model||'default';
const imageModelControls=()=>dialog.querySelector('[data-image-model-controls]');
const generationRunSummary=()=>dialog.querySelector('[data-generation-run-summary]');
const renderGenerationRunSummary=(status='queued',specs=submittedModelSpecs)=>{
const summary=generationRunSummary();
if (!summary) return;
const items=arr(specs);
if (!items.length) {
summary.hidden=true;
summary.innerHTML='';
return;
}
const stateLabel=status==='complete'?'已完成':status==='error'?'生成失败':status==='running'?'生成中':'已提交';
summary.hidden=false;
summary.innerHTML=`本次生成参数 · ${stateLabel}${items.map(item=>`${esc(item.label||item.id||'默认生图模型')}模型:${esc(item.id||'默认')} · 请求质量:${esc(modelQuality(item))}${item.quality?'':'(实际档位由服务商决定)'}`).join('')}`;
};
const renderImageModelControls=()=>{
const controls=imageModelControls();
if (!controls) return;
if (!imageModelOptions.length) {
controls.innerHTML='未读取到模型列表,请先在本地配置页填写生图测试模型。';
return;
}
controls.innerHTML=`模型对比同一图片任务会按模型并排生成,最多建议选择 3 个;模型对比建议统一使用 medium。${imageModelOptions.map(item=>``).join('')}`;
};
const loadImageModels=()=>{
if (imageModelPromise) return imageModelPromise;
imageModelPromise=fetch('/health',{cache:'no-store'}).then(response=>readJsonResponse(response,'/health')).then(payload=>{
imageModelOptions=arr(payload.imageModels).map(item=>typeof item==='string'?{id:item,label:item}:item).filter(item=>item?.id);
imageModelOptions.forEach(item=>{
if (!selectedImageQualities.has(item.id)) selectedImageQualities.set(item.id,item.quality||'');
});
if (!imageModelOptions.length&&payload.imageModel) imageModelOptions=[{id:payload.imageModel,label:payload.imageModel}];
if (!selectedImageModels.size&&imageModelOptions[0]) selectedImageModels.add(imageModelOptions[0].id);
selectedImageModels=new Set([...selectedImageModels].filter(id=>imageModelOptions.some(item=>item.id===id)));
if (!selectedImageModels.size&&imageModelOptions[0]) selectedImageModels.add(imageModelOptions[0].id);
renderImageModelControls();
syncFissionControl();
refreshGenerationConfirmation();
}).catch(()=>{
imageModelOptions=[{id:'',label:'默认生图模型'}];
selectedImageQualities.set('','');
selectedImageModels=new Set(['']);
renderImageModelControls();
refreshGenerationConfirmation();
});
return imageModelPromise;
};
const userDirection=()=>String(userDirectionInput?.value||'').trim();
const fissionEnabled=()=>!!fissionInput?.checked&&!uploadedReferences.length&&!matchReferenceShooting()&&!comparisonMode();
const matchReferenceShooting=()=>!!matchReferenceInput?.checked;
const matchReferenceDescription=()=>matchReferenceShooting()
? '已开启:第1张上传产品图只锁定商品本身;第2张采集参考图只提供展示状态;视角、动作、朝向、展开/折叠、摆放、支撑和部件关系跟随采集商品对应图片;不复制采集图中的其他商品、颜色、花型、品牌或文字。'
: '未开启:产品图只用于商品身份,采集图的拍摄方式、具体动作和摆放状态不继承;按每张图任务重新设计。';
const syncMatchReferenceControl=plan=>{
if (!matchReferenceInput) return;
const matchReferenceLabel=matchReferenceWrap?.querySelector('b');
if (matchReferenceLabel) {
if (matchReferenceShooting()) matchReferenceLabel.textContent='产品展示状态跟随采集商品对应图片';
else matchReferenceLabel.textContent='产品图身份 + 采集参考图展示状态';
}
const referenceHeaderLabel=dialog.querySelector('.reference-upload header b');
if (referenceHeaderLabel) referenceHeaderLabel.textContent=uploadedReferences.length?'产品图(商品身份)':'采集参考图(默认身份参考)';
const fallback=fallbackReferences(plan||{});
const hasReference=uploadedReferences.length>0&&fallback.length>0;
matchReferenceInput.disabled=!hasReference;
if (!hasReference) matchReferenceInput.checked=false;
if (matchReferenceNote) matchReferenceNote.textContent=hasReference?matchReferenceDescription():'请同时准备产品图和采集商品对应参考图,才能启用展示状态一致。';
if (matchReferenceWrap) matchReferenceWrap.title=hasReference?matchReferenceDescription():'需要产品图和采集参考图';
};
const ensureFissionBaseSelected=()=>{
if (!fissionEnabled()) return;
const hasAnySelection=previewSlots.some(slot=>selectedSlotKeys.has(slotKey(slot)));
const base=previewSlots.find(slot=>slot.assetType==='main'&&slot.index===1);
if (hasAnySelection&&base) selectedSlotKeys.add(slotKey(base));
};
const syncFissionControl=()=>{
if (!fissionWrap||!fissionInput) return;
const lockedByReferenceMatch=matchReferenceShooting();
const lockedByComparison=comparisonMode();
fissionWrap.hidden=!!uploadedReferences.length||lockedByReferenceMatch;
fissionInput.disabled=!!uploadedReferences.length||lockedByReferenceMatch||lockedByComparison;
if (uploadedReferences.length||lockedByReferenceMatch||lockedByComparison) fissionInput.checked=false;
if (lockedByComparison&&fissionWrap) fissionWrap.title='多模型对比使用同一参考图,暂不启用裂变基准。';
};
const directionNote=direction=>{
if (!direction?.raw) return '';
const accepted=arr(direction.accepted), rejected=arr(direction.rejected);
const ok=accepted.length?`已采纳:${accepted.join(' / ')}`:'未采纳可执行补充';
const no=rejected.length?`;已忽略:${rejected.join(' / ')}`:'';
return `${ok}${no}`;
};
const syncTypeSwitches=()=>{
if (!generationControls) return;
generationControls.querySelectorAll('[data-select-type]').forEach(input=>{
const slots=previewSlots.filter(slot=>slot.assetType===input.dataset.selectType);
const selected=slots.filter(slot=>selectedSlotKeys.has(slotKey(slot))).length;
input.checked=!!slots.length&&selected===slots.length;
input.indeterminate=selected>0&&selected{
const firstMain=[...evidenceIndex.values()].find(item=>item?.type==='image'&&item?.meta?.group==='main');
return [firstMain].filter(item=>Boolean(item&&imageUrl(item)));
};
const renderReferences=plan=>{
const fallback=fallbackReferences(plan), urls=uploadedReferences.length?uploadedReferences:fallback.map(imageUrl).filter(Boolean);
if (referenceList) {
const uploadRoot=referenceList.closest('.reference-upload');
uploadRoot?.classList.toggle('has-reference', urls.length>0);
referenceList.innerHTML=urls.map((url,index)=>`
${uploadedReferences.length?``:''}`).join('');
}
syncFissionControl();
syncMatchReferenceControl(plan);
if (referenceHint) referenceHint.textContent=uploadedReferences.length?`已上传 ${uploadedReferences.length} 张产品图;第一张是整套图片唯一商品身份基准,其他上传图不会混入生图请求。开启展示状态一致后,采集商品对应图片只负责视角和摆放。`:(fallback.length?(fissionEnabled()?'默认先裂变生成相近花型基准图,后续主图/详情围绕该基准图生成。':'当前将直接参考采集商品主图生成。'):'当前没有商品主图,请上传产品图后生成。');
};
const taskName=slot=>slot.role||`${slot.assetType==='detail'?'详情图':'主图'}任务 ${slot.index||''}`;
const taskCard=(slot,result,status='queued')=>{
const label=status==='complete'?'已完成':status==='generating'?'生成中':status==='error'?'生成失败':'等待生成';
const key=slotKey(slot), hasUserEdit=Object.prototype.hasOwnProperty.call(promptOverrides,key);
const promptText=hasUserEdit?promptOverrides[key]:'';
const mergeMode=slot.promptMerge?.mode;
const globalDirection=slot.userDirection||{};
const hasGlobalAccepted=arr(globalDirection.accepted).length>0;
const hasGlobalIgnored=arr(globalDirection.ignored).length>0;
const hasGlobalRejected=arr(globalDirection.rejected).length>0;
const mergeNote=mergeMode==='applied'?'本图已关联并优先融合本图编辑':
hasGlobalAccepted?'本图已关联并优先融合全局用户要求':
mergeMode==='ignored_unrelated'||hasGlobalIgnored||hasGlobalRejected?'本图未关联或存在不可执行要求,继续执行内置策划':
'本图执行内置策划';
const planContext=compact([slot.planProductAction,slot.planPageAction]);
const planContextHtml=planContext?`${esc(slot.planName||'当前开品方案')} · ${esc(planContext)}
`:'';
const media=result?.url?``:`${slot.assetType==='detail'?'DETAIL':'MAIN'}${String(slot.index||1).padStart(2,'0')}
`;
const referencePreview=slot.displayReferenceUrl||slot.identityReferenceUrl;
const referenceLabel=slot.displayReferenceUrl?'展示状态参考图':'产品身份参考图';
const referenceThumb=referencePreview?`
${esc(referenceLabel)} `:'';
const resultSpec=result?`${esc(modelLabel(result))} · 质量 ${esc(modelQuality(result))}`:'';
const visibleResultSpec=result?`本次实际请求${resultSpec}
`:'';
const choose=status==='queued'?``:'';
return `${referenceThumb}${visibleResultSpec}`;
};
const openPreview=button=>{
const buttons=[...generateResults.querySelectorAll('[data-preview-url]')];
openImagePreview(buttons.map(node=>({url:node.dataset.previewUrl,title:node.dataset.previewTitle||'生成图片'})),buttons.indexOf(button));
};
const comparisonCell=(slot,result,status,model)=>{
const media=result?.url
? ``
: `${status==='error'?'FAILED':status==='generating'?'RUNNING':slot.assetType==='detail'?'DETAIL':'MAIN'}${String(slot.index||1).padStart(2,'0')}
`;
const error=result?.error||'';
return `${esc(modelLabel(model))}${esc(modelQuality(model))}${status==='complete'?'已完成':status==='generating'?'生成中':status==='error'?'失败':'等待生成'}${media}
${error?`${esc(error)}
`:''}`;
};
const renderComparisonBoard=(slots,results=[],jobStatus='queued',failedSlots=[],models=selectedModelOptions())=>{
const modelList=models.length?models:imageModelOptions;
const resultKey=(model,slot)=>`${modelKey(model)}:${slot.assetType}:${slot.index}`;
const resultMap=new Map(arr(results).map(item=>[`${modelKey(item)}:${item.assetType}:${item.slotIndex}`,item]));
const failedMap=new Map(arr(failedSlots).map(item=>[`${modelKey(item)}:${item.assetType}:${item.slotIndex}`,item]));
const row=(type,title)=>{
const items=arr(slots).filter(slot=>slot.assetType===type);
if (!items.length) return '';
const completed=items.reduce((sum,slot)=>sum+modelList.filter(model=>resultMap.has(resultKey(model,slot))).length,0);
const modelHeaders=modelList.map(model=>`${esc(modelLabel(model))}${esc(modelQuality(model))}
`).join('');
const taskRows=items.map(slot=>{
const slotKeyValue=slotKey(slot);
const choose=jobStatus==='queued'
? ``
: '';
const ref=slot.displayReferenceUrl||slot.identityReferenceUrl;
const refLabel=slot.displayReferenceUrl?'展示状态参考图':'产品身份参考图';
const refThumb=ref?`
${esc(refLabel)} `:'';
const planContext=compact([slot.planProductAction,slot.planPageAction]);
const planContextHtml=planContext?`${esc(slot.planName||'当前开品方案')} · ${esc(planContext)}`:'';
const taskCell=`${slot.assetType==='detail'?'详情图':'主图'} ${String(slot.index||1).padStart(2,'0')}${esc(taskName(slot))}${planContextHtml}${refThumb}${choose}
`;
const modelCells=modelList.map(model=>{
const key=resultKey(model,slot);
const result=resultMap.get(key);
const failure=failedMap.get(key);
let status=result?'complete':'queued';
if (!result&&jobStatus==='running') status='generating';
if (!result&&(jobStatus==='error'||failure)) status='error';
return comparisonCell(slot,result||failure,status,model);
}).join('');
return taskCell+modelCells;
}).join('');
return `图片任务
${modelHeaders}${taskRows}
`;
};
generateResults.innerHTML=row('main','主图模型对比')+row('detail','详情图模型对比');
};
const renderTaskBoard=(slots,results=[],jobStatus='queued',failedSlots=[])=>{
if (!generateResults) return;
if (selectedImageModels.size>1) {
renderComparisonBoard(slots,results,jobStatus,failedSlots,selectedModelOptions());
return;
}
const selected=arr(slots);
const resultMap=new Map(arr(results).map(item=>[`${item.assetType}:${item.slotIndex}`,item]));
const failedKeys=new Set(arr(failedSlots).map(item=>`${item.assetType}:${item.slotIndex}`));
const row=(type,title)=>{
const items=selected.filter(slot=>slot.assetType===type);
if (!items.length) return '';
const heading=type==='detail'?`${title}(${items.length})`:`${title}(${items.length})`;
const right=type==='detail'?`左右滑动查看全部 ${items.length} 张`:`${items.filter(slot=>resultMap.has(`${slot.assetType}:${slot.index}`)).length} / ${items.length}`;
return `${items.map(slot=>{const key=`${slot.assetType}:${slot.index}`;const result=resultMap.get(key);let status=result?'complete':'queued';if(!result&&jobStatus==='running')status='generating';if(!result&&(jobStatus==='error'||failedKeys.has(key)))status='error';return taskCard(slot,result,status)}).join('')}
`;
};
generateResults.innerHTML=row('main','主图生成任务')+row('detail','详情图生成任务');
};
const refreshGenerationConfirmation=()=>{
ensureFissionBaseSelected();
const counts=generationCounts(), total=counts.main+counts.detail;
const modelCount=Math.max(1,selectedImageModels.size);
const requestTotal=total*modelCount;
// Keep the count calculation in one place; the single-model label follows the modal spec.
// 兼容旧版校验:确认生成:${countText(counts)}
if (generateButton) { generateButton.disabled=!total||!selectedImageModels.size; generateButton.textContent=requestTotal?(modelCount>1?`${modelCount} 个模型 × ${countText(counts)} = ${requestTotal} 张`:`确认生成:${countText(counts)}`):'请选择要生成的图片类型'; }
if (generateStatus && previewSlots.length) generateStatus.textContent=requestTotal?`本次计划生成 ${countText(counts)};${modelCount>1?`将按 ${modelCount} 个模型并排对比,共 `:''} ${requestTotal} 张。${fissionEnabled()&&counts.main?'裂变模式已计入 main:1 基准图。':''}${modelCount>1?'多模型对比使用同一参考图和同一份提示词。':''}点击按钮后还需确认。`:'请至少选择一种图片类型和一个生图模型。';
syncTypeSwitches();
if (previewSlots.length) renderTaskBoard(previewSlots);
};
dialog.addEventListener('change',event=>{
const input=event.target.closest('[data-image-model]');
const qualityInput=event.target.closest('[data-image-quality-for]');
if (qualityInput) {
selectedImageQualities.set(qualityInput.dataset.imageQualityFor,qualityInput.value);
refreshGenerationConfirmation();
return;
}
if (!input) return;
if (input.checked) selectedImageModels.add(input.dataset.imageModel);
else selectedImageModels.delete(input.dataset.imageModel);
if (!selectedImageModels.size) input.checked=true,selectedImageModels.add(input.dataset.imageModel);
syncFissionControl();
refreshGenerationConfirmation();
});
generateResults?.addEventListener('click',event=>{
const button=event.target.closest('[data-preview-url]');if(button){openPreview(button);return;}
if (event.target.closest('input,textarea,summary,details,label,button')) return;
const card=event.target.closest('[data-task-card]');
if (!card||!card.classList.contains('generation-task-queued')) return;
if (selectedSlotKeys.has(card.dataset.taskCard)) selectedSlotKeys.delete(card.dataset.taskCard);
else selectedSlotKeys.add(card.dataset.taskCard);
refreshGenerationConfirmation();
});
const refreshPromptPreview=()=>{
if (!previewSlots.length) return Promise.resolve();
const previewBody={planIndex:activePlanIndex,assetTypes:['main','detail'],referenceImages:uploadedReferences,userDirection:userDirection(),fissionPattern:fissionEnabled(),matchReferenceShooting:matchReferenceShooting(),promptOverrides};
if (context.source) previewBody.source=context.source; else if (context.data) previewBody.reportData=context.data;
return fetch('/api/image-prompt-preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(previewBody)}).then(response=>readJsonResponse(response,'/api/image-prompt-preview')).then(payload=>{
if (!payload.ok) throw new Error(payload.error||'提示词融合失败');
const promptIndexes={main:0,detail:0};
previewSlots=arr(payload.slots).map(slot=>{
const index=promptIndexes[slot.assetType]++;
const key=`${slot.assetType}:${slot.index}`;
return {...slot,prompt:arr(payload.prompts?.[slot.assetType])[index]||'',promptMerge:payload.promptMerges?.[key],userDirection:payload.userDirectionBySlot?.[key]};
});
if (generateStatus && payload.userDirection?.raw) generateStatus.textContent=directionNote(payload.userDirection);
});
};
generateResults?.addEventListener('change',event=>{
const textarea=event.target.closest('[data-prompt-slot]');
if (textarea) {
promptOverrides[textarea.dataset.promptSlot]=textarea.value;
refreshPromptPreview().then(refreshGenerationConfirmation).catch(error=>{if(generateStatus) generateStatus.textContent=error.message||'提示词融合失败';});
return;
}
const input=event.target.closest('[data-select-slot]');
if(!input)return;if(input.checked)selectedSlotKeys.add(input.dataset.selectSlot);else selectedSlotKeys.delete(input.dataset.selectSlot);refreshGenerationConfirmation();
});
generateResults?.addEventListener('input',event=>{const textarea=event.target.closest('[data-prompt-slot]');if(!textarea)return;promptOverrides[textarea.dataset.promptSlot]=textarea.value;});
generationControls?.addEventListener('change',event=>{
const input=event.target.closest('[data-select-type]');
if (!input) return;
previewSlots.filter(slot=>slot.assetType===input.dataset.selectType).forEach(slot=>{
if (input.checked) selectedSlotKeys.add(slotKey(slot));
else selectedSlotKeys.delete(slotKey(slot));
});
refreshGenerationConfirmation();
});
const pollJob = async jobId => {
let networkErrors=0;
for (let i=0;i<240;i++) {
const endpoint='/api/image-job/'+encodeURIComponent(jobId);
let state;
try {
const r=await fetch(endpoint,{cache:'no-store'});
state=await readJsonResponse(r,endpoint);
networkErrors=0;
} catch (error) {
networkErrors++;
if (generateStatus) generateStatus.textContent=`本地服务短暂断开,正在自动重连(${networkErrors}/5)…`;
if (networkErrors>=5) throw new Error('无法连接本地生图服务,请确认服务仍在运行后重试。');
await new Promise(resolve=>setTimeout(resolve,Math.min(2000*networkErrors,6000)));
continue;
}
// A fission run may inject main:1 as the shared product baseline even
// when the user selected detail-only slots. Keep that implicit asset in
// the board so the UI mirrors the manifest and does not hide the image
// that all follow-up slots used as their reference.
const expected=arr(state.expectedSlots);
if (arr(state.comparisonModels).length) {
const runModels=arr(state.comparisonModels).map(item=>({id:item.id||'',label:item.label||item.id||'默认生图模型',quality:item.quality||'',modelKey:item.modelKey}));
imageModelOptions=runModels;
selectedImageModels=new Set(imageModelOptions.map(item=>item.id));
const preservedQualities=new Map(selectedImageQualities);
runModels.forEach(item=>{
if (item.quality || !preservedQualities.has(item.id)) preservedQualities.set(item.id,item.quality||'');
});
selectedImageQualities=preservedQualities;
renderImageModelControls();
submittedModelSpecs=runModels.map(item=>({...item,quality:selectedImageQualities.get(item.id) ?? item.quality ?? ''}));
renderGenerationRunSummary(state.status,submittedModelSpecs);
}
if (expected.length) {
const known=new Set(previewSlots.map(slotKey));
const generatedKnown=new Set(generatedTaskSlots.map(slotKey));
expected.forEach(slot=>{
const key=slotKey(slot);
if (!known.has(key)) { previewSlots.push({...slot}); known.add(key); }
if (!generatedKnown.has(key)) { generatedTaskSlots.push({...slot}); generatedKnown.add(key); }
if (state.fissionPattern && slot.assetType==='main' && slot.index===1) selectedSlotKeys.add(key);
});
}
const displayResults=mergeResults(retainedResults,state.results);
const boardSlots=generatedTaskSlots.length?generatedTaskSlots:expected.length?expected:previewSlots;
if (state.status==='complete') { const gate=state.consistencyGate||{}; const shootPolicy=state.matchReferenceShooting?'已按参考图拍摄与产品展示状态生成。':'按每张图片任务重新设计拍摄和展示状态。'; const failedSlots=arr(state.failedSlots), failed=failedSlots.length; retainedResults=displayResults; if(failed){selectedSlotKeys=new Set(failedSlots.map(item=>`${item.assetType}:${item.slotIndex}`));const counts=generationCounts();if(generateButton){generateButton.disabled=false;generateButton.textContent=`仅重试失败:${countText(counts)}`;}syncTypeSwitches();} if(generateStatus) generateStatus.textContent=failed?`部分完成:累计成功 ${displayResults.length} 张,失败 ${failed} 张。点击按钮仅重试失败槽位。`:`已生成 ${displayResults.length} 张图片。${shootPolicy}${gate.status==='passed'?'一致性校验通过。':'已共享产品身份锁,发布前仍需完成一致性复核。'}`; renderTaskBoard(boardSlots,displayResults,'complete',failedSlots); return; }
if (state.status==='error') { renderTaskBoard(boardSlots,displayResults,'error',state.failedSlots); throw new Error(state.error||'生图失败'); }
renderTaskBoard(boardSlots,displayResults,'running',state.failedSlots);
const counts=generationCounts(); const expectedTotal=(counts.main+counts.detail)*Math.max(1,selectedImageModels.size); if (generateStatus) generateStatus.textContent=`正在并发生成:已完成 ${arr(state.results).length} / ${expectedTotal} 张 · ${state.progress||0}%`;
await new Promise(resolve=>setTimeout(resolve,1200));
}
throw new Error('生图任务等待超时,请稍后刷新报告查看结果。');
};
generateButton?.addEventListener('click',async event=>{
ensureFissionBaseSelected();
const types=selectedTypes(), counts=generationCounts(), total=counts.main+counts.detail;
if (!types.length) { if(generateStatus) generateStatus.textContent='至少选择主图或详情图一种类型。'; return; }
if (!total) { if(generateStatus) generateStatus.textContent='尚未完成生成数量计算,请稍后再试。'; return; }
const modelSpecs=selectedModelOptions(), requestTotal=total*Math.max(1,modelSpecs.length);
submittedModelSpecs=modelSpecs.map(item=>({...item}));
renderGenerationRunSummary('running',submittedModelSpecs);
const modelSummary=modelSpecs.map(item=>`${item.label||item.id}(${modelQuality(item)})`).join('、');
if (!window.confirm(`确认开始生成?\n\n模型:${modelSummary}\n主图:${counts.main} 张\n详情图:${counts.detail} 张\n合计:${requestTotal} 张`)) {
renderGenerationRunSummary('queued',[]);
if (generateStatus) generateStatus.textContent=`已取消。本次原计划生成 ${modelSpecs.length} 个模型 × ${countText(counts)}。`;
return;
}
const submittedSlots=previewSlots.filter(slot=>selectedSlotKeys.has(slotKey(slot)));
event.currentTarget.disabled=true; if(generateStatus) generateStatus.textContent='正在提交生图任务…'; renderTaskBoard(submittedSlots,retainedResults,'running');
try {
const body={planIndex:activePlanIndex,assetTypes:types,selectedSlots:[...selectedSlotKeys],completeSet:true,imageModels:modelSpecs.map(item=>({id:item.id,quality:item.quality||''})),referenceImages:uploadedReferences,userDirection:userDirection(),fissionPattern:fissionEnabled(),matchReferenceShooting:matchReferenceShooting(),promptOverrides};
if (context.source) body.source=context.source; else if (context.data) body.reportData=context.data;
const response=await fetch('/api/generate-images',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}), payload=await readJsonResponse(response,'/api/generate-images');
if (!response.ok || !payload.ok) throw new Error(payload.error||'无法提交生图任务');
window.dispatchEvent(new CustomEvent('image-job-submitted',{detail:{jobId:payload.jobId}}));
await pollJob(payload.jobId);
window.dispatchEvent(new CustomEvent('image-job-finished',{detail:{jobId:payload.jobId}}));
} catch (error) { renderGenerationRunSummary('error',submittedModelSpecs); if(generateStatus) generateStatus.textContent=error.message||'生图失败'; }
finally { event.currentTarget.disabled=false; }
});
const referenceUpload=dialog.querySelector('.reference-upload');
const referenceUploadButton=dialog.querySelector('.reference-upload-btn');
const referenceEmpty=dialog.querySelector('.reference-empty');
const addReferenceFiles=async fileList=>{
const files=[...fileList].slice(0,Math.max(0,4-uploadedReferences.length));
for (const file of files) {
if (!file.type.startsWith('image/')) continue;
if (file.size>9*1024*1024) { if(generateStatus) generateStatus.textContent='单张参考图不能超过 9MB。'; continue; }
try {
const dataUrl=await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(reader.result);reader.onerror=reject;reader.readAsDataURL(file)});
uploadedReferences.push(dataUrl);
} catch (_) {
if (generateStatus) generateStatus.textContent='参考图读取失败,请重新选择图片。';
}
}
renderReferences(arr(plans)[activePlanIndex]||{});
if (previewSlots.length) await refreshPromptPreview().then(refreshGenerationConfirmation).catch(error=>{if(generateStatus) generateStatus.textContent=error.message||'参考图提示词刷新失败';});
if(generateStatus) generateStatus.textContent=uploadedReferences.length?`已使用上传产品图;生成时会保持产品身份。${matchReferenceShooting()?' '+matchReferenceDescription():''}`:`将使用默认采集参考图。${matchReferenceShooting()?' '+matchReferenceDescription():''}`;
};
referenceInput?.addEventListener('change',async event=>{
await addReferenceFiles(event.target.files);
event.target.value='';
});
// Explicitly proxy the visible button to the file input. This remains
// reliable when modal CSS or browser label activation suppresses the native
// label-to-input click behavior.
referenceUploadButton?.addEventListener('click',event=>{
if (event.target===referenceInput) return;
event.preventDefault();
referenceInput?.click();
});
referenceEmpty?.addEventListener('click',()=>referenceInput?.click());
referenceUpload?.addEventListener('dragenter',event=>{
event.preventDefault();
referenceUpload.classList.add('is-dragging');
});
referenceUpload?.addEventListener('dragover',event=>event.preventDefault());
referenceUpload?.addEventListener('dragleave',event=>{
if (!referenceUpload.contains(event.relatedTarget)) referenceUpload.classList.remove('is-dragging');
});
referenceUpload?.addEventListener('drop',async event=>{
event.preventDefault();
referenceUpload.classList.remove('is-dragging');
await addReferenceFiles(event.dataTransfer?.files||[]);
});
referenceList?.addEventListener('click',event=>{
const button=event.target.closest('[data-remove-reference]'); if(!button)return;
uploadedReferences.splice(Number(button.dataset.removeReference),1);
if (!uploadedReferences.length&&fissionInput) fissionInput.checked=true;
renderReferences(arr(plans)[activePlanIndex]||{});
if (previewSlots.length) refreshPromptPreview().then(refreshGenerationConfirmation).catch(error=>{if(generateStatus) generateStatus.textContent=error.message||'参考图提示词刷新失败';});
});
fissionInput?.addEventListener('change',()=>{
renderReferences(arr(plans)[activePlanIndex]||{});
if (previewSlots.length) refreshPromptPreview().then(refreshGenerationConfirmation).catch(error=>{if(generateStatus) generateStatus.textContent=error.message||'裂变提示词刷新失败';});
});
matchReferenceInput?.addEventListener('change',async()=>{
syncMatchReferenceControl(arr(plans)[activePlanIndex]||{});
syncFissionControl();
if (!previewSlots.length) {
if (generateStatus) generateStatus.textContent=matchReferenceDescription();
return;
}
try {
await refreshPromptPreview();
refreshGenerationConfirmation();
if (generateStatus) generateStatus.textContent=matchReferenceDescription();
} catch(error) {
if (generateStatus) generateStatus.textContent=error.message||'参考图拍摄方式提示词刷新失败';
}
});
document.querySelectorAll('.generate-plan-btn').forEach(button=>button.addEventListener('click',()=>{
loadImageModels();
const plan = arr(plans)[Number(button.dataset.planIndex)] || {};
activePlanIndex=Number(button.dataset.planIndex)||0;
if (userDirectionInput) userDirectionInput.value='';
if (fissionInput) fissionInput.checked=true;
if (matchReferenceInput) matchReferenceInput.checked=false;
uploadedReferences=[]; renderReferences(plan);
previewSlots=[]; selectedSlotKeys=new Set(); promptOverrides={}; retainedResults=[]; generatedTaskSlots=[];
if (generateButton) { generateButton.disabled=true; generateButton.textContent='正在计算生成数量…'; }
submittedModelSpecs=[];
renderGenerationRunSummary('queued',submittedModelSpecs);
title.textContent = plan.name || '快速开品方案';
if (reason) { reason.textContent = plan.whyThisPlan || ''; reason.hidden = !hasContent(plan.whyThisPlan); }
[[product,plan.productAction],[page,plan.pageAction],[validation,plan.validation]].forEach(([node,value])=>{if(!node)return;node.textContent=value||'';node.closest('div').hidden=!hasContent(value)});
if (generateStatus) generateStatus.textContent='已按此方案准备生图提示词。请先选择要生成的图片类型。';
if (generateResults) generateResults.replaceChildren();
const previewBody={planIndex:activePlanIndex,assetTypes:['main','detail'],referenceImages:uploadedReferences,userDirection:userDirection(),fissionPattern:fissionEnabled(),matchReferenceShooting:matchReferenceShooting(),promptOverrides};
if (context.source) previewBody.source=context.source; else if (context.data) previewBody.reportData=context.data;
fetch('/api/image-prompt-preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(previewBody)}).then(response=>readJsonResponse(response,'/api/image-prompt-preview')).then(payload=>{
if (!payload.ok) throw new Error(payload.error||'提示词预览失败');
const promptIndexes={main:0,detail:0};
previewSlots=arr(payload.slots).map(slot=>{
const index=promptIndexes[slot.assetType]++;
const key=`${slot.assetType}:${slot.index}`;
return {...slot,prompt:arr(payload.prompts?.[slot.assetType])[index]||'',promptMerge:payload.promptMerges?.[key],userDirection:payload.userDirectionBySlot?.[key]};
});
// 默认保留完整主图链和前六个详情证明位;详情 7-15 可按需勾选。
selectedSlotKeys=new Set(previewSlots.filter(slot=>slot.assetType==='main'||slot.index<=6).map(slotKey));
refreshGenerationConfirmation();
}).catch(error=>{if(generateStatus) generateStatus.textContent=error.message||'生成数量计算失败,请重新打开方案。'});
if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open','');
}));
userDirectionInput?.addEventListener('change',()=>{
if (!previewSlots.length) return;
const previewBody={planIndex:activePlanIndex,assetTypes:['main','detail'],referenceImages:uploadedReferences,userDirection:userDirection(),fissionPattern:fissionEnabled(),matchReferenceShooting:matchReferenceShooting(),promptOverrides};
if (context.source) previewBody.source=context.source; else if (context.data) previewBody.reportData=context.data;
fetch('/api/image-prompt-preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(previewBody)}).then(response=>readJsonResponse(response,'/api/image-prompt-preview')).then(payload=>{
if (!payload.ok) throw new Error(payload.error||'用户补充判断失败');
const promptIndexes={main:0,detail:0};
previewSlots=arr(payload.slots).map(slot=>{
const index=promptIndexes[slot.assetType]++;
const key=`${slot.assetType}:${slot.index}`;
return {...slot,prompt:arr(payload.prompts?.[slot.assetType])[index]||'',promptMerge:payload.promptMerges?.[key],userDirection:payload.userDirectionBySlot?.[key]};
});
refreshGenerationConfirmation();
if (generateStatus && payload.userDirection?.raw) generateStatus.textContent=directionNote(payload.userDirection);
}).catch(error=>{if(generateStatus) generateStatus.textContent=error.message||'用户补充判断失败';});
});
dialog.querySelector('[data-close-plan]')?.addEventListener('click',()=>dialog.close ? dialog.close() : dialog.removeAttribute('open'));
loadImageModels();
}
function openImagePreview(items,startIndex=0) {
const list=arr(items).filter(item=>item?.url);
if (!list.length) return;
let index=Math.max(0,Math.min(startIndex,list.length-1));
let dialog=document.querySelector('[data-image-preview-dialog]');
if (!dialog) {
dialog=document.createElement('dialog');
dialog.className='image-preview-dialog';
dialog.setAttribute('data-image-preview-dialog','');
dialog.innerHTML='
';
document.body.append(dialog);
dialog.addEventListener('click',event=>{if(event.target===dialog||event.target.closest('[data-preview-close]')) dialog.close();});
dialog.addEventListener('keydown',event=>{if(event.key==='ArrowLeft'){event.preventDefault();dialog.querySelector('[data-preview-prev]')?.click();}if(event.key==='ArrowRight'){event.preventDefault();dialog.querySelector('[data-preview-next]')?.click();}});
}
const render=()=>{
const item=list[index]||list[0], img=dialog.querySelector('[data-preview-image]');
img.src=item.url; img.alt=item.title||'生成图片';
dialog.querySelector('[data-preview-title]').textContent=item.title||'生成图片';
dialog.querySelector('[data-preview-count]').textContent=`${index+1} / ${list.length}`;
};
dialog.querySelector('[data-preview-prev]').onclick=()=>{index=(index-1+list.length)%list.length;render();};
dialog.querySelector('[data-preview-next]').onclick=()=>{index=(index+1)%list.length;render();};
render();
if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open','');
}
function initGenerationHistory(context={}) {
const root=document.querySelector('[data-generation-history]');
if (!root) return;
const endpoint='/api/image-jobs'+(context.source?'?source='+encodeURIComponent(context.source):'');
const dialog=document.querySelector('[data-generation-history-dialog]');
const trigger=document.querySelector('[data-generation-history-open]');
const countNode=document.querySelector('[data-generation-history-count]');
let timer=null;
const openHistory=()=>{
if (dialog && typeof dialog.showModal === 'function') dialog.showModal();
else if (dialog) dialog.setAttribute('open','');
};
trigger?.addEventListener('click',openHistory);
dialog?.addEventListener('click',event=>{
if (event.target===dialog || event.target.closest('[data-generation-history-close]')) {
if (typeof dialog.close === 'function') dialog.close();
else dialog.removeAttribute('open');
}
});
const render=records=>{
const items=arr(records);
if (countNode) {
countNode.textContent=items.length>99?'99+':String(items.length);
countNode.hidden=!items.length;
}
if (!items.length) { root.innerHTML='还没有生成记录。选择具体开品方案后,可异步生成主图与详情图。
'; return; }
root.innerHTML=items.map(job=>{
const failedCount=arr(job.failedSlots).length;
const status=job.status==='complete'?(failedCount?`部分完成 · 失败 ${failedCount} 张`:'已完成'):job.status==='error'?'失败':`生成中 ${Number(job.progress||0)}%`;
const thumbs=orderedResults(job.results).slice(0,15).map(item=>``).join('');
const date=job.createdAt?new Date(Number(job.createdAt)*1000).toLocaleString('zh-CN',{hour12:false}):'';
const shootPolicy=job.matchReferenceShooting?'已按参考图拍摄与展示状态一致':'按任务重新设计拍摄与展示';
const download=job.jobId && arr(job.results).length ? `打包下载` : '';
const resultCount=arr(job.results).length;
const modelSummary=arr(job.comparisonModels).map(item=>`${item.label||item.id}(质量:${modelQuality(item)})`).filter(Boolean).join('、');
return `${modelSummary?`对比模型:${esc(modelSummary)} · `:''}${shootPolicy} · ${job.referenceShootingPolicy==='match_reference'?'锁定机位/构图/光线/动作/摆放':'仅锁定产品身份'}
${job.error?`${esc(job.error)}
`:''}${thumbs?`${thumbs}
`:''}记录编号:${esc(job.jobId||'')}${resultCount?`已保存 ${resultCount} 张图片`:''}
`;
}).join('');
};
root.addEventListener('click',event=>{
const button=event.target.closest('[data-history-preview-url]');
if (!button) return;
const record=button.closest('.generation-record')||root;
const buttons=[...record.querySelectorAll('[data-history-preview-url]')];
openImagePreview(buttons.map(node=>({url:node.dataset.historyPreviewUrl,title:node.dataset.historyPreviewTitle||'生成图片'})),buttons.indexOf(button));
});
const load=async()=>{
clearTimeout(timer);
try {
const response=await fetch(endpoint,{cache:'no-store'}), payload=await readJsonResponse(response,endpoint);
if (!response.ok||!payload.ok) throw new Error(payload.error||'生成记录读取失败');
render(payload.records);
if (arr(payload.records).some(x=>!['complete','error'].includes(x.status))) timer=setTimeout(load,1800);
} catch(error) {
if (countNode) countNode.hidden=true;
root.innerHTML=`${esc(error.message||'生成记录读取失败')}
`;
}
};
load();
window.addEventListener('image-job-submitted',load);
window.addEventListener('image-job-finished',load);
}
async function main() {
const query = new URLSearchParams(location.search);
const source = window.REPORT_DATA || query.get('data');
if (!source) throw new Error('缺少 data 参数');
let data;
if (typeof source === 'object') data = source;
else {
const response = await fetch(source,{cache:'no-store'});
if (!response.ok) throw new Error(`报告数据读取失败(${response.status})`);
data = await readJsonResponse(response,String(source));
}
evidenceIndex = new Map(arr(data.evidenceLedger).map(item=>[item.id,item]));
const exp = data.experienceSolution || {}, roles = data.roleOutputs || {}, facts = data.facts || {};
const audit = data.meta?.reportValidation;
const auditNotice = data.meta?.reportNotice || (!audit?.ok && audit?.missing?.length ? `审核未通过,报告仍已生成。请优先补齐:${audit.missing.join('、')}` : '');
const product = facts.product || {}, sales = facts.sales || {}, summary = exp.reportSummary || {};
const owner = exp.ownerOverview?.cards || roles.executiveDecision?.cards || [];
const plans = exp.newProductPlans?.plans || data.launchPlans?.plans || [];
const title = exp.titleAnalysis || roles.conversion?.title || {};
const visual = exp.visualCommerce || roles.conversion?.visual || {};
const detail = exp.detailCommerce || roles.conversion?.detail || {};
const sku = exp.skuAnalysis || roles.productSupply?.sku || {};
const customer = exp.customerExperience || roles.consumerInsight || {};
const mainEvidence = imageEntries(data,'main');
const coverEvidence = arr(data.evidenceLedger).find(item=>item?.id === summary.coverImageId && item?.type === 'image' && item?.meta?.group === 'main') || mainEvidence[0];
const orderedMain = coverEvidence ? [coverEvidence,...mainEvidence.filter(item=>item.id!==coverEvidence.id)] : mainEvidence;
const heroSlides = orderedMain.map((item,index)=>`
`).join('');
const heroDots = orderedMain.map((item,index)=>``).join('');
const heroDeckCenter = (orderedMain.length - 1) / 2;
const heroDeck = orderedMain.map((item,index)=>``).join('');
const bodies = [
['decision','老板速览','先看结论,再进入证据和执行动作',decisionCards(owner)],
['plan','下一款方向','从证据、反馈和探索方向拆分执行优先级',planCards(plans)],
['title','标题表达','标题承担点击与首次筛选任务',titleAnalysis(title)],
['main','主图在卖什么','逐张说明信息任务、视觉信号与下一步动作',mainImages(data,visual)],
['detail','详情页证明什么','图片与分析绑定,形成连续的页面证明链',detailProof(data,detail)],
['sku','SKU怎么选','先解决适配,再解决组合与外观偏好',skuAnalysis(sku)],
['review','消费者实际感受到什么','样本边界、主题频次、体验结论和买家秀共同验证',reviews(data,customer)],
['verification','产品验证与证据闭环','参数、页面表达、消费者反馈与下一步验证集中核对',supportAnalysis(data)],
['roles','七角色完整分析','完整保留专业判断,默认折叠避免干扰老板速览',roleOutputs(roles)]
].filter(([, , ,body])=>hasContent(body));
const sectionHtml = bodies.map(([id,titleText,deck,body],index)=>section(id,String(index+1).padStart(2,'0'),titleText,deck,body)).join('');
const navNames = {decision:'老板速览',detail:'详情',review:'评论',verification:'验证闭环',roles:'完整分析'};
const navLinks = bodies.filter(([id])=>navNames[id]).map(([id])=>`${navNames[id]}`).join('');
const hasPlans = bodies.some(([id])=>id==='plan');
const heroStats = [['当前价格',sales.currentPrice,'price'],['评论样本',data.baseline?.reviewCount,'sample'],['状态',data.dataQuality?.grade,'status']].filter(([,value])=>hasContent(value));
document.title = summary.title || product.title || '商品开品分析报告';
document.getElementById('app').innerHTML = `${sectionHtml}${hasPlans?'':''}`;
const modelControls=document.createElement('div');
modelControls.className='image-model-controls';
modelControls.setAttribute('data-image-model-controls','');
modelControls.innerHTML='正在读取生图模型…';
const generationControlsNode=document.querySelector('[data-generation-controls]');
generationControlsNode?.after(modelControls);
const generationHeading=document.querySelector('#quick-plan-dialog .image-generation-panel h3');
if (generationHeading) generationHeading.textContent='选择要生成的图片';
const generationButton=document.querySelector('#quick-plan-dialog [data-generate-images]');
const generationStatus=document.querySelector('#quick-plan-dialog [data-generate-status]');
const generationRunSummary=document.createElement('div');
generationRunSummary.className='generation-run-summary';
generationRunSummary.setAttribute('data-generation-run-summary','');
generationRunSummary.hidden=true;
generationStatus?.after(generationRunSummary);
const historyTrigger=document.createElement('button');
historyTrigger.className='generation-history-fab';
historyTrigger.type='button';
historyTrigger.setAttribute('data-generation-history-open','');
historyTrigger.setAttribute('aria-label','打开生成记录');
historyTrigger.innerHTML='↺生成记录0';
const historyDialog=document.createElement('dialog');
historyDialog.className='generation-history-dialog';
historyDialog.setAttribute('data-generation-history-dialog','');
historyDialog.innerHTML='AI VISUAL PRODUCTION生成记录
按任务查看生成结果;点击图片查看原图,已有成果可直接打包下载。
';
document.body.append(historyTrigger,historyDialog);
initHeroCarousel();
const userDirectionField=document.querySelector('[data-user-direction]');
if (userDirectionField) {
userDirectionField.previousElementSibling.textContent='用户最终要求';
userDirectionField.placeholder='例如:改成天丝棉材质、颜色改为浅灰;或更偏高端酒店感、背景留白更大。不要填写价格、品牌Logo、认证、二维码等平台禁区。';
}
const footer=document.querySelector('.report-footer .wrap');
if (footer) {
const brand=document.createElement('div'); brand.className='report-footer-brand'; brand.innerHTML='三笙AI商品开品分析';
footer.prepend(brand);
footer.append(document.createElement('br'),'参数事实、验证门与验证闭环统一收录在底部完整分析区。');
}
initPlanActions(plans,{source:typeof source==='string'?source:'',data:typeof source==='object'?data:null});
initGenerationHistory({source:typeof source==='string'?source:'',data:typeof source==='object'?data:null});
}
main().catch(error => { document.getElementById('app').innerHTML = `报告加载失败
${esc(error.message)}
`; });