1636 lines
70 KiB
HTML
1636 lines
70 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>商品工作台 · Airshelf</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
|
||
<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.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-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/icons.js?v=2026052608"></script>
|
||
<script src="assets/shell.js?v=2026052607"></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 => ({ '<':'<','>':'>','&':'&','"':'"' })[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="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:.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>
|