AirShelf/v2/studio.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

1635 lines
70 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>商品工作台 · 流·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">
<style>
/* ─── 撑满 .content 的限制 ─── */
.content { max-width: none !important; padding: 0 !important; }
.content > .corner-mark { display: none; }
/* ─── 工作台 layout · 左商品列表 + 右工作区 ─── */
.ws {
display: grid;
grid-template-columns: 296px 1fr;
height: calc(100vh - 57px); /* 减去 topbar */
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: 18px 16px 14px;
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);
transition: box-shadow var(--t-base);
}
.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;
display: flex; align-items: center;
}
.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);
transition: border-color var(--t-base);
}
.ws-search input:focus {
outline: none;
border-color: var(--heat-40);
background: var(--surface);
}
.ws-search input::placeholder { color: var(--black-alpha-48); }
.ws-tabs {
display: flex; gap: 2px;
padding: 10px 14px 6px;
flex-shrink: 0;
}
.ws-tabs .ws-tab {
flex: 1;
height: 28px;
display: flex; align-items: center; justify-content: center; gap: 4px;
background: transparent; border: 0; border-radius: var(--r-md);
color: var(--black-alpha-56);
font-size: 12px; font-weight: 500;
font-family: inherit; cursor: pointer;
transition: all var(--t-base);
}
.ws-tabs .ws-tab:hover { background: var(--black-alpha-4); color: var(--accent-black); }
.ws-tabs .ws-tab.active { background: var(--heat-12); color: var(--heat); }
.ws-tabs .ws-tab .n {
font-family: var(--font-mono); font-size: 10px;
padding: 0 5px; height: 16px;
display: inline-flex; align-items: center;
background: var(--background-lighter);
color: var(--black-alpha-48);
border-radius: 999px;
letter-spacing: .04em;
}
.ws-tabs .ws-tab.active .n { background: var(--heat); color: #fff; }
.ws-list-body {
flex: 1;
overflow-y: auto;
padding: 6px 8px 16px;
}
/* 单个商品卡 */
.ws-prod {
display: grid;
grid-template-columns: 44px 1fr auto;
align-items: center;
gap: 11px;
padding: 10px 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: 8px; bottom: 8px;
width: 3px; border-radius: 0 2px 2px 0;
background: var(--heat);
}
.ws-prod-thumb {
width: 44px; height: 44px;
border-radius: var(--r-md);
background: var(--background-lighter);
background-size: cover; background-position: center;
flex-shrink: 0;
}
.ws-prod-info { min-width: 0; }
.ws-prod-name {
font-size: 13px; 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: 10.5px;
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: 4px;
}
.ws-prod-count {
font-family: var(--font-mono); font-size: 10px;
padding: 1px 6px; height: 16px;
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: 4px;
font-family: var(--font-mono); font-size: 9.5px;
letter-spacing: .04em;
}
.ws-prod-task .dot-mini {
width: 6px; height: 6px; 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: 18px; height: 18px; }
.ws-prod.draft .ws-prod-meta { color: var(--heat); }
.ws-list-foot {
padding: 10px 16px;
border-top: 1px solid var(--border-faint);
flex-shrink: 0;
display: flex; align-items: center; gap: 8px;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.ws-list-foot strong { color: var(--accent-black); font-weight: 600; }
.ws-list-foot .accent { color: var(--heat); font-weight: 600; }
/* ============= 右侧:工作区 ============= */
.ws-main {
overflow-y: auto;
padding: 28px 36px 60px;
}
.ws-main-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 20px; margin-bottom: 24px;
}
.ws-main-head h1 {
font-size: 28px; font-weight: 500; color: var(--accent-black);
letter-spacing: -.02em; line-height: 1.2;
}
.ws-main-head .sub {
font-family: var(--font-mono); font-size: 12px;
color: var(--black-alpha-48); letter-spacing: .02em;
margin-top: 6px;
}
.ws-main-head .sub .accent { color: var(--heat); font-weight: 600; }
.ws-main-head .actions { display: flex; gap: 10px; flex-shrink: 0; }
/* ─── Onboarding tip ─── */
.onboard-tip {
background: var(--heat-8); border: 1px solid var(--heat-40);
border-radius: var(--r-md);
padding: 14px 18px;
margin-bottom: 22px;
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: 32px; height: 32px; background: var(--heat); color: #fff; border-radius: var(--r-md); display: grid; place-items: center; flex-shrink: 0; }
.onboard-tip .ic svg { width: 16px; height: 16px; }
.onboard-tip .body { flex: 1; }
.onboard-tip .body .t { font-size: 13.5px; font-weight: 600; color: var(--accent-black); }
.onboard-tip .body .d { font-size: 12px; color: var(--black-alpha-72); margin-top: 3px; line-height: 1.5; }
.onboard-tip .body .d strong { color: var(--heat); font-weight: 600; }
.onboard-tip .acts { display: flex; gap: 6px; }
.onboard-tip .dismiss {
background: transparent; border: 0;
color: var(--black-alpha-56); font-size: 12px; cursor: pointer;
padding: 5px 10px; border-radius: var(--r-md);
}
.onboard-tip .dismiss:hover { background: var(--black-alpha-4); color: var(--accent-black); }
/* ─── 商品概览卡(信息 + 原图,继承 product-studio 样式) ─── */
.overview-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 22px 24px;
margin-bottom: 24px;
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) 60px); }
.ov-display .ov-info-head {
display: flex; align-items: flex-start; gap: 14px;
margin-bottom: 12px;
}
.ov-display .ic {
width: 36px; height: 36px; 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: 17px; height: 17px; }
.ov-display .name { font-size: 16px; font-weight: 600; color: var(--accent-black); line-height: 1.3; }
.ov-display .meta { font-family: var(--font-mono); font-size: 11.5px; 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: 30px; padding: 0 12px;
background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-md);
color: var(--black-alpha-56); font-size: 12.5px; cursor: pointer;
font-family: inherit;
transition: all var(--t-base);
}
.ov-display .edit:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-8); }
.ov-display .edit svg { width: 12px; height: 12px; }
.ov-tags { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.ov-tags .t {
font-size: 11.5px; padding: 3px 10px;
background: var(--background-lighter); color: var(--black-alpha-56);
border: 1px solid var(--border-faint); border-radius: var(--r-sm);
}
.ov-sell {
margin-top: 14px; padding-top: 14px;
border-top: 1px dashed var(--border-faint);
font-size: 12.5px; color: var(--black-alpha-72); line-height: 1.65;
}
.ov-sell .lbl { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; margin-right: 6px; }
.ov-photos { border-left: 1px dashed var(--border-faint); padding-left: 28px; }
.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: 8px; }
.ov-photo {
aspect-ratio: 1; border-radius: var(--r-sm);
border: 1px solid var(--border-faint);
background: 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: 10.5px; font-family: var(--font-mono);
}
.overview-card.editing .ov-photo.empty-slot:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
.overview-card.editing .ov-photo.empty-slot.add-active { border-color: var(--heat-40); color: var(--heat); }
.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 .photo-x:hover { background: var(--accent-crimson); }
.overview-card.editing .ov-photo .photo-x svg { width: 10px; height: 10px; }
.overview-card.editing .ov-photo:hover .pmain { display: none; }
/* ─── 编辑表单 ─── */
.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: 6px; margin-bottom: 14px; }
.ov-edit .field-label {
font-size: 12.5px; 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: 34px; font-size: 13.5px; }
.ov-edit .ai-hint {
margin-top: 4px; padding: 8px 10px;
background: var(--heat-8); border: 1px dashed var(--heat-40);
border-radius: var(--r-md);
font-size: 11.5px; color: var(--black-alpha-72); line-height: 1.5;
display: flex; align-items: flex-start; gap: 6px;
}
.ov-edit .ai-hint svg { width: 11px; height: 11px; 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: 5px; }
.ov-sell-list li {
display: flex; align-items: center; gap: 8px;
padding: 7px 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md); font-size: 12.5px;
}
.ov-sell-list li.add { background: var(--surface); border-style: dashed; }
.ov-sell-list .num {
width: 18px; height: 18px;
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 10.5px; 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: 12.5px; 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: 20px; height: 20px;
display: grid; place-items: center;
color: var(--black-alpha-48); cursor: pointer;
background: transparent; border: 0;
opacity: 0; transition: opacity var(--t-base);
}
.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; }
/* ─── 工具箱锁定 banner ─── */
.toolbox-lock {
background: var(--background-lighter);
border: 1px dashed var(--border-muted);
border-radius: var(--r-md);
padding: 14px 18px;
margin-bottom: 16px;
display: flex; align-items: center; gap: 12px;
font-size: 13px; color: var(--black-alpha-72);
}
.toolbox-lock .ic {
width: 32px; height: 32px;
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56); flex-shrink: 0;
}
.toolbox-lock .ic svg { width: 15px; height: 15px; }
.toolbox-lock strong { color: var(--accent-black); font-weight: 600; }
.toolbox-lock .miss { color: var(--accent-crimson); }
/* ─── AI 工具箱 ─── */
.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); letter-spacing: -.01em; }
.section-title .sub { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.toolbox { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 32px; }
.tool-card {
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
cursor: pointer; position: relative;
min-height: 124px;
transition: all var(--t-base);
}
.tool-card:hover { border-color: var(--heat-40); background: var(--heat-4); transform: translateY(-1px); box-shadow: 0 4px 16px rgba(0,0,0,.04); }
.tool-card.featured { border-color: var(--heat-40); }
.tool-card.featured::after {
content: '推荐 ★';
position: absolute; top: 12px; right: 12px;
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-card .ic-box {
width: 36px; height: 36px;
background: var(--heat-12); color: var(--heat);
border-radius: var(--r-md);
display: grid; place-items: center; flex-shrink: 0;
}
.tool-card .ic-box svg { width: 16px; height: 16px; }
.tool-card .info { flex: 1; }
.tool-card .info .t { font-size: 13px; font-weight: 600; color: var(--accent-black); line-height: 1.3; }
.tool-card .info .d { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); margin-top: 5px; letter-spacing: .02em; line-height: 1.45; }
.tool-card .foot {
display: flex; align-items: center; justify-content: space-between;
padding-top: 8px; border-top: 1px dashed var(--border-faint); margin-top: auto;
}
.tool-card .foot .cost { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .02em; }
.tool-card .foot .cost b { color: var(--heat); font-weight: 600; }
.tool-card .foot .arrow {
width: 22px; height: 22px; border-radius: var(--r-sm);
background: var(--background-lighter); color: var(--black-alpha-56);
display: grid; place-items: center;
transition: all var(--t-fast);
}
.tool-card:hover .foot .arrow { background: var(--heat); color: #fff; transform: translateX(2px); }
.tool-card .foot .arrow svg { width: 10px; height: 10px; }
.tool-card.coming-soon { opacity: .65; cursor: default; }
.tool-card.coming-soon:hover { border-color: var(--border-faint); background: var(--surface); transform: none; box-shadow: none; }
.tool-card.coming-soon .foot .arrow { display: none; }
.toolbox.locked .tool-card:not(.coming-soon) { opacity: .5; cursor: not-allowed; pointer-events: none; }
/* ─── 已生成资产 ─── */
.asset-section {
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 20px 22px;
}
.asset-tabs {
display: flex; gap: 4px;
border-bottom: 1px solid var(--border-faint);
margin-bottom: 16px;
}
.asset-tab {
padding: 0 14px; height: 34px;
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 500;
color: var(--black-alpha-56);
cursor: pointer; border: 0; background: transparent; position: relative;
font-family: inherit;
transition: color var(--t-base);
}
.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(140px, 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: 9px 11px; border-top: 1px solid var(--border-faint); background: var(--surface); }
.asset-it .a-name { font-size: 12px; font-weight: 500; color: var(--accent-black); }
.asset-it .a-meta { font-family: var(--font-mono); font-size: 10px; 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); font-size: 13px;
}
.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,.42);
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 {
background: var(--surface);
border: 1px solid var(--border-faint); border-radius: var(--r-md);
max-width: 680px; width: 92%;
max-height: calc(100vh - 80px);
display: flex; flex-direction: column;
position: relative;
transform: scale(.96);
transition: transform .25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.picker-bg.show .picker { transform: scale(1); }
.picker-h {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 12px;
flex-shrink: 0;
}
.picker-h .ic { width: 32px; height: 32px; background: var(--heat-12); color: var(--heat); border-radius: var(--r-md); display: grid; place-items: center; }
.picker-h .ic svg { width: 16px; height: 16px; }
.picker-h .ti { flex: 1; }
.picker-h .ti .t { font-size: 15px; font-weight: 600; color: var(--accent-black); }
.picker-h .ti .d { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); margin-top: 3px; letter-spacing: .04em; }
.picker-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; }
.picker-h .x:hover { background: var(--black-alpha-4); color: var(--accent-crimson); }
.picker-h .x svg { width: 13px; height: 13px; }
.picker-b { padding: 20px 24px; flex: 1; overflow-y: auto; min-height: 0; }
.picker-f {
padding: 14px 22px;
border-top: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 10px;
background: var(--background-lighter);
flex-shrink: 0;
}
.picker-f .meta { flex: 1; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.picker-f .meta .accent { color: var(--heat); font-weight: 600; }
.opt-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.opt-card {
border: 1px solid var(--border-faint); border-radius: var(--r-md);
padding: 10px; cursor: pointer; background: var(--surface);
transition: all var(--t-base);
}
.opt-card:hover { border-color: var(--heat-40); background: var(--heat-4); }
.opt-card.selected { border-color: var(--heat); background: var(--heat-8); }
.opt-card .opt-thumb {
aspect-ratio: 1; background: var(--background-lighter);
border-radius: var(--r-sm); display: grid; place-items: center;
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 11px;
margin-bottom: 8px; overflow: hidden;
}
.opt-card.selected .opt-thumb { background: var(--heat-12); color: var(--heat); }
.opt-card .opt-name { font-size: 12px; font-weight: 500; color: var(--accent-black); }
.opt-card .opt-meta { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }
.opt-card.selected .opt-name { color: var(--heat); }
.opt-config { margin-top: 16px; padding-top: 16px; border-top: 1px dashed var(--border-faint); }
.opt-config-row { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; font-size: 13px; }
.opt-config-row > label { width: 80px; color: var(--black-alpha-72); }
.opt-config-row .val { color: var(--accent-black); }
.opt-config-row .pillset { display: flex; gap: 6px; }
.opt-config-row .pillset .p {
padding: 4px 10px; 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);
}
.opt-config-row .pillset .p:hover { color: var(--accent-black); border-color: var(--black-alpha-24); }
.opt-config-row .pillset .p.on { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); }
</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.8"><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-tabs">
<button class="ws-tab active" data-filter="all">全部 <span class="n" id="t-all">7</span></button>
<button class="ws-tab" data-filter="running">生成中 <span class="n" id="t-run">1</span></button>
<button class="ws-tab" 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 width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
<span><span class="accent" id="foot-running">1</span> 个任务运行中</span>
</div>
</aside>
<!-- ════════ 右侧:工作区 ════════ -->
<main class="ws-main" id="ws-main"></main>
</div>
</div>
<!-- ════════ 弹窗 ════════ -->
<div class="picker-bg" id="white-picker-bg" onclick="if(event.target===this)closePicker('white')">
<div class="picker" style="max-width: 460px;">
<div class="picker-h">
<div 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></div>
<div class="ti"><div class="t">生成白底三视图</div><div class="d" id="white-prod-name">// 透真补水面膜</div></div>
<button class="x" onclick="closePicker('white')"><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="picker-b">
<div style="font-size:13px;color:var(--black-alpha-72);line-height:1.7;margin-bottom:14px;">
AI 会以「主图」为基准,自动去白底 + 重打光 + 推算另外两个视角。
</div>
<div class="opt-config" style="margin-top:0;padding-top:0;border:0;">
<div class="opt-config-row">
<label>视角</label>
<div class="pillset" data-key="angles">
<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="opt-config-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>
</div>
</div>
<div class="picker-f">
<span class="meta">// 预计 18 秒 · <span class="accent">¥1.6</span></span>
<button class="btn" onclick="closePicker('white')">取消</button>
<button class="btn btn-primary" id="white-go-btn">开始生成</button>
</div>
</div>
</div>
<div class="picker-bg" id="model-picker-bg" onclick="if(event.target===this)closePicker('model')">
<div class="picker">
<div class="picker-h">
<div 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></div>
<div class="ti"><div class="t">选择模特</div><div class="d" id="model-prod-name">// 8 位模特 · 每次 4 张</div></div>
<button class="x" onclick="closePicker('model')"><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="picker-b">
<div class="opt-grid" id="model-grid"></div>
</div>
<div class="picker-f">
<span class="meta">// 预计 35 秒 · <span class="accent">¥3.2</span> · 失败不扣费</span>
<button class="btn" onclick="closePicker('model')">取消</button>
<button class="btn btn-primary" id="model-go-btn" disabled>开始生成</button>
</div>
</div>
</div>
<div class="picker-bg" id="platform-picker-bg" onclick="if(event.target===this)closePicker('platform')">
<div class="picker">
<div class="picker-h">
<div 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></div>
<div class="ti"><div class="t">选择平台</div><div class="d" id="platform-prod-name">// 每平台 4 张</div></div>
<button class="x" onclick="closePicker('platform')"><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="picker-b">
<div class="opt-grid" id="platform-grid"></div>
</div>
<div class="picker-f">
<span class="meta">// 预计 28 秒 · <span class="accent">¥2.4</span> · 失败不扣费</span>
<button class="btn" onclick="closePicker('platform')">取消</button>
<button class="btn btn-primary" id="platform-go-btn" disabled>开始生成</button>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'products',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品工作台' }]
});
// 弹窗提到 body 末尾,脱离 main stacking
['white-picker-bg', 'model-picker-bg', 'platform-picker-bg'].forEach(id => {
const el = document.getElementById(id);
if (el && el.parentElement !== document.body) document.body.appendChild(el);
});
// ════════════════════════════════════════════════════════
// 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)',
];
// 7 个商品 + 各自的 state
const products = [
{
id: 'p1',
name: '透真玻尿酸补水面膜', cat: '美妆个护', price: '39.9',
target: '22-32 岁女性、敏感肌、办公室通勤',
sellPoints: ['玻尿酸双效保湿', '4 小时持久水润', '敏感肌可用', '通勤补水', '平价代替'],
photos: [
{ id: 'p1-1', bg: PALETTE[0], label: '主图' },
{ id: 'p1-2', bg: PALETTE[1], label: '包装' },
{ id: 'p1-3', bg: PALETTE[4], label: '质地' },
],
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 · 5/19', color: 'linear-gradient(135deg,#ffe0b2,#ffccbc)' },
{ id: 'a5', kind: 'model', name: '林夕 · 02', meta: '林夕 · 1024 · 5/19', color: 'linear-gradient(135deg,#fde2c6,#ffbcaa)' },
{ id: 'a6', kind: 'model', name: '林夕 · 03', meta: '林夕 · 1024 · 5/19', color: 'linear-gradient(135deg,#fbcfe8,#fce7f3)' },
{ id: 'a7', kind: 'model', name: '林夕 · 04', meta: '林夕 · 1024 · 5/19', color: 'linear-gradient(135deg,#f3e8ff,#fae8ff)' },
{ id: 'a8', kind: 'platform', name: '淘宝 · 01', meta: '800×800 · 5/19', color: 'linear-gradient(135deg,#fff1f0,#ffd6cc)' },
{ id: 'a9', kind: 'platform', name: '淘宝 · 02', meta: '800×800 · 5/19', color: 'linear-gradient(135deg,#fef3c7,#fde68a)' },
{ id: 'a10', kind: 'platform', name: '淘宝 · 03', meta: '800×800 · 5/19', color: 'linear-gradient(135deg,#dcfce7,#bbf7d0)' },
{ id: 'a11', kind: 'platform', name: '淘宝 · 04', meta: '800×800 · 5/19', color: 'linear-gradient(135deg,#dbeafe,#bfdbfe)' },
{ id: 'sk1', kind: 'model', skeleton: true, name: '生成中', meta: 'pending', color: '' },
],
},
{
id: 'p2',
name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: '199',
target: '通勤族 · 学生 · 运动爱好者',
sellPoints: ['通话降噪', '续航 25 小时', '蓝牙 5.3', '人体工学'],
photos: [
{ id: 'p2-1', bg: 'linear-gradient(135deg,#e0e7ff,#c7d2fe)', label: '主图' },
{ id: 'p2-2', bg: 'linear-gradient(135deg,#cffafe,#a5f3fc)', label: '细节' },
],
task: 'done',
assets: [
{ id: 'b1', kind: 'white', name: '白底 · 正面', meta: '512×512', color: '#f4f4f4' },
{ id: 'b2', kind: 'white', name: '白底 · 侧面', meta: '512×512', color: '#f0f0f0' },
{ id: 'b3', 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)', label: '主图' },
],
task: null,
assets: [],
},
{
id: 'p4',
name: '透真清透物理防晒霜', cat: '美妆个护', price: '69',
target: '通勤女性 · 敏感肌',
sellPoints: ['SPF50+', '物理防晒', '不闷痘', '无负担'],
photos: [
{ id: 'p4-1', bg: 'linear-gradient(135deg,#fef9c3,#fef08a)', label: '主图' },
{ id: 'p4-2', bg: 'linear-gradient(135deg,#fed7aa,#fdba74)', label: '使用图' },
],
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: 'c3', kind: 'model', name: '小苏 · 02', meta: '小苏 · 1024', color: 'linear-gradient(135deg,#cfe1b9,#e9edc9)' },
],
},
{
id: 'p5',
name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: '89',
target: '早八党 · 咖啡爱好者',
sellPoints: ['3 秒速溶', '冷热皆宜', '24 颗装', '原产地豆'],
photos: [
{ id: 'p5-1', bg: 'linear-gradient(135deg,#d6d3d1,#a8a29e)', label: '主图' },
],
task: 'failed',
assets: [
{ id: 'd1', kind: 'white', name: '白底 · 正面', meta: '512×512 · failed', color: '#fef2f2' },
],
},
{
id: 'p6',
name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: '159',
target: '小户型 · 健康人群',
sellPoints: ['可视玻璃', '4L 大容量', '少油健康', '6 大模式'],
photos: [
{ id: 'p6-1', bg: 'linear-gradient(135deg,#fef2f2,#fecaca)', label: '主图' },
],
task: null,
assets: [],
},
{
id: 'p7',
name: '露露同款裸感瑜伽裤', cat: '运动户外', price: '119',
target: '健身爱好者 · 通勤女性',
sellPoints: ['裸感无痕', '高弹力', '吸湿排汗', '修身显瘦'],
photos: [
{ id: 'p7-1', bg: 'linear-gradient(135deg,#1f2937,#374151)', label: '主图' },
{ id: 'p7-2', bg: 'linear-gradient(135deg,#4b5563,#6b7280)', label: '穿着' },
],
task: null,
assets: [
{ id: 'e1', kind: 'model', name: '阿强 · 01', meta: '阿强 · 1024', color: 'linear-gradient(135deg,#bee3f8,#c3dafe)' },
],
},
];
const MODELS = [
{ id: 'm1', name: '林夕', meta: '女 · 25 · 都市白领' },
{ id: 'm2', name: '小苏', meta: '女 · 23 · 文艺学生' },
{ id: 'm3', name: '阿楠', meta: '女 · 28 · 同事型' },
{ id: 'm4', name: '小七', meta: '女 · 20 · 学生' },
{ id: 'm5', name: '王姐', meta: '女 · 38 · 居家' },
{ id: 'm6', name: '阿杰', meta: '男 · 30 · 都市' },
{ id: 'm7', name: '阿强', meta: '男 · 26 · 健身' },
{ id: 'm8', name: '+ 自定义', meta: '上传自有模特', custom: true },
];
const PLATFORMS = [
{ id: 'pl1', name: '淘宝 / 天猫', meta: '1:1 · 800×800' },
{ id: 'pl2', name: '抖店', meta: '3:4 · 750×1000' },
{ id: 'pl3', name: '拼多多', meta: '1:1 · 800×800' },
{ id: 'pl4', name: '京东', meta: '1:1 · 800×800' },
{ id: 'pl5', name: '小红书', meta: '3:4 · 600×800' },
{ id: 'pl6', name: '1688', meta: '1:1 · 750×750' },
];
// ════════════════════════════════════════════════════════
// State
// ════════════════════════════════════════════════════════
let currentId = 'p1';
let isEditing = false;
let pickerState = {
white: { angles: ['正','侧','背'], bg: '纯白' },
model: { selected: '' },
platform: { selected: '' },
};
let listFilter = 'all';
let listSearch = '';
const $ = id => document.getElementById(id);
function current() { return products.find(p => p.id === currentId); }
// ════════════════════════════════════════════════════════
// 渲染:左侧商品列表
// ════════════════════════════════════════════════════════
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) {
const hay = `${p.name} ${p.cat}`.toLowerCase();
if (!hay.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||''}"></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 escape(s) {
return String(s||'').replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]);
}
// ════════════════════════════════════════════════════════
// 渲染:右侧工作区
// ════════════════════════════════════════════════════════
function renderMain() {
const p = current();
if (!p) {
$('ws-main').innerHTML = `<div style="text-align:center;padding:80px 0;color:var(--black-alpha-48);">// 未选中商品</div>`;
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;
// 头部
const headHtml = isFresh ? `
<div class="ws-main-head">
<div>
<h1>新建商品</h1>
<div class="sub">// 填写基本信息 + 上传原图 · 保存后解锁 AI 工具</div>
</div>
<div class="actions">
<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>
</div>
</div>
` : `
<div class="ws-main-head">
<div>
<h1>${escape(p.name)}</h1>
<div class="sub">// 已生成 <span class="accent">${assetCount}</span> 张资产 · ${escape(p.cat)} · ¥${p.price}</div>
</div>
<div class="actions">
<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>
`;
// 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>。约 18 秒、¥1.6 · 失败不扣费</div>
</div>
<div class="acts">
<button class="dismiss" id="dismiss-onboard">稍后</button>
<button class="btn btn-primary" id="goto-white">现在生成</button>
</div>
</div>
` : '';
// 概览卡
const overviewHtml = isFresh ? renderOverviewEdit(p) : renderOverviewDisplay(p);
// 工具箱锁定 banner
const lockHtml = canTools ? '' : `
<div class="toolbox-lock">
<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>
<div style="flex:1;">
<strong>完成基本信息后解锁工具箱</strong>
<div style="font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-48);margin-top:3px;letter-spacing:.02em;">// 还差:<span class="miss">${[
!p.name && '商品名',
!p.cat && '品类',
p.photos.length === 0 && '≥1 张图',
].filter(Boolean).join(' / ')}</span></div>
</div>
</div>
`;
// 工具箱
const toolboxHtml = `
<div class="section-title">
<h2>AI 工具箱</h2>
<span class="sub">// 按需调用 · 失败不扣费</span>
</div>
${lockHtml}
<div class="toolbox ${canTools?'':'locked'}" id="toolbox">
<div class="tool-card featured" data-tool="white">
<div class="ic-box"><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="info">
<div class="t">白底三视图</div>
<div class="d">正/侧/背 · Seedance +60%</div>
</div>
<div class="foot"><span class="cost">~18s · <b>¥1.6</b></span><span class="arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg></span></div>
</div>
<div class="tool-card" data-tool="model">
<div class="ic-box"><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></div>
<div class="info">
<div class="t">AI 模特上身图</div>
<div class="d">8 模特库 · 每次 4 张</div>
</div>
<div class="foot"><span class="cost">~35s · <b>¥3.2</b></span><span class="arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg></span></div>
</div>
<div class="tool-card" data-tool="platform">
<div class="ic-box"><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></div>
<div class="info">
<div class="t">平台套图</div>
<div class="d">6 平台规格 · 每平台 4 张</div>
</div>
<div class="foot"><span class="cost">~28s · <b>¥2.4</b></span><span class="arrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg></span></div>
</div>
<div class="tool-card coming-soon">
<div class="ic-box" style="background:var(--background-lighter);color:var(--black-alpha-48);"><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></div>
<div class="info">
<div class="t" style="color:var(--black-alpha-72);">更多工具</div>
<div class="d">海报 / 详情页插画</div>
</div>
<div class="foot"><span class="cost" style="font-style:italic;">即将上线</span></div>
</div>
</div>
`;
// 已生成资产
const assetsHtml = renderAssetsSection(p);
$('ws-main').innerHTML = headHtml + onboardHtml + overviewHtml + toolboxHtml + assetsHtml;
bindMainHandlers(p);
}
function renderOverviewDisplay(p) {
const tags = p.sellPoints.slice(0, 5);
return `
<div class="overview-card ${isEditing?'editing':''}" id="overview-card">
<div>
<div class="ov-display" id="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.cat)} ] · ¥${p.price} · ${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>
${isEditing ? renderEditFormFields(p) : ''}
</div>
${renderPhotosBlock(p)}
</div>
`;
}
function renderOverviewEdit(p) {
return `
<div class="overview-card editing" id="overview-card">
<div>
<div class="ov-edit" style="display:block;">
${renderEditFormFields(p)}
</div>
</div>
${renderPhotosBlock(p)}
</div>
`;
}
function renderEditFormFields(p) {
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="ov-edit" style="display:block;">
<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:10px;">
<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>填上 <strong>卖点</strong> 和 <strong>人群</strong>,后续 AI 生脚本(痛点种草/剧情带货)的质量明显更高。</span>
</div>
</div>
${!p.draft ? `
<div style="display:flex;align-items:center;gap:10px;padding-top:14px;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>
` : ''}
</div>
`;
}
function renderPhotosBlock(p) {
const isEdit = isEditing || (p.draft && !p.savedOnce);
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="2" 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:.55;"></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 · ≤ 5MB</span>
</div>
<div class="ov-photos-grid">${slots}</div>
</div>
`;
}
function renderAssetsSection(p) {
const total = p.assets.filter(a => !a.skeleton).length;
const byKind = k => p.assets.filter(a => a.kind === k && !a.skeleton).length;
return `
<div class="asset-section">
<div class="section-title" style="margin-bottom: 10px;">
<h2>已生成资产</h2>
<span class="sub">// 该商品 · ${total} 张 · 自动入资产库</span>
</div>
<div class="asset-tabs">
<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>
`;
}
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 bindMainHandlers(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();
});
$('goto-white') && $('goto-white').addEventListener('click', () => {
p.showOnboard = false;
openPicker('white');
});
// 编辑表单字段
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', () => {
const i = +b.dataset.i;
p.sellPoints.splice(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],
label: '示例',
});
renderMain();
renderList();
updateSaveBtn(p);
});
});
document.querySelectorAll('.photo-x').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const i = +b.dataset.i;
p.photos.splice(i, 1);
renderMain();
renderList();
updateSaveBtn(p);
});
});
document.querySelectorAll('.ov-photo[data-start-edit]').forEach(el => {
el.addEventListener('click', () => { isEditing = true; renderMain(); });
});
// 工具箱卡
document.querySelectorAll('#toolbox .tool-card[data-tool]').forEach(c => {
c.addEventListener('click', e => {
if (c.closest('.toolbox.locked')) {
e.preventDefault(); e.stopPropagation();
Shell.toast('请先完成基本信息', '商品名 + 品类 + ≥1 张图');
return;
}
openPicker(c.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;
}
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:白底/模特/平台
// ════════════════════════════════════════════════════════
function openPicker(kind) {
const p = current();
if (!p) return;
// 更新弹窗标题里的商品名
if (kind === 'white') $('white-prod-name').textContent = `// ${p.name}`;
if (kind === 'model') {
$('model-prod-name').textContent = `// ${p.name} · 选 1 位模特,每次生成 4 张`;
$('model-grid').innerHTML = MODELS.map(m => `
<div class="opt-card" data-id="${m.id}">
<div class="opt-thumb">${m.custom ? '+' : '◐'}</div>
<div class="opt-name">${m.name}</div>
<div class="opt-meta">${m.meta}</div>
</div>
`).join('');
pickerState.model.selected = '';
$('model-go-btn').disabled = true;
$('model-grid').querySelectorAll('.opt-card').forEach(c => {
c.addEventListener('click', () => {
$('model-grid').querySelectorAll('.opt-card').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
pickerState.model.selected = c.dataset.id;
$('model-go-btn').disabled = false;
});
});
}
if (kind === 'platform') {
$('platform-prod-name').textContent = `// ${p.name} · 选 1 个平台`;
$('platform-grid').innerHTML = PLATFORMS.map(pp => `
<div class="opt-card" data-id="${pp.id}">
<div class="opt-thumb">${pp.name[0]}</div>
<div class="opt-name">${pp.name}</div>
<div class="opt-meta">${pp.meta}</div>
</div>
`).join('');
pickerState.platform.selected = '';
$('platform-go-btn').disabled = true;
$('platform-grid').querySelectorAll('.opt-card').forEach(c => {
c.addEventListener('click', () => {
$('platform-grid').querySelectorAll('.opt-card').forEach(x => x.classList.remove('selected'));
c.classList.add('selected');
pickerState.platform.selected = c.dataset.id;
$('platform-go-btn').disabled = false;
});
});
}
$(`${kind}-picker-bg`).classList.add('show');
}
function closePicker(kind) {
$(`${kind}-picker-bg`).classList.remove('show');
}
// pillset
document.addEventListener('click', e => {
const p = e.target.closest('.pillset .p');
if (!p) return;
const set = p.parentElement;
const key = set.dataset.key;
if (key === 'angles') {
p.classList.toggle('on');
} else {
set.querySelectorAll('.p').forEach(x => x.classList.remove('on'));
p.classList.add('on');
}
});
$('white-go-btn').addEventListener('click', () => {
const p = current();
if (!p) return;
const angles = [...document.querySelectorAll('#white-picker-bg .pillset[data-key="angles"] .p.on')].map(x => x.dataset.v);
if (!angles.length) return;
closePicker('white');
startGen(p, 'white', angles.map((a, i) => ({
name: `白底 · ${a}`, meta: `512×512 · 刚刚`,
color: i === 0 ? '#fafafa' : i === 1 ? '#f5f5f5' : '#f0f0f0',
})));
});
$('model-go-btn').addEventListener('click', () => {
const p = current();
if (!p || !pickerState.model.selected) return;
const m = MODELS.find(x => x.id === pickerState.model.selected);
closePicker('model');
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,
})));
});
$('platform-go-btn').addEventListener('click', () => {
const p = current();
if (!p || !pickerState.platform.selected) return;
const pl = PLATFORMS.find(x => x.id === pickerState.platform.selected);
closePicker('platform');
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,
})));
});
function startGen(p, kind, cards) {
// 任务进入运行中
p.task = 'running';
const skeletons = cards.map((_, i) => ({
id: `sk-${Date.now()}-${i}`, kind, skeleton: true,
name: '生成中', meta: 'pending', color: '',
}));
p.assets = [...skeletons, ...p.assets];
renderList();
renderMain();
// 1.5s 后完成
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);
// 3s 后清掉 done 标
setTimeout(() => {
if (p.task === 'done') { p.task = null; renderList(); }
}, 3500);
}, 1500);
}
// ════════════════════════════════════════════════════════
// 顶部 + 搜索 + Tab
// ════════════════════════════════════════════════════════
$('ws-new-btn').addEventListener('click', () => {
const id = 'new-' + Date.now().toString(36);
const newP = {
id, draft: true,
name: '', cat: '', price: '', target: '', sellPoints: [], photos: [],
task: null, assets: [],
};
products.unshift(newP);
currentId = id;
isEditing = false;
renderAll();
});
$('ws-search-input').addEventListener('input', e => {
listSearch = e.target.value.trim();
renderList();
});
document.querySelectorAll('.ws-tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('.ws-tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
listFilter = t.dataset.filter;
renderList();
});
});
// ESC 关弹窗
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
document.querySelectorAll('.picker-bg.show').forEach(p => p.classList.remove('show'));
}
});
function renderAll() {
renderList();
renderMain();
}
renderAll();
</script>
</body>
</html>