All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
1403 lines
78 KiB
HTML
1403 lines
78 KiB
HTML
<!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 => ({ '<':'<','>':'>','&':'&','"':'"' })[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>
|