1738 lines
111 KiB
HTML
1738 lines
111 KiB
HTML
<!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', '好,请给我一句话主题(5–30 字),例如:<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, '<'));
|
||
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>
|