AirShelf/v2/pipeline.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

1738 lines
111 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>补水面膜 · v3 · 流水线 · 流·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>
/* ─── Project header ─── */
.proj-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 22px; align-items: flex-start; }
.proj-head h1 { font-size: 20px; font-weight: 700; letter-spacing: -.012em; }
/* ─── Stepper ─── */
.stepper { display: flex; align-items: center; gap: 0; margin-bottom: 28px; padding: 14px 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; }
.stepper::before, .stepper::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
.stepper::before { top: -7px; left: -7px; }
.stepper::after { bottom: -7px; right: -7px; }
.stepper .corner-tr, .stepper .corner-bl { position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
.stepper .corner-tr { top: -7px; right: -7px; }
.stepper .corner-bl { bottom: -7px; left: -7px; }
.stage-step { display: flex; align-items: center; gap: 10px; padding: 6px 0; cursor: pointer; user-select: none; }
.stage-step .num { width: 26px; height: 26px; display: grid; place-items: center; font-family: var(--font-mono); font-size: 12px; font-weight: 600; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); color: var(--black-alpha-48); flex-shrink: 0; }
.stage-step.done .num { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }
.stage-step.active .num { background: var(--heat); border-color: var(--heat); color: var(--accent-white); }
.stage-step.locked { opacity: .5; cursor: not-allowed; }
.stage-step .lbl { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.stage-step.active .lbl { color: var(--accent-black); font-weight: 600; }
.stage-step .st { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); margin-left: 4px; padding: 1px 6px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); letter-spacing: .04em; }
.stage-step.active .st { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
.stage-step:hover .num { border-color: var(--heat-40); }
.stage-step:hover .lbl { color: var(--heat); }
.stage-line { flex: 1; height: 1px; background: var(--border-faint); margin: 0 14px; min-width: 30px; }
.stage-line.done { background: var(--accent-black); }
/* ─── Stage panes ─── */
.stage { display: none; }
.stage.active { display: block; }
/* Common pane */
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.pane-h { display: flex; align-items: center; gap: 8px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); }
.pane-h strong { font-size: 14px; font-weight: 600; }
/* Stage foot */
.stage-foot { display: flex; justify-content: space-between; align-items: center; padding: 18px 0 0; margin-top: 18px; border-top: 1px solid var(--border-faint); }
.stage-foot .info { font-size: 12.5px; color: var(--black-alpha-56); }
.stage-foot .info .mono { font-family: var(--font-mono); color: var(--black-alpha-48); font-size: 11.5px; letter-spacing: .02em; }
/* === STAGE 1 · 脚本(镜头脚本 : 脚本助手 = 7 : 3,助手在 3:2 基础上再缩 1/4) === */
.stage-script { display: grid; grid-template-columns: 7fr 3fr; gap: 16px; min-height: 560px; }
.chat-pane { display: flex; flex-direction: column; }
.chat-body { padding: 16px 18px; flex: 1; overflow-y: auto; max-height: 460px; display: flex; flex-direction: column; gap: 14px; }
.msg .bubble { max-width: 90%; padding: 10px 14px; font-size: 13px; line-height: 1.6; border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.msg.ai .bubble { background: var(--surface); }
.msg.user { display: flex; flex-direction: column; align-items: flex-end; }
.msg.user .bubble { background: var(--heat-12); color: var(--accent-black); border-color: var(--heat-20); }
.msg .time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 4px; letter-spacing: .02em; }
.msg .actions { display: flex; gap: 6px; margin-top: 6px; }
.ai-avatar { width: 26px; height: 26px; background: var(--heat); color: var(--accent-white); display: grid; place-items: center; font-size: 11px; font-weight: 700; border: 1px solid var(--heat); border-radius: 50%; }
.del { text-decoration: line-through; color: var(--black-alpha-48); }
.ins { background: var(--forest-bg); color: var(--accent-forest); padding: 0 3px; }
.chat-input { padding: 14px 18px; border-top: 1px solid var(--border-faint); }
.shot-list { display: flex; flex-direction: column; }
.shots-body { padding: 12px 16px; flex: 1; overflow-y: auto; max-height: 540px; display: flex; flex-direction: column; gap: 0; }
.shot-card { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; transition: border-color var(--t-base), background var(--t-base); }
.shot-card.highlight { border-color: var(--heat); background: var(--heat-12); }
.shot-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.shot-num { width: 22px; height: 22px; background: var(--accent-black); color: var(--accent-white); display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; font-weight: 700; border-radius: var(--r-sm); }
.shot-time { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); padding: 2px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
.shot-row { display: grid; grid-template-columns: 36px 1fr; gap: 8px; padding: 4px 0; }
.shot-k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); padding-top: 2px; letter-spacing: .04em; }
.shot-v { font-size: 12.5px; color: var(--accent-black); line-height: 1.55; outline: none; border-radius: var(--r-sm); padding: 2px 4px; margin: -2px -4px; transition: background var(--t-base); }
.shot-v[contenteditable="true"]:hover { background: var(--heat-12); cursor: text; }
.shot-v[contenteditable="true"]:focus { background: var(--surface); box-shadow: inset 0 0 0 1px var(--heat); }
.shot-v[data-empty="true"]::before { content: attr(data-placeholder); color: var(--black-alpha-32); font-style: italic; }
.icon-mini-btn { width: 24px; height: 24px; display: grid; place-items: center; color: var(--black-alpha-48); background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); cursor: pointer; font-size: 14px; }
.icon-mini-btn:hover { color: var(--heat); border-color: var(--heat); }
/* 镜头卡片间 hover 加分镜插槽 */
.shot-insert-gap { height: 14px; position: relative; display: flex; align-items: center; justify-content: center; }
.shot-insert-gap .add-shot-btn { opacity: 0; height: 22px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: 999px; font-size: 11.5px; font-family: var(--font-mono); cursor: pointer; box-shadow: var(--shadow-cta); transition: opacity var(--t-base); display: inline-flex; align-items: center; gap: 5px; pointer-events: none; }
.shot-insert-gap .add-shot-btn svg { width: 11px; height: 11px; }
.shot-insert-gap:hover .add-shot-btn { opacity: 1; pointer-events: auto; }
.shot-insert-gap::before { content: ''; position: absolute; left: 12px; right: 12px; top: 50%; height: 1px; background: transparent; transition: background var(--t-base); }
.shot-insert-gap:hover::before { background: var(--heat-20); }
/* 镜头脚本空缺省态 */
.shots-empty { padding: 36px 24px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 12px; color: var(--black-alpha-48); }
.shots-empty .empty-ico { width: 56px; height: 56px; border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--black-alpha-32); }
.shots-empty .empty-title { font-size: 14px; font-weight: 500; color: var(--accent-black); }
.shots-empty .empty-hint { font-size: 12px; color: var(--black-alpha-56); line-height: 1.55; max-width: 280px; font-family: var(--font-mono); letter-spacing: .02em; }
/* 对话空态三胶囊 */
.chat-empty { padding: 28px 18px 14px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
.chat-empty .ce-title { font-size: 13.5px; color: var(--accent-black); font-weight: 500; }
.chat-empty .ce-hint { font-size: 11.5px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }
.chat-modes { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.chat-mode { height: 30px; padding: 0 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 999px; font-size: 12.5px; color: var(--accent-black); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
.chat-mode:hover { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }
.chat-mode.primary { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }
.chat-mode svg { width: 13px; height: 13px; }
/* AI 思考态 typing indicator */
.ai-thinking .dots { display: inline-flex; gap: 3px; }
.ai-thinking .dots span { width: 6px; height: 6px; background: var(--black-alpha-32); border-radius: 50%; animation: thinking 1.2s ease-in-out infinite; }
.ai-thinking .dots span:nth-child(2) { animation-delay: .2s; }
.ai-thinking .dots span:nth-child(3) { animation-delay: .4s; }
@keyframes thinking { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
/* 视口锁定 · 只让主内容区滚动 (sidebar + topbar 固定,不随页面滚) */
html, body { height: 100%; overflow: hidden; max-width: 100vw; }
.app { height: 100vh; max-height: 100vh; overflow: hidden; }
.app > .sidebar { height: 100vh; overflow-y: auto; }
.app > main { height: 100vh; max-height: 100vh; overflow: hidden; display: flex; flex-direction: column; min-width: 0; }
.app > main > .topbar { flex-shrink: 0; }
.app > main > .content { flex: 1 1 0; min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden; }
/* === STAGE 2 · 基础资产 === */
.stage-assets { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; }
.stage-assets > div { min-width: 0; }
.asset-side { position: sticky; top: 16px; align-self: start; }
.asset-sec { min-width: 0; }
.asset-strip-wrap { min-width: 0; }
.asset-side .ttab { padding: 10px 12px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; border: 1px solid transparent; border-radius: var(--r-md); }
.asset-side .ttab:hover { background: var(--background-lighter); }
.asset-side .ttab.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
.asset-side .ttab .num { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); margin-left: auto; }
.asset-side .ttab.active .num { color: var(--heat); }
.asset-side .info { font-size: 12px; color: var(--black-alpha-48); padding: 14px 12px; line-height: 1.6; margin-top: 14px; border-top: 1px solid var(--border-faint); }
.asset-side .info strong { color: var(--black-alpha-56); display: block; }
.asset-side .info .mono { font-family: var(--font-mono); }
.asset-sec { scroll-margin-top: 16px; }
.asset-sec + .asset-sec { margin-top: 32px; }
.asset-sec .sec-h { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.asset-sec .sec-h h3 { font-size: 15px; font-weight: 600; }
/* .pill-tip 主样式定义在下方 (heat 主色) */
/* 预设库横滑行(卡片尺寸与主区 .asset-card-2 一致) */
.asset-strip-wrap { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-faint); }
.asset-strip-wrap .strip-h { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase; }
.asset-strip { display: flex; gap: 14px; overflow-x: auto; overflow-y: hidden; padding: 2px 2px 14px; scrollbar-width: thin; }
.asset-strip::-webkit-scrollbar { height: 8px; }
.asset-strip::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }
.asset-strip .asset-card-2 { flex: 0 0 240px; min-width: 240px; max-width: 240px; }
/* 「去 XX 库」CTA 胶囊 · 主操作色,更显眼 */
.asset-sec .sec-h .pill-tip,
.asset-strip-wrap .strip-h .pill-tip {
display: inline-flex; align-items: center; gap: 6px;
height: 28px; padding: 0 14px;
background: var(--heat-12);
border: 1px solid var(--heat-20);
border-radius: 999px;
font-size: 12px; color: var(--heat); font-weight: 500;
cursor: pointer; font-family: inherit;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.asset-sec .sec-h .pill-tip:hover,
.asset-strip-wrap .strip-h .pill-tip:hover {
background: var(--heat); color: var(--accent-white); border-color: var(--heat);
box-shadow: var(--shadow-cta);
}
.asset-sec .sec-h .pill-tip svg,
.asset-strip-wrap .strip-h .pill-tip svg { width: 12px; height: 12px; }
.asset-grid-2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
/* 商品行:左侧商品卡 + 右侧三视图预览(三视图是单张 16:9 图,不是 3 张) */
.prod-row { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.prod-row > .asset-card-2 { flex: 0 0 240px; max-width: 240px; }
.prod-preview { flex: 0 0 360px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px; display: none; flex-direction: column; gap: 10px; }
.prod-preview.show { display: flex; }
.prod-preview-h { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase; }
.prod-preview-img { aspect-ratio: 16/9; }
.prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }
.asset-card-2 { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); }
.asset-card-2:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.asset-card-2 .thumb-2 { aspect-ratio: 1; }
.asset-card-2 .body-2 { padding: 12px 14px; }
.asset-card-2 .body-2 .btn-apply { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }
.asset-card-2 .body-2 .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
/* 通用资产详情 modal */
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
.asset-modal-bg.show { display: flex; }
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(960px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
.asset-modal-h h2 { font-size: 15px; font-weight: 600; }
.asset-modal-h .x { width: 30px; height: 30px; display: grid; place-items: center; background: transparent; border: 0; cursor: pointer; color: var(--black-alpha-56); border-radius: var(--r-sm); margin-left: auto; }
.asset-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.asset-modal-body { padding: 22px 24px; overflow-y: auto; flex: 1; }
.asset-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.asset-detail-lead .placeholder { aspect-ratio: 3/4; }
/* 三视图 · 单张 16:9 图 */
.asset-detail-tri-row { margin-top: 10px; }
.asset-detail-tri-row .placeholder { aspect-ratio: 16 / 9; }
.asset-detail-tri-row .placeholder.missing { display: grid; place-items: center; border: 1px dashed var(--border-faint); background: var(--background-lighter); color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 12px; padding: 12px; text-align: center; cursor: pointer; transition: border-color var(--t-base), color var(--t-base); gap: 8px; }
.asset-detail-tri-row .placeholder.missing:hover { border-color: var(--heat); color: var(--heat); }
.asset-detail-section-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 8px; }
.asset-detail-info .row { display: flex; justify-content: space-between; align-items: baseline; padding: 8px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; }
.asset-detail-info .row:last-child { border-bottom: 0; }
.asset-detail-info .row .k { color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }
.asset-detail-info .row .v { color: var(--accent-black); }
.asset-detail-tip { margin-top: 10px; padding: 10px 12px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); display: flex; align-items: center; gap: 8px; line-height: 1.5; }
.asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
.asset-detail-tip .ai-gen-btn { margin-left: auto; height: 26px; padding: 0 10px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; flex-shrink: 0; }
.asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; justify-content: flex-end; gap: 8px; }
/* 模特库 / 场景库 全屏弹窗(沿用 model-photo .ml-modal 结构) */
.ml-modal-bg { position: fixed; inset: 0; background: var(--surface); z-index: 1000; display: none; }
.ml-modal-bg.show { display: flex; }
.ml-modal { margin: 0; flex: 1; background: var(--surface); border-radius: 0; overflow: hidden; display: flex; flex-direction: column; }
.ml-modal-h { display: flex; align-items: center; padding: 14px 28px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; }
.ml-modal-h h2 { font-size: 16px; font-weight: 600; }
.ml-modal-h .ct { margin-left: 10px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.ml-modal-h .x { margin-left: auto; width: 32px; height: 32px; display: grid; place-items: center; background: transparent; border: 0; border-radius: var(--r-sm); cursor: pointer; color: var(--black-alpha-56); transition: background var(--t-base), color var(--t-base); }
.ml-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.ml-modal-h .x svg { width: 16px; height: 16px; }
.ml-modal-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 200px 1fr; }
.ml-side { border-right: 1px solid var(--border-faint); padding: 18px 0; overflow-y: auto; }
.ml-side .ml-side-h { padding: 0 20px 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; }
.ml-side .ml-side-item { display: flex; align-items: center; gap: 8px; padding: 9px 20px; cursor: pointer; color: var(--black-alpha-72); font-size: 13px; border-left: 3px solid transparent; transition: background var(--t-base), color var(--t-base); }
.ml-side .ml-side-item:hover { background: var(--black-alpha-4); }
.ml-side .ml-side-item.active { background: var(--heat-12); color: var(--accent-black); border-left-color: var(--heat); font-weight: 600; }
.ml-side .ml-side-item .ct { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
.ml-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; min-width: 0; }
.ml-toolbar { padding: 14px 28px; border-bottom: 1px solid var(--border-faint); display: flex; align-items: center; gap: 18px; flex-shrink: 0; flex-wrap: wrap; }
.ml-toolbar .btn-up { height: 32px; padding: 0 14px; display: inline-flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--black-alpha-12); border-radius: var(--r-sm); color: var(--accent-black); font-family: inherit; font-size: 12.5px; cursor: pointer; transition: background var(--t-base), border-color var(--t-base); }
.ml-toolbar .btn-up:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.ml-toolbar .btn-up svg { width: 14px; height: 14px; }
.ml-toolbar .chip-group { display: inline-flex; align-items: center; gap: 6px; }
.ml-toolbar .chip-group .lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; margin-right: 4px; }
.ml-toolbar .chip { height: 26px; padding: 0 12px; border-radius: 999px; background: transparent; border: 1px solid var(--black-alpha-12); color: var(--black-alpha-72); font-size: 12px; cursor: pointer; font-family: inherit; transition: background var(--t-base), color var(--t-base), border-color var(--t-base); }
.ml-toolbar .chip:hover { color: var(--accent-black); }
.ml-toolbar .chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); font-weight: 600; }
.ml-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }
.ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.ml-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px; cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); display: flex; flex-direction: column; gap: 8px; }
.ml-card:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.ml-card .placeholder { aspect-ratio: 3/4; }
.ml-card .ml-card-nm { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.ml-card .ml-card-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
.ml-card .ml-card-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; }
.ml-card .ml-card-foot .pill { font-size: 10.5px; padding: 1px 7px; }
.ml-card .ml-card-foot .btn-apply { margin-left: auto; height: 26px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; }
.ml-card .ml-card-foot .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
/* 新增人物 modal · 立绘 + 三视图 上传区 */
.upload-zone { aspect-ratio: 3/4; background: var(--background-lighter); border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; transition: border-color var(--t-base), background var(--t-base); padding: 16px; text-align: center; color: var(--black-alpha-56); font-size: 12px; }
.upload-zone:hover { border-color: var(--heat); background: var(--heat-12); color: var(--heat); }
.upload-zone.lead { aspect-ratio: 3/4; }
.upload-zone svg { width: 20px; height: 20px; }
.upload-zone-tri { aspect-ratio: 1; padding: 8px; font-size: 10.5px; }
.prompt-box { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 10px 12px; font-size: 12px; color: var(--black-alpha-56); margin-top: 8px; line-height: 1.55; font-family: var(--font-mono); letter-spacing: .01em; transition: border-color var(--t-base), background var(--t-base); }
.prompt-box[contenteditable="true"] { cursor: text; outline: none; }
.prompt-box[contenteditable="true"]:hover { border-color: var(--heat-20); }
.prompt-box[contenteditable="true"]:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
.fail-icon { width: 28px; height: 28px; background: var(--accent-crimson); color: var(--accent-white); display: grid; place-items: center; font-weight: 700; font-size: 16px; border-radius: 50%; }
/* === STAGE 3 · 故事板(略缩图竖向侧栏 + 主图区)=== */
.stage-storyboard { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 16px; align-items: stretch; }
.sb-canvas { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 14px; }
.sb-scenes-col { display: flex; flex-direction: column; gap: 10px; overflow-y: auto; overflow-x: hidden; max-height: 560px; padding-right: 6px; scrollbar-width: thin; }
.sb-scenes-col::-webkit-scrollbar { width: 6px; }
.sb-scenes-col::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }
.sb-scene-thumb { flex: 0 0 auto; cursor: pointer; display: flex; flex-direction: column; gap: 6px; padding: 6px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); transition: border-color var(--t-base), background var(--t-base); }
.sb-scene-thumb:hover { background: var(--background-lighter); }
.sb-scene-thumb.selected { border-color: var(--heat); background: var(--heat-12); }
.sb-scene-thumb .placeholder { aspect-ratio: 1; }
.sb-scene-thumb .nm { font-size: 11.5px; font-weight: 500; color: var(--accent-black); }
.sb-scene-thumb .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
.sb-main-img { aspect-ratio: 16/9; min-height: 0; }
.sb-stage-actions { display: flex; gap: 8px; margin-bottom: 12px; }
/* 故事板历史版本 */
.sb-history { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-faint); }
.sb-history-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; }
.sb-history-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }
.sb-history-row::-webkit-scrollbar { height: 6px; }
.sb-history-row::-webkit-scrollbar-thumb { background: var(--border-faint); }
.sb-history-thumb { flex: 0 0 80px; min-width: 80px; display: flex; flex-direction: column; gap: 4px; padding: 4px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; transition: border-color var(--t-base); }
.sb-history-thumb:hover { border-color: var(--heat); }
.sb-history-thumb.current { border-color: var(--heat); background: var(--heat-12); }
.sb-history-thumb .placeholder { aspect-ratio: 1; }
.sb-history-thumb .ts { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); text-align: center; }
.sb-history-thumb.current .ts { color: var(--heat); font-weight: 600; }
.pill-cta { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 14px; border-radius: 999px; font-size: 12.5px; cursor: pointer; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
.pill-cta.heat { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }
.pill-cta.heat:hover { box-shadow: var(--shadow-cta-hover); }
.pill-cta.ghost { background: var(--surface); color: var(--accent-black); border: 1px solid var(--border-faint); }
.pill-cta.ghost:hover { background: var(--background-lighter); border-color: var(--heat-20); color: var(--heat); }
.pill-cta svg { width: 13px; height: 13px; }
/* === STAGE 3 / 4 跳过条 === */
.skip-row { display: flex; justify-content: flex-end; margin-bottom: 12px; }
.sb-side .pane { padding: 18px; }
.prompt-edit { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.7; color: var(--black-alpha-56); white-space: pre-wrap; min-height: 200px; outline: none; letter-spacing: .01em; }
.prompt-edit:focus { border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }
.asset-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-pill); font-size: 11.5px; }
.asset-tag .dotc { width: 14px; height: 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 50%; }
/* === STAGE 4 · 视频片段 === */
.queue-bar { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-bottom: 18px; }
.queue-bar .bar-wrap { flex: 1; height: 6px; background: var(--background-lighter); overflow: hidden; }
.queue-bar .bar-wrap > span { display: block; height: 100%; background: var(--heat); }
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
.video-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base); }
.video-card:hover { border-color: var(--heat-40); }
.video-thumb { aspect-ratio: 9/16; max-height: 320px; position: relative; border-radius: var(--r-md) var(--r-md) 0 0; }
.video-thumb .play { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,0.05); cursor: pointer; opacity: 0; transition: opacity .15s; }
.video-thumb:hover .play { opacity: 1; }
.video-thumb .btn-play { width: 36px; height: 36px; background: rgba(0,0,0,.7); color: var(--accent-white); border-radius: 50%; display: grid; place-items: center; }
.video-card .body { padding: 10px 12px; }
/* 视频详情 modal 大视频 + 历史版本 */
.vd-main-wrap { display: flex; gap: 18px; align-items: flex-start; }
.vd-main { flex: 0 0 280px; aspect-ratio: 9/16; max-height: 460px; }
.vd-main .placeholder { aspect-ratio: 9/16; height: 100%; }
.vd-info { flex: 1; min-width: 0; }
.vd-history { margin-top: 16px; }
.vd-history-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
.vd-history-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }
.vd-history-thumb { flex: 0 0 64px; min-width: 64px; display: flex; flex-direction: column; gap: 4px; padding: 4px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; transition: border-color var(--t-base); position: relative; }
.vd-history-thumb:hover { border-color: var(--heat); }
.vd-history-thumb.current { border-color: var(--heat); background: var(--heat-12); }
.vd-history-thumb.adopted::after { content: ''; position: absolute; top: 2px; right: 2px; width: 14px; height: 14px; background: var(--heat); border-radius: 50%; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 9px 9px; }
.vd-history-thumb .placeholder { aspect-ratio: 9/16; }
.vd-history-thumb .ts { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); text-align: center; }
/* === STAGE 5 · 编辑器 === */
.editor { display: grid; grid-template-columns: 1fr 280px; grid-template-rows: 1fr auto; gap: 0; height: 580px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
.editor-preview { padding: 16px; border-right: 1px solid var(--border-faint); border-bottom: 1px solid var(--border-faint); display: flex; flex-direction: column; gap: 12px; }
.editor-preview .canvas { flex: 1; aspect-ratio: 9/16; max-height: 380px; margin: 0 auto; background:
repeating-linear-gradient(135deg, rgba(0,0,0,0.03) 0 1px, transparent 1px 12px),
var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 12px; }
.editor-preview .controls { display: flex; align-items: center; gap: 8px; justify-content: center; }
.ctl-btn { width: 36px; height: 36px; border: 1px solid var(--border-faint); background: var(--surface); color: var(--black-alpha-56); border-radius: var(--r-md); display: grid; place-items: center; cursor: pointer; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
.ctl-btn:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }
.editor-props { padding: 16px; border-bottom: 1px solid var(--border-faint); overflow-y: auto; }
.props-tabs { display: flex; gap: 0; margin-bottom: 14px; border-bottom: 1px solid var(--border-faint); }
.props-tabs > div { padding: 8px 12px; font-size: 12.5px; color: var(--black-alpha-56); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.props-tabs > div.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; }
.style-swatch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.swatch-card { padding: 10px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; }
.swatch-card:hover { background: var(--background-lighter); }
.swatch-card.selected { border-color: var(--heat); background: var(--heat-12); }
.swatch-card .demo { font-size: 12px; padding: 6px 8px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); margin-bottom: 4px; text-align: center; }
.swatch-card .demo.b { background: var(--accent-black); color: var(--accent-white); font-family: serif; }
.swatch-card .demo.c { color: var(--heat); -webkit-text-stroke: 0.5px var(--accent-black); }
.swatch-card .demo.d { background: var(--accent-honey); color: var(--accent-black); font-weight: 700; }
.swatch-card .nm { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
.props-row { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; }
.props-row:last-child { border-bottom: 0; }
.props-row .k { color: var(--black-alpha-48); flex: 1; font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }
.input-mini { width: 90px; padding: 0 10px; height: 28px; font-size: 12px; border-radius: var(--r-md); background: var(--surface); border: 1px solid var(--black-alpha-12); }
.timeline { grid-column: 1 / -1; padding: 14px 16px; background: var(--background-base); }
.tl-toolbar { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border-faint); }
.tl-ruler { display: grid; grid-template-columns: 80px 1fr; align-items: center; padding: 4px 0; font-size: 10.5px; }
.tl-ruler .l { font-family: var(--font-mono); color: var(--black-alpha-48); padding-left: 4px; }
.tl-ruler .ticks { display: flex; justify-content: space-between; font-family: var(--font-mono); color: var(--black-alpha-48); padding: 0 4px; letter-spacing: .04em; }
.tl-track { display: grid; grid-template-columns: 80px 1fr; align-items: center; gap: 0; padding: 6px 0; }
.tl-track .label { font-size: 11.5px; color: var(--black-alpha-56); display: flex; align-items: center; gap: 6px; padding-left: 4px; }
.tl-track .label .dot { width: 8px; height: 8px; }
.tl-track .lane { display: flex; gap: 2px; height: 30px; position: relative; }
.clip { padding: 0 8px; font-size: 11px; display: flex; align-items: center; cursor: pointer; overflow: hidden; white-space: nowrap; user-select: none; }
.clip.video { background: var(--heat-12); border: 1px solid var(--heat-40); color: var(--heat); }
.clip.video.selected { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
.clip.subtitle { background: var(--forest-bg); border: 1px solid var(--forest-bd); color: var(--accent-forest); }
.clip.bgm { background: rgba(144, 97, 255, 0.10); border: 1px solid rgba(144, 97, 255, 0.30); color: var(--accent-amethyst); }
.clip .num { font-family: var(--font-mono); font-weight: 700; margin-right: 6px; opacity: .7; }
.playhead { position: absolute; top: -16px; bottom: -54px; width: 1px; background: var(--heat); pointer-events: none; }
.playhead::before { content: ''; position: absolute; top: -2px; left: -4px; width: 9px; height: 9px; background: var(--heat); transform: rotate(45deg); }
</style>
</head>
<body>
<div id="page">
<!-- Project header -->
<div class="proj-head">
<div style="display:flex; gap:14px; align-items:center;">
<div class="placeholder" style="width:42px;height:54px;"><span class="ph-frame">9:16</span></div>
<div>
<div style="display:flex; gap:8px; align-items:center;">
<h1>补水面膜 · 痛点种草 · v3</h1>
<span class="pill info"><span class="dot"></span>进行中</span>
</div>
<div class="muted-2 mono" style="font-size:11.5px; margin-top:4px; letter-spacing:.02em;">// 透真补水面膜 · AI 全生 · <span id="page-head-shots-meta">待生成镜头脚本</span> · 9:16</div>
</div>
</div>
<div class="hstack">
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('分享', '/projects/p3/share')">分享</button>
<button class="btn btn-sm" onclick="Shell.toast('已复制项目', '补水面膜 · v4')">复制项目</button>
<button class="btn btn-sm" onclick="Shell.toast('归档项目', '/projects/p3/archive')">归档</button>
</div>
</div>
<!-- Stage stepper -->
<div class="stepper">
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
<a class="stage-step active" data-stage="1" href="#stage-1"><div class="num">1</div><div class="lbl">脚本</div><div class="st">进行中</div></a>
<div class="stage-line"></div>
<a class="stage-step" data-stage="2" href="#stage-2"><div class="num">2</div><div class="lbl">基础资产</div><div class="st">待开始</div></a>
<div class="stage-line"></div>
<a class="stage-step" data-stage="3" href="#stage-3"><div class="num">3</div><div class="lbl">故事板</div><div class="st">待开始</div></a>
<div class="stage-line"></div>
<a class="stage-step" data-stage="4" href="#stage-4"><div class="num">4</div><div class="lbl">视频</div><div class="st">待开始</div></a>
<div class="stage-line"></div>
<a class="stage-step" data-stage="5" href="#stage-5"><div class="num">5</div><div class="lbl">拼接导出</div><div class="st">待开始</div></a>
</div>
<!-- ============= STAGE 1 · 脚本 ============= -->
<section class="stage active" data-stage-pane="1">
<div class="stage-script">
<div class="pane shot-list">
<div class="pane-h">
<strong>镜头脚本</strong>
<span class="muted-2 mono" id="shots-meta" style="font-size:11px;">· 空 · 待生成</span>
</div>
<div class="shots-body" id="shots-body">
<!-- JS 注入空态/镜头卡片 -->
</div>
</div>
<div class="pane chat-pane">
<div class="pane-h">
<div class="ai-avatar">AI</div>
<strong>脚本助手</strong>
<span class="muted-2 mono" style="font-size:11px;">· GPT-4o</span>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" id="chat-clear-btn">清空对话</button>
</div>
<div class="chat-body" id="chat-body">
<!-- JS 注入空态/对话内容 -->
</div>
<div class="chat-input">
<textarea class="textarea" id="chat-textarea" placeholder="对脚本的修改诉求 · 比如:让第 3 场更夸张一点、整体加一场结尾……" rows="2"></textarea>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm" id="chat-regen-btn">↻ 整体重写</button>
<span class="spacer"></span>
<button class="btn btn-primary" id="chat-send-btn">发送 ⌘↵</button>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ LLM 用量 ~2.4k tokens · ¥0.04 ]</span></div>
<div class="hstack">
<button class="btn" onclick="Shell.toast('重新生成', 'POST /script/regen')"><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 12a8 8 0 0 1 14-5.5L21 9"/><path d="M21 4v5h-5"/><path d="M20 12a8 8 0 0 1-14 5.5L3 15"/><path d="M3 20v-5h5"/></svg> 重新生成全部</button>
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-2'">确认脚本,进入下一步 <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="M5 12h14M12 5l7 7-7 7"/></svg></button>
</div>
</div>
</section>
<!-- ============= STAGE 2 · 基础资产 ============= -->
<section class="stage" data-stage-pane="2">
<div class="stage-assets">
<div class="asset-side">
<div class="ttab active" data-jump="asset-sec-products"><span>商品</span><span class="num">3 张</span></div>
<div class="ttab" data-jump="asset-sec-characters"><span>人物</span><span class="num">2/2</span></div>
<div class="ttab" data-jump="asset-sec-scenes"><span>场景</span><span class="num">3/3</span></div>
<div class="info">
基础资产是后续故事板的素材。所有卡片同时展示,点左侧分类直接定位。
<br><br>
<strong class="mono">// 人物 +¥0.20/张</strong>
<strong class="mono">// 场景 +¥0.15/张</strong>
<span style="color:var(--black-alpha-48);">商品图无成本(直接复用商品库)</span>
</div>
</div>
<div>
<!-- ===== 商品(项目内只有 1 个商品,从 URL ?product= 取)===== -->
<section class="asset-sec" id="asset-sec-products">
<div class="sec-h">
<h3>商品 · <span id="asset-prod-name">透真补水面膜</span></h3>
<span class="spacer"></span>
</div>
<div class="prod-row">
<div class="asset-card-2" data-asset-kind="product" data-asset-id="prod-main" id="asset-prod-card">
<div class="placeholder thumb-2"><span class="ph-frame" id="asset-prod-thumb-label">透真补水面膜 · 主图</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;" id="asset-prod-card-name">透真补水面膜</strong><span class="spacer"></span><span class="pill info" id="asset-prod-pill"><span class="dot"></span>缺三视图</span></div>
<div class="hstack" style="margin-top:10px;">
<span class="spacer"></span>
<button class="btn btn-sm btn-apply" data-stop id="asset-prod-aigen-btn" style="background: var(--heat); color: var(--accent-white); border-color: var(--heat);">AI 生成三视图</button>
</div>
</div>
</div>
<div class="prod-preview" id="asset-prod-preview">
<div class="prod-preview-h">// 三视图预览 · <span id="prod-preview-status">生成中</span></div>
<div class="placeholder prod-preview-img" id="prod-preview-img"></div>
<div class="prod-preview-foot" id="prod-preview-foot"></div>
</div>
</div>
</section>
<!-- ===== 人物 ===== -->
<section class="asset-sec" id="asset-sec-characters">
<div class="sec-h">
<h3>人物 · 2 个</h3>
<span class="spacer"></span>
<button class="btn btn-sm" id="asset-add-character">+ 新增人物</button>
</div>
<div class="asset-grid-2">
<div class="asset-card-2" data-asset-kind="character" data-asset-id="ch-linxi">
<div class="placeholder thumb-2"><span class="ph-frame">林夕 · 都市白领</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">主角 · 林夕</strong><span class="spacer"></span></div>
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>25-30 岁都市白领,长发,穿宽松米色家居服,温柔但带点疲倦感。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
</div>
</div>
</div>
<div class="asset-card-2" data-asset-kind="character" data-asset-id="ch-anan">
<div class="placeholder thumb-2">
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
<div class="spinner"></div>
<span class="ph-frame">生成中 · 约 8s</span>
</div>
</div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">朋友/同事 · 阿楠</strong><span class="spacer"></span></div>
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>25-30 岁同龄女性,短发,穿白色衬衫,妆容精致皮肤好,作为对比。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm" data-stop data-rerun disabled>重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" data-stop data-replace disabled>替换</button>
</div>
</div>
</div>
</div>
</section>
<!-- ===== 场景 ===== -->
<section class="asset-sec" id="asset-sec-scenes">
<div class="sec-h">
<h3>场景 · 3 个</h3>
<span class="spacer"></span>
<button class="btn btn-sm" id="asset-add-scene">+ 新增场景</button>
</div>
<div class="asset-grid-2">
<div class="asset-card-2" data-asset-kind="scene" data-asset-id="sc-desk">
<div class="placeholder thumb-2"><span class="ph-frame">深夜办公桌</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">深夜办公桌</strong><span class="spacer"></span></div>
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>深夜居家办公环境,木质书桌,台灯暖光,电脑屏幕亮着。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
</div>
</div>
</div>
<div class="asset-card-2" data-asset-kind="scene" data-asset-id="sc-bed">
<div class="placeholder thumb-2"><span class="ph-frame">床头特写</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">卧室床头</strong><span class="spacer"></span></div>
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>米白色床品,木质床头柜,闹钟显示晚间时间。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
</div>
</div>
</div>
<div class="asset-card-2" data-asset-kind="scene" data-asset-id="sc-subway">
<div class="placeholder thumb-2">
<div style="display:flex; flex-direction:column; gap:6px; align-items:center;">
<div class="fail-icon">!</div>
<span class="ph-frame">生成失败</span>
</div>
</div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">通勤地铁</strong><span class="spacer"></span><span class="pill err"><span class="dot"></span>失败</span></div>
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>早高峰地铁车厢,光线偏冷,年轻通勤族,氛围紧张。</div>
<div class="hstack" style="margin-top:10px;">
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
<span class="spacer"></span>
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ 已确认 ¥0.85 · 待生成 ¥0.20 · 失败 ¥0(不扣) ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-1'">← 返回脚本</button>
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-3'">确认资产,进入故事板 →</button>
</div>
</div>
</section>
<!-- ============= STAGE 3 · 故事板(按场分) ============= -->
<section class="stage" data-stage-pane="3">
<div class="skip-row">
<button class="btn btn-ghost btn-sm" onclick="location.hash='#stage-4'">跳过本步 →</button>
</div>
<div class="stage-storyboard">
<div class="sb-canvas">
<div class="sb-scenes-col" id="sb-scenes-row">
<!-- JS 注入 略缩图 (竖向) -->
</div>
<div class="placeholder sb-main-img" id="sb-main-img"><span class="ph-frame">未选择</span></div>
</div>
<div class="sb-side">
<div class="pane" style="padding:18px;">
<div class="hstack" style="margin-bottom:10px;">
<strong style="font-size:14px;">故事板 · <span id="sb-side-scene">场 1</span></strong>
<span class="spacer"></span>
<span class="pill ok"><span class="dot"></span>已生成</span>
</div>
<div class="muted-2" style="font-size:12px; line-height:1.55; margin-bottom:14px;">
整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。如需修改请编辑下方提示词整张重跑(不能局部改)。
</div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:6px; letter-spacing:.04em;">// 本场提示词</div>
<div class="prompt-edit" contenteditable="true" id="sb-prompt-edit"></div>
<div class="sb-stage-actions">
<button class="pill-cta heat" id="sb-rerun-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 0 1 14-5.5L21 9"/><path d="M21 4v5h-5"/><path d="M20 12a8 8 0 0 1-14 5.5L3 15"/><path d="M3 20v-5h5"/></svg>
整张重跑
</button>
<button class="pill-cta ghost" id="sb-apply-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
应用
</button>
<span class="spacer"></span>
<span class="muted-2 mono" style="font-size:11px; align-self: center;">~¥0.45/场</span>
</div>
<div class="sb-history">
<div class="sb-history-h">// 历史版本(<span id="sb-history-ct">0</span>)</div>
<div class="sb-history-row" id="sb-history-row">
<div style="font-size: 11.5px; color: var(--black-alpha-48); padding: 12px 4px;">// 暂无历史版本</div>
</div>
</div>
<div class="divider" style="margin-top: 16px;"></div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 绑定的资产</div>
<div style="display:flex; gap:6px; flex-wrap:wrap;" id="sb-bound-assets">
<span class="asset-tag"><span class="dotc"></span>林夕(人物)</span>
<span class="asset-tag"><span class="dotc"></span>深夜办公桌(场景)</span>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ image-2 单场 ¥0.45 · 累计 ¥1.35 ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-2'"><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="M19 12H5M12 19l-7-7 7-7"/></svg> 返回资产</button>
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-4'">确认故事板,开始生成视频 <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="M5 12h14M12 5l7 7-7 7"/></svg></button>
</div>
</div>
</section>
<!-- ============= STAGE 4 · 视频(按场分,15s/场) ============= -->
<section class="stage" data-stage-pane="4">
<div class="queue-bar">
<div>
<div style="font-size:14px; font-weight:600;">视频生成 · 3 / 3 完成</div>
<div class="muted-2 mono" style="font-size:11px; margin-top:3px; letter-spacing:.02em;">// 每场 Seedance 约 <span id="seedance-avg">15</span> 秒 · 已完成所有场次</div>
</div>
<div class="bar-wrap"><span style="width:100%"></span></div>
<span class="muted mono" style="font-size:12px;">100%</span>
<button class="btn btn-sm" onclick="Shell.toast('全部重跑', 'POST /video/regen-all')">↻ 全部重跑</button>
</div>
<div class="video-grid" id="video-grid">
<div class="video-card" data-video-id="v1" data-duration="15">
<div class="placeholder video-thumb">
<span class="ph-frame">场 1 · 0-15s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">场 1 · 深夜办公桌</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">15s · 1080×1920 · ¥0.45</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm" data-vstop>↻ 重跑</button>
<button class="btn btn-ghost btn-sm" data-vstop>⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card" data-video-id="v2" data-duration="12">
<div class="placeholder video-thumb">
<span class="ph-frame">场 2 · 15-27s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">场 2 · 面膜包装/特写</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">12s · 1080×1920 · ¥0.45</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm" data-vstop>↻ 重跑</button>
<button class="btn btn-ghost btn-sm" data-vstop>⤓ 下载</button>
</div>
</div>
</div>
<div class="video-card" data-video-id="v3" data-duration="13">
<div class="placeholder video-thumb">
<span class="ph-frame">场 3 · 27-40s</span>
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
</div>
<div class="body">
<div class="hstack"><strong style="font-size:13px;">场 3 · 化妆台/产品定格</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">13s · 1080×1920 · ¥0.45</div>
<div class="hstack" style="margin-top:8px;">
<button class="btn btn-ghost btn-sm" data-vstop>↻ 重跑</button>
<button class="btn btn-ghost btn-sm" data-vstop>⤓ 下载</button>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ 已完成 3 场 · 累计 ¥1.35 · 总时长 <span id="seedance-total">40</span>s ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-3'">← 返回故事板</button>
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-5'">确认视频,进入拼接 →</button>
</div>
</div>
</section>
<!-- ===== Stage 4 · 视频详情 modal ===== -->
<div class="asset-modal-bg" id="video-detail-modal">
<div class="asset-modal" style="width: min(880px, 100%);">
<div class="asset-modal-h">
<h2 id="vd-title">视频详情</h2>
<span style="font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48);" id="vd-sub">// 场 1 · 15s</span>
<button class="x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
<div class="asset-modal-body">
<div class="vd-main-wrap">
<div class="vd-main">
<div class="placeholder" id="vd-main-img"><span class="ph-frame">大视频预览</span></div>
</div>
<div class="vd-info">
<div class="asset-detail-section-h">// 基础信息</div>
<div class="asset-detail-info" id="vd-info"></div>
<div class="vd-history">
<div class="vd-history-h">// 历史版本 · <span id="vd-history-ct">3</span></div>
<div class="vd-history-row" id="vd-history-row"></div>
</div>
<div class="muted-2" style="font-size: 11.5px; color: var(--black-alpha-56); margin-top: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// 点击历史略缩切换 · 「采用此版」标记当前展示版为最终采用</div>
</div>
</div>
</div>
<div class="asset-modal-f">
<button class="btn btn-ghost" type="button" data-modal-close>关闭</button>
<button class="btn" type="button" id="vd-regen-btn">↻ 重跑本场</button>
<button class="btn btn-primary" type="button" id="vd-adopt-btn">采用此版</button>
</div>
</div>
</div>
<!-- ============= STAGE 5 · 拼接编辑器 ============= -->
<section class="stage" data-stage-pane="5">
<div class="editor">
<div class="editor-preview">
<div class="canvas">9:16 预览 · 1080×1920</div>
<div class="controls">
<button class="ctl-btn" title="上一帧"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M3 3v10l4-5zM9 3v10l4-5z" fill="currentColor"/></svg></button>
<button class="ctl-btn" title="播放" onclick="Shell.toast('播放', '00:08.42 / 00:15.00')"><svg width="16" height="16" viewBox="0 0 16 16"><path d="M5 4l7 4-7 4z" fill="currentColor"/></svg></button>
<button class="ctl-btn" title="下一帧"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M13 3v10l-4-5zM7 3v10l-4-5z" fill="currentColor"/></svg></button>
<span class="muted mono" style="font-size:12px; margin-left:8px;">00:08.42 / 00:15.00</span>
</div>
</div>
<div class="editor-props">
<div class="props-tabs">
<div class="active">字幕</div>
<div>转场</div>
<div>BGM</div>
</div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 字幕样式</div>
<div class="style-swatch">
<div class="swatch-card selected"><div class="demo">真实分享</div><div class="nm">朴素白底</div></div>
<div class="swatch-card"><div class="demo b">真实分享</div><div class="nm">影视黑底</div></div>
<div class="swatch-card"><div class="demo c">真实分享</div><div class="nm">手写描边</div></div>
<div class="swatch-card"><div class="demo d">真实分享</div><div class="nm">综艺暖黄</div></div>
</div>
<div class="divider"></div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 当前选中(镜 4)</div>
<div class="props-row"><span class="k">起始</span><input class="input-mini" value="00:08.00"></div>
<div class="props-row"><span class="k">时长</span><input class="input-mini" value="3.00s"></div>
<div class="props-row"><span class="k">音量</span><input class="input-mini" value="100"></div>
<div class="props-row"><span class="k">速度</span><input class="input-mini" value="1.0x"></div>
<div class="props-row"><span class="k">入场</span><span class="mono" style="font-size:11.5px;">交叉淡化</span></div>
<div class="divider"></div>
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// BGM</div>
<div class="props-row" style="border-bottom:0;">
<span style="font-size:12px; flex:1;">温柔治愈钢琴 · 0:42</span>
<button class="btn btn-ghost btn-sm">替换</button>
</div>
</div>
<div class="timeline">
<div class="tl-toolbar">
<button class="btn btn-ghost btn-sm"></button>
<button class="btn btn-ghost btn-sm"></button>
<span class="muted-2" style="font-size:12px;">|</span>
<button class="btn btn-ghost btn-sm">分割</button>
<button class="btn btn-ghost btn-sm">复制</button>
<button class="btn btn-ghost btn-sm">删除</button>
<span class="spacer"></span>
<span class="muted mono" style="font-size:11px;">缩放</span>
<input type="range" min="50" max="200" value="100" style="width:120px;">
</div>
<div class="tl-ruler">
<div class="l">// time</div>
<div class="ticks">
<span>0s</span><span>2s</span><span>5s</span><span>8s</span><span>11s</span><span>13s</span><span>15s</span>
</div>
</div>
<div class="tl-track">
<div class="label"><span class="dot" style="background:var(--heat);"></span>视频</div>
<div class="lane">
<div class="clip video" style="flex:2;"><span class="num">1</span> 深夜办公桌</div>
<div class="clip video" style="flex:3;"><span class="num">2</span> 面膜包装</div>
<div class="clip video" style="flex:3;"><span class="num">3</span> 精华液微距</div>
<div class="clip video selected" style="flex:3;"><span class="num">4</span> 敷面膜平躺</div>
<div class="clip video" style="flex:2;"><span class="num">5</span> 化妆台</div>
<div class="clip video" style="flex:2;"><span class="num">6</span> 产品定格</div>
</div>
</div>
<div class="tl-track">
<div class="label"><span class="dot" style="background:var(--accent-forest);"></span>字幕</div>
<div class="lane" style="position:relative;">
<div class="clip subtitle" style="flex:2;">加班三天 脸已经不能看了…</div>
<div class="clip subtitle" style="flex:3;">还好我有这个 透真玻尿酸面膜</div>
<div class="clip subtitle" style="flex:3;">30g 精华 一片顶三片</div>
<div class="clip subtitle" style="flex:3;">敷完起来脸是软的</div>
<div class="clip subtitle" style="flex:2;">化妆都能看出来</div>
<div class="clip subtitle" style="flex:2;">5 片 ¥39.9 囤起来</div>
<div class="playhead" style="left:56%;"></div>
</div>
</div>
<div class="tl-track">
<div class="label"><span class="dot" style="background:var(--accent-amethyst);"></span>BGM</div>
<div class="lane">
<div class="clip bgm" style="flex:15;">温柔治愈钢琴 · 0:42(循环 1 次,淡入淡出)</div>
</div>
</div>
</div>
</div>
<div class="stage-foot">
<div class="info"><span class="mono">[ 合成预估 ~30s · 不消耗 token ]</span></div>
<div class="hstack">
<button class="btn" onclick="location.hash='#stage-4'"><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="M19 12H5M12 19l-7-7 7-7"/></svg> 返回片段</button>
<button class="btn" onclick="Shell.toast('已保存草稿', '/projects/p3/draft')">保存草稿</button>
<button class="btn btn-primary btn-lg" onclick="Shell.toast('开始导出', 'POST /export · 1080P 9:16')">导出 MP4 · 1080P 9:16 <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="M12 4v12m0 0l-5-5m5 5l5-5M4 20h16"/></svg></button>
</div>
</div>
</section>
<!-- ===== Stage 2 通用 · 资产详情 modal ===== -->
<div class="asset-modal-bg" id="asset-detail-modal">
<div class="asset-modal">
<div class="asset-modal-h">
<h2 id="asset-detail-title">资产详情</h2>
<span style="font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48);" id="asset-detail-kind">// kind</span>
<button class="x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
<div class="asset-modal-body">
<div class="asset-detail-grid">
<div class="asset-detail-lead">
<div class="placeholder" id="asset-detail-lead-img"><span class="ph-frame">立绘</span></div>
<div class="asset-detail-section-h" style="margin-top: 14px;">// 三视图</div>
<div class="asset-detail-tri-row" id="asset-detail-tri">
<div class="placeholder"><span class="ph-frame">正面</span></div>
<div class="placeholder"><span class="ph-frame">侧面</span></div>
<div class="placeholder"><span class="ph-frame">背面</span></div>
</div>
</div>
<div class="asset-detail-right">
<div class="asset-detail-section-h">// 基础信息</div>
<div class="asset-detail-info" id="asset-detail-info"></div>
<div class="asset-detail-tip" id="asset-detail-tip" style="display:none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
<button class="ai-gen-btn" type="button">AI 生成三视图</button>
</div>
</div>
</div>
</div>
<div class="asset-modal-f">
<button class="btn btn-ghost" type="button" data-modal-close>关闭</button>
<button class="btn btn-primary" type="button" id="asset-detail-apply-btn">应用到当前项目</button>
</div>
</div>
</div>
<!-- ===== Stage 2 · 新增人物 modal ===== -->
<div class="asset-modal-bg" id="new-character-modal">
<div class="asset-modal" style="width: min(680px, 100%);">
<div class="asset-modal-h">
<h2>新增人物</h2>
<span style="font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48);">// 立绘必填 + 三视图(可 AI 生成)</span>
<button class="x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
<div class="asset-modal-body">
<div class="field" style="margin-bottom: 14px;">
<label class="field-label" style="display:block; font-size: 12.5px; color: var(--black-alpha-56); margin-bottom: 6px;">人物名称</label>
<input class="input" type="text" placeholder="例:林夕 · 都市白领" style="width:100%; height:36px; padding:0 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size:13px; font-family: inherit;">
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 14px;">
<div>
<div class="asset-detail-section-h">// 立绘<span style="color: var(--heat); margin-left:2px;">*</span></div>
<div class="upload-zone lead" id="nc-upload-lead">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
<span>点击上传立绘</span>
<span style="font-family: var(--font-mono); font-size: 10.5px; opacity: .7;">PNG / JPG · ≤10MB</span>
</div>
</div>
<div>
<div class="asset-detail-section-h">// 三视图<span style="color: var(--black-alpha-48); font-weight:400; margin-left: 4px; text-transform: none; letter-spacing: 0;">(可选)</span></div>
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px;">
<div class="upload-zone upload-zone-tri"><span>正面</span></div>
<div class="upload-zone upload-zone-tri"><span>侧面</span></div>
<div class="upload-zone upload-zone-tri"><span>背面</span></div>
</div>
<div class="asset-detail-tip" id="nc-tri-tip" style="margin-top: 10px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>没有三视图?上传立绘后用 AI 自动生成</span>
<button class="ai-gen-btn" type="button">AI 生成</button>
</div>
</div>
</div>
</div>
<div class="asset-modal-f">
<button class="btn btn-ghost" type="button" data-modal-close>取消</button>
<button class="btn btn-primary" type="button" id="nc-save-btn">保存人物</button>
</div>
</div>
</div>
<!-- ===== Stage 2 · 模特库 / 场景库 全屏弹窗(共享 · kind 切换内容)===== -->
<div class="ml-modal-bg" id="ml-modal-bg">
<div class="ml-modal">
<div class="ml-modal-h">
<h2 id="ml-modal-title">模特库</h2>
<span class="ct" id="ml-modal-ct">// 共 0 个</span>
<button class="x" type="button" id="ml-close-btn" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
</div>
<div class="ml-modal-body">
<aside class="ml-side" id="ml-side">
<!-- JS 注入 来源 -->
</aside>
<div class="ml-main">
<div class="ml-toolbar" id="ml-toolbar">
<!-- JS 注入 chips + 上传按钮 -->
</div>
<div class="ml-scroll">
<div class="ml-grid" id="ml-grid">
<!-- JS 注入 卡片 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({
active: 'projects',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: '补水面膜 · v3' }]
});
// hash routing
function activateStage(n) {
document.querySelectorAll('.stage').forEach(s => s.classList.remove('active'));
document.querySelector(`[data-stage-pane="${n}"]`)?.classList.add('active');
document.querySelectorAll('.stage-step').forEach(s => {
s.classList.remove('active');
if (+s.dataset.stage === +n) s.classList.add('active');
});
const stageNames = { 1:'脚本', 2:'基础资产', 3:'故事板', 4:'视频片段', 5:'拼接导出' };
Shell.toast('进入 Stage ' + n + ' · ' + stageNames[n], 'pipeline#stage-' + n);
}
function readHash() {
const m = location.hash.match(/stage-(\d)/);
if (m) activateStage(+m[1]);
// ?stage=N query 参数也接收
const q = new URLSearchParams(location.search);
const s = q.get('stage');
if (s) activateStage(+s);
}
window.addEventListener('hashchange', readHash);
readHash();
/* ============================================================
STAGE 1 · 脚本助手 + 镜头脚本 状态驱动
============================================================ */
const Stage1 = (function () {
let shots = []; // [{ id, painting, dialog, duration }]
let chatMsgs = []; // [{ role, html, time }]
let mode = null;
const $cb = () => document.getElementById('chat-body');
const $sb = () => document.getElementById('shots-body');
const $sm = () => document.getElementById('shots-meta');
function now() {
const d = new Date();
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
function pushMsg(role, html) { chatMsgs.push({ role, html, time: now() }); }
function renderChat() {
const body = $cb(); if (!body) return;
if (chatMsgs.length === 0 && !mode) {
body.innerHTML = `<div class="chat-empty">
<div class="ce-title">选择一种生成方式开始</div>
<div class="ce-hint">// 三种,由「最省事」到「最保真原意」</div>
<div class="chat-modes">
<button class="chat-mode primary" data-mode="ai"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/></svg>AI 全生</button>
<button class="chat-mode" data-mode="theme"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15 14a4.65 4.65 0 0 0 1.4-2.5A6 6 0 1 0 6 8c0 1 .23 2.23 1.5 3.5"/></svg>一句话主题</button>
<button class="chat-mode" data-mode="manual"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>自带脚本</button>
</div>
</div>`;
body.querySelectorAll('.chat-mode').forEach(btn => {
btn.addEventListener('click', () => pickMode(btn.dataset.mode));
});
return;
}
body.innerHTML = chatMsgs.map(msg => {
if (msg.role === 'ai') {
return `<div class="msg ai"><div style="display:flex; gap:10px; align-items:flex-start;"><div class="ai-avatar" style="margin-top:2px;">AI</div><div class="bubble">${msg.html}</div></div><div class="time" style="margin-left:36px;">${msg.time}</div></div>`;
}
return `<div class="msg user"><div class="bubble">${msg.html}</div><div class="time">${msg.time}</div></div>`;
}).join('');
body.scrollTop = body.scrollHeight;
}
function renderShots() {
const body = $sb(); if (!body) return;
const meta = $sm();
const phMeta = document.getElementById('page-head-shots-meta');
if (shots.length === 0) {
body.innerHTML = `<div class="shots-empty">
<div class="empty-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M9 5v14"/></svg></div>
<div class="empty-title">还没有镜头脚本</div>
<div class="empty-hint">// 跟左侧脚本助手对话<br>选择一种方式生成你的第一稿</div>
</div>`;
if (meta) meta.textContent = '· 空 · 待生成';
if (phMeta) phMeta.textContent = '待生成镜头脚本';
return;
}
let cum = 0;
let html = '';
shots.forEach((s, i) => {
const start = cum;
cum += (s.duration || 5);
const tlabel = start + '-' + cum + 's';
html += `<div class="shot-card" data-id="${s.id}">
<div class="shot-head">
<div class="shot-num">${i + 1}</div>
<div class="shot-time">${tlabel}</div>
<span class="spacer"></span>
<button class="icon-mini-btn" title="重写本场" data-act="regen" data-id="${s.id}">↻</button>
<button class="icon-mini-btn" title="删除本场" data-act="del" data-id="${s.id}">×</button>
</div>
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v" contenteditable="true" data-field="painting" data-placeholder="(画面必填)点击编辑"${s.painting ? '' : ' data-empty="true"'}>${s.painting || ''}</div></div>
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v" contenteditable="true" data-field="dialog" data-placeholder="(对白可空)点击编辑"${s.dialog ? '' : ' data-empty="true"'}>${s.dialog || ''}</div></div>
</div>`;
// 每张卡片后都跟一个 gap(包括最后一张),允许在任意位置 hover 加分镜
html += `<div class="shot-insert-gap" data-after="${s.id}"><button class="add-shot-btn" data-act="add-here" data-after="${s.id}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>添加分场</button></div>`;
});
body.innerHTML = html;
if (meta) meta.textContent = '· ' + shots.length + ' 镜 · 0-' + cum + 's';
if (phMeta) phMeta.textContent = shots.length + ' 镜 · 0-' + cum + 's';
body.querySelectorAll('.shot-v[contenteditable]').forEach(el => {
el.addEventListener('focus', () => { el.dataset.empty = 'false'; });
el.addEventListener('blur', () => {
const card = el.closest('.shot-card');
const id = card.dataset.id;
const field = el.dataset.field;
const v = el.textContent.trim();
const s = shots.find(x => x.id === id);
if (s) s[field] = v;
if (!v) el.dataset.empty = 'true';
});
});
body.querySelectorAll('[data-act]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const act = btn.dataset.act;
const id = btn.dataset.id;
const after = btn.dataset.after;
if (act === 'del') {
shots = shots.filter(x => x.id !== id);
renderShots();
} else if (act === 'regen') {
Shell.toast('已请求重写本场', '↻ shot-' + id);
} else if (act === 'add-here') {
const idx = shots.findIndex(x => x.id === after);
shots.splice(idx + 1, 0, { id: 'sh' + Date.now(), painting: '', dialog: '', duration: 5 });
renderShots();
}
});
});
}
function pickMode(m) {
mode = m;
if (m === 'ai') {
pushMsg('user', '帮我 AI 全自动生成一稿脚本');
renderChat();
setTimeout(() => {
pushMsg('ai', '<span class="ai-thinking">正在解析商品卖点与目标人群 <span class="dots"><span></span><span></span><span></span></span></span>');
renderChat();
}, 300);
// 7 镜 · 0-40s · 与 Stage 2 / 4 的 3 场切分对齐(场 1 深夜办公桌 0-15s / 场 2 面膜包装 15-27s / 场 3 化妆台定格 27-40s)
const draft = [
// ─ 场 1 · 深夜办公桌(15s)─
{ id: 'sh1', painting: '中景慢推 · 深夜居家书桌全景。屏幕仍亮着 PPT,女主背影瘫在椅子上,屏幕冷光 + 台灯暖光对比。字幕"凌晨 02:14"淡入。', dialog: '(无台词 · BGM 渐起)', duration: 5 },
{ id: 'sh2', painting: '近景 · 卫生间镜前。女主低头看脸,T 区起皮、暗沉特写,冷白灯偏惨。', dialog: '"做完这版稿又是凌晨两点……(叹气)脸已经不能看了。"', duration: 5 },
{ id: 'sh3', painting: '俯拍特写 · 回到书桌,拉开抽屉。囤好的透真补水面膜露半角,手伸进去抽出一片。', dialog: '"还好抽屉里囤了透真玻尿酸面膜。"', duration: 5 },
// ─ 场 2 · 面膜包装/特写(12s)─
{ id: 'sh4', painting: '桌面微距特写 · 撕开锡纸包装的瞬间。30g 厚精华液缓缓滴落,面膜布展开,质地拉丝可见。', dialog: '"30g 一片,精华液比普通面膜厚整整三倍。"', duration: 6 },
{ id: 'sh5', painting: '床头近景 · 女主敷好面膜闭眼躺下,台灯暖光打在脸侧。膜布贴合脸型,边缘服帖。', dialog: '"贴上去那一瞬间 —— 凉凉的,像把皮肤泡了一次澡。"', duration: 6 },
// ─ 场 3 · 化妆台/产品定格(13s)─
{ id: 'sh6', painting: '中景 · 第二天清晨化妆台。阳光透过窗帘,女主对镜上妆,皮肤透亮、粉底服帖。同事画外音"你最近用啥了"。', dialog: '"第二天脸是软的,粉底都不卡了。同事都跑来问。"', duration: 8 },
{ id: 'sh7', painting: '平铺俯拍 · 桌面五片装产品 + 单片包装。价格 "618 · 5 片 ¥39.9" 弹出,购物车图标右下角浮现。', dialog: '"618 五片 39.9,自用送人都合适。链接放评论区。"', duration: 5 },
];
let cur = 0;
const step = () => {
if (cur >= draft.length) {
// remove thinking msg
chatMsgs = chatMsgs.filter(x => !(x.role === 'ai' && /ai-thinking/.test(x.html)));
pushMsg('ai', '初稿完成。点击任意卡片文字可直接编辑;鼠标移到卡片之间会出现「+ 添加分场」。');
renderChat();
return;
}
shots.push(draft[cur++]);
renderShots();
setTimeout(step, 700);
};
setTimeout(step, 1100);
} else if (m === 'theme') {
pushMsg('ai', '好,请给我一句话主题(530 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>下面输入框直接打就行,我会按这句话扩成一稿镜头脚本。');
renderChat();
} else if (m === 'manual') {
pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框,我会按场自然切分并适配商品卖点。');
renderChat();
}
}
function init() {
renderChat();
renderShots();
document.getElementById('chat-clear-btn')?.addEventListener('click', () => {
chatMsgs = []; mode = null; shots = [];
renderChat(); renderShots();
});
document.getElementById('chat-regen-btn')?.addEventListener('click', () => {
Shell.toast('已请求整体重写', 'POST /script/regen');
});
const sendBtn = document.getElementById('chat-send-btn');
const ta = document.getElementById('chat-textarea');
if (sendBtn && ta) {
const send = () => {
const v = ta.value.trim();
if (!v) return;
pushMsg('user', v.replace(/</g, '&lt;'));
ta.value = '';
renderChat();
setTimeout(() => {
pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');
renderChat();
}, 400);
};
sendBtn.addEventListener('click', send);
ta.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); send(); }
});
}
// 顶部「+ 追加一场」按钮已移除 — 添加分场改为卡片间 hover 时出现「+ 添加分场」
}
return { init };
})();
Stage1.init();
/* ============================================================
STAGE 2 · 基础资产 · 锚点 + 横滑预设 + 详情/库 modal
============================================================ */
const Stage2 = (function () {
const MODEL_LIB = [
{ id: 'm1', name: '清新短发女', sub: '通勤白领' },
{ id: 'm2', name: '甜美长发女', sub: '学生党' },
{ id: 'm3', name: '商务套装男', sub: '总裁 IP' },
{ id: 'm4', name: '宝妈居家女', sub: '家庭决策' },
{ id: 'm5', name: '运动健身女', sub: '健身博主' },
{ id: 'm6', name: '少年学生男', sub: 'Z 世代' },
];
const SCENE_LIB = [
{ id: 's1', name: '日系卧室', sub: '居家温柔' },
{ id: 's2', name: '咖啡厅工位', sub: '通勤场景' },
{ id: 's3', name: '梳妆台', sub: '美妆个护' },
{ id: 's4', name: '健身房', sub: '运动场景' },
{ id: 's5', name: '厨房料理台', sub: '家居家电' },
{ id: 's6', name: '日落天台', sub: '氛围户外' },
];
// 从 URL ?product= 读出当前项目的商品名(在 Stage2 中只有 1 个商品)
const URL_PRODUCT_NAME = (function () {
try { return decodeURIComponent(new URLSearchParams(location.search).get('product') || ''); }
catch (e) { return ''; }
})();
const CURRENT_PRODUCT_NAME = URL_PRODUCT_NAME || '透真补水面膜';
const ASSET_DETAILS = {
'ch-linxi': { kind: 'character', title: '林夕 · 都市白领', hasTri: true, info: [['类别', '人物 · 主角'], ['年龄', '25-30'], ['服装', '宽松米色家居服'], ['妆面', '日常裸妆 · 略疲倦'], ['用途', '主角出镜 · 痛点共鸣'], ['状态', '已确认']] },
'ch-anan': { kind: 'character', title: '阿楠 · 朋友/同事', hasTri: false, info: [['类别', '人物 · 对照角色'], ['年龄', '25-30'], ['服装', '白色衬衫 / 精致'], ['用途', '对照出镜'], ['状态', '生成中']] },
'sc-desk': { kind: 'scene', title: '深夜办公桌', info: [['类别', '场景 · 室内'], ['光线', '台灯暖光 · 屏幕冷光'], ['用途', '镜 1 痛点'], ['状态', '已确认']] },
'sc-bed': { kind: 'scene', title: '卧室床头', info: [['类别', '场景 · 室内'], ['光线', '夜灯暖光'], ['用途', '镜 4 敷膜'], ['状态', '已确认']] },
'sc-subway': { kind: 'scene', title: '通勤地铁', info: [['类别', '场景 · 室内'], ['用途', '镜 5 对照'], ['状态', '失败 · 待重跑']] },
'prod-main': { kind: 'product', title: CURRENT_PRODUCT_NAME, hasTri: false, info: [['类别', '商品 · 当前项目'], ['名称', CURRENT_PRODUCT_NAME], ['三视图', '待生成'], ['状态', '缺三视图']] },
};
function openStripDetail(name, sub, kind) {
document.getElementById('asset-detail-title').textContent = name;
document.getElementById('asset-detail-kind').textContent = '// ' + (kind === 'model' ? '人物 · 预设模特' : '场景 · 预设');
document.getElementById('asset-detail-lead-img').innerHTML = `<span class="ph-frame">${name}</span>`;
const tri = document.getElementById('asset-detail-tri');
const triHeader = document.querySelector('#asset-detail-modal .asset-detail-lead .asset-detail-section-h');
const tip = document.getElementById('asset-detail-tip');
if (kind === 'scene') {
// 场景无三视图
tri.style.display = 'none';
if (triHeader) triHeader.style.display = 'none';
tip.style.display = 'none';
} else {
tri.style.display = '';
if (triHeader) triHeader.style.display = '';
tri.innerHTML = `<div class="placeholder"><span class="ph-frame">${name} · 三视图</span></div>`;
tip.style.display = 'none';
}
const info = (kind === 'model'
? [['类别', '人物 · 预设模特'], ['标签', sub], ['来源', '模特库'], ['用途', '可应用为本项目的人物资产']]
: [['类别', '场景 · 预设'], ['标签', sub], ['来源', '场景库'], ['用途', '可应用为本项目的场景资产']]);
document.getElementById('asset-detail-info').innerHTML = info.map(([k, v]) => `<div class="row"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('');
document.getElementById('asset-detail-modal').classList.add('show');
}
function renderStrip(containerId, items, kind) {
const el = document.getElementById(containerId);
if (!el) return;
el.innerHTML = items.map(it => `<div class="asset-card-2" data-strip-kind="${kind}" data-strip-id="${it.id}" data-strip-name="${it.name}" data-strip-sub="${it.sub}">
<div class="placeholder thumb-2"><span class="ph-frame">${it.name}</span></div>
<div class="body-2">
<div class="hstack"><strong style="font-size:13.5px;">${it.name}</strong><span class="spacer"></span><span class="pill info" style="font-size:10.5px;">${it.sub}</span></div>
</div>
</div>`).join('');
el.querySelectorAll('.asset-card-2').forEach(card => {
card.addEventListener('click', () => {
const name = card.dataset.stripName;
const sub = card.dataset.stripSub;
openStripDetail(name, sub, kind);
});
});
}
function openDetail(id) {
const d = ASSET_DETAILS[id];
if (!d) return;
document.getElementById('asset-detail-title').textContent = d.title;
document.getElementById('asset-detail-kind').textContent = '// ' + (d.kind === 'character' ? '人物' : d.kind === 'scene' ? '场景' : '商品');
document.getElementById('asset-detail-lead-img').innerHTML = `<span class="ph-frame">${d.title}</span>`;
const tri = document.getElementById('asset-detail-tri');
const triHeader = document.querySelector('#asset-detail-modal .asset-detail-lead .asset-detail-section-h');
const tip = document.getElementById('asset-detail-tip');
if (d.kind === 'scene') {
// 场景无三视图概念
tri.style.display = 'none';
if (triHeader) triHeader.style.display = 'none';
tip.style.display = 'none';
} else {
tri.style.display = '';
if (triHeader) triHeader.style.display = '';
if (d.hasTri) {
tri.innerHTML = `<div class="placeholder"><span class="ph-frame">${d.title} · 三视图</span></div>`;
tip.style.display = 'none';
} else {
tri.innerHTML = `<div class="placeholder missing" data-tri="0">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span>暂未生成三视图(16:9 单图)</span>
</div>`;
tip.style.display = 'flex';
}
}
const info = document.getElementById('asset-detail-info');
info.innerHTML = d.info.map(([k, v]) => `<div class="row"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('');
document.getElementById('asset-detail-modal').classList.add('show');
}
function openLib(kind) {
const isModel = kind === 'model';
const title = isModel ? '模特库' : '场景库';
const items = isModel ? MODEL_LIB : SCENE_LIB;
document.getElementById('ml-modal-title').textContent = title;
document.getElementById('ml-modal-ct').textContent = '// 共 ' + items.length + ' 个预设';
// 侧栏 · 来源
const side = document.getElementById('ml-side');
side.innerHTML = `
<div class="ml-side-h">来源</div>
<div class="ml-side-item active" data-source="all">全部 <span class="ct">${items.length}</span></div>
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">${items.length}</span></div>
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">0</span></div>
`;
side.querySelectorAll('.ml-side-item').forEach(it => {
it.addEventListener('click', () => {
side.querySelectorAll('.ml-side-item').forEach(x => x.classList.remove('active'));
it.classList.add('active');
});
});
// toolbar · chip groups + 上传按钮
const toolbar = document.getElementById('ml-toolbar');
if (isModel) {
toolbar.innerHTML = `
<div class="chip-group">
<span class="lbl">性别</span>
<button class="chip active" type="button">全部</button>
<button class="chip" type="button">女</button>
<button class="chip" type="button">男</button>
</div>
<div class="chip-group">
<span class="lbl">年龄</span>
<button class="chip active" type="button">全部</button>
<button class="chip" type="button">青年</button>
<button class="chip" type="button">中年</button>
</div>
<button class="btn-up" type="button" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
上传我的模特
</button>
`;
} else {
toolbar.innerHTML = `
<div class="chip-group">
<span class="lbl">类型</span>
<button class="chip active" type="button">全部</button>
<button class="chip" type="button">室内</button>
<button class="chip" type="button">室外</button>
</div>
<div class="chip-group">
<span class="lbl">氛围</span>
<button class="chip active" type="button">全部</button>
<button class="chip" type="button">日</button>
<button class="chip" type="button">夜</button>
</div>
<button class="btn-up" type="button" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
上传我的场景
</button>
`;
}
toolbar.querySelectorAll('.chip-group').forEach(group => {
group.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
group.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
});
toolbar.querySelector('.btn-up')?.addEventListener('click', () => {
Shell.toast('上传我的' + (isModel ? '模特' : '场景'), '占位 · 选择本地文件');
});
// 卡片网格
const grid = document.getElementById('ml-grid');
grid.innerHTML = items.map(it => `
<div class="ml-card" data-name="${it.name}" data-sub="${it.sub}">
<div class="placeholder"><span class="ph-frame">${it.name}</span></div>
<div class="ml-card-nm">${it.name}</div>
<div class="ml-card-sub">${it.sub}</div>
<div class="ml-card-foot">
<span class="pill info"><span class="dot"></span>预设</span>
<button class="btn-apply" type="button" data-apply>应用</button>
</div>
</div>
`).join('');
grid.querySelectorAll('.ml-card').forEach(card => {
card.addEventListener('click', (e) => {
const name = card.dataset.name;
const sub = card.dataset.sub;
if (e.target.closest('[data-apply]')) {
e.stopPropagation();
Shell.toast('已应用「' + name + '」', isModel ? '模特库 · 来自预设' : '场景库 · 来自预设');
document.getElementById('ml-modal-bg').classList.remove('show');
return;
}
openStripDetail(name, sub, kind);
});
});
document.getElementById('ml-modal-bg').classList.add('show');
}
function closeLib() {
document.getElementById('ml-modal-bg').classList.remove('show');
}
function init() {
// 侧栏 ttab → 锚点
document.querySelectorAll('.asset-side .ttab[data-jump]').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.asset-side .ttab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const target = document.getElementById(tab.dataset.jump);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
// 卡片 click → 详情(点击空白区域;按钮和可编辑提示词带 data-stop 不触发)
document.querySelectorAll('.asset-card-2[data-asset-id]').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('[data-stop]')) return;
openDetail(card.dataset.assetId);
});
});
// 重跑按钮
document.querySelectorAll('[data-rerun]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const card = btn.closest('.asset-card-2');
const name = card ? card.querySelector('strong').textContent : '资产';
Shell.toast('重跑「' + name + '」', 'POST /assets/regen · 使用当前提示词');
});
});
// 替换按钮
document.querySelectorAll('[data-replace]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const card = btn.closest('.asset-card-2');
const name = card ? card.querySelector('strong').textContent : '资产';
const kind = card ? card.dataset.assetKind : '';
if (kind === 'character') openLib('model');
else if (kind === 'scene') openLib('scene');
else Shell.toast('替换「' + name + '」', '请从素材库挑选或上传');
});
});
// 提示词区域 blur → 保存
document.querySelectorAll('.asset-card-2 .prompt-box[contenteditable="true"]').forEach(box => {
box.addEventListener('blur', () => {
const card = box.closest('.asset-card-2');
const name = card ? card.querySelector('strong').textContent : '资产';
Shell.toast('提示词已更新', name + ' · 下次重跑生效');
});
});
// 模特库 / 场景库
document.getElementById('open-model-lib')?.addEventListener('click', () => openLib('model'));
document.getElementById('open-scene-lib')?.addEventListener('click', () => openLib('scene'));
// 新增
document.getElementById('asset-add-character')?.addEventListener('click', () => {
document.getElementById('new-character-modal').classList.add('show');
});
document.getElementById('asset-add-scene')?.addEventListener('click', () => {
Shell.toast('+ 新增场景', '请上传场景图或填写提示词');
});
// modal 通用关闭
document.querySelectorAll('.asset-modal-bg').forEach(bg => {
bg.addEventListener('click', (e) => { if (e.target === bg) bg.classList.remove('show'); });
bg.querySelectorAll('.x, [data-modal-close]').forEach(el => {
el.addEventListener('click', () => bg.classList.remove('show'));
});
});
// 详情 modal · 应用
document.getElementById('asset-detail-apply-btn')?.addEventListener('click', () => {
const name = document.getElementById('asset-detail-title').textContent;
Shell.toast('已应用「' + name + '」', '已加入当前项目');
document.getElementById('asset-detail-modal').classList.remove('show');
});
// 详情 modal · AI 生成三视图
document.querySelectorAll('.ai-gen-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
Shell.toast('AI 生成三视图中', '约 12s · POST /assets/tri-view');
});
});
// 新增人物 · 保存
document.getElementById('nc-save-btn')?.addEventListener('click', () => {
Shell.toast('已新增人物', '保存到资产库 · 待 AI 补三视图');
document.getElementById('new-character-modal').classList.remove('show');
});
// 把所有 modal 提升到 body 直接子级,避免被 .content 滚动容器裁切
document.querySelectorAll('.asset-modal-bg, .ml-modal-bg').forEach(el => {
if (el.parentElement !== document.body) document.body.appendChild(el);
});
// 模特库 / 场景库 全屏弹窗关闭按钮(仍保留,经"替换"气泡进入)
document.getElementById('ml-close-btn')?.addEventListener('click', closeLib);
// 注入当前项目的商品名(从 URL ?product= 或默认)
const nameEls = ['asset-prod-name', 'asset-prod-card-name'];
nameEls.forEach(eid => {
const el = document.getElementById(eid);
if (el) el.textContent = CURRENT_PRODUCT_NAME;
});
const thumbLbl = document.getElementById('asset-prod-thumb-label');
if (thumbLbl) thumbLbl.textContent = CURRENT_PRODUCT_NAME + ' · 主图';
// 商品卡 · AI 生成三视图 → 右侧 prod-preview 显示单张 16:9 三视图
(function setupProdPreview() {
const aigenBtn = document.getElementById('asset-prod-aigen-btn');
const pane = document.getElementById('asset-prod-preview');
const img = document.getElementById('prod-preview-img');
const statusEl = document.getElementById('prod-preview-status');
const foot = document.getElementById('prod-preview-foot');
const pill = document.getElementById('asset-prod-pill');
if (!aigenBtn || !pane || !img || !statusEl || !foot) return;
function renderLoading() {
img.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px;align-items:center;"><div class="spinner"></div><span class="ph-frame" style="font-size:10.5px;">生成中</span></div>`;
statusEl.textContent = '生成中 · 约 12s';
foot.innerHTML = '<span class="muted-2 mono" style="font-size:11px;">// POST /assets/tri-view</span>';
aigenBtn.disabled = true;
}
function renderDone() {
aigenBtn.disabled = false;
const prodName = CURRENT_PRODUCT_NAME || (document.getElementById('asset-prod-card-name')?.textContent ?? '商品');
img.innerHTML = `<span class="ph-frame">${prodName} · 三视图(正/侧/背)</span>`;
statusEl.textContent = '已生成 · 不满意可重跑';
foot.innerHTML = `
<button class="btn btn-ghost btn-sm" id="prod-preview-rerun">↻ 重跑</button>
<span class="spacer"></span>
<span class="muted-2 mono" style="font-size:11px;">~¥0.30 / 次</span>
<button class="btn btn-sm btn-apply" id="prod-preview-adopt" style="background: var(--heat); color: var(--accent-white); border-color: var(--heat);">采用</button>
`;
document.getElementById('prod-preview-rerun')?.addEventListener('click', start);
document.getElementById('prod-preview-adopt')?.addEventListener('click', () => {
if (pill) {
pill.className = 'pill ok';
pill.innerHTML = '<span class="dot"></span>已三视图';
}
Shell.toast('已采用三视图', prodName + ' · 商品资产已更新');
});
}
function start() {
pane.classList.add('show');
renderLoading();
setTimeout(renderDone, 1800);
}
aigenBtn.addEventListener('click', (e) => {
e.stopPropagation();
start();
});
})();
}
return { init };
})();
Stage2.init();
/* ============================================================
STAGE 3 · 故事板 · 按场分 · 切换/重跑/应用/历史
============================================================ */
const Stage3 = (function () {
const scenes = [
{ id: 'sc1', name: '场 1 · 深夜办公桌', time: '0-15s', desc: '深夜居家办公环境,女主对镜叹气,皮肤干燥起皮特写。台灯暖光 + 屏幕冷光对比。', prompt: '中景 / 固定机位\n光线:台灯暖光 + 屏幕冷光\n演员:林夕(疲倦状态)\n关键道具:面膜盒(从抽屉露半角)\n氛围:午夜、安静、些许焦虑', adopted: 0, versions: [{ ts: '14:02', label: 'v1' }] },
{ id: 'sc2', name: '场 2 · 面膜包装/特写', time: '15-30s', desc: '女主从抽屉拿出补水面膜,包装盒微距特写 → 面膜布展开 → 30g 精华液滴落慢镜。', prompt: '特写 / 微距推镜\n光线:柔和暖光\n关键道具:面膜布、精华液\n节奏:慢镜头 + 水滴回弹', adopted: 0, versions: [{ ts: '14:08', label: 'v1' }, { ts: '14:21', label: 'v2' }] },
{ id: 'sc3', name: '场 3 · 化妆台/产品定格', time: '30-45s', desc: '第二天早上,女主对镜化妆,皮肤透亮。淡入产品定格大图 + 价格标签 ¥39.9。', prompt: '中景 / 定格\n光线:晨光 + 暖色滤镜\n演员:林夕(精致妆面)\n结尾:产品大图 + 价格 + 购物车浮动', adopted: 0, versions: [{ ts: '14:30', label: 'v1' }] },
];
let curId = scenes[0].id;
function renderRow() {
const row = document.getElementById('sb-scenes-row');
if (!row) return;
row.innerHTML = scenes.map(s => `<div class="sb-scene-thumb${s.id === curId ? ' selected' : ''}" data-sid="${s.id}">
<div class="placeholder"><span class="ph-frame">${s.name.split(' · ')[1] || s.name}</span></div>
<div class="nm">${s.name.split(' · ')[0]}</div>
<div class="sub">${s.time}</div>
</div>`).join('');
row.querySelectorAll('.sb-scene-thumb').forEach(t => {
t.addEventListener('click', () => { curId = t.dataset.sid; renderAll(); });
});
}
function renderMain() {
const s = scenes.find(x => x.id === curId); if (!s) return;
const v = s.versions[s.adopted];
document.getElementById('sb-main-img').innerHTML = `<span class="ph-frame">${s.name} · ${v.label}</span>`;
document.getElementById('sb-side-scene').textContent = s.name.split(' · ')[0];
document.getElementById('sb-prompt-edit').textContent = s.prompt;
// history
const ct = document.getElementById('sb-history-ct');
const hist = document.getElementById('sb-history-row');
ct.textContent = s.versions.length;
if (s.versions.length === 0) {
hist.innerHTML = '<div style="font-size: 11.5px; color: var(--black-alpha-48); padding: 12px 4px;">// 暂无历史版本</div>';
} else {
hist.innerHTML = s.versions.map((vv, i) => `<div class="sb-history-thumb${i === s.adopted ? ' current' : ''}" data-vi="${i}">
<div class="placeholder"><span class="ph-frame">${vv.label}</span></div>
<div class="ts">${vv.ts}</div>
</div>`).join('');
hist.querySelectorAll('.sb-history-thumb').forEach(t => {
t.addEventListener('click', () => {
s.adopted = +t.dataset.vi;
renderMain();
Shell.toast('已切换至 ' + s.versions[s.adopted].label, s.name);
});
});
}
}
function renderAll() { renderRow(); renderMain(); }
function init() {
renderAll();
document.getElementById('sb-rerun-btn')?.addEventListener('click', () => {
const s = scenes.find(x => x.id === curId);
const v = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (s.versions.length + 1) };
s.versions.push(v);
s.adopted = s.versions.length - 1;
Shell.toast('整张重跑', s.name + ' · ' + v.label);
renderAll();
});
document.getElementById('sb-apply-btn')?.addEventListener('click', () => {
const s = scenes.find(x => x.id === curId);
Shell.toast('已应用 ' + s.versions[s.adopted].label, s.name + ' · 进入视频生成');
});
}
return { init };
})();
Stage3.init();
/* ============================================================
STAGE 4 · 视频 · 详情 modal(大图 + 历史 + 采用)
============================================================ */
const Stage4 = (function () {
const VIDEOS = {
'v1': { title: '场 1 · 深夜办公桌', time: '0-15s', info: [['场次', '场 1'], ['时长', '15.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:32', label: 'v1' }, { ts: '14:48', label: 'v2' }, { ts: '15:02', label: 'v3' }], adopted: 2 },
'v2': { title: '场 2 · 面膜包装/特写', time: '15-27s', info: [['场次', '场 2'], ['时长', '12.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:35', label: 'v1' }, { ts: '14:52', label: 'v2' }], adopted: 1 },
'v3': { title: '场 3 · 化妆台/产品定格', time: '27-40s', info: [['场次', '场 3'], ['时长', '13.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:40', label: 'v1' }], adopted: 0 },
};
let curVid = null;
function openDetail(id) {
const v = VIDEOS[id]; if (!v) return;
curVid = id;
document.getElementById('vd-title').textContent = v.title;
document.getElementById('vd-sub').textContent = '// ' + v.title + ' · ' + v.time;
const cur = v.versions[v.adopted];
document.getElementById('vd-main-img').innerHTML = `<span class="ph-frame">${v.title} · ${cur.label}</span>`;
document.getElementById('vd-info').innerHTML = v.info.map(([k, val]) => `<div class="row"><span class="k">${k}</span><span class="v">${val}</span></div>`).join('') + `<div class="row"><span class="k">当前展示</span><span class="v" style="color: var(--heat); font-weight:600;">${cur.label} · ${cur.ts}</span></div>`;
document.getElementById('vd-history-ct').textContent = v.versions.length;
const row = document.getElementById('vd-history-row');
row.innerHTML = v.versions.map((vv, i) => `<div class="vd-history-thumb${i === v.adopted ? ' current adopted' : ''}" data-vi="${i}">
<div class="placeholder"><span class="ph-frame">${vv.label}</span></div>
<div class="ts">${vv.ts}</div>
</div>`).join('');
row.querySelectorAll('.vd-history-thumb').forEach(t => {
t.addEventListener('click', () => {
v.adopted = +t.dataset.vi;
openDetail(id);
});
});
document.getElementById('video-detail-modal').classList.add('show');
}
function init() {
// 根据各场实际时长(data-duration)计算总时长 + 单场平均(用于「每场 Seedance 约 n 秒」)
const cards = document.querySelectorAll('.video-card[data-duration]');
if (cards.length) {
let total = 0;
cards.forEach(c => { total += Number(c.dataset.duration) || 0; });
const avg = Math.round(total / cards.length);
const avgEl = document.getElementById('seedance-avg');
const totalEl = document.getElementById('seedance-total');
if (avgEl) avgEl.textContent = String(avg);
if (totalEl) totalEl.textContent = String(total);
}
document.querySelectorAll('.video-card[data-video-id]').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('[data-vstop]')) return;
openDetail(card.dataset.videoId);
});
});
document.getElementById('vd-adopt-btn')?.addEventListener('click', () => {
if (!curVid) return;
const v = VIDEOS[curVid];
Shell.toast('已采用 ' + v.versions[v.adopted].label, v.title + ' · 拼接将用此版');
document.getElementById('video-detail-modal').classList.remove('show');
});
document.getElementById('vd-regen-btn')?.addEventListener('click', () => {
if (!curVid) return;
const v = VIDEOS[curVid];
const nv = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (v.versions.length + 1) };
v.versions.push(nv);
v.adopted = v.versions.length - 1;
Shell.toast('重跑中', v.title + ' · 约 30s');
openDetail(curVid);
});
}
return { init };
})();
Stage4.init();
</script>
</body>
</html>