AirShelf/电商AI平台/studio-v2.html
iye 553014cc79
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
更新电商AI平台原型交互
2026-05-25 19:12:56 +08:00

1403 lines
78 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>商品工作台 V2 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<style>
.content { max-width: none !important; padding: 0 !important; }
.content > .corner-mark { display: none; }
/* ─── Layout ─── */
.ws {
display: grid;
grid-template-columns: 280px 1fr;
height: calc(100vh - 57px);
overflow: hidden;
background: var(--background-base);
}
/* ════════════ 左侧:商品列表 ════════════ */
.ws-list { display: flex; flex-direction: column; background: var(--surface); border-right: 1px solid var(--border-faint); overflow: hidden; }
.ws-list-h { padding: 16px 14px 12px; border-bottom: 1px solid var(--border-faint); display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
.ws-list-h .title { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; }
.ws-list-h .title .count { color: var(--heat); font-weight: 600; }
.ws-new-btn {
width: 100%; height: 36px;
display: flex; align-items: center; justify-content: center; gap: 8px;
background: var(--heat); color: #fff; border: 0; border-radius: var(--r-md);
font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit;
box-shadow: inset 0 -3px 6px rgba(250,93,25,.20), 0 1px 2px rgba(250,93,25,.12), 0 2px 4px rgba(250,93,25,.10);
}
.ws-new-btn:hover { box-shadow: inset 0 -3px 6px rgba(250,93,25,.20), 0 2px 4px rgba(250,93,25,.16), 0 4px 8px rgba(250,93,25,.20); }
.ws-new-btn svg { width: 14px; height: 14px; }
.ws-search { position: relative; }
.ws-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--black-alpha-48); z-index: 2; pointer-events: none; }
.ws-search input {
width: 100%; height: 34px; padding: 0 12px 0 34px;
background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md);
font-size: 13px; font-family: inherit; color: var(--accent-black);
}
.ws-search input:focus { outline: none; border-color: var(--heat-40); background: var(--surface); }
.ws-chip-row { display: flex; gap: 6px; padding: 8px 14px 4px; flex-shrink: 0; flex-wrap: wrap; }
.ws-chip {
display: inline-flex; align-items: center; gap: 5px;
height: 24px; padding: 0 9px;
border: 1px solid var(--border-faint); border-radius: 999px;
background: var(--surface); color: var(--black-alpha-56);
font-size: 11.5px; font-weight: 500; cursor: pointer; font-family: inherit;
}
.ws-chip:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
.ws-chip.on { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); }
.ws-chip .n { font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em; }
.ws-list-body { flex: 1; overflow-y: auto; padding: 4px 8px 14px; }
.ws-prod {
display: grid; grid-template-columns: 40px 1fr auto;
align-items: center; gap: 10px; padding: 9px 10px;
border-radius: var(--r-md); cursor: pointer; margin-bottom: 2px;
position: relative; transition: background var(--t-base);
}
.ws-prod:hover { background: var(--black-alpha-4); }
.ws-prod.active { background: var(--heat-12); }
.ws-prod.active::before { content: ''; position: absolute; left: 0; top: 7px; bottom: 7px; width: 3px; border-radius: 0 2px 2px 0; background: var(--heat); }
.ws-prod-thumb { width: 40px; height: 40px; border-radius: var(--r-md); background-size: cover; background-position: center; background-color: var(--background-lighter); flex-shrink: 0; }
.ws-prod-info { min-width: 0; }
.ws-prod-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ws-prod.active .ws-prod-name { color: var(--heat); font-weight: 600; }
.ws-prod-meta { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ws-prod-side { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.ws-prod-count { font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; height: 15px; display: inline-flex; align-items: center; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: 999px; letter-spacing: .04em; }
.ws-prod.active .ws-prod-count { background: var(--surface); color: var(--heat); border-color: var(--heat-20); }
.ws-prod-task { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; }
.ws-prod-task .dot-mini { width: 5px; height: 5px; border-radius: 999px; flex-shrink: 0; }
.ws-prod-task.running .dot-mini { background: var(--heat); animation: blink 1.4s ease-in-out infinite; }
.ws-prod-task.running { color: var(--heat); }
.ws-prod-task.done .dot-mini { background: var(--accent-forest); }
.ws-prod-task.done { color: var(--accent-forest); }
.ws-prod-task.failed .dot-mini { background: var(--accent-crimson); }
.ws-prod-task.failed { color: var(--accent-crimson); }
@keyframes blink { 0%,100% { opacity:1; transform: scale(1); } 50% { opacity:.5; transform: scale(.7); } }
.ws-prod.draft .ws-prod-thumb { background: var(--heat-8); color: var(--heat); display: grid; place-items: center; border: 1px dashed var(--heat-40); }
.ws-prod.draft .ws-prod-thumb svg { width: 16px; height: 16px; }
.ws-prod.draft .ws-prod-meta { color: var(--heat); }
.ws-list-foot { padding: 9px 14px; border-top: 1px solid var(--border-faint); flex-shrink: 0; display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.ws-list-foot .accent { color: var(--heat); font-weight: 600; }
.ws-list-foot svg { width: 12px; height: 12px; }
/* ════════════ 右侧:工作区 ════════════ */
.ws-main { overflow-y: auto; display: flex; flex-direction: column; }
/* 顶部 sticky 行动条 —— 整合商品身份卡 + 工具入口 + 主 CTA */
.action-bar {
position: sticky; top: 0; z-index: 10;
background: var(--surface);
border-bottom: 1px solid var(--border-faint);
padding: 14px 36px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 24px;
}
.action-bar .prod-id {
display: flex; align-items: center; gap: 12px;
min-width: 0;
}
.action-bar .prod-thumb {
width: 48px; height: 48px;
border-radius: var(--r-md);
background-size: cover; background-position: center;
background-color: var(--background-lighter);
flex-shrink: 0;
border: 1px solid var(--border-faint);
}
.action-bar .prod-thumb.draft { background: var(--heat-8); display: grid; place-items: center; color: var(--heat); border: 1px dashed var(--heat-40); }
.action-bar .prod-thumb.draft svg { width: 18px; height: 18px; }
.action-bar .prod-text { min-width: 0; }
.action-bar .prod-text .nm { font-size: 16px; font-weight: 600; color: var(--accent-black); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.25; }
.action-bar .prod-text .meta {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .04em;
margin-top: 4px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.action-bar .prod-text .meta .accent { color: var(--heat); font-weight: 600; }
/* 工具按钮组 —— 顶部 sticky,横排 */
.tools-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.tool-btn {
display: flex; align-items: center; gap: 9px;
height: 44px; padding: 0 16px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer; font-family: inherit;
position: relative;
transition: all var(--t-base);
}
.tool-btn:hover { border-color: var(--heat-40); background: var(--heat-4); transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0,0,0,.04); }
.tool-btn.featured {
border-color: var(--heat); background: var(--heat-8);
box-shadow: inset 0 -2px 4px rgba(250,93,25,.10), 0 1px 2px rgba(250,93,25,.08);
}
.tool-btn.featured::after {
content: '推荐'; position: absolute; top: -7px; right: 8px;
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
color: #fff; background: var(--heat);
padding: 2px 6px; border-radius: var(--r-sm); letter-spacing: .04em;
}
.tool-btn .ic { width: 22px; height: 22px; color: var(--heat); flex-shrink: 0; display: grid; place-items: center; }
.tool-btn .ic svg { width: 18px; height: 18px; }
.tool-btn .lbl { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.tool-btn .lbl .t { font-size: 13px; font-weight: 600; color: var(--accent-black); line-height: 1.1; }
.tool-btn .lbl .d { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.1; }
.tool-btn.coming-soon { opacity: .55; cursor: default; }
.tool-btn.coming-soon:hover { transform: none; box-shadow: none; border-color: var(--border-faint); background: var(--surface); }
.tool-btn.coming-soon .ic { color: var(--black-alpha-48); }
.tools-row.locked .tool-btn:not(.coming-soon) {
opacity: .45; pointer-events: none; filter: grayscale(.4);
}
.tools-row.locked .tool-btn::before {
content: '🔒';
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
font-size: 14px; opacity: 0; pointer-events: none;
}
.action-bar .right-cta { display: flex; gap: 8px; flex-shrink: 0; }
/* 工具锁定提示横条 */
.lock-row {
background: var(--heat-8); border-bottom: 1px solid var(--heat-20);
padding: 10px 36px;
display: flex; align-items: center; gap: 10px;
font-size: 12.5px; color: var(--accent-black);
}
.lock-row .ic { width: 26px; height: 26px; background: var(--heat); color: #fff; border-radius: var(--r-md); display: grid; place-items: center; flex-shrink: 0; }
.lock-row .ic svg { width: 14px; height: 14px; }
.lock-row strong { color: var(--heat); font-weight: 600; }
.lock-row .miss { font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }
/* 主内容区 */
.ws-main-body { padding: 24px 36px 60px; flex: 1; }
/* Onboarding(简化版) */
.onboard-tip {
background: var(--surface); border: 1px solid var(--heat-40);
border-radius: var(--r-md); padding: 12px 16px;
margin-bottom: 18px;
display: flex; align-items: center; gap: 12px;
animation: slideIn .4s ease;
}
@keyframes slideIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
.onboard-tip .ic { width: 28px; height: 28px; background: var(--heat); color: #fff; border-radius: var(--r-md); display: grid; place-items: center; flex-shrink: 0; }
.onboard-tip .ic svg { width: 14px; height: 14px; }
.onboard-tip .body { flex: 1; }
.onboard-tip .t { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.onboard-tip .d { font-size: 11.5px; color: var(--black-alpha-72); margin-top: 2px; }
.onboard-tip .d strong { color: var(--heat); font-weight: 600; }
.onboard-tip .dismiss { background: transparent; border: 0; color: var(--black-alpha-56); font-size: 11.5px; cursor: pointer; padding: 4px 8px; border-radius: var(--r-md); }
.onboard-tip .dismiss:hover { background: var(--black-alpha-4); color: var(--accent-black); }
/* ───── 商品概览卡(信息 + 原图)───── */
.overview-card {
background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md);
padding: 20px 22px; margin-bottom: 22px;
display: grid; grid-template-columns: 1fr 320px; gap: 28px;
transition: border-color var(--t-base);
}
.overview-card.editing { border-color: var(--heat-40); background: linear-gradient(180deg, var(--heat-4) 0%, var(--surface) 50px); }
.ov-display .ov-info-head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 12px; }
.ov-display .ic { width: 32px; height: 32px; background: var(--heat-12); color: var(--heat); border-radius: var(--r-md); display: grid; place-items: center; flex-shrink: 0; }
.ov-display .ic svg { width: 16px; height: 16px; }
.ov-display .name { font-size: 14.5px; font-weight: 600; color: var(--accent-black); line-height: 1.3; }
.ov-display .meta { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; margin-top: 4px; }
.ov-display .edit {
margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
height: 28px; padding: 0 11px;
background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-md);
color: var(--black-alpha-56); font-size: 11.5px; cursor: pointer; font-family: inherit;
}
.ov-display .edit:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-8); }
.ov-display .edit svg { width: 11px; height: 11px; }
.ov-tags { display: flex; gap: 5px; margin-top: 4px; flex-wrap: wrap; }
.ov-tags .t { font-size: 11px; padding: 2px 9px; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
.ov-sell { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border-faint); font-size: 12px; color: var(--black-alpha-72); line-height: 1.65; }
.ov-sell .lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; margin-right: 6px; }
.ov-photos { border-left: 1px dashed var(--border-faint); padding-left: 26px; }
.ov-photos-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.ov-photos-h .t { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; }
.ov-photos-h .n { color: var(--heat); font-weight: 600; }
.ov-photos-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.overview-card.editing .ov-photos-grid { grid-template-columns: repeat(3, 1fr); gap: 7px; }
.ov-photo { aspect-ratio: 1; border-radius: var(--r-sm); border: 1px solid var(--border-faint); background-color: var(--background-lighter); background-size: cover; background-position: center; position: relative; cursor: pointer; transition: all var(--t-base); }
.ov-photo:hover { border-color: var(--heat-40); transform: scale(1.04); z-index: 1; }
.ov-photo .pmain { position: absolute; top: 3px; left: 3px; font-family: var(--font-mono); font-size: 8.5px; font-weight: 600; padding: 1px 4px; background: var(--heat); color: #fff; border-radius: 3px; letter-spacing: .04em; }
.ov-photo.add { border-style: dashed; display: grid; place-items: center; color: var(--black-alpha-32); }
.ov-photo.add:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
.ov-photo.add svg { width: 14px; height: 14px; }
.overview-card.editing .ov-photo.empty-slot { border-radius: var(--r-md); border-style: dashed; cursor: pointer; display: grid; place-items: center; color: var(--black-alpha-32); font-size: 10px; font-family: var(--font-mono); }
.overview-card.editing .ov-photo.empty-slot.add-active { border-color: var(--heat-40); color: var(--heat); }
.overview-card.editing .ov-photo.empty-slot:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
.overview-card.editing .ov-photo .photo-x { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border-radius: 999px; background: rgba(21,20,15,.72); color: #fff; border: 0; cursor: pointer; display: none; place-items: center; }
.overview-card.editing .ov-photo:hover .photo-x { display: grid; }
.overview-card.editing .ov-photo:hover .pmain { display: none; }
.overview-card.editing .ov-photo .photo-x:hover { background: var(--accent-crimson); }
.overview-card.editing .ov-photo .photo-x svg { width: 10px; height: 10px; }
.ov-edit { display: none; }
.overview-card.editing .ov-display { display: none; }
.overview-card.editing .ov-edit { display: block; }
.ov-edit .field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.ov-edit .field-label { font-size: 12px; font-weight: 500; color: var(--black-alpha-72); display: flex; align-items: center; gap: 6px; }
.ov-edit .field-label .req { color: var(--accent-crimson); }
.ov-edit .field-label .opt { font-family: var(--font-mono); font-size: 10px; padding: 1px 6px; background: var(--background-lighter); color: var(--black-alpha-48); border-radius: var(--r-sm); letter-spacing: .04em; }
.ov-edit .input, .ov-edit .select { height: 32px; font-size: 13px; }
.ov-edit .ai-hint { margin-top: 4px; padding: 7px 10px; background: var(--heat-8); border: 1px dashed var(--heat-40); border-radius: var(--r-md); font-size: 11px; color: var(--black-alpha-72); line-height: 1.5; display: flex; align-items: flex-start; gap: 6px; }
.ov-edit .ai-hint svg { width: 10px; height: 10px; color: var(--heat); flex-shrink: 0; margin-top: 2px; }
.ov-edit .ai-hint strong { color: var(--heat); font-weight: 600; }
.ov-sell-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
.ov-sell-list li { display: flex; align-items: center; gap: 7px; padding: 6px 9px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 12px; }
.ov-sell-list li.add { background: var(--surface); border-style: dashed; }
.ov-sell-list .num { width: 17px; height: 17px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 10px; font-family: var(--font-mono); color: var(--black-alpha-56); display: grid; place-items: center; flex-shrink: 0; }
.ov-sell-list li.add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
.ov-sell-list .txt { flex: 1; min-width: 0; }
.ov-sell-list .bl-input { flex: 1; border: 0; background: transparent; font-size: 12px; padding: 0; font-family: inherit; color: var(--accent-black); }
.ov-sell-list .bl-input::placeholder { color: var(--black-alpha-48); }
.ov-sell-list .bl-x { width: 18px; height: 18px; display: grid; place-items: center; color: var(--black-alpha-48); cursor: pointer; background: transparent; border: 0; opacity: 0; }
.ov-sell-list li:hover .bl-x { opacity: 1; }
.ov-sell-list .bl-x:hover { color: var(--accent-crimson); }
.ov-sell-list .bl-x svg { width: 10px; height: 10px; }
/* ───── 已生成资产 ───── */
.asset-section { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px 22px; }
.section-title { display: flex; align-items: baseline; gap: 12px; margin-bottom: 14px; }
.section-title h2 { font-size: 15px; font-weight: 600; color: var(--accent-black); }
.section-title .sub { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; }
.asset-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border-faint); margin-bottom: 14px; }
.asset-tab { padding: 0 13px; height: 32px; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 500; color: var(--black-alpha-56); cursor: pointer; border: 0; background: transparent; position: relative; font-family: inherit; }
.asset-tab .count { font-family: var(--font-mono); font-size: 10px; padding: 1px 6px; background: var(--background-lighter); color: var(--black-alpha-48); border-radius: var(--r-pill); letter-spacing: .04em; }
.asset-tab:hover { color: var(--accent-black); }
.asset-tab.active { color: var(--accent-black); }
.asset-tab.active::after { content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--heat); }
.asset-tab.active .count { color: var(--heat); background: var(--heat-12); }
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); gap: 10px; }
.asset-it { position: relative; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; background: var(--background-lighter); cursor: pointer; transition: all var(--t-base); }
.asset-it:hover { border-color: var(--heat-40); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(0,0,0,.04); }
.asset-it .a-thumb { aspect-ratio: 1; background-size: cover; background-position: center; position: relative; }
.asset-it .a-thumb.skeleton { background: linear-gradient(110deg, var(--background-lighter) 8%, var(--black-alpha-4) 18%, var(--background-lighter) 33%); background-size: 200% 100%; animation: shimmer 1.4s linear infinite; display: grid; place-items: center; color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.asset-it .a-tag { position: absolute; top: 6px; left: 6px; font-family: var(--font-mono); font-size: 9.5px; padding: 2px 6px; background: rgba(255,255,255,.92); color: var(--black-alpha-72); border-radius: var(--r-sm); letter-spacing: .04em; }
.asset-it .a-body { padding: 8px 10px; border-top: 1px solid var(--border-faint); background: var(--surface); }
.asset-it .a-name { font-size: 11.5px; font-weight: 500; color: var(--accent-black); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.asset-it .a-meta { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }
.asset-empty { grid-column: 1 / -1; text-align: center; padding: 36px 0; color: var(--black-alpha-48); }
.asset-empty .ic-empty { width: 40px; height: 40px; margin: 0 auto 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--black-alpha-48); }
.asset-empty .ic-empty svg { width: 18px; height: 18px; }
.asset-empty .t { font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 3px; }
.asset-empty .d { font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }
/* ════════════ 大型 Picker(双栏:左列表 + 右详情) ════════════ */
.picker-bg {
position: fixed; inset: 0;
background: rgba(21,20,15,.50);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1000;
display: none; align-items: center; justify-content: center;
opacity: 0; transition: opacity .2s;
}
.picker-bg.show { display: flex; opacity: 1; }
.picker-pro {
background: var(--surface);
border: 1px solid var(--border-faint); border-radius: var(--r-md);
width: 92%; max-width: 1080px;
height: min(720px, calc(100vh - 60px));
display: grid;
grid-template-rows: auto 1fr auto;
overflow: hidden;
transform: scale(.96);
transition: transform .25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.picker-bg.show .picker-pro { transform: scale(1); }
.pp-h {
padding: 18px 24px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 14px;
}
.pp-h .ic { width: 32px; height: 32px; background: var(--heat-12); color: var(--heat); border-radius: var(--r-md); display: grid; place-items: center; }
.pp-h .ic svg { width: 16px; height: 16px; }
.pp-h .ti { flex: 1; min-width: 0; }
.pp-h .ti .t { font-size: 15px; font-weight: 600; color: var(--accent-black); }
.pp-h .ti .d { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); margin-top: 3px; letter-spacing: .04em; }
.pp-h .pp-filters { display: flex; gap: 6px; flex-shrink: 0; }
.pp-h .pp-filter {
height: 28px; padding: 0 10px;
display: inline-flex; align-items: center; gap: 5px;
border: 1px solid var(--border-faint); border-radius: 999px;
background: var(--surface); color: var(--black-alpha-56);
font-size: 11.5px; font-weight: 500; cursor: pointer; font-family: inherit;
}
.pp-h .pp-filter:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
.pp-h .pp-filter.on { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); }
.pp-h .pp-filter .n { font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em; }
.pp-h .x { width: 30px; height: 30px; border-radius: var(--r-md); display: grid; place-items: center; color: var(--black-alpha-56); cursor: pointer; background: transparent; border: 0; flex-shrink: 0; }
.pp-h .x:hover { background: var(--black-alpha-4); color: var(--accent-crimson); }
.pp-h .x svg { width: 14px; height: 14px; }
/* 双栏 body */
.pp-body {
display: grid;
grid-template-columns: 1fr 380px;
min-height: 0; height: 100%;
}
.pp-body .pp-left {
border-right: 1px solid var(--border-faint);
overflow-y: auto;
padding: 18px 20px;
background: var(--background-lighter);
}
.pp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
/* 模特/平台大卡 */
.opt-pro {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 0; overflow: hidden;
cursor: pointer;
transition: all var(--t-base);
position: relative;
}
.opt-pro:hover { border-color: var(--heat-40); transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,0,0,.04); }
.opt-pro.active { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-20); }
.opt-pro.active::before {
content: ''; position: absolute; top: 8px; right: 8px;
width: 22px; height: 22px; border-radius: 999px;
background: var(--heat); z-index: 2;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
background-size: 14px; background-position: center; background-repeat: no-repeat;
}
.opt-pro .opt-thumb {
aspect-ratio: 3/4;
background-size: cover; background-position: center;
background-color: var(--background-lighter);
display: grid; place-items: center;
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 11px;
position: relative;
}
.opt-pro .opt-thumb.placeholder::before {
content: ''; position: absolute; inset: 0;
background: linear-gradient(135deg, var(--background-lighter), var(--black-alpha-4));
}
.opt-pro .opt-thumb .glyph { z-index: 1; font-size: 32px; opacity: .35; }
.opt-pro .opt-body { padding: 9px 11px; }
.opt-pro .opt-name { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.opt-pro.active .opt-name { color: var(--heat); }
.opt-pro .opt-meta { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); margin-top: 3px; letter-spacing: .02em; }
.opt-pro .opt-tag-mini {
font-family: var(--font-mono); font-size: 9px;
padding: 1px 5px; background: var(--background-lighter); color: var(--black-alpha-56);
border-radius: var(--r-sm); letter-spacing: .04em;
display: inline-block; margin-top: 4px;
}
.opt-pro .opt-uses {
position: absolute; bottom: 56px; left: 8px;
font-family: var(--font-mono); font-size: 9px; letter-spacing: .04em;
padding: 2px 6px;
background: rgba(255,255,255,.92); color: var(--black-alpha-72);
border-radius: var(--r-sm);
}
.opt-pro.custom .opt-thumb {
background: var(--heat-8); color: var(--heat);
border: 1px dashed var(--heat-40);
}
.opt-pro.custom .opt-thumb .glyph { color: var(--heat); opacity: 1; }
/* 右侧详情 */
.pp-right {
display: flex; flex-direction: column;
overflow-y: auto;
padding: 20px;
}
.pp-detail-hero {
aspect-ratio: 3/4;
border-radius: var(--r-md);
background: var(--background-lighter); background-size: cover; background-position: center;
margin-bottom: 14px;
position: relative; overflow: hidden;
display: grid; place-items: center;
}
.pp-detail-hero .glyph { font-size: 88px; opacity: .25; color: var(--black-alpha-48); }
.pp-detail-name { font-size: 18px; font-weight: 600; color: var(--accent-black); }
.pp-detail-meta { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); margin-top: 4px; letter-spacing: .04em; }
.pp-detail-tags { display: flex; gap: 5px; margin-top: 10px; flex-wrap: wrap; }
.pp-detail-tags .t { font-size: 11px; padding: 2px 9px; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
.pp-detail-block { margin-top: 16px; padding-top: 14px; border-top: 1px dashed var(--border-faint); }
.pp-detail-block .lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; margin-bottom: 8px; }
.pp-detail-block .lbl .accent { color: var(--heat); font-weight: 600; }
.pp-detail-uses { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.pp-detail-uses .pdu { aspect-ratio: 1; border-radius: var(--r-sm); background-size: cover; background-position: center; background-color: var(--background-lighter); border: 1px solid var(--border-faint); }
.pp-detail-uses .pdu.empty { display: grid; place-items: center; color: var(--black-alpha-32); font-family: var(--font-mono); font-size: 18px; }
.pp-detail-empty {
height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
color: var(--black-alpha-48); padding: 40px 20px; text-align: center;
}
.pp-detail-empty .ic-em { width: 48px; height: 48px; border-radius: var(--r-md); background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; margin-bottom: 12px; }
.pp-detail-empty .ic-em svg { width: 20px; height: 20px; }
.pp-detail-empty .t { font-size: 13px; color: var(--accent-black); font-weight: 600; margin-bottom: 4px; }
.pp-detail-empty .d { font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }
/* Footer */
.pp-f {
padding: 14px 24px;
border-top: 1px solid var(--border-faint);
background: var(--background-lighter);
display: grid;
grid-template-columns: 1fr auto;
gap: 14px; align-items: center;
}
.pp-f .pp-config { display: flex; flex-direction: column; gap: 6px; }
.pp-f-row { display: flex; align-items: center; gap: 14px; font-size: 12px; }
.pp-f-row > label { width: 60px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.pp-f-row .pillset { display: flex; gap: 5px; flex-wrap: wrap; }
.pp-f-row .pillset .p { padding: 3px 9px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); font-size: 11.5px; color: var(--black-alpha-56); cursor: pointer; transition: all var(--t-base); }
.pp-f-row .pillset .p:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
.pp-f-row .pillset .p.on { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); }
.pp-f .pp-cta { display: flex; align-items: center; gap: 10px; }
.pp-f .pp-meta { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.pp-f .pp-meta .accent { color: var(--heat); font-weight: 600; }
</style>
</head>
<body>
<div id="page">
<div class="ws">
<!-- ════════ 左侧:商品列表 ════════ -->
<aside class="ws-list">
<div class="ws-list-h">
<div class="title">
<span>// 商品库 · <span class="count" id="ws-count">7</span></span>
<span>// MCN · 老张</span>
</div>
<button class="ws-new-btn" id="ws-new-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
新建商品
</button>
<div class="ws-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="ws-search-input" placeholder="搜索商品" />
</div>
</div>
<div class="ws-chip-row">
<button class="ws-chip on" data-filter="all">全部 <span class="n" id="t-all">7</span></button>
<button class="ws-chip" data-filter="running">生成中 <span class="n" id="t-run">1</span></button>
<button class="ws-chip" data-filter="draft">草稿 <span class="n" id="t-draft">0</span></button>
</div>
<div class="ws-list-body" id="ws-list-body"></div>
<div class="ws-list-foot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/></svg>
<span><span class="accent" id="foot-running">1</span> 个任务运行中</span>
</div>
</aside>
<!-- ════════ 右侧:工作区 ════════ -->
<main class="ws-main" id="ws-main"></main>
</div>
</div>
<!-- ════════ 大型 Picker ════════ -->
<div class="picker-bg" id="picker-bg" onclick="if(event.target===this)closePicker()">
<div class="picker-pro" id="picker-pro">
<div class="pp-h" id="pp-h">
<div class="ic" id="pp-ic"></div>
<div class="ti"><div class="t" id="pp-title">选择模特</div><div class="d" id="pp-sub">// 8 位模特可选</div></div>
<div class="pp-filters" id="pp-filters"></div>
<button class="x" onclick="closePicker()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
</div>
<div class="pp-body">
<div class="pp-left">
<div class="pp-grid" id="pp-grid"></div>
</div>
<aside class="pp-right" id="pp-right"></aside>
</div>
<div class="pp-f">
<div class="pp-config" id="pp-config"></div>
<div class="pp-cta">
<span class="pp-meta" id="pp-meta">// ~35 秒 · <span class="accent">¥3.2</span></span>
<button class="btn" onclick="closePicker()">取消</button>
<button class="btn btn-primary" id="pp-go-btn" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
<span id="pp-go-text">生成 4 张</span>
</button>
</div>
</div>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script>
Shell.render({
active: 'products',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品工作台' }]
});
const _pickerEl = document.getElementById('picker-bg');
if (_pickerEl && _pickerEl.parentElement !== document.body) document.body.appendChild(_pickerEl);
// ════════════════════════════════════════════════════════
// Data
// ════════════════════════════════════════════════════════
const PALETTE = [
'linear-gradient(135deg,#fde2c6,#ffd0a8)',
'linear-gradient(135deg,#dceafe,#c3e0fe)',
'linear-gradient(135deg,#fbcfe8,#fce7f3)',
'linear-gradient(135deg,#dcfce7,#bbf7d0)',
'linear-gradient(135deg,#fef3c7,#fde68a)',
];
// 模特 · 带详细信息
const MODELS = [
{ id: 'm1', name: '林夕', gender: '女', age: 25, style: '都市白领', uses: 12, tags: ['通勤', '气质', '简约'], bg: 'linear-gradient(135deg,#fcd5ce,#f8edeb)', glyph: '◐', portfolio: [0,1,2,3] },
{ id: 'm2', name: '小苏', gender: '女', age: 23, style: '文艺学生', uses: 7, tags: ['学生', '清新', '校园'], bg: 'linear-gradient(135deg,#cfe1b9,#e9edc9)', glyph: '◑', portfolio: [0,1,2,3] },
{ id: 'm3', name: '阿楠', gender: '女', age: 28, style: '同事型', uses: 5, tags: ['职场', '亲和', '商务'], bg: 'linear-gradient(135deg,#bee3f8,#c3dafe)', glyph: '◓', portfolio: [0,1,2,3] },
{ id: 'm4', name: '小七', gender: '女', age: 20, style: '学生', uses: 9, tags: ['少女', '甜美', '校园'], bg: 'linear-gradient(135deg,#fde2e4,#fad2e1)', glyph: '◒', portfolio: [0,1,2,3] },
{ id: 'm5', name: '王姐', gender: '女', age: 38, style: '居家', uses: 3, tags: ['妈妈', '居家', '温暖'], bg: 'linear-gradient(135deg,#f3e8ff,#fae8ff)', glyph: '◍', portfolio: [0,1,2,3] },
{ id: 'm6', name: '阿杰', gender: '男', age: 30, style: '都市', uses: 4, tags: ['通勤', '商务', '阳光'], bg: 'linear-gradient(135deg,#dbeafe,#bfdbfe)', glyph: '◐', portfolio: [0,1,2,3] },
{ id: 'm7', name: '阿强', gender: '男', age: 26, style: '健身', uses: 6, tags: ['运动', '阳光', '健身'], bg: 'linear-gradient(135deg,#e0f2fe,#bae6fd)', glyph: '◑', portfolio: [0,1,2,3] },
{ id: 'm8', name: '+ 上传自有模特', custom: true, gender: '', age: '', style: '自定义', uses: 0, tags: ['企业账号专用'], bg: '', glyph: '+' },
];
const MODEL_THUMB_PALETTE = [
'linear-gradient(135deg,#fde2c6,#ffd0a8)',
'linear-gradient(135deg,#fcd5ce,#f8edeb)',
'linear-gradient(135deg,#dcfce7,#bbf7d0)',
'linear-gradient(135deg,#bee3f8,#c3dafe)',
];
const PLATFORMS = [
{ id: 'pl1', name: '淘宝 / 天猫', size: '800×800', ratio: '1:1', tags: ['主图', '详情头图'], uses: 4, desc: '强调白底/干净背景,主图允许少量文字。详情页头图 750×1000。' },
{ id: 'pl2', name: '抖店', size: '750×1000', ratio: '3:4', tags: ['沉浸竖屏', '电商'], uses: 2, desc: '竖版构图,符合短视频生态调性。背景可丰富,商品居中突出。' },
{ id: 'pl3', name: '拼多多', size: '800×800', ratio: '1:1', tags: ['夸张利益点', '热闹'], uses: 0, desc: '允许节日/促销元素,色彩饱和。利益点(价格/优惠)突出。' },
{ id: 'pl4', name: '京东', size: '800×800', ratio: '1:1', tags: ['品质感', '商务'], uses: 0, desc: '干净背景,商品居中,标注规格与品质标识。' },
{ id: 'pl5', name: '小红书', size: '600×800', ratio: '3:4', tags: ['种草感', '生活方式'], uses: 1, desc: '强调使用场景,弱化电商感。允许图文叠加。' },
{ id: 'pl6', name: '1688', size: '750×750', ratio: '1:1', tags: ['批发', '工厂'], uses: 0, desc: '突出价格/库存/规格,可加品类标识与厂家信息。' },
];
// 7 个商品 + 各自的 state
const products = [
{ id: 'p1', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: '39.9', target: '22-32 岁女性、敏感肌、办公室通勤', sellPoints: ['玻尿酸双效保湿', '4 小时持久水润', '敏感肌可用', '通勤补水', '平价代替'],
photos: [{ id: 'p1-1', bg: PALETTE[0] }, { id: 'p1-2', bg: PALETTE[1] }, { id: 'p1-3', bg: PALETTE[4] }],
task: 'running',
assets: [
{ id: 'a1', kind: 'white', name: '白底 · 正面', meta: '512×512 · 5/19', color: '#f4f4f4' },
{ id: 'a2', kind: 'white', name: '白底 · 侧面', meta: '512×512 · 5/19', color: '#f0f0f0' },
{ id: 'a3', kind: 'white', name: '白底 · 背面', meta: '512×512 · 5/19', color: '#eeeeee' },
{ id: 'a4', kind: 'model', name: '林夕 · 01', meta: '林夕 · 1024', color: 'linear-gradient(135deg,#ffe0b2,#ffccbc)' },
{ id: 'a5', kind: 'model', name: '林夕 · 02', meta: '林夕 · 1024', color: 'linear-gradient(135deg,#fde2c6,#ffbcaa)' },
{ id: 'a6', kind: 'model', name: '林夕 · 03', meta: '林夕 · 1024', color: 'linear-gradient(135deg,#fbcfe8,#fce7f3)' },
{ id: 'a7', kind: 'model', name: '林夕 · 04', meta: '林夕 · 1024', color: 'linear-gradient(135deg,#f3e8ff,#fae8ff)' },
{ id: 'a8', kind: 'platform', name: '淘宝 · 01', meta: '800×800', color: 'linear-gradient(135deg,#fff1f0,#ffd6cc)' },
{ id: 'a9', kind: 'platform', name: '淘宝 · 02', meta: '800×800', color: 'linear-gradient(135deg,#fef3c7,#fde68a)' },
{ id: 'a10', kind: 'platform', name: '淘宝 · 03', meta: '800×800', color: 'linear-gradient(135deg,#dcfce7,#bbf7d0)' },
{ id: 'a11', kind: 'platform', name: '淘宝 · 04', meta: '800×800', color: 'linear-gradient(135deg,#dbeafe,#bfdbfe)' },
{ id: 'sk1', kind: 'model', skeleton: true, name: '生成中', meta: 'pending' },
],
},
{ id: 'p2', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: '199', target: '通勤族·学生', sellPoints: ['通话降噪', '续航 25 小时', '蓝牙 5.3'],
photos: [{ id: 'p2-1', bg: 'linear-gradient(135deg,#e0e7ff,#c7d2fe)' }, { id: 'p2-2', bg: 'linear-gradient(135deg,#cffafe,#a5f3fc)' }],
task: 'done',
assets: [{ id: 'b1', kind: 'white', name: '白底 · 正面', meta: '512×512', color: '#f4f4f4' }, { id: 'b2', kind: 'platform', name: '抖店 · 01', meta: '750×1000', color: 'linear-gradient(135deg,#fef3c7,#fde68a)' }],
},
{ id: 'p3', name: '滋啦速食牛肉面 6 桶装', cat: '食品饮料', price: '49.9', target: '加班党·独居青年', sellPoints: ['正宗川味', '加班 5 分钟搞定'],
photos: [{ id: 'p3-1', bg: 'linear-gradient(135deg,#fed7aa,#fdba74)' }],
task: null, assets: [],
},
{ id: 'p4', name: '透真清透物理防晒霜', cat: '美妆个护', price: '69', target: '通勤女性·敏感肌', sellPoints: ['SPF50+', '物理防晒', '不闷痘'],
photos: [{ id: 'p4-1', bg: 'linear-gradient(135deg,#fef9c3,#fef08a)' }, { id: 'p4-2', bg: 'linear-gradient(135deg,#fed7aa,#fdba74)' }],
task: null,
assets: [{ id: 'c1', kind: 'white', name: '白底', meta: '512×512', color: '#f4f4f4' }, { id: 'c2', kind: 'model', name: '小苏 · 01', meta: '小苏 · 1024', color: 'linear-gradient(135deg,#fcd5ce,#f8edeb)' }],
},
{ id: 'p5', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: '89', target: '早八党·咖啡爱好者', sellPoints: ['3 秒速溶', '冷热皆宜'],
photos: [{ id: 'p5-1', bg: 'linear-gradient(135deg,#d6d3d1,#a8a29e)' }],
task: 'failed',
assets: [{ id: 'd1', kind: 'white', name: '白底', meta: 'failed', color: '#fef2f2' }],
},
{ id: 'p6', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: '159', target: '小户型·健康人群', sellPoints: ['可视玻璃', '4L 大容量'],
photos: [{ id: 'p6-1', bg: 'linear-gradient(135deg,#fef2f2,#fecaca)' }], task: null, assets: [],
},
{ id: 'p7', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: '119', target: '健身爱好者·通勤女性', sellPoints: ['裸感无痕', '高弹力'],
photos: [{ id: 'p7-1', bg: 'linear-gradient(135deg,#1f2937,#374151)' }, { id: 'p7-2', bg: 'linear-gradient(135deg,#4b5563,#6b7280)' }],
task: null,
assets: [{ id: 'e1', kind: 'model', name: '阿强 · 01', meta: '阿强 · 1024', color: 'linear-gradient(135deg,#bee3f8,#c3dafe)' }],
},
];
// ════════════════════════════════════════════════════════
// State
// ════════════════════════════════════════════════════════
let currentId = 'p1';
let isEditing = false;
let listFilter = 'all';
let listSearch = '';
// Picker 状态
let pickerKind = null; // 'white' | 'model' | 'platform'
let pickerFilter = 'all';
let pickerSelectedId = null; // 当前在右侧详情展示 + 即将生成用的
let pickerConfig = {}; // 各类型的配置
const $ = id => document.getElementById(id);
function current() { return products.find(p => p.id === currentId); }
function escape(s) { return String(s||'').replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]); }
// ════════════════════════════════════════════════════════
// 左侧渲染
// ════════════════════════════════════════════════════════
function renderList() {
const body = $('ws-list-body');
const q = listSearch.toLowerCase();
const filtered = products.filter(p => {
if (listFilter === 'running' && p.task !== 'running') return false;
if (listFilter === 'draft' && !p.draft) return false;
if (q && !`${p.name} ${p.cat}`.toLowerCase().includes(q)) return false;
return true;
});
body.innerHTML = filtered.map(p => {
const isActive = p.id === currentId;
const isDraft = p.draft;
const assetCount = p.assets ? p.assets.filter(a => !a.skeleton).length : 0;
const taskEl = p.task ? `<span class="ws-prod-task ${p.task}"><span class="dot-mini"></span>${p.task === 'running' ? '生成中' : p.task === 'done' ? '完成' : '失败'}</span>` : '';
if (isDraft) {
return `<div class="ws-prod draft ${isActive?'active':''}" data-id="${p.id}">
<div class="ws-prod-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div>
<div class="ws-prod-info">
<div class="ws-prod-name">${p.name || '未命名商品'}</div>
<div class="ws-prod-meta">// 草稿</div>
</div>
<div class="ws-prod-side"></div>
</div>`;
}
return `<div class="ws-prod ${isActive?'active':''}" data-id="${p.id}">
<div class="ws-prod-thumb" style="background:${(p.photos[0]||{}).bg||''};background-size:cover;background-position:center;"></div>
<div class="ws-prod-info">
<div class="ws-prod-name">${escape(p.name)}</div>
<div class="ws-prod-meta">${escape(p.cat)} · ¥${p.price}</div>
</div>
<div class="ws-prod-side"><span class="ws-prod-count">${assetCount}</span>${taskEl}</div>
</div>`;
}).join('');
body.querySelectorAll('.ws-prod').forEach(el => {
el.addEventListener('click', () => {
currentId = el.dataset.id;
isEditing = false;
renderAll();
});
});
$('ws-count').textContent = products.filter(p => !p.draft).length;
$('t-all').textContent = products.length;
$('t-run').textContent = products.filter(p => p.task === 'running').length;
$('t-draft').textContent = products.filter(p => p.draft).length;
$('foot-running').textContent = products.filter(p => p.task === 'running').length;
}
// ════════════════════════════════════════════════════════
// 右侧渲染:顶部行动条 + 主内容
// ════════════════════════════════════════════════════════
function renderMain() {
const p = current();
if (!p) { $('ws-main').innerHTML = ''; return; }
const isFresh = !!p.draft && !p.savedOnce;
const canTools = p.name && p.cat && p.photos.length > 0;
const assetCount = p.assets ? p.assets.filter(a => !a.skeleton).length : 0;
// 顶部 sticky 行动条
const actionBar = `
<div class="action-bar">
<div class="prod-id">
${isFresh
? `<div class="prod-thumb draft"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div>`
: `<div class="prod-thumb" style="background:${(p.photos[0]||{}).bg||''};background-size:cover;background-position:center;"></div>`}
<div class="prod-text">
<div class="nm">${isFresh ? '新建商品' : escape(p.name)}</div>
<div class="meta">${isFresh
? `// 填写商品信息 · 至少 1 张图 · 即可解锁工具`
: `// ${escape(p.cat)} · ¥${p.price} · 已生成 <span class="accent">${assetCount}</span> 张资产`}</div>
</div>
</div>
<div class="tools-row ${canTools?'':'locked'}" id="tools-row">
<button class="tool-btn featured" data-tool="white">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg></span>
<span class="lbl"><span class="t">白底三视图</span><span class="d">~18s · ¥1.6</span></span>
</button>
<button class="tool-btn" data-tool="model">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v2"/></svg></span>
<span class="lbl"><span class="t">AI 模特上身</span><span class="d">~35s · ¥3.2</span></span>
</button>
<button class="tool-btn" data-tool="platform">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg></span>
<span class="lbl"><span class="t">平台套图</span><span class="d">~28s · ¥2.4</span></span>
</button>
<button class="tool-btn coming-soon">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 3"/></svg></span>
<span class="lbl"><span class="t">更多</span><span class="d">海报 · 详情页</span></span>
</button>
</div>
<div class="right-cta">
${isFresh
? `<button class="btn" id="cancel-new">放弃</button>
<button class="btn btn-primary" id="save-new" ${canTools?'':'disabled'}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存
</button>`
: `<a class="btn btn-primary" href="projects-new.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>
创建视频项目
</a>`}
</div>
</div>
`;
// 锁定提示
const lockRow = !canTools ? `
<div class="lock-row">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<span><strong>完成基本信息</strong> 后解锁 AI 工具 · </span>
<span class="miss">还差:${[
!p.name && '商品名',
!p.cat && '品类',
p.photos.length === 0 && '≥1 张图',
].filter(Boolean).join(' / ')}</span>
</div>
` : '';
// Onboarding
const onboardHtml = p.showOnboard ? `
<div class="onboard-tip">
<div class="ic"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/></svg></div>
<div class="body">
<div class="t">商品已建档 · 第一次?推荐先生成白底三视图</div>
<div class="d">AI 用主图生成正/侧/背 3 张白底图,<strong>Seedance 视频效果 +60%</strong></div>
</div>
<button class="dismiss" id="dismiss-onboard">知道了</button>
</div>
` : '';
// 主内容区
const main = `
<div class="ws-main-body">
${onboardHtml}
${renderOverview(p, isFresh)}
<div class="asset-section">
<div class="section-title" style="margin-bottom: 8px;">
<h2>已生成资产</h2>
<span class="sub">// 该商品 · 自动入资产库</span>
</div>
<div class="asset-tabs" id="asset-tabs">
${(() => {
const total = p.assets.filter(a => !a.skeleton).length;
const byKind = k => p.assets.filter(a => a.kind === k && !a.skeleton).length;
return `
<button class="asset-tab active" data-tab="all">全部 <span class="count">${total}</span></button>
<button class="asset-tab" data-tab="white">白底 <span class="count">${byKind('white')}</span></button>
<button class="asset-tab" data-tab="model">模特 <span class="count">${byKind('model')}</span></button>
<button class="asset-tab" data-tab="platform">平台 <span class="count">${byKind('platform')}</span></button>
`;
})()}
</div>
<div class="asset-grid" id="asset-grid">${renderAssetItems(p, 'all')}</div>
</div>
</div>
`;
$('ws-main').innerHTML = actionBar + lockRow + main;
bindMain(p);
}
function renderOverview(p, isFresh) {
if (isFresh) {
return `<div class="overview-card editing"><div><div class="ov-edit" style="display:block;">${renderEditFields(p)}</div></div>${renderPhotos(p, true)}</div>`;
}
if (isEditing) {
return `<div class="overview-card editing"><div><div class="ov-edit" style="display:block;">${renderEditFields(p, true)}</div></div>${renderPhotos(p, true)}</div>`;
}
const tags = p.sellPoints.slice(0, 5);
return `<div class="overview-card">
<div>
<div class="ov-display">
<div class="ov-info-head">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg></div>
<div style="flex:1;min-width:0;">
<div class="name">${escape(p.name)}</div>
<div class="meta">${escape(p.target||'')}</div>
</div>
<button class="edit" id="start-edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
编辑
</button>
</div>
${tags.length ? `<div class="ov-tags">${tags.map(t => `<span class="t">${escape(t)}</span>`).join('')}</div>` : ''}
${p.sellPoints.length ? `<div class="ov-sell"><span class="lbl">// 卖点</span>${p.sellPoints.map(escape).join(' · ')}</div>` : ''}
</div>
</div>
${renderPhotos(p, false)}
</div>`;
}
function renderEditFields(p, includeSaveBar) {
const sellHtml = p.sellPoints.map((pt, i) => `<li><span class="num">${i+1}</span><span class="txt">${escape(pt)}</span><button class="bl-x" type="button" data-i="${i}"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg></button></li>`).join('');
return `
<div class="field">
<label class="field-label">商品名称 <span class="req">*</span></label>
<input class="input" id="e-name" value="${escape(p.name)}" placeholder="例: 透真补水面膜">
</div>
<div class="field" style="display:grid;grid-template-columns:1.5fr 1fr;gap:10px;">
<div>
<label class="field-label">品类 <span class="req">*</span></label>
<select class="select" id="e-cat">
<option value="">— 选择 —</option>
${['美妆个护','服饰内衣','食品饮料','家居家电','数码 3C','个护清洁','运动户外','母婴亲子'].map(c => `<option ${c===p.cat?'selected':''}>${c}</option>`).join('')}
</select>
</div>
<div>
<label class="field-label">价格 <span class="opt">选填</span></label>
<input class="input" id="e-price" type="number" value="${escape(p.price)}" placeholder="¥">
</div>
</div>
<div class="field">
<label class="field-label">目标人群 <span class="opt">选填</span></label>
<input class="input" id="e-target" value="${escape(p.target||'')}" placeholder="例: 22-32 岁女性、敏感肌">
</div>
<div class="field" style="margin-bottom:8px;">
<label class="field-label">核心卖点 <span class="opt">选填 · 推荐</span></label>
<ul class="ov-sell-list" id="e-sell-list">
${sellHtml}
<li class="add"><span class="num">+</span><input class="bl-input" id="e-sell-input" placeholder="回车添加 · 例: 玻尿酸双效保湿"></li>
</ul>
<div class="ai-hint">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/></svg>
<span>填上卖点 & 人群,后续 AI 生<strong>痛点种草</strong>脚本质量更高。</span>
</div>
</div>
${includeSaveBar ? `
<div style="display:flex;align-items:center;gap:10px;padding-top:12px;border-top:1px dashed var(--border-faint);">
<span style="flex:1;font-family:var(--font-mono);font-size:11px;color:var(--black-alpha-48);">// 编辑模式</span>
<button class="btn" id="cancel-edit">取消</button>
<button class="btn btn-primary" id="save-edit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存
</button>
</div>
` : ''}
`;
}
function renderPhotos(p, isEdit) {
const MAX = 5;
let slots = '';
if (isEdit) {
for (let i = 0; i < MAX; i++) {
const ph = p.photos[i];
if (ph) {
slots += `<div class="ov-photo" style="background:${ph.bg};background-size:cover;background-position:center;">
${i===0?'<span class="pmain">MAIN</span>':''}
<button class="photo-x" data-i="${i}" type="button"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>
</div>`;
} else if (i === p.photos.length) {
slots += `<div class="ov-photo empty-slot add-active" data-add-photo>+ 上传</div>`;
} else {
slots += `<div class="ov-photo empty-slot" style="opacity:.5;"></div>`;
}
}
} else {
p.photos.forEach((ph, i) => {
slots += `<div class="ov-photo" style="background:${ph.bg};background-size:cover;background-position:center;">${i===0?'<span class="pmain">MAIN</span>':''}</div>`;
});
if (p.photos.length < MAX) slots += `<div class="ov-photo add" data-start-edit><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div>`;
}
return `<div class="ov-photos">
<div class="ov-photos-h"><span class="t">原图册 · <span class="n">${p.photos.length}</span> / ${MAX}</span><span class="t" style="color:var(--black-alpha-32);">JPG / PNG</span></div>
<div class="ov-photos-grid">${slots}</div>
</div>`;
}
function renderAssetItems(p, tab) {
const filtered = tab === 'all' ? p.assets : p.assets.filter(a => a.kind === tab);
if (filtered.length === 0) {
return `<div class="asset-empty">
<div class="ic-empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg></div>
<div class="t">还没有资产</div>
<div class="d">// 上方工具开始生成</div>
</div>`;
}
return filtered.map(a => {
const isGradient = (a.color||'').startsWith('linear');
const tagText = { white: '白底', model: '模特', platform: '平台' }[a.kind];
return `<div class="asset-it">
<div class="a-thumb${a.skeleton?' skeleton':''}" style="${a.skeleton?'':(isGradient?`background:${a.color}`:`background-color:${a.color}`)}">
${a.skeleton ? '生成中…' : `<span class="a-tag">${tagText}</span>`}
</div>
${a.skeleton ? '' : `<div class="a-body"><div class="a-name">${escape(a.name)}</div><div class="a-meta">${escape(a.meta)}</div></div>`}
</div>`;
}).join('');
}
// ════════════════════════════════════════════════════════
// 事件
// ════════════════════════════════════════════════════════
function bindMain(p) {
$('cancel-new') && $('cancel-new').addEventListener('click', () => {
products.splice(products.indexOf(p), 1);
currentId = products[0]?.id || '';
isEditing = false;
renderAll();
});
$('save-new') && $('save-new').addEventListener('click', () => saveProduct(p, true));
$('start-edit') && $('start-edit').addEventListener('click', () => { isEditing = true; renderMain(); });
$('cancel-edit') && $('cancel-edit').addEventListener('click', () => { isEditing = false; renderMain(); });
$('save-edit') && $('save-edit').addEventListener('click', () => saveProduct(p, false));
$('dismiss-onboard') && $('dismiss-onboard').addEventListener('click', () => { p.showOnboard = false; renderMain(); });
// 编辑字段
const eName = $('e-name'), eCat = $('e-cat'), ePrice = $('e-price'), eTarget = $('e-target');
if (eName) {
eName.addEventListener('input', () => { p.name = eName.value; updateSaveBtn(p); });
eCat.addEventListener('change', () => { p.cat = eCat.value; updateSaveBtn(p); });
ePrice.addEventListener('input', () => { p.price = ePrice.value; });
eTarget.addEventListener('input', () => { p.target = eTarget.value; });
}
const sellInput = $('e-sell-input');
if (sellInput) {
sellInput.addEventListener('keydown', e => {
if (e.key !== 'Enter') return;
e.preventDefault();
const t = sellInput.value.trim();
if (!t) return;
p.sellPoints.push(t);
sellInput.value = '';
renderMain();
setTimeout(() => $('e-sell-input')?.focus(), 50);
});
}
document.querySelectorAll('#e-sell-list .bl-x').forEach(b => {
b.addEventListener('click', () => { p.sellPoints.splice(+b.dataset.i, 1); renderMain(); });
});
document.querySelectorAll('.ov-photo[data-add-photo]').forEach(el => {
el.addEventListener('click', () => {
if (p.photos.length >= 5) return;
p.photos.push({ id: Date.now().toString(36), bg: PALETTE[p.photos.length % PALETTE.length] });
renderMain(); renderList(); updateSaveBtn(p);
});
});
document.querySelectorAll('.photo-x').forEach(b => {
b.addEventListener('click', e => { e.stopPropagation(); p.photos.splice(+b.dataset.i, 1); renderMain(); renderList(); updateSaveBtn(p); });
});
document.querySelectorAll('.ov-photo[data-start-edit]').forEach(el => {
el.addEventListener('click', () => { isEditing = true; renderMain(); });
});
// 工具入口
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => {
b.addEventListener('click', e => {
if (b.closest('.tools-row.locked')) {
e.preventDefault(); e.stopPropagation();
Shell.toast('请先完成基本信息', '商品名 + 品类 + ≥1 张图');
return;
}
openPicker(b.dataset.tool);
});
});
// 资产 tab
document.querySelectorAll('.asset-tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('.asset-tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
$('asset-grid').innerHTML = renderAssetItems(p, t.dataset.tab);
});
});
}
function updateSaveBtn(p) {
const ok = p.name && p.cat && p.photos.length > 0;
const btn = $('save-new');
if (btn) btn.disabled = !ok;
// 顶部 tools-row 锁定状态也跟着同步
const tools = $('tools-row');
if (tools) {
if (ok) tools.classList.remove('locked');
else tools.classList.add('locked');
}
}
function saveProduct(p, isFresh) {
if (!p.name || !p.cat || p.photos.length === 0) return;
if (isFresh) {
delete p.draft; p.savedOnce = true; p.showOnboard = true; isEditing = false;
Shell.toast('商品已建档', `+ ${p.name}`);
} else {
isEditing = false;
Shell.toast('已保存', p.name);
}
renderAll();
}
// ════════════════════════════════════════════════════════
// Picker · 双栏(左列表 + 右详情)
// ════════════════════════════════════════════════════════
const PICKER_CONFIG = {
white: {
title: '生成白底三视图',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg>',
filters: [],
meta: '~18 秒 · ¥1.6',
goText: '开始生成',
},
model: {
title: '选择模特',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v2"/></svg>',
filters: [
{ v: 'all', label: '全部' },
{ v: '女', label: '女性' },
{ v: '男', label: '男性' },
],
meta: '~35 秒 · ¥3.2',
goText: '用此模特生成 4 张',
},
platform: {
title: '选择平台',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>',
filters: [],
meta: '~28 秒 · ¥2.4',
goText: '用此规格生成 4 张',
},
};
function openPicker(kind) {
pickerKind = kind;
pickerFilter = 'all';
pickerSelectedId = null;
pickerConfig = {};
const cfg = PICKER_CONFIG[kind];
$('pp-ic').innerHTML = cfg.icon;
$('pp-title').textContent = cfg.title;
$('pp-sub').innerHTML = `// 商品:<span style="color:var(--accent-black);">${escape(current().name)}</span>`;
$('pp-meta').innerHTML = `// ${cfg.meta} · <span class="accent">失败不扣费</span>`;
$('pp-go-text').textContent = cfg.goText;
// 顶部 filters
$('pp-filters').innerHTML = cfg.filters.map(f =>
`<button class="pp-filter ${f.v==='all'?'on':''}" data-v="${f.v}">${f.label}${f.v==='all' && kind==='model' ? ` <span class="n">${MODELS.filter(m=>!m.custom).length}</span>`:''}</button>`
).join('');
$('pp-filters').querySelectorAll('.pp-filter').forEach(b => {
b.addEventListener('click', () => {
pickerFilter = b.dataset.v;
$('pp-filters').querySelectorAll('.pp-filter').forEach(x => x.classList.toggle('on', x.dataset.v === pickerFilter));
renderPickerList();
});
});
renderPickerList();
renderPickerDetail();
renderPickerFooter();
$('picker-bg').classList.add('show');
}
function closePicker() { $('picker-bg').classList.remove('show'); }
function renderPickerList() {
const grid = $('pp-grid');
if (pickerKind === 'white') {
// 白底没有列表选择,只有参数
grid.innerHTML = `
<div style="grid-column:1/-1;background:var(--surface);border:1px solid var(--border-faint);border-radius:var(--r-md);padding:18px;">
<div style="font-size:14px;font-weight:600;color:var(--accent-black);margin-bottom:8px;">将以主图为基准生成 3 个视角</div>
<div style="font-family:var(--font-mono);font-size:11.5px;color:var(--black-alpha-56);line-height:1.7;letter-spacing:.02em;">
// AI 自动去白底 + 重打光<br>
// 推算正面、侧面、背面构图<br>
// 商品形态保持稳定
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:18px;">
<div style="aspect-ratio:1;background:#fafafa;border-radius:var(--r-md);border:1px solid var(--border-faint);display:grid;place-items:center;color:var(--black-alpha-32);font-family:var(--font-mono);font-size:11px;">[ 正面预览 ]</div>
<div style="aspect-ratio:1;background:#f5f5f5;border-radius:var(--r-md);border:1px solid var(--border-faint);display:grid;place-items:center;color:var(--black-alpha-32);font-family:var(--font-mono);font-size:11px;">[ 侧面预览 ]</div>
<div style="aspect-ratio:1;background:#f0f0f0;border-radius:var(--r-md);border:1px solid var(--border-faint);display:grid;place-items:center;color:var(--black-alpha-32);font-family:var(--font-mono);font-size:11px;">[ 背面预览 ]</div>
</div>
</div>
`;
return;
}
if (pickerKind === 'model') {
const filtered = pickerFilter === 'all' ? MODELS : MODELS.filter(m => m.custom || m.gender === pickerFilter);
grid.innerHTML = filtered.map(m => {
if (m.custom) {
return `<div class="opt-pro custom" data-id="${m.id}">
<div class="opt-thumb"><span class="glyph">+</span></div>
<div class="opt-body">
<div class="opt-name">${escape(m.name)}</div>
<div class="opt-meta">${escape(m.style)}</div>
</div>
</div>`;
}
return `<div class="opt-pro ${pickerSelectedId===m.id?'active':''}" data-id="${m.id}">
<div class="opt-thumb" style="background:${m.bg};">
<span class="glyph">${m.glyph}</span>
${m.uses ? `<span class="opt-uses">用过 ${m.uses}</span>` : ''}
</div>
<div class="opt-body">
<div class="opt-name">${escape(m.name)}</div>
<div class="opt-meta">${escape(m.gender)} · ${m.age} · ${escape(m.style)}</div>
</div>
</div>`;
}).join('');
} else if (pickerKind === 'platform') {
grid.innerHTML = PLATFORMS.map(pl => `
<div class="opt-pro ${pickerSelectedId===pl.id?'active':''}" data-id="${pl.id}">
<div class="opt-thumb placeholder"><span class="glyph">${escape(pl.name[0])}</span>${pl.uses ? `<span class="opt-uses">用过 ${pl.uses}</span>` : ''}</div>
<div class="opt-body">
<div class="opt-name">${escape(pl.name)}</div>
<div class="opt-meta">${pl.ratio} · ${pl.size}</div>
</div>
</div>
`).join('');
}
grid.querySelectorAll('.opt-pro').forEach(c => {
c.addEventListener('click', () => {
if (c.classList.contains('custom')) {
Shell.toast('上传自有模特', '企业账号专用');
return;
}
pickerSelectedId = c.dataset.id;
grid.querySelectorAll('.opt-pro').forEach(x => x.classList.toggle('active', x.dataset.id === pickerSelectedId));
renderPickerDetail();
$('pp-go-btn').disabled = false;
});
});
}
function renderPickerDetail() {
const right = $('pp-right');
if (pickerKind === 'white') {
right.innerHTML = `
<div class="pp-detail-empty">
<div class="ic-em"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg></div>
<div class="t">无需选择</div>
<div class="d">// 调整参数后点开始生成</div>
</div>
`;
// 白底默认可生成
$('pp-go-btn').disabled = false;
return;
}
if (!pickerSelectedId) {
right.innerHTML = `
<div class="pp-detail-empty">
<div class="ic-em"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M9 9h.01M15 9h.01M9 15c1 1 2 1 3 1s2 0 3-1"/></svg></div>
<div class="t">${pickerKind === 'model' ? '选一位模特看详情' : '选一个平台看规格'}</div>
<div class="d">// 点击左侧任意卡片</div>
</div>
`;
return;
}
if (pickerKind === 'model') {
const m = MODELS.find(x => x.id === pickerSelectedId);
if (!m) return;
right.innerHTML = `
<div class="pp-detail-hero" style="background:${m.bg};">
<span class="glyph">${m.glyph}</span>
</div>
<div class="pp-detail-name">${escape(m.name)}</div>
<div class="pp-detail-meta">${escape(m.gender)} · ${m.age} 岁 · ${escape(m.style)}</div>
<div class="pp-detail-tags">${m.tags.map(t => `<span class="t">${escape(t)}</span>`).join('')}</div>
<div class="pp-detail-block">
<div class="lbl">// 已用 <span class="accent">${m.uses}</span> 次</div>
<div class="pp-detail-uses">
${[0,1,2,3].map(i => `<div class="pdu" style="background:${MODEL_THUMB_PALETTE[i]};"></div>`).join('')}
</div>
</div>
<div class="pp-detail-block">
<div class="lbl">// 适用场景</div>
<div style="font-size:12px;color:var(--black-alpha-72);line-height:1.6;">
${m.style} 风格,适合${m.tags.join('/')}类商品。
</div>
</div>
`;
} else if (pickerKind === 'platform') {
const pl = PLATFORMS.find(x => x.id === pickerSelectedId);
if (!pl) return;
right.innerHTML = `
<div class="pp-detail-hero" style="background:linear-gradient(135deg,#fef9c3,#fde68a);">
<span class="glyph">${escape(pl.name[0])}</span>
</div>
<div class="pp-detail-name">${escape(pl.name)}</div>
<div class="pp-detail-meta">${pl.ratio} · ${pl.size}</div>
<div class="pp-detail-tags">${pl.tags.map(t => `<span class="t">${escape(t)}</span>`).join('')}</div>
<div class="pp-detail-block">
<div class="lbl">// 平台规格说明</div>
<div style="font-size:12px;color:var(--black-alpha-72);line-height:1.7;">${escape(pl.desc)}</div>
</div>
<div class="pp-detail-block">
<div class="lbl">// 历史使用 <span class="accent">${pl.uses}</span> 次</div>
<div class="pp-detail-uses">
${[0,1,2,3].map(i => pl.uses > i ? `<div class="pdu" style="background:linear-gradient(135deg,#fff7ed,#ffedd5);"></div>` : `<div class="pdu empty">·</div>`).join('')}
</div>
</div>
`;
}
}
function renderPickerFooter() {
const c = $('pp-config');
if (pickerKind === 'white') {
c.innerHTML = `
<div class="pp-f-row">
<label>视角</label>
<div class="pillset" data-key="angles" data-multi="1">
<span class="p on" data-v="正">正面</span>
<span class="p on" data-v="侧">侧面</span>
<span class="p on" data-v="背">背面</span>
</div>
</div>
<div class="pp-f-row">
<label>背景</label>
<div class="pillset" data-key="bg">
<span class="p on" data-v="纯白">纯白</span>
<span class="p" data-v="浅灰">浅灰</span>
<span class="p" data-v="渐变">柔和渐变</span>
</div>
</div>
`;
} else if (pickerKind === 'model') {
c.innerHTML = `
<div class="pp-f-row">
<label>场景</label>
<div class="pillset" data-key="scene">
<span class="p on" data-v="室内自拍">室内自拍</span>
<span class="p" data-v="梳妆台">梳妆台</span>
<span class="p" data-v="户外清晨">户外清晨</span>
<span class="p" data-v="纯色背景">纯色背景</span>
</div>
</div>
<div class="pp-f-row">
<label>构图</label>
<div class="pillset" data-key="frame">
<span class="p on" data-v="半身">半身</span>
<span class="p" data-v="特写">特写</span>
<span class="p" data-v="全身">全身</span>
</div>
</div>
`;
} else if (pickerKind === 'platform') {
c.innerHTML = `
<div class="pp-f-row">
<label>类型</label>
<div class="pillset" data-key="ptype">
<span class="p on" data-v="主图">主图(1:1)</span>
<span class="p" data-v="详情头图">详情头图(750×1000)</span>
<span class="p" data-v="活动横幅">活动横幅(750×260)</span>
</div>
</div>
<div class="pp-f-row">
<label>风格</label>
<div class="pillset" data-key="pstyle">
<span class="p on" data-v="干净电商">干净电商</span>
<span class="p" data-v="种草风">种草风</span>
<span class="p" data-v="节日热闹">节日热闹</span>
</div>
</div>
`;
}
}
// pillset 切换
document.addEventListener('click', e => {
const p = e.target.closest('.pillset .p');
if (!p) return;
const set = p.parentElement;
if (set.dataset.multi) {
p.classList.toggle('on');
} else {
set.querySelectorAll('.p').forEach(x => x.classList.remove('on'));
p.classList.add('on');
}
});
// 开始生成
$('pp-go-btn').addEventListener('click', () => {
const p = current();
if (!p) return;
if (pickerKind === 'white') {
const angles = [...document.querySelectorAll('#pp-config .pillset[data-key="angles"] .p.on')].map(x => x.dataset.v);
if (!angles.length) return;
closePicker();
startGen(p, 'white', angles.map((a, i) => ({ name: `白底 · ${a}`, meta: `512×512 · 刚刚`, color: i === 0 ? '#fafafa' : i === 1 ? '#f5f5f5' : '#f0f0f0' })));
} else if (pickerKind === 'model') {
if (!pickerSelectedId) return;
const m = MODELS.find(x => x.id === pickerSelectedId);
closePicker();
const palette = ['linear-gradient(135deg,#fcd5ce,#f8edeb)', 'linear-gradient(135deg,#cfe1b9,#e9edc9)', 'linear-gradient(135deg,#bee3f8,#c3dafe)', 'linear-gradient(135deg,#fde2e4,#fad2e1)'];
startGen(p, 'model', palette.map((color, i) => ({ name: `${m.name} · 0${i+1}`, meta: `${m.name} · 1024 · 刚刚`, color })));
m.uses = (m.uses || 0) + 1;
} else if (pickerKind === 'platform') {
if (!pickerSelectedId) return;
const pl = PLATFORMS.find(x => x.id === pickerSelectedId);
closePicker();
const palette = ['linear-gradient(135deg,#fff7ed,#ffedd5)', 'linear-gradient(135deg,#fef3c7,#fde68a)', 'linear-gradient(135deg,#ecfccb,#d9f99d)', 'linear-gradient(135deg,#cffafe,#a5f3fc)'];
startGen(p, 'platform', palette.map((color, i) => ({ name: `${pl.name} · 0${i+1}`, meta: `${pl.name} · 刚刚`, color })));
pl.uses = (pl.uses || 0) + 1;
}
});
function startGen(p, kind, cards) {
p.task = 'running';
const skeletons = cards.map((_, i) => ({ id: `sk-${Date.now()}-${i}`, kind, skeleton: true, name: '生成中', meta: 'pending' }));
p.assets = [...skeletons, ...p.assets];
renderList(); renderMain();
setTimeout(() => {
const newAssets = cards.map((c, i) => ({ id: `g-${Date.now()}-${i}`, kind, name: c.name, meta: c.meta, color: c.color }));
p.assets = p.assets.filter(a => !skeletons.find(s => s.id === a.id));
p.assets = [...newAssets, ...p.assets];
p.task = 'done';
renderList(); renderMain();
Shell.toast(`已生成 ${newAssets.length}`, p.name);
setTimeout(() => { if (p.task === 'done') { p.task = null; renderList(); } }, 3500);
}, 1500);
}
// 左侧顶部
$('ws-new-btn').addEventListener('click', () => {
const id = 'new-' + Date.now().toString(36);
products.unshift({ id, draft: true, name: '', cat: '', price: '', target: '', sellPoints: [], photos: [], task: null, assets: [] });
currentId = id; isEditing = false;
renderAll();
});
$('ws-search-input').addEventListener('input', e => { listSearch = e.target.value.trim(); renderList(); });
document.querySelectorAll('.ws-chip').forEach(c => {
c.addEventListener('click', () => {
document.querySelectorAll('.ws-chip').forEach(x => x.classList.toggle('on', x === c));
listFilter = c.dataset.filter;
renderList();
});
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closePicker();
});
function renderAll() { renderList(); renderMain(); }
renderAll();
</script>
</body>
</html>