All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
5752 lines
319 KiB
HTML
5752 lines
319 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title id="page-title">流水线 · Airshelf</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
|
||
<style>
|
||
/* ─── 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; }
|
||
|
||
/* ─── 顶部胶囊式 Stage 状态 · 注入到 .topbar 中部 ─── */
|
||
.topbar { position: relative; } /* 锚定 pill */
|
||
.pipeline-topbar-left {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
max-width: min(36vw, 520px);
|
||
}
|
||
.pipeline-back {
|
||
height: 34px;
|
||
padding: 0 13px 0 11px;
|
||
border-radius: var(--r-pill);
|
||
flex: 0 0 auto;
|
||
}
|
||
.pipeline-back svg { width: 14px; height: 14px; }
|
||
.pipeline-topbar-title {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 13.5px;
|
||
font-weight: 500;
|
||
color: var(--accent-black);
|
||
}
|
||
.pipeline-topbar-title .mono {
|
||
margin-left: 8px;
|
||
font-size: 10.5px;
|
||
font-weight: 400;
|
||
letter-spacing: .04em;
|
||
color: var(--black-alpha-48);
|
||
}
|
||
@media (max-width: 1500px) {
|
||
.pipeline-topbar-title { display: none; }
|
||
}
|
||
.stage-pill {
|
||
position: absolute; left: 50%; top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: inline-flex; align-items: center; gap: 0;
|
||
padding: 6px 16px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-pill);
|
||
z-index: 3;
|
||
}
|
||
.stage-pill .sp-dot {
|
||
position: relative;
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 2px 8px;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
border-radius: var(--r-sm);
|
||
transition: background var(--t-base);
|
||
}
|
||
.stage-pill .sp-dot:hover { background: var(--background-lighter); }
|
||
/* 圆点本体 · 默认(待开始):浅灰实心 · 对齐 .prog span 默认 */
|
||
.stage-pill .sp-dot .d {
|
||
width: 10px; height: 10px; border-radius: 50%;
|
||
background: var(--black-alpha-8);
|
||
border: 1.5px solid transparent;
|
||
transition: background var(--t-base), border-color var(--t-base), box-shadow var(--t-base);
|
||
}
|
||
/* 文字标签 · 始终显示 */
|
||
.stage-pill .sp-dot .l {
|
||
font-size: 12px; color: var(--black-alpha-56);
|
||
font-weight: 500; letter-spacing: .01em;
|
||
white-space: nowrap;
|
||
transition: color var(--t-base);
|
||
}
|
||
.stage-pill .sp-dot:hover .l { color: var(--accent-black); }
|
||
/* done · 森林绿 · 对齐 .prog span.done */
|
||
.stage-pill .sp-dot.done .d {
|
||
background: var(--accent-forest);
|
||
border-color: var(--accent-forest);
|
||
}
|
||
.stage-pill .sp-dot.done .l { color: var(--accent-black); }
|
||
/* active · 主橙 + 光晕 + 脉动 · 对齐 .prog span.cur(单橙锚点) */
|
||
.stage-pill .sp-dot.active .d {
|
||
background: var(--heat);
|
||
border-color: var(--heat);
|
||
box-shadow: 0 0 0 3px var(--heat-12);
|
||
animation: prog-pulse 1.4s ease-in-out infinite;
|
||
}
|
||
.stage-pill .sp-dot.active .l { color: var(--heat); font-weight: 600; }
|
||
/* fail · crimson · 对齐 .prog span.fail */
|
||
.stage-pill .sp-dot.fail .d {
|
||
background: var(--accent-crimson);
|
||
border-color: var(--accent-crimson);
|
||
}
|
||
.stage-pill .sp-dot.fail .l { color: var(--accent-crimson); }
|
||
/* 连接线 · 对齐 .prog 色卡 */
|
||
.stage-pill .sp-line {
|
||
width: 14px; height: 1.5px;
|
||
background: var(--black-alpha-8);
|
||
transition: background var(--t-base);
|
||
}
|
||
.stage-pill .sp-line.done { background: var(--accent-forest); }
|
||
/* anchor 节点本身只承载初始模板,挂入 topbar 后变 .stage-pill */
|
||
.stage-pill-anchor[hidden] { display: none; }
|
||
|
||
/* ─── 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-foot .hstack { gap: 10px; align-items: center; }
|
||
.stage-foot .btn {
|
||
height: 40px;
|
||
min-height: 40px;
|
||
padding: 0 18px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
font-size: 13.5px;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
}
|
||
.stage-foot .btn-primary { padding: 0 20px; font-weight: 600; }
|
||
.stage-foot .btn svg { width: 14px; height: 14px; flex: 0 0 14px; }
|
||
|
||
/* ─── 全高度布局 · 除 Stage 2 外,操作模块 hug content、内容区域 fill content ─── */
|
||
/* JS 在 activateStage(1/3/4/5) 时给 .content 加 .content--fh,Stage 2 留常规文档流 */
|
||
.content.content--fh {
|
||
display: flex; flex-direction: column;
|
||
overflow: hidden; /* 内部 .stage 接管滚动 */
|
||
padding: 24px 28px 20px; /* 默认上下 padding · 让 stage-foot 真正贴近视口底部 */
|
||
}
|
||
/* flat 模式 · 像 model-photo 那样的双区扁平布局 · 取消卡片,全宽贴边 */
|
||
.content.content--fh-flat {
|
||
padding: 0;
|
||
}
|
||
/* flat 模式下 · 取消 .pane 卡片描边/圆角(Stage 1 两侧面板) */
|
||
.content--fh-flat .stage.active > .stage-script .pane {
|
||
background: var(--surface);
|
||
border: 0; border-radius: 0;
|
||
}
|
||
/* 左/右 pane 白底贴边 · 仅内部元素对齐到 topbar 28px 版心 */
|
||
/* Stage 1 · 镜头脚本 + 脚本助手 */
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.shot-list > .pane-h,
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.shot-list > .shots-body {
|
||
padding-left: 28px;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.chat-pane > .pane-h,
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.chat-pane > .chat-body,
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.chat-pane > .chat-input {
|
||
padding-left: 28px;
|
||
padding-right: 28px;
|
||
}
|
||
/* Stage 1 · flat 模式去除分割线:两侧 pane-h 底线 · gutter 竖线 · chat-input 顶线 */
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.shot-list > .pane-h,
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.chat-pane > .pane-h {
|
||
border-bottom: 0;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .pane.chat-pane > .chat-input {
|
||
border-top: 0;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .stage-script-gutter::after {
|
||
background: transparent;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .stage-script-gutter:hover::after,
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script > .stage-script-gutter.dragging::after {
|
||
background: var(--heat);
|
||
}
|
||
/* Stage 2/3 的内部对齐规则放到对应 flat 块里(避免被后面规则吃掉) */
|
||
/* Stage 5 · editor-preview 左 · editor-props 右 · timeline 两侧 */
|
||
.content--fh-flat .stage[data-stage-pane="5"].active > .editor > .editor-preview {
|
||
padding-left: 28px;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="5"].active > .editor > .editor-props {
|
||
padding-right: 28px;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="5"].active > .editor > .timeline {
|
||
padding-left: 28px;
|
||
padding-right: 28px;
|
||
}
|
||
/* 镜头脚本 | 拖拽分隔条 | 脚本助手 三列布局 · 分隔条可拖动 */
|
||
.content--fh-flat .stage[data-stage-pane="1"].active > .stage-script {
|
||
gap: 0;
|
||
grid-template-columns: minmax(0, 1fr) 6px var(--chat-w, 520px);
|
||
}
|
||
/* 拖拽 gutter · 默认 1px 灰线,hover/拖中加重 · 鼠标 col-resize */
|
||
.stage-script-gutter {
|
||
position: relative;
|
||
background: transparent;
|
||
cursor: col-resize;
|
||
transition: background var(--t-base);
|
||
}
|
||
.stage-script-gutter::after {
|
||
content: '';
|
||
position: absolute; top: 0; bottom: 0; left: 50%;
|
||
width: 1px; transform: translateX(-50%);
|
||
background: var(--border-faint);
|
||
transition: background var(--t-base), width var(--t-base);
|
||
}
|
||
.stage-script-gutter:hover::after,
|
||
.stage-script-gutter.dragging::after {
|
||
background: var(--heat);
|
||
width: 2px;
|
||
}
|
||
.stage-script-gutter.dragging { background: var(--heat-12); }
|
||
/* flat 模式 · stage-foot 改为全宽 flat 底栏:白底 + border-top,不再像卡片下沿 */
|
||
.content--fh-flat .stage.active > .stage-foot {
|
||
margin-top: 0;
|
||
padding: 14px 28px;
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border-faint);
|
||
}
|
||
|
||
/* ─── Stage 3 flat · 故事板双区:左 canvas | 右 side,用 border-right 分隔 ─── */
|
||
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard {
|
||
gap: 0;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-canvas {
|
||
border: 0; border-radius: 0;
|
||
background: var(--surface);
|
||
padding: 18px 14px 18px 28px;
|
||
align-items: center;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-canvas > .sb-main-img {
|
||
width: 100%;
|
||
}
|
||
/* sb-side · 撑满网格行高 · 内 .pane fill content + 内部可滚 */
|
||
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-side {
|
||
display: flex; flex-direction: column; min-height: 0;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="3"].active > .stage-storyboard > .sb-side > .pane {
|
||
flex: 1 1 0; min-height: 0; overflow-y: auto;
|
||
border: 0; border-radius: 0;
|
||
background: var(--surface);
|
||
padding: 18px 28px;
|
||
}
|
||
/* 解除 sb-scenes-col 560 上限,跟随父级填满 */
|
||
.content--fh-flat .stage[data-stage-pane="3"].active .sb-scenes-col { max-height: none; }
|
||
|
||
/* ─── Stage 2 flat · 左 200 资产侧栏 | 右 内容,用 border-right 分隔 ─── */
|
||
.content--fh-flat .stage[data-stage-pane="2"].active > .stage-assets {
|
||
gap: 0;
|
||
height: 100%;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="2"].active > .stage-assets > .asset-side {
|
||
position: static; align-self: stretch;
|
||
padding: 18px 16px;
|
||
background: var(--background-base);
|
||
overflow-y: auto;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="2"].active > .stage-assets > .asset-main {
|
||
padding: 18px 28px;
|
||
overflow-y: auto;
|
||
background: var(--background-base);
|
||
}
|
||
|
||
/* ─── Stage 4 flat · 顶部 queue-bar 改 toolbar · 视频卡片网格区 ─── */
|
||
.content--fh-flat .stage[data-stage-pane="4"].active > .queue-bar {
|
||
border: 0; border-radius: 0;
|
||
border-bottom: 1px solid var(--border-faint);
|
||
margin: 0;
|
||
padding: 14px 28px;
|
||
}
|
||
.content--fh-flat .stage[data-stage-pane="4"].active > .video-grid {
|
||
padding: 18px 28px;
|
||
background: var(--background-base);
|
||
}
|
||
|
||
/* ─── Stage 5 flat · 编辑器外壳去卡片 ─── */
|
||
.content--fh-flat .stage[data-stage-pane="5"].active > .editor {
|
||
border: 0; border-radius: 0;
|
||
}
|
||
|
||
.content--fh .stage.active {
|
||
display: flex; flex-direction: column;
|
||
flex: 1 1 auto; min-height: 0;
|
||
}
|
||
/* 内容主体(stage-foot 之上的最后一个块)· fill content */
|
||
.content--fh .stage[data-stage-pane="1"].active > .stage-script,
|
||
.content--fh .stage[data-stage-pane="2"].active > .stage-assets,
|
||
.content--fh .stage[data-stage-pane="3"].active > .stage-storyboard,
|
||
.content--fh .stage[data-stage-pane="4"].active > .video-grid,
|
||
.content--fh .stage[data-stage-pane="5"].active > .editor {
|
||
flex: 1 1 0; min-height: 0;
|
||
}
|
||
/* Stage 1 / 2 主体不整体滚,把滚动交给左右栏各自的 body */
|
||
.content--fh .stage[data-stage-pane="1"].active > .stage-script,
|
||
.content--fh .stage[data-stage-pane="2"].active > .stage-assets { overflow: hidden; }
|
||
/* 其他 stage 主体仍可整体滚动 */
|
||
.content--fh .stage[data-stage-pane="3"].active > .stage-storyboard,
|
||
.content--fh .stage[data-stage-pane="4"].active > .video-grid,
|
||
.content--fh .stage[data-stage-pane="5"].active > .editor { overflow-y: auto; }
|
||
/* stage-foot · hug content:高度只由按钮决定,不拉伸、不压缩 */
|
||
.content--fh .stage.active > .stage-foot { flex: 0 0 auto; }
|
||
/* Stage 1 脚本网格:取消固定 min-height,允许 flex 接管 */
|
||
.content--fh .stage[data-stage-pane="1"].active > .stage-script { min-height: 0; }
|
||
/* Stage 1 内部 panes:左右栏各自独立滚动 */
|
||
.content--fh .stage[data-stage-pane="1"].active > .stage-script > .shot-list,
|
||
.content--fh .stage[data-stage-pane="1"].active > .stage-script > .chat-pane {
|
||
min-height: 0; min-width: 0;
|
||
}
|
||
.content--fh .stage[data-stage-pane="1"].active .shots-body,
|
||
.content--fh .stage[data-stage-pane="1"].active .chat-body {
|
||
max-height: none;
|
||
flex: 1 1 0; min-height: 0; overflow-y: auto;
|
||
}
|
||
/* Stage 5 编辑器:解除固定 580px 高,让 flex 接管 */
|
||
.content--fh .stage[data-stage-pane="5"].active > .editor { height: auto; }
|
||
|
||
/* === 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; flex-shrink: 0; 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 18px; border-top: 1px solid var(--border-faint); }
|
||
.chat-input-card {
|
||
background: var(--background-base);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
padding: 12px 14px 10px;
|
||
transition: border-color var(--t-base), box-shadow var(--t-base);
|
||
}
|
||
.chat-input-card:focus-within { border-color: var(--accent-black); box-shadow: 0 0 0 3px rgba(0,0,0,.04); }
|
||
.chat-input-area {
|
||
width: 100%; border: none; outline: none; background: transparent;
|
||
font-family: var(--font-sans); font-size: 13px; color: var(--accent-black);
|
||
line-height: 1.55; resize: none; padding: 0; min-height: 42px;
|
||
}
|
||
.chat-input-area::placeholder { color: var(--black-alpha-40); }
|
||
.chat-input-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
||
.chat-input-foot .hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-40); letter-spacing: .02em; }
|
||
.chat-input-foot .spacer { flex: 1; }
|
||
.chat-icon-btn {
|
||
width: 28px; height: 28px; display: grid; place-items: center;
|
||
background: transparent; border: 1px solid var(--border-faint);
|
||
border-radius: 50%; color: var(--black-alpha-56); cursor: pointer;
|
||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||
}
|
||
.chat-icon-btn:hover { border-color: var(--accent-black); color: var(--accent-black); }
|
||
.chat-send-btn {
|
||
width: 32px; height: 32px; display: grid; place-items: center;
|
||
background: var(--accent-black); border: 1px solid var(--accent-black);
|
||
border-radius: 50%; color: var(--accent-white); cursor: pointer;
|
||
transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);
|
||
}
|
||
.chat-send-btn:hover { background: var(--heat); border-color: var(--heat); }
|
||
.chat-send-btn:active { transform: scale(.95); }
|
||
.chat-send-btn:disabled { background: var(--black-alpha-12); border-color: var(--black-alpha-12); color: var(--black-alpha-40); cursor: not-allowed; transform: none; }
|
||
.chat-attach-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||
.chat-attach-chip {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
padding: 3px 6px 3px 8px; background: var(--surface);
|
||
border: 1px solid var(--border-faint); border-radius: var(--r-sm);
|
||
font-family: var(--font-mono); font-size: 11px; color: var(--accent-black);
|
||
}
|
||
.chat-attach-chip .x { width: 14px; height: 14px; display: grid; place-items: center; background: transparent; border: none; color: var(--black-alpha-48); cursor: pointer; border-radius: 50%; }
|
||
.chat-attach-chip .x:hover { background: var(--black-alpha-08); color: var(--accent-black); }
|
||
|
||
.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 加分镜插槽 */
|
||
/* 镜头卡片间 hover 加分镜插槽 · 卡片移开一行 + 渐显按钮 */
|
||
.shot-insert-gap {
|
||
height: 10px;
|
||
position: relative;
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: 0;
|
||
transition: height .24s cubic-bezier(.18,.72,.28,1), padding .24s cubic-bezier(.18,.72,.28,1);
|
||
}
|
||
.shot-insert-gap:hover {
|
||
height: 72px;
|
||
padding: 14px 0;
|
||
}
|
||
.shot-insert-gap .add-shot-btn {
|
||
opacity: 0;
|
||
transform: translateY(4px) scale(.96);
|
||
height: 28px; padding: 0 14px;
|
||
background: var(--surface);
|
||
color: var(--heat);
|
||
border: 1px dashed var(--heat-40);
|
||
border-radius: var(--r-md);
|
||
font-size: 12.5px; font-family: inherit; font-weight: 500;
|
||
cursor: pointer;
|
||
transition: opacity .2s ease .04s, transform .24s cubic-bezier(.18,.72,.28,1) .04s, background var(--t-base), border-color var(--t-base), color var(--t-base);
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
pointer-events: none;
|
||
white-space: nowrap;
|
||
}
|
||
.shot-insert-gap .add-shot-btn svg { width: 12px; height: 12px; }
|
||
.shot-insert-gap:hover .add-shot-btn {
|
||
opacity: 1; transform: translateY(0) scale(1);
|
||
pointer-events: auto;
|
||
}
|
||
.shot-insert-gap .add-shot-btn:hover {
|
||
background: var(--heat-12);
|
||
border-style: solid;
|
||
border-color: var(--heat);
|
||
color: var(--heat);
|
||
}
|
||
|
||
/* 镜头脚本顶栏 · 自动从脚本抓取的人物/场景标签 · 可编辑/删除/添加 */
|
||
.shot-list > .pane-h { flex-wrap: wrap; row-gap: 8px; }
|
||
.shot-headline { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
|
||
.script-brief-summary { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
|
||
.script-brief-pill { gap: 4px; padding: 3px 8px; font-size: 11px; }
|
||
.script-brief-pill .k { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
.script-brief-pill .v { color: var(--accent-black); max-width: 116px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.script-tags { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 14px; margin-left: 6px; }
|
||
.script-tags .tag-group { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.script-tags .tg-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; flex-shrink: 0; }
|
||
.script-tags .script-tag { display: inline-flex; align-items: center; gap: 2px; padding: 2px 2px 2px 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: 999px; font-size: 11.5px; color: var(--accent-black); transition: border-color var(--t-base), background var(--t-base); }
|
||
.script-tags .script-tag:hover { border-color: var(--heat-40); background: var(--heat-12); }
|
||
.script-tags .script-tag .t { outline: none; padding: 0 4px; border-radius: 3px; min-width: 8px; cursor: text; }
|
||
.script-tags .script-tag .t:focus { background: var(--surface); box-shadow: inset 0 0 0 1px var(--heat); }
|
||
.script-tags .script-tag .x { width: 16px; height: 16px; display: grid; place-items: center; background: transparent; border: 0; color: var(--black-alpha-40); border-radius: 50%; cursor: pointer; font-size: 13px; line-height: 1; transition: background var(--t-base), color var(--t-base); }
|
||
.script-tags .script-tag .x:hover { background: var(--black-alpha-08); color: var(--accent-crimson); }
|
||
.script-tags .tag-add { width: 20px; height: 20px; display: grid; place-items: center; background: transparent; border: 1px dashed var(--black-alpha-24); border-radius: 50%; color: var(--black-alpha-48); cursor: pointer; font-size: 13px; line-height: 1; padding: 0; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }
|
||
.script-tags .tag-add:hover { border-style: solid; border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||
|
||
/* 镜头脚本空缺省态 */
|
||
.shots-empty { padding: 36px 24px; margin: auto; 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; margin: auto; 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; }
|
||
.script-brief-card { margin-top: 8px; padding: 12px; background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: flex; flex-direction: column; gap: 10px; }
|
||
.script-brief-row { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 10px; row-gap: 4px; align-items: center; }
|
||
.script-brief-row .k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
.script-brief-row .why { grid-column: 2 / 3; font-size: 11.5px; color: var(--black-alpha-48); line-height: 1.5; }
|
||
.script-brief-select { position: relative; display: inline-flex; width: 100%; min-width: 0; }
|
||
.script-brief-value {
|
||
width: 100%;
|
||
min-width: 0;
|
||
height: 36px;
|
||
padding: 0 12px;
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
background: var(--surface);
|
||
color: var(--accent-black);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
font-family: inherit;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
|
||
}
|
||
.script-brief-value .v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
|
||
.script-brief-value::after {
|
||
content: '';
|
||
width: 5px;
|
||
height: 5px;
|
||
border-right: 1px solid currentColor;
|
||
border-bottom: 1px solid currentColor;
|
||
transform: rotate(45deg) translateY(-1px);
|
||
transition: transform var(--t-base);
|
||
color: var(--black-alpha-48);
|
||
flex-shrink: 0;
|
||
}
|
||
.script-brief-value:hover {
|
||
background: var(--heat-12);
|
||
border-color: var(--heat-20);
|
||
color: var(--heat);
|
||
}
|
||
.script-brief-select.open .script-brief-value {
|
||
background: var(--heat-12);
|
||
border-color: var(--heat);
|
||
color: var(--heat);
|
||
}
|
||
.script-brief-select.open .script-brief-value::after {
|
||
color: var(--heat);
|
||
transform: rotate(225deg) translate(-1px, -1px);
|
||
}
|
||
.script-brief-select.open .chip-menu { display: block; }
|
||
.script-brief-select .chip-menu { min-width: 168px; right: 0; left: auto; z-index: 80; }
|
||
.script-brief-select .chip-menu .mi { width: 100%; border: 0; background: transparent; font-family: inherit; text-align: left; }
|
||
.script-brief-actions { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 10px; align-items: center; padding-top: 2px; }
|
||
.script-brief-actions .action-row { grid-column: 2 / 3; display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
||
|
||
/* 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; }
|
||
/* 三视图卡固定 360 高;商品卡同高,宽度按 3:5 比例反推(≈216px),内部元素 flex 自适应 */
|
||
.prod-row > .asset-card-2 {
|
||
flex: 0 0 auto;
|
||
width: auto; max-width: none; min-width: 0;
|
||
height: 360px;
|
||
aspect-ratio: 3 / 5;
|
||
}
|
||
.prod-preview { flex: 0 0 360px; height: 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; }
|
||
/* 三视图历史版本缩略图 strip */
|
||
.prod-preview-history { display: none; flex-direction: column; gap: 6px; }
|
||
.prod-preview-history.show { display: flex; }
|
||
.prod-preview-history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
.prod-preview-history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }
|
||
.prod-preview-history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; scrollbar-width: thin; }
|
||
.prod-preview-history .h-row::-webkit-scrollbar { height: 4px; }
|
||
.prod-preview-history .h-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
|
||
.prod-preview-history .h-thumb {
|
||
flex: 0 0 auto;
|
||
width: 72px; aspect-ratio: 16/9;
|
||
background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);
|
||
position: relative; cursor: pointer; transition: border-color var(--t-base), transform var(--t-base);
|
||
display: grid; place-items: center; overflow: hidden;
|
||
}
|
||
.prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }
|
||
/* 已采用版本:主橙描边 + 「已采用」徽标 */
|
||
.prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
|
||
/* 仅预览(未采用):黑色描边,无徽标 */
|
||
.prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }
|
||
.prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
|
||
.prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }
|
||
.prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }
|
||
.prod-preview-history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
|
||
.prod-preview-history .h-thumb.adopted .badge { display: block; }
|
||
|
||
/* 「已采用」状态 · 浅橙 + 主橙文字,与已采用徽标视觉呼应 */
|
||
#prod-preview-adopt:disabled,
|
||
#prod-preview-adopt:disabled:hover {
|
||
color: var(--heat);
|
||
border-color: var(--heat-40);
|
||
background: var(--heat-12);
|
||
cursor: not-allowed;
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 主图可点击放大 */
|
||
.prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }
|
||
.prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }
|
||
.prod-preview-img.is-zoomable::after {
|
||
content: '';
|
||
position: absolute; top: 8px; right: 8px;
|
||
width: 22px; height: 22px;
|
||
background: rgba(21,20,15,.72) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='M21 21l-4.3-4.3M8 11h6M11 8v6'/></svg>") center/14px no-repeat;
|
||
border-radius: var(--r-sm);
|
||
opacity: 0; transition: opacity var(--t-base);
|
||
pointer-events: none;
|
||
}
|
||
.prod-preview-img.is-zoomable:hover::after { opacity: 1; }
|
||
|
||
/* 三视图放大查看 lightbox */
|
||
#tri-lightbox-bg { z-index: 80; }
|
||
#tri-lightbox-bg .tri-lightbox {
|
||
position: relative;
|
||
width: min(1100px, 92vw);
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
padding: 18px 20px 20px;
|
||
display: flex; flex-direction: column; gap: 12px;
|
||
box-shadow: 0 24px 64px rgba(0,0,0,.24);
|
||
}
|
||
.tri-lightbox-head {
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-family: var(--font-mono);
|
||
font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
|
||
color: var(--black-alpha-56);
|
||
padding-right: 32px;
|
||
}
|
||
.tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }
|
||
.tri-lightbox-head .lb-tag {
|
||
margin-left: 6px;
|
||
padding: 2px 6px;
|
||
background: var(--heat-12); color: var(--heat);
|
||
border-radius: 3px;
|
||
font-size: 10px;
|
||
}
|
||
.tri-lightbox-close {
|
||
position: absolute;
|
||
top: 12px; right: 12px;
|
||
width: 28px; height: 28px;
|
||
display: grid; place-items: center;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
color: var(--black-alpha-56);
|
||
cursor: pointer;
|
||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||
z-index: 2;
|
||
}
|
||
.tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }
|
||
.tri-lightbox-close svg { width: 14px; height: 14px; }
|
||
.tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }
|
||
.tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
|
||
.tri-lightbox-foot .spc { flex: 1; }
|
||
.tri-lightbox-foot kbd {
|
||
display: inline-block;
|
||
padding: 1px 5px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-bottom-width: 2px;
|
||
border-radius: 3px;
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
color: var(--black-alpha-72);
|
||
}
|
||
.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); overflow: hidden; display: flex; flex-direction: column; }
|
||
.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:disabled,
|
||
.asset-card-2 .body-2 .btn.disabled {
|
||
background: transparent;
|
||
border-color: transparent;
|
||
color: var(--black-alpha-32);
|
||
box-shadow: none;
|
||
cursor: not-allowed;
|
||
opacity: .72;
|
||
transform: none;
|
||
}
|
||
.asset-card-2 .body-2 .btn:disabled:hover,
|
||
.asset-card-2 .body-2 .btn.disabled:hover {
|
||
background: transparent;
|
||
border-color: transparent;
|
||
color: var(--black-alpha-32);
|
||
box-shadow: none;
|
||
transform: none;
|
||
}
|
||
.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); }
|
||
|
||
/* stage2 商品卡 · 与商品库 .product-card 视觉一致 */
|
||
.asset-card-2.prod-lib-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
|
||
/* 商品图 · 占据卡片剩余高度(fill);宽度 stretch 到卡片宽 */
|
||
.asset-card-2.prod-lib-card .prod-thumb { flex: 1 1 0; min-height: 0; position: relative; aspect-ratio: auto; }
|
||
.asset-card-2.prod-lib-card .prod-body { padding: 14px 14px 12px; flex: 0 0 auto; }
|
||
.asset-card-2.prod-lib-card .prod-name {
|
||
font-size: 14px; font-weight: 600;
|
||
color: var(--accent-black);
|
||
line-height: 1.3;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-cat {
|
||
display: inline-flex; align-items: center;
|
||
margin-top: 8px;
|
||
padding: 2px 8px;
|
||
background: var(--background-lighter);
|
||
color: var(--black-alpha-72);
|
||
border-radius: var(--r-sm);
|
||
font-size: 11.5px;
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-date {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--black-alpha-48);
|
||
margin-top: 10px;
|
||
letter-spacing: .02em;
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-footer {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
align-items: center;
|
||
column-gap: 8px;
|
||
padding: 10px 12px;
|
||
border-top: 1px solid var(--border-faint);
|
||
font-size: 11.5px;
|
||
color: var(--black-alpha-56);
|
||
background: var(--background-base);
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-footer .stat {
|
||
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
||
padding: 3px 8px;
|
||
border-radius: var(--r-sm);
|
||
font-family: var(--font-mono);
|
||
letter-spacing: .02em;
|
||
white-space: nowrap;
|
||
justify-self: center;
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-footer .stat svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
|
||
.asset-card-2.prod-lib-card .prod-footer .stat b { color: var(--accent-black); font-weight: 600; }
|
||
.asset-card-2.prod-lib-card .prod-footer .sep { color: var(--black-alpha-24); font-family: var(--font-mono); flex-shrink: 0; }
|
||
.asset-card-2.prod-lib-card .prod-action {
|
||
padding: 10px 12px;
|
||
border-top: 1px solid var(--border-faint);
|
||
background: var(--surface);
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-action[hidden] { display: none; }
|
||
.asset-card-2.prod-lib-card .prod-action .btn-aigen {
|
||
width: 100%;
|
||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||
height: 34px; padding: 0 14px;
|
||
background: var(--heat);
|
||
color: var(--accent-white);
|
||
border: 1px solid var(--heat);
|
||
border-radius: var(--r-sm);
|
||
font-size: 13px; font-weight: 500;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
box-shadow:
|
||
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
|
||
0 1px 1px rgba(250, 93, 25, 0.12),
|
||
0 2px 4px rgba(250, 93, 25, 0.10);
|
||
transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-action .btn-aigen:hover {
|
||
background: #FB6E2E;
|
||
box-shadow:
|
||
inset 0 -2px 4px rgba(250, 93, 25, 0.24),
|
||
0 2px 4px rgba(250, 93, 25, 0.20),
|
||
0 4px 12px rgba(250, 93, 25, 0.18);
|
||
transform: translateY(-1px);
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-action .btn-aigen:active { transform: translateY(0); }
|
||
.asset-card-2.prod-lib-card .prod-action .btn-aigen:disabled {
|
||
opacity: .65; cursor: not-allowed; transform: none;
|
||
box-shadow: inset 0 -2px 4px rgba(250, 93, 25, 0.20);
|
||
}
|
||
.asset-card-2.prod-lib-card .prod-action .btn-aigen .ai-spark {
|
||
width: 14px; height: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 通用资产详情 modal · 参考布局 v2 */
|
||
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1010; 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(1040px, 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 .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.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: 20px 24px 24px; overflow-y: auto; flex: 1; }
|
||
.asset-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }
|
||
/* 左栏:大立绘 + 缩略 */
|
||
.asset-detail-lead { display: flex; flex-direction: column; gap: 10px; }
|
||
.asset-detail-lead .ad-lead-wrap { position: relative; }
|
||
.asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); }
|
||
/* 查看大图 icon · 悬浮容器才显示 · 32×32 icon-only */
|
||
.ad-zoom-btn {
|
||
position: absolute; right: 8px; bottom: 8px;
|
||
width: 32px; height: 32px; padding: 0;
|
||
background: rgba(21,20,15,.7); color: #fff;
|
||
border: 0; border-radius: var(--r-sm);
|
||
display: grid; place-items: center;
|
||
cursor: pointer; backdrop-filter: blur(4px);
|
||
opacity: 0;
|
||
transition: opacity var(--t-base), background var(--t-base);
|
||
z-index: 3;
|
||
}
|
||
.ad-zoom-btn:hover { background: rgba(21,20,15,.92); }
|
||
.ad-zoom-btn svg { width: 14px; height: 14px; }
|
||
.asset-detail-lead .ad-lead-wrap:hover .ad-zoom-btn,
|
||
.asset-detail-tri-row .placeholder:hover .ad-zoom-btn { opacity: 1; }
|
||
.asset-detail-tri-row .placeholder { position: relative; }
|
||
.asset-detail-lead .ad-thumbs {
|
||
display: flex; gap: 8px;
|
||
}
|
||
.asset-detail-lead .ad-thumbs .thumb {
|
||
flex: 0 0 64px;
|
||
aspect-ratio: 3/4;
|
||
border-radius: var(--r-sm);
|
||
border: 1px solid var(--border-faint);
|
||
cursor: pointer; overflow: hidden;
|
||
transition: border-color var(--t-base);
|
||
}
|
||
.asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }
|
||
.asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }
|
||
|
||
/* 右栏 section 通用 */
|
||
.asset-detail-right .ad-section + .ad-section { margin-top: 18px; }
|
||
.asset-detail-section-h {
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-size: 13px; font-weight: 600; color: var(--accent-black);
|
||
margin-bottom: 10px;
|
||
}
|
||
.asset-detail-section-h .ic {
|
||
width: 14px; height: 14px;
|
||
color: var(--heat); flex-shrink: 0;
|
||
display: grid; place-items: center;
|
||
}
|
||
.asset-detail-section-h .ic svg { width: 14px; height: 14px; }
|
||
.asset-detail-section-h .ad-ratio-chip {
|
||
margin-left: auto;
|
||
font-family: var(--font-mono); font-size: 10.5px;
|
||
padding: 2px 8px; border-radius: var(--r-sm);
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
color: var(--black-alpha-56); letter-spacing: .02em;
|
||
}
|
||
.asset-detail-section-h .ad-icon-btn {
|
||
width: 28px; height: 28px;
|
||
display: grid; place-items: center;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
color: var(--black-alpha-56); cursor: pointer;
|
||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||
}
|
||
.asset-detail-section-h .ad-icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }
|
||
.asset-detail-section-h .ad-icon-btn svg { width: 12px; height: 12px; }
|
||
|
||
/* 三视图 — 始终单张 16:9 大图 (不分 3 张) */
|
||
.asset-detail-tri-row { margin-top: 0; }
|
||
.asset-detail-tri-row .placeholder { aspect-ratio: 16 / 9; border-radius: var(--r-md); }
|
||
/* 三视图 · 用户上传 历史版本 strip */
|
||
.md-view-versions { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }
|
||
.md-view-versions .v-thumb { flex: 0 0 auto; width: 72px; aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); position: relative; cursor: pointer; display: grid; place-items: center; overflow: hidden; transition: border-color var(--t-base); }
|
||
.md-view-versions .v-thumb:hover { border-color: var(--heat-40); }
|
||
.md-view-versions .v-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
|
||
.md-view-versions .v-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
|
||
.md-view-versions .v-thumb.active .v { color: var(--heat); font-weight: 600; }
|
||
.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); }
|
||
|
||
/* 简介文字 + 标签 */
|
||
.ad-intro {
|
||
font-size: 13px; line-height: 1.65;
|
||
color: var(--black-alpha-72);
|
||
margin: 0 0 12px;
|
||
}
|
||
.ad-tags {
|
||
display: flex; flex-wrap: wrap; gap: 8px;
|
||
}
|
||
.ad-tags .ad-tag-chip {
|
||
height: 26px; padding: 0 12px;
|
||
display: inline-flex; align-items: center;
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
font-size: 12px; color: var(--accent-black);
|
||
}
|
||
.ad-tags .ad-tag-add {
|
||
width: 26px; height: 26px;
|
||
display: grid; place-items: center;
|
||
background: var(--background-lighter);
|
||
border: 1px dashed var(--black-alpha-24);
|
||
border-radius: var(--r-sm);
|
||
color: var(--black-alpha-56); cursor: pointer;
|
||
transition: border-color var(--t-base), color var(--t-base);
|
||
}
|
||
.ad-tags .ad-tag-add:hover { border-color: var(--heat); color: var(--heat); }
|
||
.ad-tags .ad-tag-add svg { width: 12px; height: 12px; }
|
||
|
||
/* 属性表 · 3 列 × N 行 */
|
||
.ad-props {
|
||
margin-top: 18px;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
column-gap: 24px;
|
||
row-gap: 0;
|
||
border-top: 1px solid var(--border-faint);
|
||
padding-top: 16px;
|
||
}
|
||
.ad-props .ad-prop {
|
||
display: flex; align-items: baseline;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid var(--border-faint);
|
||
font-size: 12.5px;
|
||
min-height: 38px;
|
||
}
|
||
.ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }
|
||
.ad-props .ad-prop .k {
|
||
flex: 0 0 64px;
|
||
color: var(--black-alpha-56);
|
||
font-family: var(--font-mono); font-size: 11px;
|
||
}
|
||
.ad-props .ad-prop .v {
|
||
color: var(--accent-black);
|
||
font-weight: 500;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.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; }
|
||
|
||
/* footer · 左侧统计 + 右侧按钮 */
|
||
.asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }
|
||
.asset-modal-f .ad-foot-stats { display: flex; gap: 6px; margin-right: auto; }
|
||
.asset-modal-f .ad-stat-btn {
|
||
height: 32px; padding: 0 12px;
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
background: transparent;
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
color: var(--black-alpha-72);
|
||
font-size: 12.5px; font-family: inherit;
|
||
cursor: pointer;
|
||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||
}
|
||
.asset-modal-f .ad-stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
|
||
.asset-modal-f .ad-stat-btn svg { width: 13px; height: 13px; }
|
||
.asset-modal-f .ad-stat-btn b { color: var(--accent-black); font-weight: 600; }
|
||
|
||
/* 演员库 / 场景库 全屏弹窗(沿用 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; position: relative; }
|
||
.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; }
|
||
/* 卡片 · 视觉对齐 model-photo .model-card (padding 8 / gap 6 / 无 foot 行) */
|
||
.ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; }
|
||
.ml-card {
|
||
position: relative;
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
transition: background var(--t-base), border-color var(--t-base);
|
||
}
|
||
.ml-card:hover { background: var(--surface); }
|
||
.ml-card .placeholder { aspect-ratio: 3/4; border-radius: var(--r-sm); }
|
||
.ml-card .ml-card-nm { font-size: 12.5px; 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); letter-spacing: .02em; }
|
||
|
||
/* 「添加演员/场景」入口卡 · 与 model-photo 模特库视觉一致 */
|
||
.ml-card.ml-upload-card { border: 1.5px dashed var(--black-alpha-24); background: var(--surface); display: flex; flex-direction: column; gap: 8px; transition: border-color var(--t-base), background var(--t-base); }
|
||
.ml-card.ml-upload-card:hover { border-color: var(--heat); background: var(--heat-12); box-shadow: none; }
|
||
.ml-card.ml-upload-card:focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }
|
||
.ml-card.ml-upload-card .up-thumb { aspect-ratio: 3/4; border-radius: var(--r-sm); background: transparent; display: grid; place-items: center; }
|
||
.ml-card.ml-upload-card .up-plus { width: 44px; height: 44px; border-radius: 50%; background: var(--surface); border: 1px solid var(--black-alpha-12); color: var(--black-alpha-56); display: grid; place-items: center; transition: background var(--t-base), color var(--t-base), border-color var(--t-base), transform var(--t-base); }
|
||
.ml-card.ml-upload-card:hover .up-plus { background: var(--heat); border-color: var(--heat); color: var(--accent-white); transform: scale(1.06); }
|
||
.ml-card.ml-upload-card .up-plus svg { width: 22px; height: 22px; }
|
||
.ml-card.ml-upload-card .ml-card-nm { color: var(--accent-black); }
|
||
.ml-card.ml-upload-card:hover .ml-card-nm { color: var(--heat); }
|
||
|
||
/* ════════ 添加演员 / 场景 · 工作台画布 ════════ */
|
||
.ml-canvas {
|
||
position: absolute; inset: 0; z-index: 10;
|
||
background: var(--background-base);
|
||
display: flex; flex-direction: column;
|
||
opacity: 0; visibility: hidden;
|
||
transform: scale(.94); transform-origin: 32px 80px;
|
||
transition: opacity .28s ease, transform .32s cubic-bezier(.18,.72,.28,1), visibility .32s;
|
||
pointer-events: none;
|
||
}
|
||
.ml-canvas.show { opacity: 1; visibility: visible; transform: scale(1); pointer-events: auto; }
|
||
.ml-canvas-h { display: flex; align-items: center; gap: 12px; padding: 14px 28px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; background: var(--surface); }
|
||
.ml-canvas-h .back-btn { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px; background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-72); font-family: inherit; font-size: 12px; cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }
|
||
.ml-canvas-h .back-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }
|
||
.ml-canvas-h .back-btn svg { width: 12px; height: 12px; }
|
||
.ml-canvas-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }
|
||
.ml-canvas-h .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
|
||
.ml-canvas-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 2fr 1fr; overflow: hidden; }
|
||
.mc-ai { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--background-base); }
|
||
.mc-up { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--surface); border-left: 1px solid var(--border-faint); }
|
||
|
||
.mc-stream { flex: 1; min-height: 0; overflow-y: auto; padding: 28px 28px 220px; background: var(--background-base); }
|
||
.mc-stream-inner { width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 28px; }
|
||
.mc-empty { flex: 1; min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 40px; color: var(--black-alpha-56); text-align: center; }
|
||
.mc-empty .badge { font-family: var(--font-mono); font-size: 11px; letter-spacing: .08em; color: var(--black-alpha-48); text-transform: uppercase; }
|
||
.mc-empty h2 { font-size: 22px; font-weight: 600; color: var(--accent-black); letter-spacing: -.015em; margin: 0; }
|
||
.mc-empty p { font-size: 13px; max-width: 460px; line-height: 1.6; margin: 0; }
|
||
.mc-empty .ic { width: 64px; height: 64px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--heat); }
|
||
.mc-empty .ic svg { width: 28px; height: 28px; }
|
||
.mc-empty .examples { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; max-width: 720px; }
|
||
.mc-empty .examples .ex { padding: 6px 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-pill); font-size: 12px; color: var(--black-alpha-72); cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }
|
||
.mc-empty .examples .ex:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }
|
||
|
||
.mc-msg { display: flex; flex-direction: column; gap: 14px; }
|
||
.mc-msg-prompt { display: flex; align-items: flex-start; gap: 12px; }
|
||
.mc-msg-prompt .quote { flex-shrink: 0; width: 28px; height: 28px; border-radius: var(--r-sm); background: var(--surface); border: 1px solid var(--border-faint); color: var(--heat); display: grid; place-items: center; }
|
||
.mc-msg-prompt .quote svg { width: 13px; height: 13px; }
|
||
.mc-msg-prompt .pt { flex: 1; min-width: 0; padding-top: 4px; }
|
||
.mc-msg-prompt .pt-text { font-size: 14px; color: var(--accent-black); line-height: 1.55; word-break: break-word; }
|
||
.mc-msg-prompt .pt-tags { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; align-items: center; }
|
||
.mc-msg-prompt .pt-tags .meta-chip { padding: 2px 8px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
|
||
.mc-msg-prompt .pt-tags .sep { color: var(--black-alpha-24); }
|
||
.mc-msg-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||
@media (max-width: 1280px) { .mc-msg-grid { grid-template-columns: repeat(3, 1fr); } }
|
||
.mc-cell { position: relative; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; aspect-ratio: 3/4; cursor: pointer; }
|
||
.mc-cell.selected { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-12); }
|
||
.mc-cell .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32); letter-spacing: .02em; background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); }
|
||
.mc-cell.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }
|
||
@keyframes mc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
|
||
.mc-cell:hover { border-color: var(--black-alpha-32); }
|
||
.mc-cell .pick-badge { position: absolute; top: 6px; left: 6px; background: var(--heat); color: var(--accent-white); padding: 2px 7px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; display: none; }
|
||
.mc-cell.selected .pick-badge { display: block; }
|
||
.mc-cell .cell-ops { position: absolute; top: 6px; right: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity var(--t-base); z-index: 2; }
|
||
.mc-cell:hover .cell-ops { opacity: 1; }
|
||
.mc-cell .cell-ops button { width: 26px; height: 26px; background: rgba(255,255,255,.92); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--accent-black); cursor: pointer; display: grid; place-items: center; backdrop-filter: blur(4px); transition: border-color var(--t-base), color var(--t-base); }
|
||
.mc-cell .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }
|
||
.mc-cell .cell-ops button svg { width: 12px; height: 12px; }
|
||
|
||
.mc-msg-ops { display: flex; gap: 8px; }
|
||
.mc-msg-ops button { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 12.5px; color: var(--accent-black); font-family: inherit; cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }
|
||
.mc-msg-ops button:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }
|
||
.mc-msg-ops button svg { width: 13px; height: 13px; }
|
||
|
||
.mc-input-wrap { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 28px 22px; background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px); z-index: 5; }
|
||
.mc-input { max-width: 720px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 18px; padding: 12px 14px 10px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 6px 24px rgba(0,0,0,.06); transition: border-color var(--t-base); }
|
||
.mc-input:focus-within { border-color: var(--heat-40); }
|
||
.mc-input-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.mc-input-top .add-btn { flex-shrink: 0; width: 64px; height: 64px; background: var(--background-lighter); border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--black-alpha-56); cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }
|
||
.mc-input-top .add-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }
|
||
.mc-input-top .add-btn svg { width: 22px; height: 22px; }
|
||
.mc-input-refs { display: contents; }
|
||
.mc-input-ref { position: relative; width: 64px; height: 64px; border-radius: var(--r-md); overflow: hidden; background: var(--background-lighter); border: 1px solid var(--border-faint); flex-shrink: 0; }
|
||
.mc-input-ref img { width: 100%; height: 100%; object-fit: cover; }
|
||
.mc-input-ref .x { position: absolute; top: 3px; right: 3px; width: 18px; height: 18px; background: rgba(0,0,0,.7); color: var(--accent-white); border: 0; border-radius: 50%; display: grid; place-items: center; cursor: pointer; }
|
||
.mc-input-ref .x svg { width: 10px; height: 10px; }
|
||
.mc-input textarea#mc-input-text { width: 100%; border: 0; outline: 0; resize: none; background: transparent; font-family: inherit; font-size: 14px; line-height: 1.5; color: var(--accent-black); min-height: 44px; max-height: 220px; padding: 4px 2px; }
|
||
.mc-input textarea#mc-input-text::placeholder { color: var(--black-alpha-48); }
|
||
.mc-input-bottom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.mc-input-bottom .param { position: relative; display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 9px; background: var(--background-lighter); border: 1px solid transparent; border-radius: var(--r-pill); font-size: 11.5px; color: var(--black-alpha-72); cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }
|
||
.mc-input-bottom .param:hover { background: var(--surface); border-color: var(--border-faint); }
|
||
.mc-input-bottom .param .lbl-mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-right: 1px; }
|
||
.mc-input-bottom .param svg { width: 10px; height: 10px; opacity: .6; }
|
||
.mc-input-bottom .right-meta { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.mc-input-bottom .right-meta .val { color: var(--accent-black); }
|
||
.mc-input .send-btn { flex-shrink: 0; width: 32px; height: 32px; background: var(--heat); color: var(--accent-white); border: 0; border-radius: var(--r-md); cursor: pointer; display: grid; place-items: center; transition: opacity var(--t-base), filter var(--t-base); margin-left: 8px; }
|
||
.mc-input .send-btn:hover { filter: brightness(1.05); }
|
||
.mc-input .send-btn:disabled { opacity: .4; cursor: not-allowed; }
|
||
.mc-input .send-btn svg { width: 15px; height: 15px; }
|
||
|
||
.mc-up-tabs { display: flex; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; background: var(--surface); }
|
||
.mc-up-tab { flex: 1; height: 44px; background: transparent; border: 0; border-bottom: 2px solid transparent; font-family: inherit; font-size: 13px; font-weight: 500; color: var(--black-alpha-56); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||
.mc-up-tab:hover { color: var(--accent-black); background: var(--background-lighter); }
|
||
.mc-up-tab.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; background: var(--surface); }
|
||
.mc-up-body { flex: 1; min-height: 0; padding: 18px 20px 14px; display: flex; flex-direction: column; gap: 18px; overflow-y: auto; }
|
||
.mc-up-section { display: flex; flex-direction: column; gap: 8px; }
|
||
.mc-up-sec-h { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
.mc-up-name { width: 100%; height: 36px; padding: 0 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-family: inherit; font-size: 13.5px; color: var(--accent-black); outline: none; transition: border-color var(--t-base), background var(--t-base); }
|
||
.mc-up-name:focus { border-color: var(--heat-40); background: var(--surface); }
|
||
.mc-up-name::placeholder { color: var(--black-alpha-40); }
|
||
|
||
.mc-portrait-ai .empty { aspect-ratio: 3/4; max-height: 220px; border: 1.5px dashed var(--black-alpha-24); border-radius: var(--r-md); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; background: var(--background-lighter); text-align: center; padding: 14px; }
|
||
.mc-portrait-ai .empty[hidden] { display: none; }
|
||
.mc-portrait-ai .picked[hidden] { display: none; }
|
||
.mc-portrait-ai .empty .ic { width: 38px; height: 38px; border-radius: 50%; background: var(--surface); border: 1px solid var(--border-faint); display: grid; place-items: center; color: var(--black-alpha-48); }
|
||
.mc-portrait-ai .empty .ic svg { width: 16px; height: 16px; }
|
||
.mc-portrait-ai .empty .desc { font-size: 12.5px; color: var(--black-alpha-72); line-height: 1.55; }
|
||
.mc-portrait-ai .empty .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
.mc-portrait-ai .picked { position: relative; aspect-ratio: 3/4; max-height: 280px; background: var(--background-lighter); border: 1.5px solid var(--heat); border-radius: var(--r-md); overflow: hidden; }
|
||
.mc-portrait-ai .picked .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32); letter-spacing: .02em; background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); }
|
||
.mc-portrait-ai .picked .ops { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; }
|
||
.mc-portrait-ai .picked .ops button { width: 26px; height: 26px; background: rgba(255,255,255,.92); border: 1px solid var(--border-faint); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--accent-black); cursor: pointer; transition: border-color var(--t-base), color var(--t-base); }
|
||
.mc-portrait-ai .picked .ops button:hover { border-color: var(--heat); color: var(--heat); }
|
||
.mc-portrait-ai .picked .ops button svg { width: 12px; height: 12px; }
|
||
.mc-portrait-ai .picked .badge { position: absolute; top: 8px; left: 8px; background: var(--heat); color: var(--accent-white); padding: 2px 7px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; }
|
||
|
||
.mc-portrait-local .drop { border: 1.5px dashed var(--black-alpha-24); border-radius: var(--r-md); padding: 20px 14px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; background: var(--background-lighter); transition: border-color var(--t-base), background var(--t-base); text-align: center; }
|
||
.mc-portrait-local .drop:hover, .mc-portrait-local .drop.dragover { border-color: var(--heat); background: var(--heat-12); }
|
||
.mc-portrait-local .drop .ic { width: 32px; height: 32px; background: var(--heat); color: var(--accent-white); border-radius: 50%; display: grid; place-items: center; }
|
||
.mc-portrait-local .drop .ic svg { width: 14px; height: 14px; }
|
||
.mc-portrait-local .drop .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
|
||
.mc-portrait-local .drop .d { font-size: 11px; color: var(--black-alpha-48); }
|
||
.mc-portrait-local .list-h { display: flex; align-items: center; gap: 4px; margin-top: 6px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
.mc-portrait-local .list-h .ct { color: var(--accent-black); font-weight: 600; }
|
||
.mc-portrait-local .list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||
.mc-portrait-local .list:empty { display: none; }
|
||
.mc-portrait-local .thumb { position: relative; aspect-ratio: 3/4; border-radius: var(--r-sm); overflow: hidden; background: var(--background-lighter); border: 1px solid var(--border-faint); }
|
||
.mc-portrait-local .thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||
.mc-portrait-local .thumb .x { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; background: rgba(0,0,0,.7); color: var(--accent-white); border: 0; border-radius: 50%; display: grid; place-items: center; cursor: pointer; }
|
||
.mc-portrait-local .thumb .x svg { width: 10px; height: 10px; }
|
||
|
||
.mc-up[data-kind="scene"] .mc-triview { display: none; }
|
||
.mc-triview .result-wrap { display: flex; flex-direction: column; gap: 8px; }
|
||
.mc-triview .result { position: relative; aspect-ratio: 16/9; background: var(--background-lighter); border: 1.5px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; transition: border-color var(--t-base); }
|
||
.mc-triview.has-result .result { border-color: var(--heat); }
|
||
.mc-triview .result .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32); letter-spacing: .02em; background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); pointer-events: none; }
|
||
.mc-triview .result.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }
|
||
.mc-triview .overlay-gen-btn { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 18px; background: var(--heat); color: var(--accent-white); border: 0; border-radius: var(--r-pill); font-family: inherit; font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(250, 93, 25, .28); transition: filter var(--t-base), opacity var(--t-base), transform var(--t-base), box-shadow var(--t-base); }
|
||
.mc-triview .overlay-gen-btn:hover:not(:disabled) { filter: brightness(1.06); transform: translate(-50%, -50%) scale(1.03); box-shadow: 0 6px 16px rgba(250, 93, 25, .36); }
|
||
.mc-triview .overlay-gen-btn:disabled { background: var(--black-alpha-24); color: var(--surface); cursor: not-allowed; box-shadow: none; }
|
||
.mc-triview .overlay-gen-btn svg { width: 13px; height: 13px; }
|
||
.mc-triview .overlay-hint { position: absolute; left: 0; right: 0; bottom: 10px; text-align: center; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; pointer-events: none; z-index: 2; }
|
||
.mc-triview .result-ops { display: flex; gap: 6px; align-items: center; }
|
||
.mc-triview .result-ops .cost { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.mc-triview .result-ops button { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-family: inherit; font-size: 11.5px; color: var(--accent-black); cursor: pointer; transition: border-color var(--t-base), color var(--t-base); }
|
||
.mc-triview .result-ops button:hover { border-color: var(--heat); color: var(--heat); }
|
||
.mc-triview .result-ops button svg { width: 11px; height: 11px; }
|
||
.mc-triview .history { display: flex; flex-direction: column; gap: 6px; }
|
||
.mc-triview .history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
.mc-triview .history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }
|
||
.mc-triview .history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }
|
||
.mc-triview .history .h-thumb { flex: 0 0 auto; width: 72px; aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); position: relative; cursor: pointer; display: grid; place-items: center; overflow: hidden; transition: border-color var(--t-base); }
|
||
.mc-triview .history .h-thumb:hover { border-color: var(--heat-40); }
|
||
.mc-triview .history .h-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
|
||
.mc-triview .history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
|
||
.mc-triview .history .h-thumb.active .v { color: var(--heat); font-weight: 600; }
|
||
.mc-triview .history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
|
||
.mc-triview .history .h-thumb.active .badge { display: block; }
|
||
|
||
.mc-up-foot { padding: 12px 20px 16px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 10px; flex-shrink: 0; flex-wrap: wrap; }
|
||
.mc-up-foot .stat { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.mc-up-foot .stat b { color: var(--accent-black); font-weight: 600; }
|
||
.mc-up-foot .stat.ok { color: var(--heat); }
|
||
.mc-up-foot .commit-btn { display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 14px; background: var(--heat); color: var(--accent-white); border: 0; border-radius: var(--r-sm); font-family: inherit; font-size: 12.5px; cursor: pointer; transition: filter var(--t-base), opacity var(--t-base); }
|
||
.mc-up-foot .commit-btn:hover { filter: brightness(1.05); }
|
||
.mc-up-foot .commit-btn:disabled { opacity: .4; cursor: not-allowed; filter: none; }
|
||
.mc-up-foot .commit-btn svg { width: 12px; height: 12px; }
|
||
|
||
/* 离开工作台 · 二次确认 */
|
||
.mc-leave-bg { position: fixed; inset: 0; background: rgba(21,20,15,.42); backdrop-filter: blur(8px); z-index: 1300; display: none; align-items: center; justify-content: center; padding: 40px; }
|
||
.mc-leave-bg.show { display: flex; }
|
||
.mc-leave { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: 420px; max-width: 100%; box-shadow: 0 16px 48px rgba(0,0,0,.18); overflow: hidden; }
|
||
.mc-leave .lv-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px 10px; }
|
||
.mc-leave .lv-h .ic { width: 28px; height: 28px; display: grid; place-items: center; border-radius: var(--r-sm); background: var(--crimson-bg); color: var(--accent-crimson); flex-shrink: 0; }
|
||
.mc-leave .lv-h .ic svg { width: 16px; height: 16px; }
|
||
.mc-leave .lv-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }
|
||
.mc-leave .lv-h .mono { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
.mc-leave .lv-b { padding: 4px 20px 18px; font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); }
|
||
.mc-leave .lv-b b { color: var(--accent-black); font-weight: 600; }
|
||
.mc-leave .lv-f { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--border-faint); background: var(--background-lighter); }
|
||
.mc-leave .lv-f .spacer { flex: 1; }
|
||
.mc-leave .lv-f .btn { height: 34px; padding: 0 14px; font-size: 13px; }
|
||
.mc-leave .btn-danger { background: var(--accent-crimson); color: var(--accent-white); border-color: var(--accent-crimson); font-weight: 600; }
|
||
.mc-leave .btn-danger:hover { background: var(--accent-crimson); border-color: var(--accent-crimson); filter: brightness(.95); }
|
||
|
||
/* ─── 添加来源 · 选择 modal (AI 生成 / 本地上传) ─── */
|
||
.ml-up-choice-bg { position: fixed; inset: 0; z-index: 1200; background: rgba(21, 20, 15, .42); display: none; place-items: center; padding: 16px; }
|
||
.ml-up-choice-bg.show { display: grid; }
|
||
.ml-up-choice { width: min(560px, 92vw); background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); box-shadow: 0 16px 48px rgba(21, 20, 15, .18); overflow: hidden; position: relative; }
|
||
.ml-up-choice .uc-h { display: flex; align-items: center; gap: 12px; padding: 18px 22px 14px; border-bottom: 1px solid var(--border-faint); }
|
||
.ml-up-choice .uc-h .ic-m { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--heat-12); color: var(--heat); display: grid; place-items: center; flex-shrink: 0; }
|
||
.ml-up-choice .uc-h .ic-m svg { width: 18px; height: 18px; }
|
||
.ml-up-choice .uc-h .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||
.ml-up-choice .uc-h .ti strong { font-size: 15px; color: var(--accent-black); font-weight: 600; }
|
||
.ml-up-choice .uc-h .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.ml-up-choice .uc-h .uc-x { margin-left: auto; width: 28px; height: 28px; background: transparent; border: 0; border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; display: grid; place-items: center; }
|
||
.ml-up-choice .uc-h .uc-x:hover { background: var(--background-lighter); color: var(--accent-black); }
|
||
.ml-up-choice .uc-h .uc-x svg { width: 14px; height: 14px; }
|
||
.ml-up-choice .uc-body { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 20px 22px 22px; }
|
||
.ml-up-choice .uc-option { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px 16px; text-align: left; cursor: pointer; font-family: inherit; display: flex; flex-direction: column; gap: 10px; transition: border-color var(--t-base), background var(--t-base); }
|
||
.ml-up-choice .uc-option:hover { border-color: var(--heat); background: var(--heat-12); }
|
||
.ml-up-choice .uc-option .opt-ic { width: 40px; height: 40px; border-radius: var(--r-md); background: var(--background-lighter); color: var(--heat); border: 1px solid var(--heat-20); display: grid; place-items: center; transition: background var(--t-base), color var(--t-base); }
|
||
.ml-up-choice .uc-option:hover .opt-ic { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
|
||
.ml-up-choice .uc-option .opt-ic svg { width: 18px; height: 18px; }
|
||
.ml-up-choice .uc-option .opt-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }
|
||
.ml-up-choice .uc-option .opt-d { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-56); letter-spacing: .02em; line-height: 1.55; }
|
||
.ml-up-choice .uc-option .opt-tag { margin-top: auto; align-self: flex-start; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); color: var(--black-alpha-72); letter-spacing: .04em; }
|
||
.ml-up-choice .uc-option:hover .opt-tag { background: var(--surface); color: var(--heat); }
|
||
|
||
/* 新增人物 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-rerun-note {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 14px;
|
||
background: rgba(180,83,9,.08);
|
||
border: 1px solid rgba(180,83,9,.20);
|
||
border-radius: var(--r-md);
|
||
color: #7C3A05;
|
||
line-height: 1.55;
|
||
}
|
||
.sb-rerun-note .warn-ic {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: var(--r-sm);
|
||
background: rgba(180,83,9,.12);
|
||
color: #B45309;
|
||
display: grid;
|
||
place-items: center;
|
||
flex: 0 0 22px;
|
||
}
|
||
.sb-rerun-note .warn-ic svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
.sb-rerun-note .note-copy {
|
||
min-width: 0;
|
||
font-size: 11.5px;
|
||
}
|
||
.sb-rerun-note strong { color: #B45309; }
|
||
.sb-rerun-note a { color: #B45309; text-decoration: underline; text-underline-offset: 2px; }
|
||
|
||
.sb-stage-actions { display: flex; gap: 8px; margin-top: 14px; 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(--accent-black); white-space: pre-wrap; min-height: 200px; outline: none; letter-spacing: .01em; cursor: text; transition: border-color var(--t-base), background var(--t-base), box-shadow var(--t-base); }
|
||
.prompt-edit:hover { border-color: var(--heat-20); }
|
||
.prompt-edit:focus { border-color: var(--heat); background: var(--surface); 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(196px, 216px)); gap: 14px; align-content: start; align-items: start; justify-content: start; }
|
||
.video-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base), background var(--t-base); overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
|
||
.video-card:hover { border-color: var(--heat-40); background: var(--background-lighter); }
|
||
.video-thumb { width: 100%; aspect-ratio: 9/16; position: relative; border-radius: var(--r-md) var(--r-md) 0 0; overflow: hidden; }
|
||
.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: 12px 12px 14px; flex: 1 1 auto; min-height: 118px; display: flex; flex-direction: column; }
|
||
.video-card-head { display: flex; align-items: flex-start; gap: 8px; }
|
||
.video-card-title { min-width: 0; flex: 1 1 auto; font-size: 13px; line-height: 1.4; font-weight: 600; color: var(--accent-black); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.video-card-head .pill { flex: 0 0 auto; }
|
||
.video-meta { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 5px; }
|
||
.video-actions { margin-top: auto; padding-top: 12px; display: flex; align-items: center; gap: 10px; }
|
||
|
||
/* 视频详情 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-prompt-field { margin-top: 16px; }
|
||
.vd-prompt-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px; }
|
||
.vd-prompt-head .label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--black-alpha-56); letter-spacing: .04em; }
|
||
.vd-prompt-head .hint { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.vd-prompt-edit { min-height: 120px; }
|
||
.vd-prompt-edit:empty::before { content: attr(data-placeholder); color: var(--black-alpha-40); }
|
||
.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; }
|
||
.vd-modal-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
||
.vd-modal-actions .pill-cta { min-width: 96px; justify-content: 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 1 0; min-height: 0; aspect-ratio: 9/16; 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); }
|
||
|
||
/* ── 时间轴 · 剪映风格(Restraint 浅色规范) ── */
|
||
.timeline { position: relative; grid-column: 1 / -1; padding: 14px 16px; background: var(--background-base); }
|
||
|
||
/* 工具栏 */
|
||
.tl-toolbar { display: flex; align-items: center; gap: 4px; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--border-faint); }
|
||
.tl-toolbar .tl-action { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; background: transparent; border: 1px solid transparent; border-radius: var(--r-sm); color: var(--black-alpha-72); font-size: 12px; font-family: inherit; cursor: pointer; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
|
||
.tl-toolbar .tl-action:hover { background: var(--surface); border-color: var(--border-faint); color: var(--accent-black); }
|
||
.tl-toolbar .tl-action.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }
|
||
.tl-toolbar .tl-action svg { width: 13px; height: 13px; }
|
||
.tl-toolbar .tl-sep { width: 1px; height: 16px; background: var(--border-faint); margin: 0 4px; }
|
||
.tl-toolbar .tl-zoom { display: inline-flex; align-items: center; gap: 8px; }
|
||
.tl-toolbar .tl-zoom .lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
.tl-toolbar .tl-zoom input[type="range"] { width: 120px; accent-color: var(--heat); }
|
||
|
||
/* 时间刻度 · 主/次刻度 */
|
||
.tl-ruler { display: grid; grid-template-columns: 80px 1fr; align-items: end; padding: 0; margin-bottom: 4px; }
|
||
.tl-ruler .l { font-family: var(--font-mono); color: var(--black-alpha-48); padding: 0 4px 4px; font-size: 10.5px; letter-spacing: .04em; align-self: end; }
|
||
.tl-ruler .rule-track { position: relative; height: 22px; border-bottom: 1px solid var(--border-faint); }
|
||
.tl-ruler .rule-track .tick { position: absolute; bottom: 0; width: 1px; background: var(--black-alpha-24); }
|
||
.tl-ruler .rule-track .tick.major { height: 8px; background: var(--black-alpha-48); }
|
||
.tl-ruler .rule-track .tick.minor { height: 4px; }
|
||
.tl-ruler .rule-track .t { position: absolute; bottom: 10px; transform: translateX(-50%); font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; white-space: nowrap; }
|
||
|
||
/* 轨道行 */
|
||
.tl-track { display: grid; grid-template-columns: 80px 1fr; align-items: center; padding: 3px 0; }
|
||
.tl-track .label { display: flex; align-items: center; gap: 6px; padding-left: 4px; font-size: 11.5px; color: var(--black-alpha-72); font-weight: 500; }
|
||
.tl-track .label .ico { width: 18px; height: 18px; display: grid; place-items: center; border-radius: var(--r-sm); flex-shrink: 0; }
|
||
.tl-track .label .ico svg { width: 12px; height: 12px; }
|
||
.tl-track .label.video .ico { background: var(--heat-12); color: var(--heat); }
|
||
.tl-track .label.subtitle .ico { background: var(--forest-bg); color: var(--accent-forest); }
|
||
.tl-track .label.bgm .ico { background: rgba(144, 97, 255, .10); color: var(--accent-amethyst); }
|
||
|
||
/* 轨道 lane · 绝对定位容器 + 1s 网格线 */
|
||
.tl-track .lane {
|
||
position: relative;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
}
|
||
.tl-track.video-track .lane { height: 46px; }
|
||
.tl-track.subtitle-track .lane { height: 28px; }
|
||
.tl-track.bgm-track .lane { height: 34px; }
|
||
.tl-track .lane::before {
|
||
content: ""; position: absolute; inset: 0;
|
||
background-image: repeating-linear-gradient(to right,
|
||
var(--border-faint) 0, var(--border-faint) 1px,
|
||
transparent 1px, transparent calc(100% / 15));
|
||
pointer-events: none; opacity: .55;
|
||
border-radius: inherit;
|
||
}
|
||
.tl-track .lane.is-snapping .clip,
|
||
.tl-track .lane.is-reordering .clip:not(.dragging) { transition: left .16s ease, width .16s ease; }
|
||
.tl-align-guide {
|
||
position: absolute;
|
||
width: 1.5px;
|
||
background: var(--accent-forest);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
z-index: 9;
|
||
}
|
||
.tl-align-guide.show { opacity: 1; }
|
||
.tl-insert-ghost {
|
||
position: absolute; top: 3px; bottom: 3px;
|
||
border: 1px dashed var(--heat);
|
||
background: var(--heat-12);
|
||
border-radius: 4px;
|
||
pointer-events: none;
|
||
box-sizing: border-box;
|
||
z-index: 2;
|
||
}
|
||
|
||
/* 片段公共 · 绝对定位 · left/width 由 data 驱动 */
|
||
.clip {
|
||
position: absolute; top: 3px; bottom: 3px;
|
||
display: flex; align-items: center; gap: 6px;
|
||
padding: 0 8px; font-size: 11px;
|
||
border: 1px solid transparent;
|
||
border-radius: 4px;
|
||
cursor: grab; overflow: hidden; white-space: nowrap; user-select: none;
|
||
box-sizing: border-box;
|
||
}
|
||
.clip:hover { filter: brightness(1.04); }
|
||
.clip .num { font-family: var(--font-mono); font-weight: 700; opacity: .85; flex-shrink: 0; }
|
||
.clip .lbl { overflow: hidden; text-overflow: ellipsis; }
|
||
|
||
/* 视频片段 · 内嵌胶卷帧条(预览缩略) */
|
||
.clip.video {
|
||
background: var(--heat-12); border-color: var(--heat-40); color: var(--heat);
|
||
}
|
||
.clip.video .frames {
|
||
position: absolute; top: 0; bottom: 0; left: 0;
|
||
width: var(--src-width, 100%);
|
||
transform: translateX(var(--src-offset, 0%));
|
||
display: flex; gap: 0;
|
||
pointer-events: none; z-index: 0;
|
||
border-radius: inherit;
|
||
overflow: hidden;
|
||
}
|
||
.clip.video .frames .fr {
|
||
flex: 1; min-width: 0;
|
||
background:
|
||
repeating-linear-gradient(45deg,
|
||
transparent 0, transparent 4px,
|
||
rgba(38,38,38,.06) 4px, rgba(38,38,38,.06) 5px),
|
||
rgba(250,93,25,.10);
|
||
}
|
||
.clip.video .frames .fr + .fr { border-left: 1px solid rgba(255,255,255,.55); }
|
||
.clip.video:hover .frames .fr { background-color: rgba(250,93,25,.18); }
|
||
.clip.video .num, .clip.video .lbl { position: relative; z-index: 1; }
|
||
.clip.video.selected {
|
||
background: var(--heat); color: var(--accent-white); border-color: var(--heat);
|
||
box-shadow: var(--shadow-cta);
|
||
z-index: 3;
|
||
}
|
||
.clip.video.selected .frames .fr {
|
||
background:
|
||
repeating-linear-gradient(45deg,
|
||
transparent 0, transparent 4px,
|
||
rgba(255,255,255,.22) 4px, rgba(255,255,255,.22) 5px),
|
||
rgba(255,255,255,.06);
|
||
}
|
||
.clip.video.selected .frames .fr + .fr { border-left-color: rgba(255,255,255,.28); }
|
||
|
||
/* 字幕片段 · 薄条 + 引号符号 */
|
||
.clip.subtitle {
|
||
background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest);
|
||
font-size: 11px;
|
||
}
|
||
.clip.subtitle .lbl::before {
|
||
content: "“"; font-family: serif; font-size: 14px; opacity: .55; margin-right: 2px;
|
||
}
|
||
.clip.subtitle:hover { background: rgba(31, 138, 81, .14); }
|
||
.clip.subtitle.selected {
|
||
background: var(--accent-forest); color: var(--accent-white); border-color: var(--accent-forest);
|
||
box-shadow: 0 2px 6px rgba(31, 138, 81, .35);
|
||
z-index: 3;
|
||
}
|
||
.clip.subtitle.selected .lbl::before { opacity: .8; }
|
||
|
||
/* BGM 片段 · 内嵌波形 */
|
||
.clip.bgm {
|
||
background: rgba(144,97,255,.10); border-color: rgba(144,97,255,.30);
|
||
color: var(--accent-amethyst);
|
||
}
|
||
.clip.bgm .wave {
|
||
position: absolute; inset: 6px 10px; pointer-events: none;
|
||
opacity: .5; display: block; z-index: 0;
|
||
}
|
||
.clip.bgm .wave svg { width: 100%; height: 100%; display: block; }
|
||
.clip.bgm:hover { background: rgba(144,97,255,.16); }
|
||
.clip.bgm.selected {
|
||
background: var(--accent-amethyst); color: var(--accent-white); border-color: var(--accent-amethyst);
|
||
box-shadow: 0 2px 6px rgba(144, 97, 255, .35);
|
||
z-index: 3;
|
||
}
|
||
.clip.bgm.selected .wave { opacity: .75; }
|
||
.clip.bgm.selected .wave svg rect { fill: rgba(255,255,255,.9); }
|
||
.clip.bgm .lbl, .clip.bgm .num { position: relative; z-index: 1; }
|
||
|
||
/* 通用 trim 把手 · 三轨皆可剪 */
|
||
.clip.selected .trim-l,
|
||
.clip.selected .trim-r {
|
||
position: absolute; top: 3px; bottom: 3px; width: 4px;
|
||
background: rgba(255,255,255,.92); border-radius: 2px;
|
||
z-index: 5;
|
||
cursor: ew-resize; pointer-events: auto;
|
||
}
|
||
.clip.selected .trim-l { left: 2px; }
|
||
.clip.selected .trim-r { right: 2px; }
|
||
.clip.selected .trim-l::before,
|
||
.clip.selected .trim-r::before {
|
||
content: ""; position: absolute; top: 50%; left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 2px; height: 10px;
|
||
background: rgba(38,38,38,.4); border-radius: 1px;
|
||
}
|
||
|
||
/* 拖拽时片段移动光标 */
|
||
.clip { cursor: grab; }
|
||
.clip.selected { cursor: grab; }
|
||
.clip:active,
|
||
.clip.selected:active { cursor: grabbing; }
|
||
.clip.dragging { cursor: grabbing; opacity: .9; z-index: 7 !important; }
|
||
|
||
/* Playhead · 顶到时间尺、贯穿三条轨 · 可拖拽 */
|
||
.playhead {
|
||
position: absolute; top: -90px; bottom: -44px;
|
||
width: 18px; transform: translateX(-50%);
|
||
background: transparent;
|
||
z-index: 10;
|
||
pointer-events: auto;
|
||
cursor: ew-resize;
|
||
touch-action: none;
|
||
}
|
||
.playhead::after {
|
||
content: ''; position: absolute; top: 0; bottom: 0; left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 1.5px; background: var(--heat);
|
||
pointer-events: none;
|
||
}
|
||
.playhead::before {
|
||
content: ''; position: absolute; top: -4px; left: 50%;
|
||
transform: translateX(-50%) rotate(45deg);
|
||
width: 10px; height: 10px; background: var(--heat);
|
||
box-shadow: 0 0 0 1.5px var(--surface);
|
||
border-radius: 1px;
|
||
pointer-events: none;
|
||
}
|
||
.playhead .ph-grab {
|
||
position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
|
||
width: 24px; height: 24px;
|
||
cursor: ew-resize; pointer-events: auto;
|
||
border-radius: 50%;
|
||
}
|
||
.playhead.is-dragging::after { background: var(--heat); }
|
||
.timeline.is-dragging-playhead { cursor: ew-resize; user-select: none; }
|
||
|
||
/* Ruler 可点击 seek */
|
||
.tl-ruler .rule-track { cursor: pointer; }
|
||
.tl-ruler .rule-track:hover .tick.major { background: var(--heat-40); }
|
||
|
||
/* Lane 也可点击 seek (空白处) */
|
||
.tl-track .lane { cursor: pointer; }
|
||
|
||
/* 播放/暂停按钮 active 态 · ctl-btn.is-playing 显示暂停 icon */
|
||
.ctl-btn { transition: color var(--t-base); }
|
||
.ctl-btn.is-playing { color: var(--heat); }
|
||
|
||
/* 工具栏按钮 disabled */
|
||
.tl-action:disabled { opacity: .4; cursor: not-allowed; }
|
||
.tl-action:disabled:hover { background: transparent; border-color: transparent; color: var(--black-alpha-72); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="page">
|
||
|
||
<!-- Project header 移除 · 改为 topbar 中部 stage-pill(Shell.render 后注入)-->
|
||
<!-- Stage stepper 移除 · 由 topbar 中部 5 个圆点替代,JS 仍读 .sp-dot[data-stage] 维持单一源 -->
|
||
<div class="stage-pill-anchor" id="stage-pill-anchor" hidden>
|
||
<a class="sp-dot" data-stage="1" href="#stage-1"><span class="d"></span><span class="l">脚本</span></a>
|
||
<span class="sp-line"></span>
|
||
<a class="sp-dot" data-stage="2" href="#stage-2"><span class="d"></span><span class="l">基础资产</span></a>
|
||
<span class="sp-line"></span>
|
||
<a class="sp-dot" data-stage="3" href="#stage-3"><span class="d"></span><span class="l">故事板</span></a>
|
||
<span class="sp-line"></span>
|
||
<a class="sp-dot" data-stage="4" href="#stage-4"><span class="d"></span><span class="l">视频</span></a>
|
||
<span class="sp-line"></span>
|
||
<a class="sp-dot" data-stage="5" href="#stage-5"><span class="d"></span><span class="l">拼接导出</span></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">
|
||
<div class="shot-headline">
|
||
<strong>镜头脚本</strong>
|
||
<span class="muted-2 mono" id="shots-meta" style="font-size:11px;">· 空 · 待生成</span>
|
||
</div>
|
||
<div class="script-brief-summary" aria-label="当前创作方向">
|
||
<span class="pill neutral script-brief-pill"><span class="k">来源</span><span class="v" id="brief-source">未选择</span></span>
|
||
<span class="pill neutral script-brief-pill"><span class="k">风格</span><span class="v" id="brief-style">待确认</span></span>
|
||
<span class="pill neutral script-brief-pill"><span class="k">人物</span><span class="v" id="brief-persona">待确认</span></span>
|
||
</div>
|
||
<div class="script-tags" id="script-tags">
|
||
<div class="tag-group" data-kind="char">
|
||
<span class="tg-lbl">// 人物</span>
|
||
<button class="tag-add" type="button" aria-label="添加人物">+</button>
|
||
</div>
|
||
<div class="tag-group" data-kind="scene">
|
||
<span class="tg-lbl">// 场景</span>
|
||
<button class="tag-add" type="button" aria-label="添加场景">+</button>
|
||
</div>
|
||
</div>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" id="chat-regen-btn">↻ 整体重写</button>
|
||
</div>
|
||
<div class="shots-body" id="shots-body">
|
||
<!-- JS 注入空态/镜头卡片 -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stage-script-gutter" id="stage-script-gutter" role="separator" aria-orientation="vertical" aria-label="拖动调整脚本助手宽度"></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">
|
||
<div class="chat-input-card">
|
||
<div class="chat-attach-row" id="chat-attach-row" hidden></div>
|
||
<textarea class="chat-input-area" id="chat-textarea" placeholder="直接说怎么改,如:更像小红书种草 / 换成熬夜党" rows="2"></textarea>
|
||
<div class="chat-input-foot">
|
||
<button class="chat-icon-btn" id="chat-upload-btn" title="上传脚本附件" aria-label="上传脚本附件">
|
||
<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 5v14M5 12h14"/></svg>
|
||
</button>
|
||
<span class="spacer"></span>
|
||
<button class="chat-send-btn" id="chat-send-btn" title="发送" aria-label="发送">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<input type="file" id="chat-upload-input" hidden accept=".txt,.md,.docx,.doc,.pdf,.srt,.json">
|
||
</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 class="asset-main">
|
||
<!-- ===== 商品(项目内只有 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 prod-lib-card" data-asset-kind="product" data-asset-id="prod-main" id="asset-prod-card">
|
||
<div class="placeholder prod-thumb">
|
||
<span class="tri-missing-badge" id="asset-prod-tri-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||
<span class="ico" aria-hidden="true"></span>
|
||
<span class="lbl-mono">缺三视图</span>
|
||
<span class="tri-missing-pop" role="tooltip">
|
||
<span class="pop-h">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||
MISSING TRI-VIEW
|
||
</span>
|
||
<span class="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
|
||
<span class="pop-tip">建议:点右下 <b>AI 生成三视图</b> 先补齐三视图,再发起后续生成。</span>
|
||
</span>
|
||
</span>
|
||
<span class="ph-frame" id="asset-prod-thumb-label">透真补水面膜 · 主图</span>
|
||
</div>
|
||
<div class="prod-body">
|
||
<div class="prod-name" id="asset-prod-card-name">透真补水面膜</div>
|
||
<div class="prod-cat">美妆个护</div>
|
||
<div class="prod-date">2026-05-15 创建</div>
|
||
</div>
|
||
<div class="prod-action" id="asset-prod-action">
|
||
<button class="btn-aigen" type="button" data-stop id="asset-prod-aigen-btn">
|
||
<svg class="ai-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3z"/>
|
||
<path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7L19 14z"/>
|
||
</svg>
|
||
AI 生成三视图
|
||
</button>
|
||
</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 class="prod-preview-history" id="prod-preview-history">
|
||
<div class="h-lbl">// 历史版本 · <span class="ct" id="prod-preview-history-count">0</span> 版</div>
|
||
<div class="h-row" id="prod-preview-history-row"></div>
|
||
</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" hidden>+ 新增人物</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" hidden>+ 新增场景</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'"><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-3'">确认资产,进入故事板 <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 3 · 故事板(按场分) ============= -->
|
||
<section class="stage" data-stage-pane="3">
|
||
<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:10px;">
|
||
整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。
|
||
</div>
|
||
<div class="sb-rerun-note">
|
||
<span class="warn-ic" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4M12 17h.01"/></svg>
|
||
</span>
|
||
<div class="note-copy">
|
||
<strong>仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href="#stage-1">Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。
|
||
</div>
|
||
</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>
|
||
<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="Quota.preflight({stage:'Stage 4 视频片段 · 全部重跑', est: 1.35, force: true, demo:'block'})">↻ 全部重跑</button>
|
||
<input type="file" id="stage4-upload-input" accept="video/*" multiple hidden>
|
||
<button class="btn btn-sm" type="button" onclick="document.getElementById('stage4-upload-input').click()">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v12"/></svg>
|
||
上传视频
|
||
</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="video-card-head"><strong class="video-card-title">场 1 · 深夜办公桌</strong><span class="pill ok"><span class="dot"></span>完成</span></div>
|
||
<div class="video-meta">15s · 1080×1920 · ¥0.45</div>
|
||
<div class="video-actions">
|
||
<button class="btn btn-ghost btn-sm" type="button" data-vstop>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" type="button" 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="video-card-head"><strong class="video-card-title">场 2 · 面膜包装/特写</strong><span class="pill ok"><span class="dot"></span>完成</span></div>
|
||
<div class="video-meta">12s · 1080×1920 · ¥0.45</div>
|
||
<div class="video-actions">
|
||
<button class="btn btn-ghost btn-sm" type="button" data-vstop>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" type="button" 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="video-card-head"><strong class="video-card-title">场 3 · 化妆台/产品定格</strong><span class="pill ok"><span class="dot"></span>完成</span></div>
|
||
<div class="video-meta">13s · 1080×1920 · ¥0.45</div>
|
||
<div class="video-actions">
|
||
<button class="btn btn-ghost btn-sm" type="button" data-vstop>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" type="button" 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'"><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-5'">确认视频,进入拼接 <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 · 视频详情 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" style="margin-top:18px;">
|
||
<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="vd-prompt-field">
|
||
<div class="vd-prompt-head">
|
||
<span class="label">// 视频提示词</span>
|
||
</div>
|
||
<div class="prompt-edit vd-prompt-edit" contenteditable="true" role="textbox" aria-label="视频提示词" spellcheck="false" id="vd-prompt-edit" data-placeholder="// 输入本场 Seedance 提示词"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-modal-f vd-modal-f">
|
||
<button class="btn btn-ghost" type="button" data-modal-close>关闭</button>
|
||
<div class="vd-modal-actions">
|
||
<button class="pill-cta ghost" type="button" id="vd-regen-btn">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
|
||
重跑本场
|
||
</button>
|
||
<button class="pill-cta heat" type="button" id="vd-adopt-btn">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||
采用此版
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============= STAGE 5 · 拼接编辑器 ============= -->
|
||
<section class="stage" data-stage-pane="5">
|
||
<div class="editor">
|
||
<div class="editor-preview">
|
||
<div class="canvas" id="ed-canvas"><span id="ed-canvas-label">9:16 预览 · 1080×1920</span></div>
|
||
<div class="controls">
|
||
<button class="ctl-btn" id="ed-prev-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" id="ed-play-btn" title="播放 / 暂停 (空格)"><svg id="ed-play-icon" width="16" height="16" viewBox="0 0 16 16"><path d="M5 4l7 4-7 4z" fill="currentColor"/></svg></button>
|
||
<button class="ctl-btn" id="ed-next-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;"><span id="ed-cur-time">00:00.00</span> / <span id="ed-total-time">00:15.00</span></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;">// 当前选中(<span id="ed-inspect-name">未选</span>)</div>
|
||
<div class="props-row"><span class="k">起始</span><input class="input-mini" id="ed-inspect-start" value="—"></div>
|
||
<div class="props-row"><span class="k">时长</span><input class="input-mini" id="ed-inspect-dur" value="—"></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" id="ed-timeline">
|
||
<div class="tl-toolbar">
|
||
<button class="tl-action" id="ed-undo-btn" title="撤销">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
|
||
</button>
|
||
<button class="tl-action" id="ed-redo-btn" title="重做">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 15-6.7L21 13"/></svg>
|
||
</button>
|
||
<span class="tl-sep"></span>
|
||
<button class="tl-action" id="ed-split-btn" title="在播放头处分割选中片段">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M20 4L8.12 15.88"/><path d="M14.47 14.48L20 20"/><path d="M8.12 8.12L12 12"/></svg>
|
||
分割
|
||
</button>
|
||
<button class="tl-action" id="ed-copy-btn" title="复制选中片段">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||
复制
|
||
</button>
|
||
<button class="tl-action danger" id="ed-del-btn" title="删除选中片段 (Delete)">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1.5 14a2 2 0 0 1-2 1.8H8.5a2 2 0 0 1-2-1.8L5 6"/></svg>
|
||
删除
|
||
</button>
|
||
<span class="spacer"></span>
|
||
<div class="tl-zoom">
|
||
<span class="lbl">// zoom</span>
|
||
<input type="range" min="50" max="200" value="100" id="ed-zoom-input">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-ruler">
|
||
<div class="l">// time</div>
|
||
<div class="rule-track" id="ed-ruler">
|
||
<span class="tick major" style="left:0%"><span class="t">0s</span></span>
|
||
<span class="tick minor" style="left:6.67%"></span>
|
||
<span class="tick major" style="left:13.33%"><span class="t">2s</span></span>
|
||
<span class="tick minor" style="left:20%"></span>
|
||
<span class="tick major" style="left:26.67%"><span class="t">4s</span></span>
|
||
<span class="tick minor" style="left:33.33%"></span>
|
||
<span class="tick major" style="left:40%"><span class="t">6s</span></span>
|
||
<span class="tick minor" style="left:46.67%"></span>
|
||
<span class="tick major" style="left:53.33%"><span class="t">8s</span></span>
|
||
<span class="tick minor" style="left:60%"></span>
|
||
<span class="tick major" style="left:66.67%"><span class="t">10s</span></span>
|
||
<span class="tick minor" style="left:73.33%"></span>
|
||
<span class="tick major" style="left:80%"><span class="t">12s</span></span>
|
||
<span class="tick minor" style="left:86.67%"></span>
|
||
<span class="tick major" style="left:93.33%"><span class="t">14s</span></span>
|
||
<span class="tick major" style="left:100%"><span class="t">15s</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-track video-track">
|
||
<div class="label video">
|
||
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18"/><path d="M7 2v20M17 2v20M2 12h20M2 7h5M2 17h5M17 17h5M17 7h5"/></svg></span>
|
||
视频
|
||
</div>
|
||
<div class="lane" id="ed-lane-video" data-track="video">
|
||
<div class="clip video" data-track="video" data-label="深夜办公桌" data-dur="2" data-max="2"><span class="frames"><span class="fr"></span><span class="fr"></span><span class="fr"></span></span><span class="num">1</span><span class="lbl">深夜办公桌</span></div>
|
||
<div class="clip video" data-track="video" data-label="面膜包装" data-dur="3" data-max="3"><span class="frames"><span class="fr"></span><span class="fr"></span><span class="fr"></span><span class="fr"></span></span><span class="num">2</span><span class="lbl">面膜包装</span></div>
|
||
<div class="clip video" data-track="video" data-label="精华液微距" data-dur="3" data-max="3"><span class="frames"><span class="fr"></span><span class="fr"></span><span class="fr"></span><span class="fr"></span></span><span class="num">3</span><span class="lbl">精华液微距</span></div>
|
||
<div class="clip video" data-track="video" data-label="敷面膜平躺" data-dur="3" data-max="3"><span class="frames"><span class="fr"></span><span class="fr"></span><span class="fr"></span><span class="fr"></span></span><span class="num">4</span><span class="lbl">敷面膜平躺</span></div>
|
||
<div class="clip video" data-track="video" data-label="化妆台" data-dur="2" data-max="2"><span class="frames"><span class="fr"></span><span class="fr"></span><span class="fr"></span></span><span class="num">5</span><span class="lbl">化妆台</span></div>
|
||
<div class="clip video" data-track="video" data-label="产品定格" data-dur="2" data-max="2"><span class="frames"><span class="fr"></span><span class="fr"></span><span class="fr"></span></span><span class="num">6</span><span class="lbl">产品定格</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-track subtitle-track">
|
||
<div class="label subtitle">
|
||
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/></svg></span>
|
||
字幕
|
||
</div>
|
||
<div class="lane" id="ed-lane-subtitle" data-track="subtitle">
|
||
<div class="clip subtitle" data-track="subtitle" data-label="加班三天 脸已经不能看了…" data-dur="2" data-max="2"><span class="lbl">加班三天 脸已经不能看了…</span></div>
|
||
<div class="clip subtitle" data-track="subtitle" data-label="还好我有这个 透真玻尿酸面膜" data-dur="3" data-max="3"><span class="lbl">还好我有这个 透真玻尿酸面膜</span></div>
|
||
<div class="clip subtitle" data-track="subtitle" data-label="30g 精华 一片顶三片" data-dur="3" data-max="3"><span class="lbl">30g 精华 一片顶三片</span></div>
|
||
<div class="clip subtitle" data-track="subtitle" data-label="敷完起来脸是软的" data-dur="3" data-max="3"><span class="lbl">敷完起来脸是软的</span></div>
|
||
<div class="clip subtitle" data-track="subtitle" data-label="化妆都能看出来" data-dur="2" data-max="2"><span class="lbl">化妆都能看出来</span></div>
|
||
<div class="clip subtitle" data-track="subtitle" data-label="5 片 ¥39.9 囤起来" data-dur="2" data-max="2"><span class="lbl">5 片 ¥39.9 囤起来</span></div>
|
||
<div class="playhead" id="ed-playhead" style="left:0%;"><span class="ph-grab"></span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-track bgm-track">
|
||
<div class="label bgm">
|
||
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg></span>
|
||
BGM
|
||
</div>
|
||
<div class="lane">
|
||
<div class="clip bgm" data-track="bgm" data-label="温柔治愈钢琴" data-dur="15" data-max="15">
|
||
<span class="wave"><svg viewBox="0 0 600 20" preserveAspectRatio="none" fill="currentColor"><rect x="0" y="8" width="2" height="4"/><rect x="4" y="6" width="2" height="8"/><rect x="8" y="3" width="2" height="14"/><rect x="12" y="7" width="2" height="6"/><rect x="16" y="4" width="2" height="12"/><rect x="20" y="2" width="2" height="16"/><rect x="24" y="6" width="2" height="8"/><rect x="28" y="8" width="2" height="4"/><rect x="32" y="5" width="2" height="10"/><rect x="36" y="3" width="2" height="14"/><rect x="40" y="7" width="2" height="6"/><rect x="44" y="4" width="2" height="12"/><rect x="48" y="6" width="2" height="8"/><rect x="52" y="2" width="2" height="16"/><rect x="56" y="5" width="2" height="10"/><rect x="60" y="7" width="2" height="6"/><rect x="64" y="3" width="2" height="14"/><rect x="68" y="6" width="2" height="8"/><rect x="72" y="8" width="2" height="4"/><rect x="76" y="4" width="2" height="12"/><rect x="80" y="2" width="2" height="16"/><rect x="84" y="5" width="2" height="10"/><rect x="88" y="7" width="2" height="6"/><rect x="92" y="3" width="2" height="14"/><rect x="96" y="6" width="2" height="8"/><rect x="100" y="4" width="2" height="12"/><rect x="104" y="8" width="2" height="4"/><rect x="108" y="5" width="2" height="10"/><rect x="112" y="2" width="2" height="16"/><rect x="116" y="7" width="2" height="6"/><rect x="120" y="3" width="2" height="14"/><rect x="124" y="6" width="2" height="8"/><rect x="128" y="4" width="2" height="12"/><rect x="132" y="8" width="2" height="4"/><rect x="136" y="5" width="2" height="10"/><rect x="140" y="3" width="2" height="14"/><rect x="144" y="6" width="2" height="8"/><rect x="148" y="2" width="2" height="16"/><rect x="152" y="7" width="2" height="6"/><rect x="156" y="4" width="2" height="12"/><rect x="160" y="5" width="2" height="10"/><rect x="164" y="3" width="2" height="14"/><rect x="168" y="6" width="2" height="8"/><rect x="172" y="8" width="2" height="4"/><rect x="176" y="4" width="2" height="12"/><rect x="180" y="2" width="2" height="16"/><rect x="184" y="5" width="2" height="10"/><rect x="188" y="7" width="2" height="6"/><rect x="192" y="3" width="2" height="14"/><rect x="196" y="6" width="2" height="8"/><rect x="200" y="8" width="2" height="4"/><rect x="204" y="4" width="2" height="12"/><rect x="208" y="2" width="2" height="16"/><rect x="212" y="6" width="2" height="8"/><rect x="216" y="5" width="2" height="10"/><rect x="220" y="3" width="2" height="14"/><rect x="224" y="7" width="2" height="6"/><rect x="228" y="4" width="2" height="12"/><rect x="232" y="8" width="2" height="4"/><rect x="236" y="6" width="2" height="8"/><rect x="240" y="2" width="2" height="16"/><rect x="244" y="5" width="2" height="10"/><rect x="248" y="3" width="2" height="14"/><rect x="252" y="7" width="2" height="6"/><rect x="256" y="6" width="2" height="8"/><rect x="260" y="4" width="2" height="12"/><rect x="264" y="8" width="2" height="4"/><rect x="268" y="5" width="2" height="10"/><rect x="272" y="2" width="2" height="16"/><rect x="276" y="7" width="2" height="6"/><rect x="280" y="3" width="2" height="14"/><rect x="284" y="6" width="2" height="8"/><rect x="288" y="4" width="2" height="12"/><rect x="292" y="8" width="2" height="4"/><rect x="296" y="5" width="2" height="10"/><rect x="300" y="3" width="2" height="14"/><rect x="304" y="6" width="2" height="8"/><rect x="308" y="2" width="2" height="16"/><rect x="312" y="7" width="2" height="6"/><rect x="316" y="4" width="2" height="12"/><rect x="320" y="5" width="2" height="10"/><rect x="324" y="3" width="2" height="14"/><rect x="328" y="6" width="2" height="8"/><rect x="332" y="8" width="2" height="4"/><rect x="336" y="4" width="2" height="12"/><rect x="340" y="2" width="2" height="16"/><rect x="344" y="5" width="2" height="10"/><rect x="348" y="7" width="2" height="6"/><rect x="352" y="3" width="2" height="14"/><rect x="356" y="6" width="2" height="8"/><rect x="360" y="8" width="2" height="4"/><rect x="364" y="4" width="2" height="12"/><rect x="368" y="2" width="2" height="16"/><rect x="372" y="6" width="2" height="8"/><rect x="376" y="5" width="2" height="10"/><rect x="380" y="3" width="2" height="14"/><rect x="384" y="7" width="2" height="6"/><rect x="388" y="4" width="2" height="12"/><rect x="392" y="8" width="2" height="4"/><rect x="396" y="6" width="2" height="8"/><rect x="400" y="2" width="2" height="16"/><rect x="404" y="5" width="2" height="10"/><rect x="408" y="3" width="2" height="14"/><rect x="412" y="7" width="2" height="6"/><rect x="416" y="6" width="2" height="8"/><rect x="420" y="4" width="2" height="12"/><rect x="424" y="8" width="2" height="4"/><rect x="428" y="5" width="2" height="10"/><rect x="432" y="2" width="2" height="16"/><rect x="436" y="7" width="2" height="6"/><rect x="440" y="3" width="2" height="14"/><rect x="444" y="6" width="2" height="8"/><rect x="448" y="4" width="2" height="12"/><rect x="452" y="8" width="2" height="4"/><rect x="456" y="5" width="2" height="10"/><rect x="460" y="3" width="2" height="14"/><rect x="464" y="6" width="2" height="8"/><rect x="468" y="2" width="2" height="16"/><rect x="472" y="7" width="2" height="6"/><rect x="476" y="4" width="2" height="12"/><rect x="480" y="5" width="2" height="10"/><rect x="484" y="3" width="2" height="14"/><rect x="488" y="6" width="2" height="8"/><rect x="492" y="8" width="2" height="4"/><rect x="496" y="4" width="2" height="12"/><rect x="500" y="2" width="2" height="16"/><rect x="504" y="5" width="2" height="10"/><rect x="508" y="7" width="2" height="6"/><rect x="512" y="3" width="2" height="14"/><rect x="516" y="6" width="2" height="8"/><rect x="520" y="8" width="2" height="4"/><rect x="524" y="4" width="2" height="12"/><rect x="528" y="2" width="2" height="16"/><rect x="532" y="6" width="2" height="8"/><rect x="536" y="5" width="2" height="10"/><rect x="540" y="3" width="2" height="14"/><rect x="544" y="7" width="2" height="6"/><rect x="548" y="4" width="2" height="12"/><rect x="552" y="8" width="2" height="4"/><rect x="556" y="6" width="2" height="8"/><rect x="560" y="2" width="2" height="16"/><rect x="564" y="5" width="2" height="10"/><rect x="568" y="3" width="2" height="14"/><rect x="572" y="7" width="2" height="6"/><rect x="576" y="6" width="2" height="8"/><rect x="580" y="4" width="2" height="12"/><rect x="584" y="8" width="2" height="4"/><rect x="588" y="5" width="2" height="10"/><rect x="592" y="3" width="2" height="14"/><rect x="596" y="6" width="2" height="8"/></svg></span>
|
||
<span class="lbl">温柔治愈钢琴 · 0:42(循环 1 次,淡入淡出)</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stage-foot">
|
||
<div class="info"><span class="mono">[ 合成预估 ~30s · 拼接 / 导出全程 0 token · 已结算 ¥1.39 ]</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 (参考布局 v2) ===== -->
|
||
<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 class="ad-tag" 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="ad-lead-wrap">
|
||
<div class="placeholder ad-lead-img" id="asset-detail-lead-img"><span class="ph-frame">立绘</span></div>
|
||
<button class="ad-zoom-btn" type="button" id="asset-detail-zoom-btn" aria-label="查看大图" title="查看大图">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="ad-thumbs" id="asset-detail-thumbs"></div>
|
||
</div>
|
||
<!-- 右栏 · 三视图 + 简介 + 属性 -->
|
||
<div class="asset-detail-right">
|
||
<!-- 三视图 -->
|
||
<div class="ad-section" id="asset-detail-tri-section">
|
||
<div class="asset-detail-section-h">
|
||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
|
||
<span class="t">三视图</span>
|
||
<span class="ad-ratio-chip" id="asset-detail-ratio">16:9</span>
|
||
<button class="ad-icon-btn" type="button" title="下载">
|
||
<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-4M7 10l5 5 5-5M12 15V3"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="asset-detail-tri-row" id="asset-detail-tri">
|
||
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
|
||
</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.5" 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 class="ad-section">
|
||
<div class="asset-detail-section-h">
|
||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></span>
|
||
<span class="t">简介</span>
|
||
</div>
|
||
<p class="ad-intro" id="asset-detail-intro"></p>
|
||
<div class="ad-tags" id="asset-detail-tags"></div>
|
||
</div>
|
||
<!-- 属性表 -->
|
||
<div class="ad-props" id="asset-detail-props"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-modal-f">
|
||
<div class="ad-foot-stats">
|
||
<button class="ad-stat-btn" type="button" id="asset-detail-download">
|
||
<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-4M7 10l5 5 5-5M12 15V3"/></svg>
|
||
下载
|
||
</button>
|
||
</div>
|
||
<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;">(可选 · 16:9 单图)</span></div>
|
||
<div>
|
||
<div class="upload-zone upload-zone-tri" style="aspect-ratio: 16/9;"><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.5" 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 class="ml-canvas" id="ml-canvas" aria-hidden="true">
|
||
<div class="ml-canvas-h">
|
||
<button class="back-btn" type="button" id="ml-canvas-back" aria-label="返回">
|
||
<svg 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>
|
||
<h3 id="ml-canvas-title">添加演员</h3>
|
||
<span class="mono" id="ml-canvas-mono">// 添加演员 · 工作台</span>
|
||
<span style="flex:1;"></span>
|
||
</div>
|
||
<div class="ml-canvas-body">
|
||
<section class="mc-ai">
|
||
<div class="mc-stream" id="mc-stream">
|
||
<div class="mc-stream-inner" id="mc-stream-inner">
|
||
<div class="mc-empty">
|
||
<div class="ic">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>
|
||
</div>
|
||
<span class="badge">// AI · STUDIO</span>
|
||
<h2 id="mc-empty-title">用 AI 生成一位新演员</h2>
|
||
<p id="mc-empty-desc">描述外形 + 风格 + 服装,AI 会同时生成立绘 + 正/侧/背三视图,加入演员库。</p>
|
||
<div class="examples" id="mc-empty-examples"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mc-input-wrap">
|
||
<div class="mc-input">
|
||
<div class="mc-input-top">
|
||
<div class="mc-input-refs" id="mc-input-refs"></div>
|
||
<button class="add-btn" type="button" id="mc-add-btn" title="上传参考图">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||
</button>
|
||
<input type="file" id="mc-ai-ref-input" accept="image/*" multiple hidden>
|
||
</div>
|
||
<textarea id="mc-input-text" rows="1" placeholder="描述外形、风格、服饰…"></textarea>
|
||
<div class="mc-input-bottom">
|
||
<div class="param"><span class="lbl-mono">比例</span><span>3:4</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></div>
|
||
<div class="param"><span class="lbl-mono">风格</span><span>默认</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></div>
|
||
<div class="param"><span class="lbl-mono">张数</span><span>4</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></div>
|
||
<span class="right-meta">预估 <span class="val">¥0.80</span> · 余额 <span class="val">¥327.40</span></span>
|
||
<button class="send-btn" type="button" id="mc-send-btn" disabled title="生成">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="mc-up" id="mc-up" data-kind="actor">
|
||
<div class="mc-up-tabs">
|
||
<button class="mc-up-tab active" type="button" data-tab="ai">AI 生成</button>
|
||
<button class="mc-up-tab" type="button" data-tab="local">本地上传</button>
|
||
</div>
|
||
<div class="mc-up-body">
|
||
<div class="mc-up-section">
|
||
<div class="mc-up-sec-h" id="mc-up-name-label">// 演员姓名</div>
|
||
<input class="mc-up-name" type="text" id="mc-up-name" placeholder="给演员起个名字…" maxlength="20">
|
||
</div>
|
||
<div class="mc-up-section">
|
||
<div class="mc-up-sec-h" id="mc-up-portrait-label">// 演员立绘</div>
|
||
<div class="mc-portrait-ai" data-show="ai">
|
||
<div class="empty" id="mc-portrait-ai-empty">
|
||
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/></svg></div>
|
||
<div class="desc" id="mc-portrait-ai-empty-desc">在左侧 AI 生成后<br>点击想要的立绘添加到这里</div>
|
||
<div class="mono">// 待选中</div>
|
||
</div>
|
||
<div class="picked" id="mc-portrait-ai-picked" hidden>
|
||
<span class="badge">已选用</span>
|
||
<div class="ph-frame" id="mc-portrait-ai-label">立绘</div>
|
||
<div class="ops">
|
||
<button type="button" id="mc-portrait-ai-clear" title="移除">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mc-portrait-local" data-show="local" hidden>
|
||
<div class="drop" id="mc-portrait-local-drop" tabindex="0" role="button" aria-label="点击或拖入立绘">
|
||
<div class="ic"><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 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div>
|
||
<div class="t" id="mc-portrait-local-t">点击或拖入立绘</div>
|
||
<div class="d">支持多张 JPG / PNG / WEBP · ≤ 10MB / 张</div>
|
||
</div>
|
||
<div class="list-h">
|
||
<span>// 已上传</span>
|
||
<span class="ct" id="mc-portrait-local-count">0</span>
|
||
<span>张</span>
|
||
</div>
|
||
<div class="list" id="mc-portrait-local-list"></div>
|
||
<input type="file" id="mc-portrait-local-input" accept="image/*" multiple hidden>
|
||
</div>
|
||
</div>
|
||
<div class="mc-up-section mc-triview" id="mc-triview-sec">
|
||
<div class="mc-up-sec-h">// 三视图</div>
|
||
<div class="result-wrap">
|
||
<div class="result" id="mc-triview-result">
|
||
<div class="ph-frame" id="mc-triview-frame">三视图(正/侧/背)</div>
|
||
<button class="overlay-gen-btn" type="button" id="mc-triview-gen-btn" disabled>
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/></svg>
|
||
生成三视图
|
||
</button>
|
||
<div class="overlay-hint" id="mc-triview-hint">// 先选中左侧 AI 立绘</div>
|
||
</div>
|
||
<div class="result-ops" id="mc-triview-ops" hidden>
|
||
<button type="button" id="mc-triview-rerun"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg> 重跑</button>
|
||
<span class="cost">~¥0.30 / 次</span>
|
||
</div>
|
||
<div class="history" id="mc-triview-history" hidden>
|
||
<div class="h-lbl">// 历史版本 · <span class="ct" id="mc-triview-history-count">0</span> 版</div>
|
||
<div class="h-row" id="mc-triview-history-row"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mc-up-foot">
|
||
<span class="stat" id="mc-up-stat">// 待完成</span>
|
||
<button type="button" class="commit-btn" id="mc-up-commit" disabled>
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||
<span id="mc-up-commit-label">加入演员库</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 离开工作台 · 二次确认弹窗 ===== -->
|
||
<div class="mc-leave-bg" id="mc-leave-bg" aria-hidden="true">
|
||
<div class="mc-leave" role="dialog" aria-modal="true" aria-labelledby="mc-leave-title">
|
||
<div class="lv-h">
|
||
<span class="ic">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z"/></svg>
|
||
</span>
|
||
<h3 id="mc-leave-title">退出工作台?</h3>
|
||
<span class="mono">// UNSAVED</span>
|
||
</div>
|
||
<div class="lv-b" id="mc-leave-body">
|
||
工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「加入」来保留进度。
|
||
</div>
|
||
<div class="lv-f">
|
||
<span class="spacer"></span>
|
||
<button class="btn" type="button" id="mc-leave-cancel">继续编辑</button>
|
||
<button class="btn btn-danger" type="button" id="mc-leave-confirm">不保存,退出</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 添加演员 / 场景 · 选择来源 modal ===== -->
|
||
<div class="ml-up-choice-bg" id="ml-up-choice-bg">
|
||
<div class="ml-up-choice" role="dialog" aria-label="添加来源">
|
||
<div class="uc-h">
|
||
<div class="ic-m">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/><path d="M19 8v6M22 11h-6"/></svg>
|
||
</div>
|
||
<div class="ti">
|
||
<strong id="ml-up-title">添加</strong>
|
||
<span class="mono">// 选择来源 · AI 生成或本地上传</span>
|
||
</div>
|
||
<button class="uc-x" type="button" id="ml-up-x" aria-label="关闭">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="uc-body">
|
||
<button type="button" class="uc-option" id="ml-up-ai">
|
||
<span class="opt-ic">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>
|
||
</span>
|
||
<div class="opt-t">AI 生成</div>
|
||
<div class="opt-d" id="ml-up-ai-desc">描述外形 + 风格,AI 自动生成新形象与三视图</div>
|
||
<span class="opt-tag">[ AI · STUDIO ]</span>
|
||
</button>
|
||
<button type="button" class="uc-option" id="ml-up-local">
|
||
<span class="opt-ic">
|
||
<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 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||
</span>
|
||
<div class="opt-t">本地上传</div>
|
||
<div class="opt-d" id="ml-up-local-desc">上传真人 / 既有素材,后续可生成三视图统一镜头</div>
|
||
<span class="opt-tag">[ UPLOAD ]</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<input type="file" id="ml-up-file" accept="image/*" multiple hidden>
|
||
|
||
<!-- ===== 额度预检 modal · PRD §10.3 四层预检 ===== -->
|
||
<div class="modal-bg" id="quota-bg" onclick="if(event.target===this)Shell.closeModal('quota-bg')">
|
||
<div class="modal" id="quota-modal" style="width: min(440px, 92vw);">
|
||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||
<div class="modal-h">
|
||
<div class="ic-m" id="quota-ic">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
|
||
</div>
|
||
<div class="ti" id="quota-title">额度预检通过<span id="quota-sub">// 4 层检查 · 全部通过</span></div>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div id="quota-stage-row" style="font-size:13px; color:var(--black-alpha-72); margin-bottom: 14px;"></div>
|
||
<div style="display:flex; flex-direction:column; gap:8px;" id="quota-checks">
|
||
<!-- JS 注入 4 行检查 -->
|
||
</div>
|
||
<div id="quota-block-tip" style="display:none; margin-top:14px; padding:10px 12px; background: rgba(235,52,36,.08); border: 1px solid rgba(235,52,36,.24); border-radius: var(--r-sm); font-size:12.5px; color: var(--accent-crimson); line-height:1.5;">
|
||
<strong>任务已拦截</strong> · 余额或额度不足以覆盖本次预估。请联系超管充值,或将团队月限额调高。
|
||
</div>
|
||
</div>
|
||
<div class="modal-f">
|
||
<button class="btn" type="button" onclick="Shell.closeModal('quota-bg')" id="quota-cancel">关闭</button>
|
||
<button class="btn btn-primary" type="button" id="quota-confirm" style="display:none;">确认扣费 · 开始任务</button>
|
||
<a class="btn btn-primary" id="quota-topup" href="account.html" style="display:none;">前往充值</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<script src="assets/icons.js?v=2026052608"></script>
|
||
<script src="assets/shell.js?v=2026052607"></script>
|
||
<script>
|
||
/* ─── 商品名贯穿全流程(从 ?product= 读取,无参数时回退到 mock 默认值)─── */
|
||
const URL_PRODUCT_NAME = (function () {
|
||
try { return decodeURIComponent(new URLSearchParams(location.search).get('product') || ''); }
|
||
catch (e) { return ''; }
|
||
})();
|
||
const URL_PROJECT_VER = (function () {
|
||
try { return new URLSearchParams(location.search).get('v') || 'v3'; }
|
||
catch (e) { return 'v3'; }
|
||
})();
|
||
const CURRENT_PRODUCT_NAME = URL_PRODUCT_NAME || '透真补水面膜';
|
||
// 项目名 = 「{product 简称} · 痛点种草 · v3」 — 简称取末 4 字符作为视觉收敛
|
||
function shortProductName(name) {
|
||
if (name.length <= 5) return name;
|
||
// 尝试匹配常见品类后缀,否则取末 4 字
|
||
const suffixes = ['面膜', '防晒', '口红', '耳机', '速食面', '咖啡', '瑜伽裤', '保温杯'];
|
||
for (const s of suffixes) { if (name.endsWith(s)) return name.slice(-Math.max(s.length + 2, 4)); }
|
||
return name.slice(-4);
|
||
}
|
||
const PROJECT_TITLE = shortProductName(CURRENT_PRODUCT_NAME) + ' · 痛点种草 · ' + URL_PROJECT_VER;
|
||
|
||
Shell.render({
|
||
active: 'projects',
|
||
crumbs: []
|
||
});
|
||
|
||
/* 渲染贯穿商品名 / 项目名 */
|
||
document.getElementById('page-title').textContent = PROJECT_TITLE + ' · 流水线 · Airshelf';
|
||
|
||
(function _injectPipelineTopbarLeft() {
|
||
const topbar = document.querySelector('.topbar');
|
||
const right = topbar?.querySelector('.right');
|
||
if (!topbar || !right) return;
|
||
const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
const title = esc(PROJECT_TITLE);
|
||
const left = document.createElement('div');
|
||
left.className = 'pipeline-topbar-left';
|
||
left.innerHTML = `
|
||
<a class="btn btn-ghost pipeline-back" href="projects.html" aria-label="返回视频项目">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
|
||
返回视频项目
|
||
</a>
|
||
<div class="pipeline-topbar-title" title="${title}">
|
||
${title}<span class="mono">// PIPELINE</span>
|
||
</div>
|
||
`;
|
||
topbar.insertBefore(left, right);
|
||
})();
|
||
|
||
const ProjectStore = (function () {
|
||
const safeId = (PROJECT_TITLE + '|' + CURRENT_PRODUCT_NAME).replace(/[^\w\u4e00-\u9fa5-]+/g, '_');
|
||
const key = 'airshelf:pipeline:' + safeId;
|
||
const defaults = {
|
||
product: CURRENT_PRODUCT_NAME,
|
||
title: PROJECT_TITLE,
|
||
currentStage: 1,
|
||
completedStage: 0,
|
||
fields: {},
|
||
actions: [],
|
||
jobs: {},
|
||
stage1: null,
|
||
stage2: {},
|
||
stage3: null,
|
||
stage4: null,
|
||
updatedAt: Date.now(),
|
||
};
|
||
let data;
|
||
try {
|
||
data = { ...defaults, ...(JSON.parse(localStorage.getItem(key) || '{}') || {}) };
|
||
} catch (e) {
|
||
data = { ...defaults };
|
||
}
|
||
|
||
function save() {
|
||
data.updatedAt = Date.now();
|
||
localStorage.setItem(key, JSON.stringify(data));
|
||
try {
|
||
const indexKey = 'airshelf:pipeline-index';
|
||
const list = JSON.parse(localStorage.getItem(indexKey) || '[]');
|
||
const compact = {
|
||
key,
|
||
title: data.title,
|
||
product: data.product,
|
||
currentStage: data.currentStage,
|
||
completedStage: data.completedStage,
|
||
updatedAt: data.updatedAt,
|
||
runningJobs: Object.values(data.jobs || {})
|
||
.filter(j => j.status === 'running')
|
||
.map(j => ({ stage: j.stage, label: j.label, finishAt: j.finishAt })),
|
||
};
|
||
const next = [compact, ...list.filter(item => item.key !== key)].slice(0, 30);
|
||
localStorage.setItem(indexKey, JSON.stringify(next));
|
||
} catch (e) {}
|
||
}
|
||
|
||
function record(type, detail = {}) {
|
||
data.actions = data.actions || [];
|
||
data.actions.unshift({ type, detail, at: Date.now() });
|
||
data.actions = data.actions.slice(0, 80);
|
||
save();
|
||
}
|
||
|
||
function setStage(n) {
|
||
data.currentStage = Number(n) || 1;
|
||
data.completedStage = Math.max(Number(data.completedStage) || 0, Math.max(0, data.currentStage - 1));
|
||
save();
|
||
}
|
||
|
||
function saveFieldsFrom(root = document) {
|
||
root.querySelectorAll('[id][contenteditable="true"], input[id], textarea[id], select[id]').forEach(el => {
|
||
if (el.type === 'file') return;
|
||
data.fields[el.id] = {
|
||
kind: el.matches('[contenteditable="true"]') ? 'text' : 'value',
|
||
value: el.matches('[contenteditable="true"]') ? el.textContent : el.value,
|
||
};
|
||
});
|
||
save();
|
||
}
|
||
|
||
function restoreFields(root = document) {
|
||
Object.entries(data.fields || {}).forEach(([id, item]) => {
|
||
const el = root.getElementById ? root.getElementById(id) : document.getElementById(id);
|
||
if (!el || item.value == null) return;
|
||
if (item.kind === 'text' && el.matches('[contenteditable="true"]')) el.textContent = item.value;
|
||
else if ('value' in el && el.type !== 'file') el.value = item.value;
|
||
});
|
||
}
|
||
|
||
function startJob(id, payload) {
|
||
data.jobs[id] = { ...payload, status: 'running', startedAt: Date.now(), updatedAt: Date.now() };
|
||
save();
|
||
}
|
||
|
||
function finishJob(id, patch = {}) {
|
||
if (!data.jobs[id]) return;
|
||
data.jobs[id] = { ...data.jobs[id], ...patch, status: 'done', finishedAt: Date.now(), updatedAt: Date.now() };
|
||
save();
|
||
}
|
||
|
||
function getJob(id) { return data.jobs?.[id] || null; }
|
||
function clearJob(id) { if (data.jobs?.[id]) { delete data.jobs[id]; save(); } }
|
||
|
||
function saveStage(name, value) {
|
||
data[name] = value;
|
||
save();
|
||
}
|
||
|
||
window.addEventListener('beforeunload', () => saveFieldsFrom());
|
||
document.addEventListener('input', (e) => {
|
||
if (e.target.closest('[contenteditable="true"], input[id], textarea[id], select[id]')) {
|
||
saveFieldsFrom();
|
||
}
|
||
});
|
||
|
||
return { key, data, save, record, setStage, saveFieldsFrom, restoreFields, startJob, finishJob, getJob, clearJob, saveStage };
|
||
})();
|
||
|
||
/* ─── 把 stage-pill anchor 注入 .topbar 中部(圆点全状态都靠 .sp-dot 实时同步)─── */
|
||
(function _injectStagePill() {
|
||
const anchor = document.getElementById('stage-pill-anchor');
|
||
const topbar = document.querySelector('.topbar');
|
||
if (!anchor || !topbar) return;
|
||
anchor.classList.add('stage-pill');
|
||
anchor.classList.remove('stage-pill-anchor');
|
||
anchor.removeAttribute('hidden');
|
||
anchor.id = 'stage-pill';
|
||
topbar.appendChild(anchor); // position: absolute · 自动锚定 topbar 中部
|
||
})();
|
||
|
||
/* ─── Stage 1 拖拽分隔条 · 控制脚本助手宽度 · clamp 在 [380, 680] ─── */
|
||
(function _setupStageScriptGutter() {
|
||
const gutter = document.getElementById('stage-script-gutter');
|
||
const grid = document.querySelector('.stage-script');
|
||
if (!gutter || !grid) return;
|
||
const MIN = 380, MAX = 680;
|
||
let dragging = false;
|
||
gutter.addEventListener('mousedown', (e) => {
|
||
dragging = true;
|
||
gutter.classList.add('dragging');
|
||
document.body.style.cursor = 'col-resize';
|
||
document.body.style.userSelect = 'none';
|
||
e.preventDefault();
|
||
});
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!dragging) return;
|
||
const rect = grid.getBoundingClientRect();
|
||
const newRight = rect.right - e.clientX;
|
||
const clamped = Math.max(MIN, Math.min(MAX, newRight));
|
||
grid.style.setProperty('--chat-w', clamped + 'px');
|
||
});
|
||
document.addEventListener('mouseup', () => {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
gutter.classList.remove('dragging');
|
||
document.body.style.cursor = '';
|
||
document.body.style.userSelect = '';
|
||
});
|
||
})();
|
||
|
||
// hash routing
|
||
function activateStage(n) {
|
||
const cur = Number(n);
|
||
document.querySelectorAll('.stage').forEach(s => s.classList.remove('active'));
|
||
document.querySelector(`[data-stage-pane="${cur}"]`)?.classList.add('active');
|
||
ProjectStore.setStage(cur);
|
||
|
||
// 圆点状态:< cur → done(森林绿) · = cur → active(橙实心+光晕) · > cur → 默认(浅灰)
|
||
const completed = Math.max(Number(ProjectStore.data.completedStage) || 0, cur - 1);
|
||
document.querySelectorAll('#stage-pill .sp-dot').forEach(s => {
|
||
const i = +s.dataset.stage;
|
||
s.classList.remove('active', 'done');
|
||
if (i === cur) s.classList.add('active');
|
||
else if (i <= completed) s.classList.add('done');
|
||
});
|
||
// 连接线 · idx+1 < cur 时染森林绿
|
||
document.querySelectorAll('#stage-pill .sp-line').forEach((ln, idx) => {
|
||
ln.classList.toggle('done', (idx + 1) <= completed);
|
||
});
|
||
|
||
// 全高度布局:所有 stage 操作模块 hug content、内容区域 fill content
|
||
// 全部走 flat(像 model-photo 那样的全宽扁平布局,去卡片)
|
||
const contentEl = document.getElementById('page-content');
|
||
if (contentEl) {
|
||
contentEl.classList.add('content--fh');
|
||
contentEl.classList.add('content--fh-flat');
|
||
}
|
||
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
requestAnimationFrame(() => ProjectStore.restoreFields());
|
||
}
|
||
function readHash() {
|
||
const m = location.hash.match(/stage-(\d)/);
|
||
if (m) { activateStage(+m[1]); return; }
|
||
// ?stage=N query 参数也接收
|
||
const q = new URLSearchParams(location.search);
|
||
const s = q.get('stage');
|
||
if (s) { activateStage(+s); return; }
|
||
// 兜底:回到上次离开的 stage,等待生成时离开页面也能继续当前项目进度
|
||
activateStage(ProjectStore.data.currentStage || 1);
|
||
}
|
||
window.addEventListener('hashchange', readHash);
|
||
readHash();
|
||
|
||
/* ============================================================
|
||
STAGE 1 · 脚本助手 + 镜头脚本 状态驱动
|
||
============================================================ */
|
||
const Stage1 = (function () {
|
||
let shots = []; // [{ id, painting, dialog, duration }]
|
||
let chatMsgs = []; // [{ role, html, time }]
|
||
let mode = null;
|
||
let scriptTags = { char: [], scene: [] }; // 自动抓取的人物 / 场景 · 可编辑
|
||
const MODE_LABELS = { ai: 'AI 全生', theme: '一句话主题', manual: '自带脚本' };
|
||
const MODE_USER_COPY = {
|
||
ai: '我用 AI 全生',
|
||
theme: '我用一句话主题',
|
||
manual: '我用自带脚本',
|
||
};
|
||
const BRIEF_OPTIONS = {
|
||
style: ['真实测评', '痛点种草', '小红书种草', '开箱测评', '对比展示'],
|
||
persona: ['通勤敏感肌女生', '熬夜党通勤女性', '学生党女生', '精致宝妈', '成分党用户'],
|
||
};
|
||
function makeBrief(nextMode) {
|
||
if (!nextMode) {
|
||
return {
|
||
source: '未选择',
|
||
style: '待确认',
|
||
persona: '待确认',
|
||
styleNote: '选择脚本来源后由助手推荐',
|
||
personaNote: '选择脚本来源后由助手推荐',
|
||
};
|
||
}
|
||
return {
|
||
source: MODE_LABELS[nextMode] || '未选择',
|
||
style: nextMode === 'theme' ? '痛点种草' : '真实测评',
|
||
persona: '通勤敏感肌女生',
|
||
styleNote: nextMode === 'manual' ? '参考文本识别不足 · 根据商品类目推荐' : '根据商品信息推荐',
|
||
personaNote: '根据商品目标人群推荐',
|
||
};
|
||
}
|
||
let scriptBrief = makeBrief(null);
|
||
|
||
const $cb = () => document.getElementById('chat-body');
|
||
const $sb = () => document.getElementById('shots-body');
|
||
const $sm = () => document.getElementById('shots-meta');
|
||
const safeHtml = s => String(s || '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]));
|
||
function renderBriefSummary() {
|
||
const source = document.getElementById('brief-source');
|
||
const style = document.getElementById('brief-style');
|
||
const persona = document.getElementById('brief-persona');
|
||
if (source) source.textContent = scriptBrief.source || '未选择';
|
||
if (style) style.textContent = scriptBrief.style || '待确认';
|
||
if (persona) persona.textContent = scriptBrief.persona || '待确认';
|
||
}
|
||
function briefConfirmHtml(intro) {
|
||
const optionMenu = (kind, current) => `
|
||
<span class="script-brief-select">
|
||
<button class="script-brief-value" type="button" data-brief-act="${kind}" aria-haspopup="listbox" aria-expanded="false" aria-label="选择${kind === 'style' ? '脚本风格' : '人物设定'}">
|
||
<span class="v">${safeHtml(current)}</span>
|
||
</button>
|
||
<span class="chip-menu align-right" role="listbox">
|
||
${BRIEF_OPTIONS[kind].map(opt => `
|
||
<button class="mi${opt === current ? ' selected' : ''}" type="button" data-brief-pick="${kind}" data-value="${safeHtml(opt)}" role="option" aria-selected="${opt === current ? 'true' : 'false'}">
|
||
<svg class="mi-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m20 6-11 11-5-5"/></svg>
|
||
${safeHtml(opt)}
|
||
</button>`).join('')}
|
||
</span>
|
||
</span>`;
|
||
return `${intro}
|
||
<div class="script-brief-card">
|
||
<div class="script-brief-row">
|
||
<span class="k">风格</span>
|
||
${optionMenu('style', scriptBrief.style)}
|
||
<span class="why">${safeHtml(scriptBrief.styleNote)}</span>
|
||
</div>
|
||
<div class="script-brief-row">
|
||
<span class="k">人物</span>
|
||
${optionMenu('persona', scriptBrief.persona)}
|
||
<span class="why">${safeHtml(scriptBrief.personaNote)}</span>
|
||
</div>
|
||
<div class="script-brief-actions">
|
||
<div class="action-row">
|
||
<button class="btn btn-ghost" type="button" data-brief-act="reroll">重新推荐</button>
|
||
<button class="btn" type="button" data-brief-act="accept">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
function migrateBriefMessages() {
|
||
let changed = false;
|
||
chatMsgs = chatMsgs.map(msg => {
|
||
if (!msg || typeof msg.html !== 'string') return msg;
|
||
if (msg.role === 'ai' && /script-brief-card/.test(msg.html)) {
|
||
const cardIndex = msg.html.indexOf('<div class="script-brief-card"');
|
||
const intro = cardIndex > -1 ? msg.html.slice(0, cardIndex).trim() : '';
|
||
changed = true;
|
||
return {
|
||
...msg,
|
||
html: briefConfirmHtml(intro || '已更新创作方向。确认后我会按这个方向重写镜头脚本。'),
|
||
};
|
||
}
|
||
if (msg.role === 'user' && msg.html === '保持推荐,生成镜头脚本') {
|
||
changed = true;
|
||
return { ...msg, html: '确定' };
|
||
}
|
||
return msg;
|
||
});
|
||
return changed;
|
||
}
|
||
function tuneBriefByText(text) {
|
||
if (/小红书|种草/.test(text)) scriptBrief.style = '小红书种草';
|
||
else if (/测评|评测/.test(text)) scriptBrief.style = '真实测评';
|
||
else if (/痛点/.test(text)) scriptBrief.style = '痛点种草';
|
||
if (/熬夜/.test(text)) scriptBrief.persona = '熬夜党通勤女性';
|
||
else if (/学生/.test(text)) scriptBrief.persona = '学生党女生';
|
||
else if (/宝妈|妈妈/.test(text)) scriptBrief.persona = '精致宝妈';
|
||
}
|
||
function startScriptGeneration() {
|
||
ProjectStore.startJob('stage1-script', {
|
||
stage: 1,
|
||
label: '脚本初稿生成',
|
||
finishAt: Date.now() + 6500,
|
||
});
|
||
if (!chatMsgs.some(x => /正在解析商品卖点/.test(x.html))) {
|
||
pushMsg('ai', '<span class="ai-thinking">正在解析商品卖点与创作方向 <span class="dots"><span></span><span></span><span></span></span></span>');
|
||
}
|
||
ProjectStore.record('stage1.script.generate', { mode, brief: scriptBrief });
|
||
saveState();
|
||
renderChat();
|
||
window.setTimeout(completeAiJob, 6500);
|
||
}
|
||
function refreshLatestBriefMessage(intro) {
|
||
for (let i = chatMsgs.length - 1; i >= 0; i--) {
|
||
if (chatMsgs[i].role === 'ai' && /script-brief-card/.test(chatMsgs[i].html)) {
|
||
chatMsgs[i].html = briefConfirmHtml(intro);
|
||
return;
|
||
}
|
||
}
|
||
pushMsg('ai', briefConfirmHtml(intro));
|
||
}
|
||
function handleBriefPick(kind, value) {
|
||
if (!value) return;
|
||
if (kind === 'style') {
|
||
scriptBrief.style = value;
|
||
scriptBrief.styleNote = '已手动选择';
|
||
} else if (kind === 'persona') {
|
||
scriptBrief.persona = value;
|
||
scriptBrief.personaNote = '已手动选择';
|
||
}
|
||
refreshLatestBriefMessage('已更新创作方向。确认后我会按这个方向重写镜头脚本。');
|
||
ProjectStore.record('stage1.brief.option.selected', { kind, value });
|
||
saveState();
|
||
renderChat();
|
||
}
|
||
function handleBriefAction(action, trigger) {
|
||
if (action === 'accept') {
|
||
pushMsg('user', '确定');
|
||
startScriptGeneration();
|
||
} else if (action === 'reroll') {
|
||
scriptBrief = {
|
||
...scriptBrief,
|
||
style: scriptBrief.style === '真实测评' ? '痛点种草' : '真实测评',
|
||
persona: scriptBrief.persona === '通勤敏感肌女生' ? '熬夜党通勤女性' : '通勤敏感肌女生',
|
||
styleNote: '已根据商品卖点重新推荐',
|
||
personaNote: '已根据使用场景重新推荐',
|
||
};
|
||
pushMsg('ai', briefConfirmHtml('我重新给你配了一组创作方向,确认后就生成镜头脚本。'));
|
||
saveState();
|
||
renderChat();
|
||
} else if (action === 'style') {
|
||
const box = trigger?.closest('.script-brief-select');
|
||
if (!box) return;
|
||
document.querySelectorAll('.script-brief-select.open').forEach(el => { if (el !== box) el.classList.remove('open'); });
|
||
box.classList.toggle('open');
|
||
trigger.setAttribute('aria-expanded', box.classList.contains('open') ? 'true' : 'false');
|
||
} else if (action === 'persona') {
|
||
const box = trigger?.closest('.script-brief-select');
|
||
if (!box) return;
|
||
document.querySelectorAll('.script-brief-select.open').forEach(el => { if (el !== box) el.classList.remove('open'); });
|
||
box.classList.toggle('open');
|
||
trigger.setAttribute('aria-expanded', box.classList.contains('open') ? 'true' : 'false');
|
||
}
|
||
}
|
||
|
||
/* 自动从脚本(painting + dialog 文本)抽取人物 / 场景关键词 · 白名单匹配 */
|
||
const CHAR_KEYWORDS = ['女主', '男主', '同事', '闺蜜', '男友', '女友', '妈妈', '爸爸', '老师', '同学', '朋友', '路人', '主播', '老板'];
|
||
const SCENE_KEYWORDS = ['书桌', '卫生间', '床头', '化妆台', '办公桌', '客厅', '厨房', '卧室', '阳台', '电梯', '咖啡店', '公司', '镜前', '桌面', '会议室', '车里', '公园', '商场'];
|
||
function extractScriptTags() {
|
||
const text = shots.map(s => (s.painting || '') + ' ' + (s.dialog || '')).join(' ');
|
||
const dedup = arr => Array.from(new Set(arr));
|
||
const exist = { char: new Set(scriptTags.char), scene: new Set(scriptTags.scene) };
|
||
const newChar = CHAR_KEYWORDS.filter(k => text.includes(k) && !exist.char.has(k));
|
||
const newScene = SCENE_KEYWORDS.filter(k => text.includes(k) && !exist.scene.has(k));
|
||
scriptTags.char = dedup([...scriptTags.char, ...newChar]);
|
||
scriptTags.scene = dedup([...scriptTags.scene, ...newScene]);
|
||
}
|
||
function renderScriptTags() {
|
||
['char', 'scene'].forEach(kind => {
|
||
const group = document.querySelector(`.script-tags .tag-group[data-kind="${kind}"]`);
|
||
if (!group) return;
|
||
const addBtn = group.querySelector('.tag-add');
|
||
group.querySelectorAll('.script-tag').forEach(el => el.remove());
|
||
scriptTags[kind].forEach((name, i) => {
|
||
const chip = document.createElement('span');
|
||
chip.className = 'script-tag';
|
||
chip.innerHTML = `<span class="t" contenteditable="true" spellcheck="false" data-i="${i}">${name}</span><button class="x" type="button" aria-label="删除">×</button>`;
|
||
group.insertBefore(chip, addBtn);
|
||
});
|
||
group.querySelectorAll('.script-tag').forEach((chip, i) => {
|
||
const t = chip.querySelector('.t');
|
||
const x = chip.querySelector('.x');
|
||
x.addEventListener('click', (e) => { e.stopPropagation(); scriptTags[kind].splice(i, 1); saveState(); renderScriptTags(); });
|
||
t.addEventListener('blur', () => {
|
||
const v = (t.textContent || '').trim();
|
||
if (!v) { scriptTags[kind].splice(i, 1); renderScriptTags(); }
|
||
else { scriptTags[kind][i] = v; }
|
||
saveState();
|
||
});
|
||
t.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); t.blur(); } });
|
||
});
|
||
});
|
||
}
|
||
function bindTagAdders() {
|
||
document.querySelectorAll('.script-tags .tag-add').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const kind = btn.parentElement.dataset.kind;
|
||
scriptTags[kind].push('');
|
||
saveState();
|
||
renderScriptTags();
|
||
const group = document.querySelector(`.script-tags .tag-group[data-kind="${kind}"]`);
|
||
const chips = group.querySelectorAll('.script-tag .t');
|
||
const last = chips[chips.length - 1];
|
||
if (last) {
|
||
last.focus();
|
||
const sel = window.getSelection();
|
||
const range = document.createRange();
|
||
range.selectNodeContents(last);
|
||
sel.removeAllRanges(); sel.addRange(range);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
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 saveState() {
|
||
ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags, scriptBrief });
|
||
renderBriefSummary();
|
||
}
|
||
function loadState() {
|
||
const saved = ProjectStore.data.stage1;
|
||
if (!saved) { renderBriefSummary(); return; }
|
||
if (Array.isArray(saved.shots)) shots = saved.shots;
|
||
if (Array.isArray(saved.chatMsgs)) chatMsgs = saved.chatMsgs;
|
||
if (saved.mode) mode = saved.mode;
|
||
if (saved.scriptBrief && typeof saved.scriptBrief === 'object') scriptBrief = { ...scriptBrief, ...saved.scriptBrief };
|
||
else if (mode) scriptBrief = makeBrief(mode);
|
||
if (saved.scriptTags && Array.isArray(saved.scriptTags.char) && Array.isArray(saved.scriptTags.scene)) {
|
||
scriptTags = saved.scriptTags;
|
||
}
|
||
if (migrateBriefMessages()) {
|
||
ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags, scriptBrief });
|
||
}
|
||
renderBriefSummary();
|
||
}
|
||
function getDefaultDraft() {
|
||
return [
|
||
{ id: 'sh1', painting: '中景慢推 · 深夜居家书桌全景。屏幕仍亮着 PPT,女主背影瘫在椅子上,屏幕冷光 + 台灯暖光对比。字幕"凌晨 02:14"淡入。', dialog: '(无台词 · BGM 渐起)', duration: 5 },
|
||
{ id: 'sh2', painting: '近景 · 卫生间镜前。女主低头看脸,T 区起皮、暗沉特写,冷白灯偏惨。', dialog: '"做完这版稿又是凌晨两点……(叹气)脸已经不能看了。"', duration: 5 },
|
||
{ id: 'sh3', painting: '俯拍特写 · 回到书桌,拉开抽屉。囤好的透真补水面膜露半角,手伸进去抽出一片。', dialog: '"还好抽屉里囤了透真玻尿酸面膜。"', duration: 5 },
|
||
{ id: 'sh4', painting: '桌面微距特写 · 撕开锡纸包装的瞬间。30g 厚精华液缓缓滴落,面膜布展开,质地拉丝可见。', dialog: '"30g 一片,精华液比普通面膜厚整整三倍。"', duration: 6 },
|
||
{ id: 'sh5', painting: '床头近景 · 女主敷好面膜闭眼躺下,台灯暖光打在脸侧。膜布贴合脸型,边缘服帖。', dialog: '"贴上去那一瞬间 —— 凉凉的,像把皮肤泡了一次澡。"', duration: 6 },
|
||
{ id: 'sh6', painting: '中景 · 第二天清晨化妆台。阳光透过窗帘,女主对镜上妆,皮肤透亮、粉底服帖。同事画外音"你最近用啥了"。', dialog: '"第二天脸是软的,粉底都不卡了。同事都跑来问。"', duration: 8 },
|
||
{ id: 'sh7', painting: '平铺俯拍 · 桌面五片装产品 + 单片包装。价格 "618 · 5 片 ¥39.9" 弹出,购物车图标右下角浮现。', dialog: '"618 五片 39.9,自用送人都合适。链接放评论区。"', duration: 5 },
|
||
];
|
||
}
|
||
function completeAiJob() {
|
||
const existing = new Set(shots.map(s => s.id));
|
||
getDefaultDraft().forEach(s => {
|
||
if (!existing.has(s.id)) shots.push(s);
|
||
});
|
||
chatMsgs = chatMsgs.filter(x => !(x.role === 'ai' && /ai-thinking/.test(x.html)));
|
||
if (!chatMsgs.some(x => x.html.includes('初稿完成'))) {
|
||
pushMsg('ai', '初稿完成。点击任意卡片文字可直接编辑;鼠标移到卡片之间会出现「+ 添加分镜」。');
|
||
}
|
||
ProjectStore.finishJob('stage1-script');
|
||
ProjectStore.record('stage1.script.ready', { shots: shots.length });
|
||
saveState();
|
||
renderChat();
|
||
renderShots();
|
||
}
|
||
function resumeAiJobIfNeeded() {
|
||
const job = ProjectStore.getJob('stage1-script');
|
||
if (!job || job.status !== 'running') return;
|
||
const remaining = Math.max(0, (job.finishAt || Date.now()) - Date.now());
|
||
if (!chatMsgs.some(x => /ai-thinking/.test(x.html))) {
|
||
pushMsg('ai', '<span class="ai-thinking">脚本生成仍在后台排队 <span class="dots"><span></span><span></span><span></span></span></span>');
|
||
}
|
||
saveState();
|
||
renderChat();
|
||
window.setTimeout(completeAiJob, remaining);
|
||
}
|
||
|
||
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.5" 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.5" 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.5" 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.querySelectorAll('[data-brief-act]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
handleBriefAction(btn.dataset.briefAct, btn);
|
||
});
|
||
});
|
||
body.querySelectorAll('[data-brief-pick]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
handleBriefPick(btn.dataset.briefPick, btn.dataset.value);
|
||
});
|
||
});
|
||
body.scrollTop = body.scrollHeight;
|
||
}
|
||
|
||
function renderShots() {
|
||
const body = $sb(); if (!body) return;
|
||
const meta = $sm();
|
||
const phMeta = document.getElementById('page-head-shots-meta'); // 头部已移除,可能为 null · 下方写入全程 ?. 防御
|
||
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 = '待生成镜头脚本';
|
||
renderScriptTags();
|
||
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="1.5" 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';
|
||
ProjectStore.record('stage1.shot.edited', { id, field });
|
||
saveState();
|
||
});
|
||
});
|
||
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);
|
||
ProjectStore.record('stage1.shot.deleted', { id });
|
||
saveState();
|
||
renderShots();
|
||
} else if (act === 'regen') {
|
||
Shell.toast('已请求重写本场', '↻ shot-' + id);
|
||
ProjectStore.record('stage1.shot.regen', { 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 });
|
||
ProjectStore.record('stage1.shot.added', { after });
|
||
saveState();
|
||
renderShots();
|
||
}
|
||
});
|
||
});
|
||
// 镜头有变,刷新人物/场景标签(增量抽取 · 已有的保留)
|
||
extractScriptTags();
|
||
renderScriptTags();
|
||
}
|
||
|
||
function pickMode(m) {
|
||
mode = m;
|
||
scriptBrief = makeBrief(m);
|
||
ProjectStore.record('stage1.mode.selected', { mode: m });
|
||
pushMsg('user', MODE_USER_COPY[m] || '选择脚本来源');
|
||
if (m === 'ai') {
|
||
pushMsg('ai', briefConfirmHtml('我会根据商品信息直接生成第一版。生成前先确认创作方向。'));
|
||
} else if (m === 'theme') {
|
||
pushMsg('ai', '好,请给我一句话主题(5-30 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>我会先根据这句话补全风格和人物设定,再让你确认。');
|
||
} else if (m === 'manual') {
|
||
pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框。我会先识别风格和人物设定;识别不到的部分,再用商品信息补齐。');
|
||
}
|
||
saveState();
|
||
renderChat();
|
||
}
|
||
|
||
function init() {
|
||
loadState();
|
||
renderChat();
|
||
renderShots();
|
||
resumeAiJobIfNeeded();
|
||
ProjectStore.restoreFields();
|
||
document.getElementById('chat-clear-btn')?.addEventListener('click', () => {
|
||
chatMsgs = []; mode = null; shots = []; scriptTags = { char: [], scene: [] };
|
||
scriptBrief = makeBrief(null);
|
||
ProjectStore.clearJob('stage1-script');
|
||
ProjectStore.record('stage1.cleared');
|
||
saveState();
|
||
renderChat(); renderShots();
|
||
});
|
||
bindTagAdders();
|
||
document.getElementById('chat-regen-btn')?.addEventListener('click', () => {
|
||
Shell.toast('已请求整体重写', 'POST /script/regen');
|
||
});
|
||
document.addEventListener('click', (e) => {
|
||
if (e.target.closest('.script-brief-select')) return;
|
||
document.querySelectorAll('.script-brief-select.open').forEach(el => el.classList.remove('open'));
|
||
});
|
||
const sendBtn = document.getElementById('chat-send-btn');
|
||
const ta = document.getElementById('chat-textarea');
|
||
const attachRow = document.getElementById('chat-attach-row');
|
||
let attachments = [];
|
||
const renderAttach = () => {
|
||
if (!attachRow) return;
|
||
if (!attachments.length) { attachRow.hidden = true; attachRow.innerHTML = ''; return; }
|
||
attachRow.hidden = false;
|
||
attachRow.innerHTML = attachments.map((f, i) => `
|
||
<span class="chat-attach-chip">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>
|
||
${f.name.replace(/</g, '<')}
|
||
<button class="x" data-rm="${i}" aria-label="移除">
|
||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</span>`).join('');
|
||
attachRow.querySelectorAll('button[data-rm]').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
attachments.splice(+b.dataset.rm, 1);
|
||
renderAttach();
|
||
});
|
||
});
|
||
};
|
||
const upBtn = document.getElementById('chat-upload-btn');
|
||
const upInput = document.getElementById('chat-upload-input');
|
||
if (upBtn && upInput) {
|
||
upBtn.addEventListener('click', () => upInput.click());
|
||
upInput.addEventListener('change', (e) => {
|
||
const files = Array.from(e.target.files || []);
|
||
if (!files.length) return;
|
||
attachments.push(...files);
|
||
renderAttach();
|
||
Shell.toast('已附加脚本文件', files.map(f => f.name).join('、'));
|
||
upInput.value = '';
|
||
});
|
||
}
|
||
if (sendBtn && ta) {
|
||
const send = () => {
|
||
const v = ta.value.trim();
|
||
if (!v && !attachments.length) return;
|
||
const fileTags = attachments.length
|
||
? `<div class="hstack" style="gap:6px; flex-wrap:wrap; margin-bottom:6px;">${attachments.map(f => `<span class="pill" style="font-family:var(--font-mono); font-size:10.5px;">📎 ${f.name.replace(/</g, '<')}</span>`).join('')}</div>`
|
||
: '';
|
||
pushMsg('user', fileTags + (v ? v.replace(/</g, '<') : '<span class="muted-2">(已附加文件)</span>'));
|
||
const fileCt = attachments.length;
|
||
ta.value = '';
|
||
attachments = []; renderAttach();
|
||
ProjectStore.record('stage1.chat.sent', { hasText: !!v, files: fileCt });
|
||
saveState();
|
||
renderChat();
|
||
setTimeout(() => {
|
||
const directionIntent = /小红书|种草|测评|评测|痛点|熬夜|学生|宝妈|妈妈|人物|风格|口吻|换成/.test(v);
|
||
if (mode && (!shots.length || directionIntent)) {
|
||
tuneBriefByText(v);
|
||
const intro = directionIntent
|
||
? '已更新创作方向。确认后我会按这个方向重写镜头脚本。'
|
||
: '我先根据你给的内容补全创作方向,确认后再生成镜头脚本。';
|
||
pushMsg('ai', briefConfirmHtml(intro));
|
||
} else {
|
||
pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');
|
||
}
|
||
saveState();
|
||
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: '通勤白领',
|
||
gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '中等', build: '纤细',
|
||
hairLen: '短发', hairColor: '黑色', vibe: '清新', feature: '邻家气质 · 微笑亲和' },
|
||
{ id: 'm2', name: '甜美长发女', sub: '学生党',
|
||
gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '偏小', build: '纤细',
|
||
hairLen: '长发', hairColor: '深棕', vibe: '甜美', feature: '校园风 · 书卷气重' },
|
||
{ id: 'm3', name: '商务套装男', sub: '总裁 IP',
|
||
gender: '男', age: '中年', region: '东亚', skin: '健康', height: '偏高', build: '标准',
|
||
hairLen: '短发', hairColor: '黑色', vibe: '稳重', feature: '商务精英范 · 西装常驻' },
|
||
{ id: 'm4', name: '宝妈居家女', sub: '家庭决策',
|
||
gender: '女', age: '中年', region: '东亚', skin: '白皙', height: '中等', build: '标准',
|
||
hairLen: '中发', hairColor: '棕黑', vibe: '温柔', feature: '居家氛围 · 决策力强' },
|
||
{ id: 'm5', name: '运动健身女', sub: '健身博主',
|
||
gender: '女', age: '青年', region: '东亚', skin: '健康', height: '偏高', build: '运动',
|
||
hairLen: '中发', hairColor: '栗色', vibe: '活力', feature: '马尾 · 健身房常客' },
|
||
{ id: 'm6', name: '少年学生男', sub: 'Z 世代',
|
||
gender: '男', age: '青年', region: '东亚', skin: '白皙', height: '中等', build: '纤细',
|
||
hairLen: '短发', hairColor: '黑色', vibe: '阳光', feature: '校服感 · 朝气十足' },
|
||
];
|
||
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: '氛围户外' },
|
||
];
|
||
|
||
// 商品名 / 项目名已提升到 page 顶层 script(see CURRENT_PRODUCT_NAME / PROJECT_TITLE)
|
||
// 此处通过闭包引用即可
|
||
|
||
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 _hashCode(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }
|
||
function _fmtAssetId(name, kind) {
|
||
const seed = _hashCode(name);
|
||
const code = String(seed % 1000).padStart(3, '0');
|
||
return 'ASSET-20240520-' + (kind === 'model' ? 'M' : kind === 'scene' ? 'S' : 'P') + code;
|
||
}
|
||
function _fmtFileSize(name) { const seed = _hashCode(name); return (4 + (seed % 100) / 10).toFixed(1) + 'MB'; }
|
||
function _fmtFavCt(name) { return String(8 + _hashCode(name) % 80); }
|
||
function _fmtDlCt(name) { const n = 200 + _hashCode(name) % 1800; return (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)); }
|
||
|
||
function renderAssetDetail(payload) {
|
||
// payload: { title, tagText, leadLabel, kind ('actor'|'scene'|'product'), ratio, intro, tags[], thumbs[], props[], hasTri, missingTriHint, applyLabel, ownPortraits[], ownTriVersions[] }
|
||
document.getElementById('asset-detail-title').textContent = payload.title;
|
||
document.getElementById('asset-detail-kind').textContent = '/ ' + payload.tagText;
|
||
|
||
// 主立绘 · 用户上传有 portraits[0].url 时显示真实图片;否则占位
|
||
const leadEl = document.getElementById('asset-detail-lead-img');
|
||
// 把当前主图 src/name 挂在 leadEl 的 dataset 上,zoom 按钮读 dataset 始终拿到最新值
|
||
function _adSetLead(p) {
|
||
if (!leadEl) return;
|
||
const old = leadEl.querySelector('img.ad-lead-pic');
|
||
if (old) old.remove();
|
||
const ph = leadEl.querySelector('.ph-frame');
|
||
if (p && p.url) {
|
||
if (ph) ph.style.display = 'none';
|
||
const img = document.createElement('img');
|
||
img.className = 'ad-lead-pic'; img.src = p.url; img.alt = p.name || payload.title;
|
||
img.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;';
|
||
leadEl.appendChild(img);
|
||
leadEl.dataset.curSrc = p.url; leadEl.dataset.curName = p.name || payload.title;
|
||
} else {
|
||
if (ph) { ph.style.display = ''; ph.textContent = (p && p.label) || payload.leadLabel || payload.title; }
|
||
leadEl.dataset.curSrc = ''; leadEl.dataset.curName = (p && p.label) || payload.leadLabel || payload.title;
|
||
}
|
||
}
|
||
leadEl.innerHTML = `<span class="ph-frame">${payload.leadLabel || payload.title}</span>`;
|
||
// 立绘 zoom 按钮 → 打开 lightbox(单次绑定,值从 dataset 读)
|
||
const _leadZoomBtn = document.getElementById('asset-detail-zoom-btn');
|
||
if (_leadZoomBtn && !_leadZoomBtn.dataset.bound) {
|
||
_leadZoomBtn.dataset.bound = '1';
|
||
_leadZoomBtn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
if (window.Shell?._openLightbox) Shell._openLightbox(leadEl.dataset.curSrc || '', leadEl.dataset.curName || '');
|
||
});
|
||
}
|
||
|
||
// 缩略图 strip · 用户上传有 ownPortraits 数组 → 显示多张真实图;平台预设 → 仅 1 张占位
|
||
const ownPortraits = (payload.ownPortraits && payload.ownPortraits.length) ? payload.ownPortraits : null;
|
||
const thumbs = ownPortraits || (payload.thumbs && payload.thumbs.length ? payload.thumbs.map(t => ({ label: t })) : [{ label: payload.kind === 'scene' ? '场景' : '立绘' }]);
|
||
const thumbsEl = document.getElementById('asset-detail-thumbs');
|
||
thumbsEl.innerHTML = thumbs.map((p, i) => {
|
||
const inner = p.url
|
||
? `<img src="${p.url}" alt="${(p.name||'').replace(/"/g,'"')}" style="width:100%;height:100%;object-fit:cover;display:block;">`
|
||
: `<span class="ph-frame">${p.label || ('v'+(i+1))}</span>`;
|
||
return `<div class="thumb placeholder${i === 0 ? ' active' : ''}" data-idx="${i}">${inner}</div>`;
|
||
}).join('');
|
||
_adSetLead(thumbs[0]);
|
||
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
|
||
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
_adSetLead(thumbs[+t.dataset.idx]);
|
||
}));
|
||
|
||
// 三视图区
|
||
const tri = document.getElementById('asset-detail-tri');
|
||
const triSection = document.getElementById('asset-detail-tri-section');
|
||
const tip = document.getElementById('asset-detail-tip');
|
||
const ratioChip = document.getElementById('asset-detail-ratio');
|
||
const ownTri = (payload.ownTriVersions && payload.ownTriVersions.length) ? payload.ownTriVersions : null;
|
||
if (payload.kind === 'scene') {
|
||
triSection.style.display = 'none';
|
||
} else if (payload.kind === 'actor') {
|
||
triSection.style.display = '';
|
||
tri.style.display = '';
|
||
tri.classList.remove('actor');
|
||
ratioChip.textContent = '16:9';
|
||
tip.style.display = 'none';
|
||
const _zoomBtn = `<button class="ad-zoom-btn" type="button" data-zoom-tri aria-label="查看大图" title="查看大图"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg></button>`;
|
||
if (ownTri) {
|
||
const cur = ownTri[ownTri.length - 1];
|
||
const stripHtml = ownTri.map((v, i) => `<div class="v-thumb${i === ownTri.length - 1 ? ' active' : ''}" data-idx="${i}" title="${v.label} · ${v.ts}"><span class="v">${v.label}</span></div>`).join('');
|
||
tri.innerHTML = `
|
||
<div class="placeholder"><span class="ph-frame" id="ad-tri-main-lbl">${payload.title} · ${cur.label} · ${cur.ts}</span>${_zoomBtn}</div>
|
||
<div class="md-view-versions" style="margin-top:8px;">${stripHtml}</div>`;
|
||
const mainLbl = tri.querySelector('#ad-tri-main-lbl');
|
||
tri.querySelectorAll('.v-thumb').forEach(t => t.addEventListener('click', () => {
|
||
tri.querySelectorAll('.v-thumb').forEach(x => x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
const v = ownTri[+t.dataset.idx];
|
||
if (mainLbl) mainLbl.textContent = `${payload.title} · ${v.label} · ${v.ts}`;
|
||
}));
|
||
} else {
|
||
tri.innerHTML = `<div class="placeholder"><span class="ph-frame">${payload.title} · 三视图 (正/侧/背)</span>${_zoomBtn}</div>`;
|
||
}
|
||
tri.querySelector('[data-zoom-tri]')?.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const lbl = tri.querySelector('#ad-tri-main-lbl')?.textContent || (payload.title + ' · 三视图');
|
||
if (window.Shell?._openLightbox) Shell._openLightbox('', lbl);
|
||
});
|
||
} else {
|
||
// product
|
||
triSection.style.display = '';
|
||
tri.style.display = '';
|
||
tri.classList.remove('actor');
|
||
ratioChip.textContent = '16:9';
|
||
if (payload.hasTri !== false) {
|
||
tri.innerHTML = `<div class="placeholder"><span class="ph-frame">${payload.title} · 三视图</span><button class="ad-zoom-btn" type="button" aria-label="查看大图" title="查看大图"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg></button></div>`;
|
||
tip.style.display = 'none';
|
||
tri.querySelector('.ad-zoom-btn')?.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
if (window.Shell?._openLightbox) Shell._openLightbox('', payload.title + ' · 三视图');
|
||
});
|
||
} 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>${payload.missingTriHint || '暂未生成三视图(16:9 单图)'}</span>
|
||
</div>`;
|
||
tip.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
// 简介
|
||
document.getElementById('asset-detail-intro').textContent = payload.intro || '暂无简介';
|
||
|
||
// 标签 chips
|
||
const tags = payload.tags && payload.tags.length ? payload.tags : [];
|
||
document.getElementById('asset-detail-tags').innerHTML = tags.map(t => `<span class="ad-tag-chip">${t}</span>`).join('')
|
||
+ `<button class="ad-tag-add" type="button" title="添加标签"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>`;
|
||
|
||
// 属性表 (3 列 × N 行 grid)
|
||
const props = payload.props || [];
|
||
document.getElementById('asset-detail-props').innerHTML = props
|
||
.map(([k, v]) => `<div class="ad-prop"><span class="k">${k}</span><span class="v">${v}</span></div>`)
|
||
.join('');
|
||
|
||
// apply 按钮文案 (默认「使用该资产」)
|
||
const applyBtn = document.getElementById('asset-detail-apply-btn');
|
||
if (applyBtn) applyBtn.textContent = payload.applyLabel || '使用该资产';
|
||
|
||
document.getElementById('asset-detail-modal').classList.add('show');
|
||
}
|
||
|
||
function openStripDetail(name, sub, kind) {
|
||
// 优先查用户上传:演员 MODEL_OWN / 场景 SCENE_OWN(带 portraits + triVersions)
|
||
const ownArr = kind === 'model' ? MODEL_OWN : SCENE_OWN;
|
||
const ownItem = ownArr.find(x => x.name === name) || null;
|
||
const isOwn = !!ownItem;
|
||
const sourceLabel = isOwn ? '我的上传' : '平台预设';
|
||
|
||
if (kind === 'model') {
|
||
const actor = !isOwn ? MODEL_LIB.find(x => x.name === name) : null;
|
||
const a = actor || { gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '中等',
|
||
build: '标准', hairLen: '中发', hairColor: '黑色', vibe: '清新', feature: sub || (isOwn ? '我的上传演员' : '预设演员') };
|
||
const tags = [a.vibe, a.age, a.hairLen, a.region, a.skin].filter(Boolean);
|
||
const props = [
|
||
['性别', a.gender], ['种族', a.region], ['作品ID', _fmtAssetId(name, 'model')],
|
||
['年龄段', a.age], ['气质', a.vibe], ['创作人', 'Airshelf'],
|
||
['身高', a.height], ['体格', a.build], ['文件大小', _fmtFileSize(name)],
|
||
['发型', a.hairLen + ' · ' + a.hairColor], ['来源', sourceLabel], ['发布时间', '2024-05-20'],
|
||
];
|
||
renderAssetDetail({
|
||
title: name, tagText: '人物 · ' + (isOwn ? '我的演员' : '预设演员'), leadLabel: name + ' · 立绘',
|
||
kind: 'actor', intro: a.feature || sub || '',
|
||
tags, props, applyLabel: '应用到当前项目',
|
||
ownPortraits: isOwn ? ownItem.portraits : null,
|
||
ownTriVersions: isOwn ? ownItem.triVersions : null,
|
||
});
|
||
} else {
|
||
const props = [
|
||
['类别', '场景 · ' + (isOwn ? '我的上传' : '预设')], ['标签', sub || '-'], ['作品ID', _fmtAssetId(name, 'scene')],
|
||
['来源', sourceLabel], ['用途', '本项目场景资产'], ['创作人', 'Airshelf'],
|
||
['镜头', '通用'], ['光线', '自然光'], ['文件大小', _fmtFileSize(name)],
|
||
];
|
||
renderAssetDetail({
|
||
title: name, tagText: '场景 · ' + (isOwn ? '我的场景' : '预设'), leadLabel: name + ' · 主图',
|
||
kind: 'scene', intro: sub || '场景资产',
|
||
tags: [sub].filter(Boolean), props, applyLabel: '应用到当前项目',
|
||
ownPortraits: isOwn ? ownItem.portraits : null,
|
||
});
|
||
}
|
||
}
|
||
|
||
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;
|
||
const kindMap = { character: 'actor', scene: 'scene', product: 'product' };
|
||
const kindLabelMap = { character: '人物', scene: '场景', product: '商品' };
|
||
const baseProps = d.info.slice();
|
||
baseProps.push(['作品ID', _fmtAssetId(d.title, d.kind === 'character' ? 'model' : d.kind === 'scene' ? 'scene' : 'product')]);
|
||
baseProps.push(['创作人', 'Airshelf']);
|
||
baseProps.push(['文件大小', _fmtFileSize(d.title)]);
|
||
baseProps.push(['发布时间', '2024-05-20']);
|
||
renderAssetDetail({
|
||
title: d.title,
|
||
tagText: kindLabelMap[d.kind] + (d.kind === 'character' ? ' · 主角/对照' : d.kind === 'product' ? ' · 当前项目' : ' · 预设'),
|
||
leadLabel: d.title,
|
||
kind: kindMap[d.kind],
|
||
intro: (d.info.find(r => r[0] === '用途') || [, ''])[1] || '资产用于本项目生成',
|
||
tags: d.info.slice(0, 3).map(r => r[1]).filter(Boolean),
|
||
props: baseProps,
|
||
hasTri: !!d.hasTri,
|
||
missingTriHint: '暂未生成三视图(16:9 单图)',
|
||
applyLabel: '应用到当前项目',
|
||
});
|
||
}
|
||
// 用户上传的演员 / 场景(分别累积,source='own')
|
||
const MODEL_OWN = [];
|
||
const SCENE_OWN = [];
|
||
let _curLibKind = 'model';
|
||
let _curLibSource = 'all';
|
||
function _libItemsForSource(kind, src) {
|
||
const isModel = kind === 'model';
|
||
const presets = isModel ? MODEL_LIB : SCENE_LIB;
|
||
const owns = isModel ? MODEL_OWN : SCENE_OWN;
|
||
if (src === 'preset') return presets;
|
||
if (src === 'own') return owns;
|
||
return [...owns, ...presets];
|
||
}
|
||
function _renderLibGrid() {
|
||
const isModel = _curLibKind === 'model';
|
||
const items = _libItemsForSource(_curLibKind, _curLibSource);
|
||
const grid = document.getElementById('ml-grid');
|
||
|
||
// 「添加演员 / 添加场景」入口卡 · 平台预设是只读素材,不展示入口
|
||
const uploadCardHTML = (_curLibSource === 'preset') ? '' : `
|
||
<div class="ml-card ml-upload-card" id="ml-upload-card" role="button" tabindex="0" aria-label="${isModel ? '添加演员' : '添加场景'}">
|
||
<div class="up-thumb">
|
||
<div class="up-plus">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||
</div>
|
||
</div>
|
||
<div class="ml-card-nm">${isModel ? '添加演员' : '添加场景'}</div>
|
||
<div class="ml-card-sub">// AI 生成 / 本地上传</div>
|
||
</div>
|
||
`;
|
||
|
||
grid.innerHTML = uploadCardHTML + 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>
|
||
`).join('');
|
||
|
||
const upCard = grid.querySelector('#ml-upload-card');
|
||
if (upCard) {
|
||
upCard.addEventListener('click', () => _openLibUploadChoice());
|
||
upCard.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _openLibUploadChoice(); }
|
||
});
|
||
}
|
||
|
||
// 普通卡片 click → 应用 / 详情
|
||
grid.querySelectorAll('.ml-card:not(.ml-upload-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 ? '演员库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设') : '场景库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设'));
|
||
document.getElementById('ml-modal-bg').classList.remove('show');
|
||
return;
|
||
}
|
||
openStripDetail(name, sub, _curLibKind);
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ════════ 添加演员 / 场景 · 工作台(同模特库 ml-canvas)════════ */
|
||
// 词典:模特→演员、立绘→立绘/场景图、三视图(场景隐藏)
|
||
const _MC_LABELS = {
|
||
actor: {
|
||
title: '添加演员', mono: '// 添加演员 · 工作台',
|
||
emptyTitle: '用 AI 生成一位新演员',
|
||
emptyDesc: '描述外形 + 风格 + 服装,AI 会同时生成立绘 + 正/侧/背三视图,加入演员库。',
|
||
nameSec: '// 演员姓名', namePh: '给演员起个名字…',
|
||
portraitSec: '// 演员立绘', portraitPick: '点击或拖入立绘',
|
||
portraitAiDesc: '在左侧 AI 生成后<br>点击想要的立绘添加到这里',
|
||
promptPh: '描述演员外形、年龄、风格、服饰…例如:清新校园风女生',
|
||
commit: '加入演员库', toast: '已加入演员库',
|
||
examples: [
|
||
{ ex: '清新校园风女生,黑色长直发,白色 T 恤 + 牛仔短裙,室内自然光', lbl: '清新校园风女生' },
|
||
{ ex: '都市 OL 通勤,黑色西装套装,30 岁知性气质', lbl: '都市 OL 通勤' },
|
||
{ ex: '健身房教练男性,运动背心 + 短裤,健身房布景', lbl: '健身教练 · 男' },
|
||
{ ex: '日系简约女生,棕色短发,米色针织衫,温柔气质', lbl: '日系简约' },
|
||
],
|
||
},
|
||
scene: {
|
||
title: '添加场景', mono: '// 添加场景 · 工作台',
|
||
emptyTitle: '用 AI 生成一处新场景',
|
||
emptyDesc: '描述类型 + 氛围 + 光线,AI 会生成场景图,加入场景库。',
|
||
nameSec: '// 场景名称', namePh: '给场景起个名字…',
|
||
portraitSec: '// 场景图', portraitPick: '点击或拖入场景图',
|
||
portraitAiDesc: '在左侧 AI 生成后<br>点击想要的场景图添加到这里',
|
||
promptPh: '描述场景类型、氛围、光线…例如:夜景咖啡馆,暖光灯',
|
||
commit: '加入场景库', toast: '已加入场景库',
|
||
examples: [
|
||
{ ex: '日系咖啡馆室内,木质桌面,落地窗自然光,午后氛围', lbl: '日系咖啡馆' },
|
||
{ ex: '城市天台夜景,霓虹灯背景,冷色调高对比', lbl: '城市天台夜景' },
|
||
{ ex: '极简白色摄影棚,无缝背景,顶光均匀', lbl: '极简摄影棚' },
|
||
{ ex: '复古港风街头,湿润地面,招牌灯反射', lbl: '港风街头' },
|
||
],
|
||
},
|
||
};
|
||
|
||
function _mcKind() { return _curLibKind === 'model' ? 'actor' : 'scene'; }
|
||
function _mcOwnsArr() { return _curLibKind === 'model' ? MODEL_OWN : SCENE_OWN; }
|
||
function _mcPresetsArr() { return _curLibKind === 'model' ? MODEL_LIB : SCENE_LIB; }
|
||
|
||
// ─── 工作台 DOM 引用 ───
|
||
const _uploadCanvas = document.getElementById('ml-canvas');
|
||
const _mcUp = document.getElementById('mc-up');
|
||
|
||
function _applyKindToCanvas() {
|
||
const k = _mcKind();
|
||
const L = _MC_LABELS[k];
|
||
_mcUp.dataset.kind = k;
|
||
document.getElementById('ml-canvas-title').textContent = L.title;
|
||
document.getElementById('ml-canvas-mono').textContent = L.mono;
|
||
document.getElementById('mc-empty-title').textContent = L.emptyTitle;
|
||
document.getElementById('mc-empty-desc').textContent = L.emptyDesc;
|
||
document.getElementById('mc-up-name-label').textContent = L.nameSec;
|
||
document.getElementById('mc-up-name').placeholder = L.namePh;
|
||
document.getElementById('mc-up-portrait-label').textContent = L.portraitSec;
|
||
document.getElementById('mc-portrait-ai-empty-desc').innerHTML = L.portraitAiDesc;
|
||
document.getElementById('mc-portrait-local-t').textContent = L.portraitPick;
|
||
// 场景仅允许 1 张本地图;演员允许多张
|
||
const _localInput = document.getElementById('mc-portrait-local-input');
|
||
const _localD = document.querySelector('.mc-portrait-local .drop .d');
|
||
if (k === 'scene') {
|
||
_localInput.removeAttribute('multiple');
|
||
if (_localD) _localD.textContent = '仅支持 1 张 JPG / PNG / WEBP · ≤ 10MB';
|
||
} else {
|
||
_localInput.setAttribute('multiple', '');
|
||
if (_localD) _localD.textContent = '支持多张 JPG / PNG / WEBP · ≤ 10MB / 张';
|
||
}
|
||
document.getElementById('mc-input-text').placeholder = L.promptPh;
|
||
document.getElementById('mc-up-commit-label').textContent = L.commit;
|
||
// 示例 chip 重渲(场景需要不同的示例)
|
||
const ex = document.getElementById('mc-empty-examples');
|
||
if (ex && !ex.dataset.cleanInit) {
|
||
ex.dataset.cleanInit = '1';
|
||
}
|
||
ex.innerHTML = L.examples.map(e => `<button class="ex" type="button" data-ex="${e.ex.replace(/"/g, '"')}">${e.lbl}</button>`).join('');
|
||
// 重新绑定示例 click
|
||
ex.querySelectorAll('.ex').forEach(b => {
|
||
b.addEventListener('click', () => {
|
||
const t = document.getElementById('mc-input-text');
|
||
t.value = b.dataset.ex;
|
||
t.dispatchEvent(new Event('input'));
|
||
t.focus();
|
||
});
|
||
});
|
||
}
|
||
|
||
function _openLibUploadChoice() {
|
||
_applyKindToCanvas();
|
||
// 当前 kind 下重渲右侧栏:刷新底部状态文案(场景:名称 / 场景图,无三视图)
|
||
if (typeof _renderRight === 'function') { try { _renderRight(); } catch(e) {} }
|
||
_uploadCanvas.classList.add('show');
|
||
_uploadCanvas.setAttribute('aria-hidden', 'false');
|
||
}
|
||
function _closeUploadCanvasNow() {
|
||
_uploadCanvas.classList.remove('show');
|
||
_uploadCanvas.setAttribute('aria-hidden', 'true');
|
||
}
|
||
|
||
// ─── 工作台状态(AI / Local 两个 tab 各自独立)───
|
||
let _mcRightTab = 'ai';
|
||
const _mcAi = { name: '', portrait: null, triVersions: [], triActiveIdx: -1 };
|
||
const _mcLocal = { name: '', portraits: [], triVersions: [], triActiveIdx: -1 };
|
||
function _mcCurState() { return _mcRightTab === 'ai' ? _mcAi : _mcLocal; }
|
||
function _hasTri(s) { return s.triVersions.length > 0 && s.triActiveIdx >= 0; }
|
||
function _kindRequiresTri() { return _mcKind() === 'actor'; }
|
||
|
||
function _isWorkbenchDirty() {
|
||
if (!_uploadCanvas.classList.contains('show')) return false;
|
||
if ((_mcAi.name || '').trim()) return true;
|
||
if (_mcAi.portrait) return true;
|
||
if (_mcAi.triVersions.length > 0) return true;
|
||
if ((_mcLocal.name || '').trim()) return true;
|
||
if (_mcLocal.portraits.length > 0) return true;
|
||
if (_mcLocal.triVersions.length > 0) return true;
|
||
return false;
|
||
}
|
||
function _resetWorkbenchState() {
|
||
if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');
|
||
_mcAi.name = ''; _mcAi.portrait = null; _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
|
||
_mcLocal.name = ''; _mcLocal.portraits = []; _mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;
|
||
const nameInput = document.getElementById('mc-up-name');
|
||
if (nameInput) nameInput.value = '';
|
||
_renderRight();
|
||
}
|
||
|
||
// ─── 二次确认弹窗 ───
|
||
const _leaveBg = document.getElementById('mc-leave-bg');
|
||
const _leaveBody = document.getElementById('mc-leave-body');
|
||
let _leavePending = null;
|
||
function _openLeaveConfirm(mode, onConfirm) {
|
||
const ki = _mcKind() === 'actor' ? '加入演员库' : '加入场景库';
|
||
if (mode === 'nav') {
|
||
_leaveBody.innerHTML = `工作台已有内容,跳转到其他页面后<b>不会保存</b>。可继续编辑并点「${ki}」来保留进度。`;
|
||
} else {
|
||
_leaveBody.innerHTML = `工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「${ki}」来保留进度。`;
|
||
}
|
||
_leavePending = onConfirm || null;
|
||
_leaveBg.classList.add('show');
|
||
_leaveBg.setAttribute('aria-hidden', 'false');
|
||
}
|
||
function _closeLeaveConfirm() {
|
||
_leaveBg.classList.remove('show');
|
||
_leaveBg.setAttribute('aria-hidden', 'true');
|
||
_leavePending = null;
|
||
}
|
||
document.getElementById('mc-leave-cancel').addEventListener('click', _closeLeaveConfirm);
|
||
_leaveBg.addEventListener('click', e => { if (e.target === _leaveBg) _closeLeaveConfirm(); });
|
||
document.getElementById('mc-leave-confirm').addEventListener('click', () => {
|
||
const fn = _leavePending;
|
||
_closeLeaveConfirm();
|
||
if (typeof fn === 'function') fn();
|
||
});
|
||
|
||
function _closeUploadChoice() {
|
||
if (_isWorkbenchDirty()) {
|
||
_openLeaveConfirm('exit', () => { _resetWorkbenchState(); _closeUploadCanvasNow(); });
|
||
return;
|
||
}
|
||
_closeUploadCanvasNow();
|
||
}
|
||
document.getElementById('ml-canvas-back').addEventListener('click', _closeUploadChoice);
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && _uploadCanvas.classList.contains('show') && !_leaveBg.classList.contains('show')) {
|
||
_closeUploadChoice();
|
||
}
|
||
});
|
||
|
||
// 全局拦截 · 工作台展开时,根据脏态决定是否二次确认
|
||
document.addEventListener('click', e => {
|
||
if (!_uploadCanvas.classList.contains('show')) return;
|
||
const dirty = _isWorkbenchDirty();
|
||
|
||
// 库 X · 关闭整个库 modal(脏态确认 · 非脏直接关)
|
||
if (e.target.closest('#ml-close-btn')) {
|
||
e.preventDefault(); e.stopPropagation();
|
||
const doClose = () => {
|
||
_resetWorkbenchState();
|
||
_closeUploadCanvasNow();
|
||
document.getElementById('ml-modal-bg').classList.remove('show');
|
||
};
|
||
if (dirty) _openLeaveConfirm('exit', doClose); else doClose();
|
||
return;
|
||
}
|
||
// 库左侧「来源」筛选项 · 切换 = 离开工作台
|
||
const sideItem = e.target.closest('#ml-side .ml-side-item');
|
||
if (sideItem) {
|
||
e.preventDefault(); e.stopPropagation();
|
||
const src = sideItem.dataset.source;
|
||
const doSwitch = () => {
|
||
_resetWorkbenchState();
|
||
_closeUploadCanvasNow();
|
||
_curLibSource = src;
|
||
document.querySelectorAll('#ml-side .ml-side-item').forEach(x =>
|
||
x.classList.toggle('active', x.dataset.source === src));
|
||
_renderLibGrid();
|
||
};
|
||
if (dirty) _openLeaveConfirm('nav', doSwitch); else doSwitch();
|
||
return;
|
||
}
|
||
|
||
if (!dirty) return; // 后续仅脏态时拦截
|
||
|
||
// 外页跳转
|
||
const a = e.target.closest('a[href]');
|
||
if (a) {
|
||
const href = a.getAttribute('href');
|
||
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
|
||
e.preventDefault(); e.stopPropagation();
|
||
_openLeaveConfirm('nav', () => { _resetWorkbenchState(); _closeUploadCanvasNow(); location.href = href; });
|
||
return;
|
||
}
|
||
// 余额胶囊
|
||
if (e.target.closest('.balance-chip')) {
|
||
e.preventDefault(); e.stopPropagation();
|
||
_openLeaveConfirm('nav', () => { _resetWorkbenchState(); _closeUploadCanvasNow(); location.href = 'account.html'; });
|
||
return;
|
||
}
|
||
}, true);
|
||
|
||
window.addEventListener('beforeunload', e => {
|
||
if (_isWorkbenchDirty()) { e.preventDefault(); e.returnValue = ''; return ''; }
|
||
});
|
||
|
||
// ─── 左:AI 生成区 ───
|
||
const _mcInputText = document.getElementById('mc-input-text');
|
||
const _mcSendBtn = document.getElementById('mc-send-btn');
|
||
_mcInputText?.addEventListener('input', () => {
|
||
_mcSendBtn.disabled = _mcInputText.value.trim().length === 0;
|
||
_mcInputText.style.height = 'auto';
|
||
_mcInputText.style.height = Math.min(_mcInputText.scrollHeight, 220) + 'px';
|
||
});
|
||
const _MC_SVG = {
|
||
rerun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>',
|
||
dl: '<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 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>',
|
||
more: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.6" fill="currentColor" stroke="none"/></svg>',
|
||
adopt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>',
|
||
del: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>',
|
||
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
|
||
};
|
||
|
||
function _mcAppendMsg(prompt, refs) {
|
||
const inner = document.getElementById('mc-stream-inner');
|
||
if (!inner) return;
|
||
const empty = inner.querySelector('.mc-empty');
|
||
if (empty) empty.remove();
|
||
const safe = String(prompt).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'})[c]);
|
||
const tags = ['3:4', '默认', '4 张'];
|
||
const tagsHtml = tags.map(t => `<span class="meta-chip">${t}</span>`).join('<span class="sep">·</span>');
|
||
const subjLabel = _mcKind() === 'actor' ? '演员' : '场景';
|
||
const cells = Array.from({ length: 4 }, (_, i) => `
|
||
<div class="mc-cell gen" data-idx="${i}">
|
||
<div class="ph-frame">生成中 · v${i + 1}</div>
|
||
<div class="cell-ops" hidden>
|
||
<button type="button" data-act="cell-rerun" title="再次生成">${_MC_SVG.rerun}</button>
|
||
<button type="button" data-act="cell-dl" title="下载">${_MC_SVG.dl}</button>
|
||
</div>
|
||
</div>`).join('');
|
||
const msg = document.createElement('div');
|
||
msg.className = 'mc-msg';
|
||
msg.innerHTML = `
|
||
<div class="mc-msg-prompt">
|
||
<div class="quote">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/><circle cx="9" cy="8" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
|
||
</div>
|
||
<div class="pt"><div class="pt-text">${safe}</div><div class="pt-tags">${tagsHtml}</div></div>
|
||
</div>
|
||
<div class="mc-msg-grid">${cells}</div>
|
||
<div class="mc-msg-ops">
|
||
<button type="button" data-act="edit">${_MC_SVG.edit}重新编辑</button>
|
||
<button type="button" data-act="rerun">${_MC_SVG.rerun}再次生成</button>
|
||
</div>`;
|
||
inner.appendChild(msg);
|
||
setTimeout(() => {
|
||
msg.querySelectorAll('.mc-cell').forEach((c, i) => {
|
||
c.classList.remove('gen');
|
||
const ph = c.querySelector('.ph-frame');
|
||
if (ph) ph.textContent = subjLabel + ' · v' + (i + 1);
|
||
const ops = c.querySelector('.cell-ops');
|
||
if (ops) ops.hidden = false;
|
||
});
|
||
_bindMcCellPick();
|
||
}, 1600);
|
||
msg.querySelectorAll('[data-act="rerun"]').forEach(b =>
|
||
b.addEventListener('click', e => { e.stopPropagation(); _mcAppendMsg(prompt, []); }));
|
||
msg.querySelectorAll('[data-act="edit"]').forEach(b =>
|
||
b.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
_mcInputText.value = prompt;
|
||
_mcInputText.focus();
|
||
_mcInputText.dispatchEvent(new Event('input'));
|
||
}));
|
||
msg.querySelectorAll('[data-act="cell-rerun"]').forEach(b =>
|
||
b.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const cell = b.closest('.mc-cell');
|
||
const ops = cell.querySelector('.cell-ops');
|
||
const ph = cell.querySelector('.ph-frame');
|
||
const idx = Number(cell.dataset.idx || 0);
|
||
cell.classList.add('gen'); cell.classList.remove('selected');
|
||
if (ops) ops.hidden = true;
|
||
if (ph) ph.textContent = '生成中 · v' + (idx + 1);
|
||
setTimeout(() => {
|
||
cell.classList.remove('gen');
|
||
if (ph) ph.textContent = subjLabel + ' · v' + (idx + 1);
|
||
if (ops) ops.hidden = false;
|
||
}, 1200 + Math.random() * 600);
|
||
Shell.toast('已重跑', '该图重新生成中');
|
||
}));
|
||
msg.querySelectorAll('[data-act="cell-dl"]').forEach(b =>
|
||
b.addEventListener('click', e => { e.stopPropagation(); Shell.toast('下载', '已开始下载 · MOCK'); }));
|
||
const stream = document.getElementById('mc-stream');
|
||
if (stream) stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' });
|
||
}
|
||
|
||
_mcSendBtn?.addEventListener('click', () => {
|
||
const txt = _mcInputText.value.trim();
|
||
if (!txt) return;
|
||
_mcAppendMsg(txt, _mcRefList.slice());
|
||
_mcInputText.value = '';
|
||
_mcInputText.style.height = 'auto';
|
||
_mcSendBtn.disabled = true;
|
||
_mcRefList = [];
|
||
_renderMcRefs();
|
||
});
|
||
_mcInputText?.addEventListener('keydown', e => {
|
||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); _mcSendBtn.click(); }
|
||
});
|
||
|
||
// 参考图上传
|
||
const _mcAiRefInput = document.getElementById('mc-ai-ref-input');
|
||
const _mcRefs = document.getElementById('mc-input-refs');
|
||
let _mcRefList = [];
|
||
document.getElementById('mc-add-btn')?.addEventListener('click', () => _mcAiRefInput.click());
|
||
_mcAiRefInput?.addEventListener('change', e => {
|
||
const files = [...(e.target.files || [])].filter(f => /^image\//.test(f.type));
|
||
files.forEach(f => _mcRefList.push({ name: f.name, url: URL.createObjectURL(f) }));
|
||
e.target.value = '';
|
||
_renderMcRefs();
|
||
});
|
||
function _renderMcRefs() {
|
||
if (!_mcRefs) return;
|
||
_mcRefs.innerHTML = _mcRefList.map((r, i) => `
|
||
<div class="mc-input-ref">
|
||
<img src="${r.url}" alt="${r.name}">
|
||
<button class="x" data-idx="${i}" aria-label="移除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
|
||
</div>`).join('');
|
||
_mcRefs.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {
|
||
_mcRefList.splice(+x.dataset.idx, 1);
|
||
_renderMcRefs();
|
||
}));
|
||
}
|
||
|
||
// Tab 切换
|
||
document.querySelectorAll('.mc-up-tab').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
_mcRightTab = btn.dataset.tab;
|
||
document.querySelectorAll('.mc-up-tab').forEach(b => b.classList.toggle('active', b === btn));
|
||
document.querySelectorAll('.mc-up-body [data-show]').forEach(el => {
|
||
el.hidden = el.dataset.show !== _mcRightTab;
|
||
});
|
||
document.getElementById('mc-up-name').value = _mcCurState().name;
|
||
_renderRight();
|
||
});
|
||
});
|
||
|
||
const _mcUpName = document.getElementById('mc-up-name');
|
||
_mcUpName?.addEventListener('input', () => {
|
||
_mcCurState().name = _mcUpName.value;
|
||
_updateCommit();
|
||
});
|
||
|
||
// AI tab · 立绘选中态
|
||
const _aiEmpty = document.getElementById('mc-portrait-ai-empty');
|
||
const _aiPicked = document.getElementById('mc-portrait-ai-picked');
|
||
const _aiLabel = document.getElementById('mc-portrait-ai-label');
|
||
function _renderAiPortrait() {
|
||
if (_mcAi.portrait) {
|
||
_aiEmpty.hidden = true; _aiPicked.hidden = false;
|
||
_aiLabel.textContent = _mcAi.portrait.label || '立绘';
|
||
} else { _aiEmpty.hidden = false; _aiPicked.hidden = true; }
|
||
}
|
||
function _setAiPortrait(data) {
|
||
_mcAi.portrait = data;
|
||
_renderAiPortrait();
|
||
_updateTriBtn(); _updateCommit();
|
||
}
|
||
function _clearAiPortrait() {
|
||
if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');
|
||
_mcAi.portrait = null;
|
||
_mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
|
||
_renderAiPortrait(); _renderTriView(); _updateTriBtn(); _updateCommit();
|
||
}
|
||
document.getElementById('mc-portrait-ai-clear').addEventListener('click', _clearAiPortrait);
|
||
|
||
// Local tab · 多张立绘 / 场景图
|
||
const _lpDrop = document.getElementById('mc-portrait-local-drop');
|
||
const _lpInput = document.getElementById('mc-portrait-local-input');
|
||
const _lpList = document.getElementById('mc-portrait-local-list');
|
||
const _lpCount = document.getElementById('mc-portrait-local-count');
|
||
function _renderLocalPortraits() {
|
||
_lpCount.textContent = _mcLocal.portraits.length;
|
||
_lpList.innerHTML = _mcLocal.portraits.map((p, i) => `
|
||
<div class="thumb">
|
||
<img src="${p.url}" alt="${p.name}">
|
||
<button class="x" data-idx="${i}" aria-label="移除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
|
||
</div>`).join('');
|
||
_lpList.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {
|
||
_mcLocal.portraits.splice(+x.dataset.idx, 1);
|
||
if (_mcLocal.portraits.length === 0) {
|
||
_mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;
|
||
}
|
||
_renderLocalPortraits(); _renderTriView(); _updateTriBtn(); _updateCommit();
|
||
}));
|
||
}
|
||
function _lpAdd(files) {
|
||
const imgs = [...(files || [])].filter(f => /^image\//.test(f.type));
|
||
if (!imgs.length) return;
|
||
// 场景:仅允许 1 张 · 新选直接替换旧的(后选覆盖前选)
|
||
if (_mcKind() === 'scene') {
|
||
const f = imgs[0];
|
||
_mcLocal.portraits = [{ file: f, url: URL.createObjectURL(f), name: f.name, size: f.size }];
|
||
} else {
|
||
imgs.forEach(f => _mcLocal.portraits.push({ file: f, url: URL.createObjectURL(f), name: f.name, size: f.size }));
|
||
}
|
||
_renderLocalPortraits(); _updateTriBtn(); _updateCommit();
|
||
}
|
||
_lpDrop?.addEventListener('click', () => _lpInput.click());
|
||
_lpDrop?.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _lpInput.click(); } });
|
||
_lpInput?.addEventListener('change', e => { _lpAdd(e.target.files); e.target.value = ''; });
|
||
['dragenter', 'dragover'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.add('dragover'); }));
|
||
['dragleave', 'drop'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.remove('dragover'); }));
|
||
_lpDrop?.addEventListener('drop', e => _lpAdd(e.dataTransfer?.files));
|
||
|
||
// 三视图模块 · 演员使用 · 场景隐藏
|
||
const _triSec = document.getElementById('mc-triview-sec');
|
||
const _triResultImg = document.getElementById('mc-triview-result');
|
||
const _triFrame = document.getElementById('mc-triview-frame');
|
||
const _triGenBtn = document.getElementById('mc-triview-gen-btn');
|
||
const _triHint = document.getElementById('mc-triview-hint');
|
||
const _triOps = document.getElementById('mc-triview-ops');
|
||
const _triRerunBtn = document.getElementById('mc-triview-rerun');
|
||
const _triHistory = document.getElementById('mc-triview-history');
|
||
const _triHistoryRow = document.getElementById('mc-triview-history-row');
|
||
const _triHistoryCount = document.getElementById('mc-triview-history-count');
|
||
let _triGenerating = false;
|
||
|
||
function _portraitReady() {
|
||
return _mcRightTab === 'ai' ? !!_mcAi.portrait : _mcLocal.portraits.length > 0;
|
||
}
|
||
function _renderTriView() {
|
||
if (!_kindRequiresTri()) return; // 场景模式整个模块被 CSS 隐藏
|
||
const s = _mcCurState();
|
||
const has = _hasTri(s);
|
||
_triSec.classList.toggle('has-result', has);
|
||
if (_triGenerating) {
|
||
_triGenBtn.hidden = true; _triHint.hidden = true;
|
||
_triOps.hidden = !has; _triHistory.hidden = !has;
|
||
_triFrame.textContent = '三视图生成中…';
|
||
_triResultImg.classList.add('gen');
|
||
_triRerunBtn.disabled = true;
|
||
if (has) _renderTriHistory(s);
|
||
} else if (has) {
|
||
_triGenBtn.hidden = true; _triHint.hidden = true;
|
||
_triOps.hidden = false; _triHistory.hidden = false;
|
||
const ver = s.triVersions[s.triActiveIdx];
|
||
_triFrame.textContent = `三视图(正/侧/背) · ${ver.label}`;
|
||
_triResultImg.classList.remove('gen');
|
||
_triRerunBtn.disabled = false;
|
||
_renderTriHistory(s);
|
||
} else {
|
||
_triGenBtn.hidden = false; _triHint.hidden = false;
|
||
_triOps.hidden = true; _triHistory.hidden = true;
|
||
_triFrame.textContent = '三视图(正/侧/背)';
|
||
_triResultImg.classList.remove('gen');
|
||
}
|
||
}
|
||
function _renderTriHistory(s) {
|
||
_triHistoryCount.textContent = s.triVersions.length;
|
||
_triHistoryRow.innerHTML = s.triVersions.map((ver, i) => `
|
||
<div class="h-thumb${i === s.triActiveIdx ? ' active' : ''}" data-idx="${i}" title="${ver.label} · ${ver.ts}${i === s.triActiveIdx ? ' · 当前采用' : ''}">
|
||
<span class="badge">当前</span><span class="v">${ver.label}</span>
|
||
</div>`).join('');
|
||
_triHistoryRow.querySelectorAll('.h-thumb').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const idx = Number(el.dataset.idx);
|
||
if (idx === s.triActiveIdx) return;
|
||
s.triActiveIdx = idx;
|
||
_renderTriView(); _updateCommit();
|
||
});
|
||
});
|
||
}
|
||
function _updateTriBtn() {
|
||
if (!_kindRequiresTri()) return;
|
||
if (_hasTri(_mcCurState())) return;
|
||
const ok = _portraitReady() && !_triGenerating;
|
||
_triGenBtn.disabled = !ok;
|
||
_triHint.textContent = _portraitReady()
|
||
? '// 一键生成正/侧/背 三视图'
|
||
: (_mcRightTab === 'ai' ? '// 先选中左侧 AI 立绘' : '// 先上传至少 1 张立绘');
|
||
}
|
||
function _startTriGen() {
|
||
if (!_kindRequiresTri()) return;
|
||
if (!_portraitReady() || _triGenerating) return;
|
||
_triGenerating = true;
|
||
const stateAtStart = _mcCurState();
|
||
_renderTriView();
|
||
setTimeout(() => {
|
||
_triGenerating = false;
|
||
const now = new Date();
|
||
const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
||
const label = 'v' + (stateAtStart.triVersions.length + 1);
|
||
stateAtStart.triVersions.push({ ts, label });
|
||
stateAtStart.triActiveIdx = stateAtStart.triVersions.length - 1;
|
||
if (stateAtStart === _mcCurState()) _renderTriView();
|
||
_updateTriBtn(); _updateCommit();
|
||
}, 1600);
|
||
}
|
||
_triGenBtn.addEventListener('click', _startTriGen);
|
||
_triRerunBtn.addEventListener('click', _startTriGen);
|
||
|
||
// 整体渲染 + 提交
|
||
const _mcUpCommit = document.getElementById('mc-up-commit');
|
||
const _mcUpStat = document.getElementById('mc-up-stat');
|
||
function _updateCommit() {
|
||
const s = _mcCurState();
|
||
const nameOk = s.name.trim().length > 0;
|
||
const portraitOk = _portraitReady();
|
||
const needTri = _kindRequiresTri();
|
||
const triOk = needTri ? _hasTri(s) : true;
|
||
const ready = nameOk && portraitOk && triOk;
|
||
_mcUpCommit.disabled = !ready;
|
||
if (ready) {
|
||
_mcUpStat.classList.add('ok');
|
||
_mcUpStat.innerHTML = `✓ 已就绪 · <b>${s.name}</b>`;
|
||
} else {
|
||
_mcUpStat.classList.remove('ok');
|
||
const miss = [];
|
||
if (!nameOk) miss.push(_mcKind() === 'actor' ? '姓名' : '名称');
|
||
if (!portraitOk) miss.push(_mcKind() === 'actor' ? '立绘' : '场景图');
|
||
if (needTri && !triOk) miss.push('三视图');
|
||
_mcUpStat.innerHTML = `// 待完成 · <b>${miss.join(' / ')}</b>`;
|
||
}
|
||
}
|
||
function _renderRight() {
|
||
_renderAiPortrait();
|
||
_renderLocalPortraits();
|
||
_renderTriView();
|
||
_updateTriBtn();
|
||
_updateCommit();
|
||
}
|
||
_renderRight();
|
||
|
||
_mcUpCommit?.addEventListener('click', () => {
|
||
const s = _mcCurState();
|
||
if (_mcUpCommit.disabled) return;
|
||
const baseName = s.name.trim().slice(0, 12);
|
||
const ts = Date.now().toString(36);
|
||
const subjLabel = _mcKind() === 'actor' ? '演员' : '场景';
|
||
const ownsArr = _mcOwnsArr();
|
||
// 持久化多张立绘 / 场景图 + 三视图历史版本
|
||
const portraits = _mcRightTab === 'ai'
|
||
? (_mcAi.portrait ? [{ url: '', name: _mcAi.portrait.label || baseName, label: _mcAi.portrait.label || subjLabel }] : [])
|
||
: _mcLocal.portraits.map((p, i) => ({ url: p.url, name: p.name || `${baseName}-${i+1}`, label: `本地 ${i+1}` }));
|
||
const triVersions = _kindRequiresTri() ? s.triVersions.map(v => ({ ts: v.ts, label: v.label })) : [];
|
||
ownsArr.unshift({
|
||
id: 'up-' + ts,
|
||
name: baseName,
|
||
sub: (_mcRightTab === 'ai' ? 'AI 生成' : '我的上传') + ' · ' + (_kindRequiresTri() ? '已含三视图' : subjLabel),
|
||
source: 'own',
|
||
portraits, triVersions,
|
||
kind: _mcKind(),
|
||
});
|
||
const L = _MC_LABELS[_mcKind()];
|
||
Shell.toast(L.toast, `${baseName} · 来源 ${_mcRightTab === 'ai' ? 'AI 生成' : '我的上传'}`);
|
||
_resetWorkbenchState();
|
||
_curLibSource = 'own';
|
||
const side = document.getElementById('ml-side');
|
||
side.querySelectorAll('.ml-side-item').forEach(x =>
|
||
x.classList.toggle('active', x.dataset.source === 'own'));
|
||
const ownCt = side.querySelector('.ml-side-item[data-source="own"] .ct');
|
||
if (ownCt) ownCt.textContent = ownsArr.length;
|
||
const allCt = side.querySelector('.ml-side-item[data-source="all"] .ct');
|
||
if (allCt) allCt.textContent = ownsArr.length + _mcPresetsArr().length;
|
||
_renderLibGrid();
|
||
_closeUploadCanvasNow();
|
||
});
|
||
|
||
// 左侧 cell 可点击选为立绘(仅 AI tab)
|
||
function _bindMcCellPick() {
|
||
document.querySelectorAll('#mc-stream .mc-cell').forEach(cell => {
|
||
if (cell.dataset.boundPick) return;
|
||
cell.dataset.boundPick = '1';
|
||
if (!cell.querySelector('.pick-badge')) {
|
||
const b = document.createElement('span');
|
||
b.className = 'pick-badge';
|
||
b.textContent = '已选用';
|
||
cell.appendChild(b);
|
||
}
|
||
cell.addEventListener('click', () => {
|
||
if (_mcRightTab !== 'ai') {
|
||
Shell.toast('请切到「AI 生成」标签', '只有 AI 模式才能选用左侧立绘');
|
||
return;
|
||
}
|
||
if (cell.classList.contains('gen')) return;
|
||
if (cell.classList.contains('selected')) { _clearAiPortrait(); return; }
|
||
document.querySelectorAll('#mc-stream .mc-cell.selected').forEach(c => c.classList.remove('selected'));
|
||
cell.classList.add('selected');
|
||
const label = cell.querySelector('.ph-frame')?.textContent || '立绘';
|
||
_mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
|
||
_renderTriView();
|
||
_setAiPortrait({ label, cellEl: cell });
|
||
});
|
||
});
|
||
}
|
||
|
||
function openLib(kind) {
|
||
_curLibKind = kind;
|
||
_curLibSource = 'all';
|
||
const isModel = kind === 'model';
|
||
const title = isModel ? '演员库' : '场景库';
|
||
const presets = isModel ? MODEL_LIB : SCENE_LIB;
|
||
const owns = isModel ? MODEL_OWN : SCENE_OWN;
|
||
|
||
document.getElementById('ml-modal-title').textContent = title;
|
||
document.getElementById('ml-modal-ct').textContent = '// 共 ' + (presets.length + owns.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">${presets.length + owns.length}</span></div>
|
||
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">${presets.length}</span></div>
|
||
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">${owns.length}</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');
|
||
_curLibSource = it.dataset.source;
|
||
_renderLibGrid();
|
||
});
|
||
});
|
||
|
||
// toolbar · chip groups (去掉了 btn-up 上传按钮,改用网格内入口卡)
|
||
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>
|
||
`;
|
||
} 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>
|
||
`;
|
||
}
|
||
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');
|
||
});
|
||
});
|
||
});
|
||
|
||
// 卡片网格(含 + 入口 + apply 绑定都在 _renderLibGrid 内完成)
|
||
_renderLibGrid();
|
||
|
||
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;
|
||
document.getElementById('asset-detail-modal').classList.remove('show');
|
||
document.getElementById('ml-modal-bg')?.classList.remove('show');
|
||
Shell.toast('已应用「' + name + '」', '已加入当前项目');
|
||
});
|
||
// 详情 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 · 预览/采用 双状态 + 点击主图放大
|
||
(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 triBadge = document.getElementById('asset-prod-tri-badge');
|
||
const prodAction = document.getElementById('asset-prod-action');
|
||
const history = document.getElementById('prod-preview-history');
|
||
const historyRow = document.getElementById('prod-preview-history-row');
|
||
const historyCount = document.getElementById('prod-preview-history-count');
|
||
if (!aigenBtn || !pane || !img || !statusEl || !foot || !history || !historyRow) return;
|
||
|
||
const versions = []; // [{ ts, label }]
|
||
let previewIdx = -1; // 主图正在「预览」哪一版(浏览态,不动采用状态)
|
||
let adoptedIdx = -1; // 真正被「采用」的那一版,决定商品资产生效版本
|
||
let generating = false;
|
||
const savedTri = ProjectStore.data.stage2?.productTri || null;
|
||
if (savedTri) {
|
||
if (Array.isArray(savedTri.versions)) versions.push(...savedTri.versions);
|
||
if (Number.isInteger(savedTri.previewIdx)) previewIdx = savedTri.previewIdx;
|
||
if (Number.isInteger(savedTri.adoptedIdx)) adoptedIdx = savedTri.adoptedIdx;
|
||
}
|
||
|
||
function saveTriState() {
|
||
ProjectStore.saveStage('stage2', {
|
||
...(ProjectStore.data.stage2 || {}),
|
||
productTri: { versions, previewIdx, adoptedIdx },
|
||
});
|
||
}
|
||
|
||
function prodName() {
|
||
return CURRENT_PRODUCT_NAME || (document.getElementById('asset-prod-card-name')?.textContent ?? '商品');
|
||
}
|
||
|
||
function renderHistory() {
|
||
if (versions.length === 0) {
|
||
history.classList.remove('show');
|
||
return;
|
||
}
|
||
history.classList.add('show');
|
||
historyCount.textContent = versions.length;
|
||
historyRow.innerHTML = versions.map((ver, i) => {
|
||
const isAdopted = i === adoptedIdx;
|
||
const isPreview = i === previewIdx;
|
||
const cls = [
|
||
isAdopted ? 'adopted' : '',
|
||
isPreview && !isAdopted ? 'previewing' : '',
|
||
].filter(Boolean).join(' ');
|
||
const titleParts = [ver.label, ver.ts];
|
||
if (isAdopted) titleParts.push('已采用');
|
||
else if (isPreview) titleParts.push('预览中');
|
||
return `
|
||
<div class="h-thumb ${cls}" data-idx="${i}" title="${titleParts.join(' · ')}">
|
||
<span class="badge">已采用</span>
|
||
<span class="v">${ver.label}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
historyRow.querySelectorAll('.h-thumb').forEach(el => {
|
||
el.addEventListener('click', () => {
|
||
const idx = Number(el.dataset.idx);
|
||
if (idx === previewIdx) return;
|
||
setPreview(idx);
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderMain() {
|
||
if (previewIdx < 0) return;
|
||
const ver = versions[previewIdx];
|
||
const isAdopted = previewIdx === adoptedIdx;
|
||
img.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
|
||
img.classList.add('is-zoomable');
|
||
img.title = '点击放大查看';
|
||
statusEl.textContent = isAdopted
|
||
? `${ver.label} · 已采用,不满意可重跑`
|
||
: `${ver.label} · 预览中(未采用)`;
|
||
foot.innerHTML = `
|
||
<button class="btn btn-ghost btn-sm" id="prod-preview-rerun">↻ 重跑</button>
|
||
<button class="btn btn-sm ${isAdopted ? '' : 'btn-primary'}" id="prod-preview-adopt" ${isAdopted ? 'disabled title="此版本已采用"' : 'title="将此版本设为商品采用版本,其余转为不通过"'} style="display:inline-flex; align-items:center; gap:4px;">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||
${isAdopted ? '已采用' : '采用此版本'}
|
||
</button>
|
||
<span class="spacer"></span>
|
||
<span class="muted-2 mono" style="font-size:11px;">~¥0.30 / 次</span>
|
||
`;
|
||
document.getElementById('prod-preview-rerun')?.addEventListener('click', start);
|
||
document.getElementById('prod-preview-adopt')?.addEventListener('click', adoptPreview);
|
||
}
|
||
|
||
// 仅切预览主图,不动采用/不动商品资产
|
||
function setPreview(idx) {
|
||
previewIdx = idx;
|
||
renderHistory();
|
||
renderMain();
|
||
saveTriState();
|
||
}
|
||
|
||
// 显式「采用」当前预览版本 · 同步商品资产 + 隐藏缺三视图徽标
|
||
function adoptPreview() {
|
||
if (previewIdx < 0) return;
|
||
if (previewIdx === adoptedIdx) return;
|
||
adoptedIdx = previewIdx;
|
||
applyAdoption(/* fromClick */ true);
|
||
}
|
||
|
||
function applyAdoption(fromClick) {
|
||
const ver = versions[adoptedIdx];
|
||
if (triBadge) triBadge.hidden = true;
|
||
const detail = ASSET_DETAILS['prod-main'];
|
||
if (detail) {
|
||
detail.hasTri = true;
|
||
detail.currentVersion = ver.label;
|
||
detail.info = [
|
||
['类别', '商品 · 当前项目'],
|
||
['名称', prodName()],
|
||
['三视图', '已采用 · ' + ver.label],
|
||
['状态', '已三视图'],
|
||
];
|
||
}
|
||
renderHistory();
|
||
renderMain();
|
||
saveTriState();
|
||
if (fromClick) Shell.toast('已采用 ' + ver.label, prodName() + ' · 商品资产已更新为该版本');
|
||
}
|
||
|
||
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>`;
|
||
img.classList.remove('is-zoomable');
|
||
img.removeAttribute('title');
|
||
statusEl.textContent = '生成中 · 约 12s';
|
||
foot.innerHTML = '<span class="muted-2 mono" style="font-size:11px;">// POST /assets/tri-view</span>';
|
||
aigenBtn.disabled = true;
|
||
}
|
||
|
||
function finishGeneration() {
|
||
if (!generating && !ProjectStore.getJob('stage2-product-tri')) return;
|
||
generating = false;
|
||
aigenBtn.disabled = false;
|
||
const now = new Date();
|
||
const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
||
const newVer = { ts, label: 'v' + (versions.length + 1) };
|
||
versions.push(newVer);
|
||
const newIdx = versions.length - 1;
|
||
previewIdx = newIdx;
|
||
if (adoptedIdx === -1) {
|
||
adoptedIdx = newIdx;
|
||
applyAdoption(/* fromClick */ false);
|
||
} else {
|
||
renderHistory();
|
||
renderMain();
|
||
Shell.toast('三视图已生成 ' + newVer.label, prodName() + ' · 预览中,满意请点「采用此版本」');
|
||
}
|
||
ProjectStore.finishJob('stage2-product-tri');
|
||
ProjectStore.record('stage2.productTri.ready', { version: newVer.label });
|
||
saveTriState();
|
||
}
|
||
|
||
function start() {
|
||
if (generating) return;
|
||
generating = true;
|
||
pane.classList.add('show');
|
||
renderLoading();
|
||
ProjectStore.startJob('stage2-product-tri', {
|
||
stage: 2,
|
||
label: '商品三视图生成',
|
||
finishAt: Date.now() + 12000,
|
||
});
|
||
ProjectStore.record('stage2.productTri.started', { product: prodName() });
|
||
saveTriState();
|
||
setTimeout(finishGeneration, 1800);
|
||
}
|
||
|
||
function resumeGenerationIfNeeded() {
|
||
const job = ProjectStore.getJob('stage2-product-tri');
|
||
if (!job || job.status !== 'running') return;
|
||
generating = true;
|
||
pane.classList.add('show');
|
||
renderLoading();
|
||
const remaining = Math.max(0, (job.finishAt || Date.now()) - Date.now());
|
||
setTimeout(finishGeneration, remaining);
|
||
}
|
||
|
||
// 主图点击 → 放大查看
|
||
img.addEventListener('click', (e) => {
|
||
if (!img.classList.contains('is-zoomable')) return;
|
||
if (previewIdx < 0) return;
|
||
e.stopPropagation();
|
||
openTriLightbox(versions[previewIdx], previewIdx === adoptedIdx, prodName());
|
||
});
|
||
|
||
aigenBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
start();
|
||
});
|
||
if (versions.length && previewIdx >= 0) {
|
||
pane.classList.add('show');
|
||
if (adoptedIdx >= 0) applyAdoption(false);
|
||
else { renderHistory(); renderMain(); }
|
||
}
|
||
resumeGenerationIfNeeded();
|
||
})();
|
||
}
|
||
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;
|
||
const savedStage3 = ProjectStore.data.stage3;
|
||
if (savedStage3) {
|
||
if (savedStage3.curId) curId = savedStage3.curId;
|
||
if (Array.isArray(savedStage3.scenes)) {
|
||
savedStage3.scenes.forEach(ss => {
|
||
const s = scenes.find(x => x.id === ss.id);
|
||
if (!s) return;
|
||
if (Array.isArray(ss.versions)) s.versions = ss.versions;
|
||
if (Number.isInteger(ss.adopted)) s.adopted = ss.adopted;
|
||
if (typeof ss.prompt === 'string') s.prompt = ss.prompt;
|
||
});
|
||
}
|
||
}
|
||
|
||
function saveState() {
|
||
ProjectStore.saveStage('stage3', {
|
||
curId,
|
||
scenes: scenes.map(s => ({ id: s.id, prompt: s.prompt, adopted: s.adopted, versions: s.versions })),
|
||
});
|
||
}
|
||
|
||
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; saveState(); 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];
|
||
const promptEdit = document.getElementById('sb-prompt-edit');
|
||
promptEdit.textContent = s.prompt;
|
||
promptEdit.oninput = () => {
|
||
s.prompt = promptEdit.textContent.trim();
|
||
saveState();
|
||
};
|
||
// 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;
|
||
ProjectStore.record('stage3.storyboard.version.selected', { scene: s.id, version: s.versions[s.adopted]?.label });
|
||
saveState();
|
||
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;
|
||
ProjectStore.record('stage3.storyboard.rerun', { scene: s.id, version: v.label });
|
||
saveState();
|
||
Shell.toast('整张重跑', s.name + ' · ' + v.label);
|
||
renderAll();
|
||
});
|
||
}
|
||
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, prompt: '中景 / 固定机位\n光线:台灯暖光 + 屏幕冷光\n演员:林夕(疲倦状态)\n关键道具:面膜盒(从抽屉露半角)\n氛围:午夜、安静、些许焦虑' },
|
||
'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, prompt: '特写 / 缓推镜\n光线:柔光顶打 + 背景虚化\n关键道具:面膜包装、撕开瞬间\n氛围:精致、放心、产品感' },
|
||
'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, prompt: '中景 / 定格\n光线:晨光 + 暖色滤镜\n演员:林夕(精致妆面)\n结尾:产品大图 + 价格 + 购物车浮动' },
|
||
};
|
||
const savedStage4 = ProjectStore.data.stage4;
|
||
if (savedStage4?.videos) {
|
||
Object.entries(savedStage4.videos).forEach(([id, sv]) => {
|
||
if (!VIDEOS[id]) return;
|
||
if (Array.isArray(sv.versions)) VIDEOS[id].versions = sv.versions;
|
||
if (Number.isInteger(sv.adopted)) VIDEOS[id].adopted = sv.adopted;
|
||
if (Number.isInteger(sv.preview)) VIDEOS[id].preview = sv.preview;
|
||
if (typeof sv.prompt === 'string') VIDEOS[id].prompt = sv.prompt;
|
||
});
|
||
}
|
||
let curVid = null;
|
||
|
||
function saveState() {
|
||
const videos = {};
|
||
Object.entries(VIDEOS).forEach(([id, v]) => {
|
||
videos[id] = { versions: v.versions, adopted: v.adopted, preview: v.preview, prompt: v.prompt };
|
||
});
|
||
ProjectStore.saveStage('stage4', { videos });
|
||
}
|
||
|
||
function getPreviewIndex(v) {
|
||
const idx = Number.isInteger(v.preview) ? v.preview : v.adopted;
|
||
return v.versions[idx] ? idx : Math.max(0, v.adopted || 0);
|
||
}
|
||
|
||
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 previewIdx = getPreviewIndex(v);
|
||
const cur = v.versions[previewIdx];
|
||
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>`;
|
||
const promptEl = document.getElementById('vd-prompt-edit');
|
||
if (promptEl) {
|
||
promptEl.textContent = v.prompt || '';
|
||
promptEl.oninput = () => { v.prompt = promptEl.textContent.trim(); saveState(); };
|
||
}
|
||
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 === previewIdx ? ' current' : ''}${i === v.adopted ? ' 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.preview = +t.dataset.vi;
|
||
ProjectStore.record('stage4.video.version.previewed', { id, version: v.versions[v.preview]?.label });
|
||
saveState();
|
||
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];
|
||
v.adopted = getPreviewIndex(v);
|
||
v.preview = v.adopted;
|
||
ProjectStore.record('stage4.video.version.adopted', { id: curVid, version: v.versions[v.adopted]?.label });
|
||
saveState();
|
||
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 promptEl = document.getElementById('vd-prompt-edit');
|
||
const promptText = (promptEl?.textContent || '').trim();
|
||
if (promptText) v.prompt = promptText;
|
||
const nv = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (v.versions.length + 1) };
|
||
v.versions.push(nv);
|
||
v.preview = v.versions.length - 1;
|
||
ProjectStore.record('stage4.video.rerun', { id: curVid, version: nv.label });
|
||
saveState();
|
||
Shell.toast('重跑中', v.title + ' · 约 30s · 新版预览中');
|
||
openDetail(curVid);
|
||
});
|
||
}
|
||
return { init };
|
||
})();
|
||
Stage4.init();
|
||
|
||
/* ============================================================
|
||
Quota · 全局额度预检(PRD §10.3 四层)
|
||
============================================================ */
|
||
window.Quota = (function () {
|
||
// mock 团队/个人额度快照 - 与 team.html / account.html 数据保持一致
|
||
const SNAP = {
|
||
userDailyLeft: 499.55, // 个人日剩余
|
||
userMonthlyLeft: 9837.40, // 个人月剩余
|
||
teamMonthlyLeft: 2837.40, // 团队月剩余(月限额 3000 - 当月已用 162.60)
|
||
teamBalance: 327.40, // 团队总余额
|
||
};
|
||
|
||
function buildChecks(est, demoBlock) {
|
||
const need = est * 1.2; // PRD §10.3 任务预估 × 1.2
|
||
const layers = [
|
||
{ name: '个人日剩余', left: SNAP.userDailyLeft, need },
|
||
{ name: '个人月剩余', left: SNAP.userMonthlyLeft, need },
|
||
{ name: '团队月剩余', left: SNAP.teamMonthlyLeft, need },
|
||
{ name: '团队总余额', left: demoBlock ? 0.50 : SNAP.teamBalance, need },
|
||
];
|
||
return layers.map((l, i) => ({
|
||
...l,
|
||
ok: l.left >= l.need,
|
||
idx: i + 1,
|
||
}));
|
||
}
|
||
|
||
function fmt(n) { return '¥' + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
|
||
|
||
function preflight({ stage, est = 0, hash = '', demo = '', force = false } = {}) {
|
||
const isDemoBlock = demo === 'block';
|
||
const checks = buildChecks(est, isDemoBlock);
|
||
const allOk = checks.every(c => c.ok);
|
||
|
||
// 渲染 stage 行
|
||
document.getElementById('quota-stage-row').innerHTML =
|
||
'<span style="font-family:var(--font-mono);font-size:11.5px;color:var(--black-alpha-48);letter-spacing:.02em;">// 任务</span> <strong>' + stage + '</strong> · 预估扣费 <strong style="color:var(--heat);font-variant-numeric:tabular-nums;">' + fmt(est) + '</strong> <span style="font-family:var(--font-mono);font-size:11px;color:var(--black-alpha-48);">(×1.2 预留 = ' + fmt(est * 1.2) + ')</span>';
|
||
|
||
// 渲染 4 行检查
|
||
document.getElementById('quota-checks').innerHTML = checks.map(c => `
|
||
<div style="display:grid;grid-template-columns:22px 1fr auto;gap:8px;align-items:baseline;font-size:12.5px;padding:8px 10px;background:var(--background-lighter);border:1px solid var(--border-faint);border-radius:var(--r-sm);">
|
||
<span style="width:18px;height:18px;border-radius:50%;display:grid;place-items:center;font-family:var(--font-mono);font-size:10.5px;font-weight:600;background:${c.ok ? 'rgba(66,195,102,.12)' : 'rgba(235,52,36,.12)'};color:${c.ok ? 'var(--accent-forest)' : 'var(--accent-crimson)'};">${c.idx}</span>
|
||
<span><strong style="color:var(--accent-black);font-weight:500;">${c.name}</strong> <span style="color:var(--black-alpha-48);font-family:var(--font-mono);font-size:11px;">${fmt(c.left)} ≥ ${fmt(c.need)}</span></span>
|
||
<span style="font-family:var(--font-mono);font-size:11px;color:${c.ok ? 'var(--accent-forest)' : 'var(--accent-crimson)'};font-weight:600;">${c.ok ? '✓ 通过' : '✗ 不足'}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
// 标题 + footer 按钮
|
||
const ic = document.getElementById('quota-ic');
|
||
const title = document.getElementById('quota-title');
|
||
const sub = document.getElementById('quota-sub');
|
||
const tip = document.getElementById('quota-block-tip');
|
||
const confirmBtn = document.getElementById('quota-confirm');
|
||
const topupBtn = document.getElementById('quota-topup');
|
||
|
||
if (allOk) {
|
||
ic.style.background = 'rgba(66,195,102,.12)';
|
||
ic.style.color = 'var(--accent-forest)';
|
||
ic.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>';
|
||
title.firstChild.textContent = '额度预检通过';
|
||
sub.textContent = '// 4 层检查 · 全部通过';
|
||
tip.style.display = 'none';
|
||
confirmBtn.style.display = '';
|
||
topupBtn.style.display = 'none';
|
||
confirmBtn.onclick = () => {
|
||
Shell.closeModal('quota-bg');
|
||
Shell.toast('已确认扣费', stage + ' · 预估 ' + fmt(est));
|
||
if (hash) location.hash = hash;
|
||
};
|
||
} else {
|
||
ic.style.background = 'rgba(235,52,36,.12)';
|
||
ic.style.color = 'var(--accent-crimson)';
|
||
ic.innerHTML = '<svg 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="10"/><path d="M12 8v4M12 16h.01"/></svg>';
|
||
title.firstChild.textContent = '额度预检未通过';
|
||
sub.textContent = '// ' + checks.filter(c => !c.ok).length + ' 层不通过 · 任务已拦截';
|
||
tip.style.display = '';
|
||
confirmBtn.style.display = 'none';
|
||
topupBtn.style.display = '';
|
||
}
|
||
|
||
Shell.openModal('quota-bg');
|
||
}
|
||
|
||
return { preflight };
|
||
})();
|
||
|
||
/* ============================================================
|
||
三视图 · 放大查看 lightbox · setupProdPreview 共用
|
||
============================================================ */
|
||
function openTriLightbox(ver, isAdopted, prodName) {
|
||
let bg = document.getElementById('tri-lightbox-bg');
|
||
if (!bg) {
|
||
bg = document.createElement('div');
|
||
bg.id = 'tri-lightbox-bg';
|
||
bg.className = 'modal-bg';
|
||
bg.innerHTML = `
|
||
<div class="tri-lightbox" role="dialog" aria-label="三视图放大查看">
|
||
<button class="tri-lightbox-close" type="button" aria-label="关闭">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
<div class="tri-lightbox-head">
|
||
// 三视图(正/侧/背) · <span class="lb-ver" id="tri-lightbox-label">v1</span>
|
||
<span class="lb-tag" id="tri-lightbox-tag" hidden>已采用</span>
|
||
</div>
|
||
<div class="placeholder tri-lightbox-img" id="tri-lightbox-img"></div>
|
||
<div class="tri-lightbox-foot">
|
||
<span id="tri-lightbox-meta">// 生成于 --:--</span>
|
||
<span class="spc"></span>
|
||
<span><kbd>Esc</kbd> 关闭</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(bg);
|
||
bg.addEventListener('click', (e) => {
|
||
if (e.target === bg) Shell.closeModal('tri-lightbox-bg');
|
||
});
|
||
bg.querySelector('.tri-lightbox-close')?.addEventListener('click', () => {
|
||
Shell.closeModal('tri-lightbox-bg');
|
||
});
|
||
}
|
||
bg.querySelector('#tri-lightbox-img').innerHTML =
|
||
`<span class="ph-frame">${prodName} · 三视图(正/侧/背) · ${ver.label}</span>`;
|
||
bg.querySelector('#tri-lightbox-label').textContent = ver.label;
|
||
const tag = bg.querySelector('#tri-lightbox-tag');
|
||
tag.hidden = !isAdopted;
|
||
bg.querySelector('#tri-lightbox-meta').textContent = `// 生成于 ${ver.ts}`;
|
||
Shell.openModal('tri-lightbox-bg');
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
STAGE 5 · 时间轴编辑器交互(剪映式)
|
||
select / seek / drag-playhead / play-pause / zoom / del / split / trim
|
||
═══════════════════════════════════════════════════════════ */
|
||
(function initTimelineEditor() {
|
||
const $tl = document.getElementById('ed-timeline');
|
||
if (!$tl) return;
|
||
const $ruler = document.getElementById('ed-ruler');
|
||
const $playhead = document.getElementById('ed-playhead');
|
||
const $laneV = document.getElementById('ed-lane-video');
|
||
const $laneS = document.getElementById('ed-lane-subtitle');
|
||
const $playBtn = document.getElementById('ed-play-btn');
|
||
const $playIcon = document.getElementById('ed-play-icon');
|
||
const $prevBtn = document.getElementById('ed-prev-btn');
|
||
const $nextBtn = document.getElementById('ed-next-btn');
|
||
const $splitBtn = document.getElementById('ed-split-btn');
|
||
const $copyBtn = document.getElementById('ed-copy-btn');
|
||
const $delBtn = document.getElementById('ed-del-btn');
|
||
const $zoom = document.getElementById('ed-zoom-input');
|
||
const $curTime = document.getElementById('ed-cur-time');
|
||
const $totalTime= document.getElementById('ed-total-time');
|
||
const $insName = document.getElementById('ed-inspect-name');
|
||
const $insStart = document.getElementById('ed-inspect-start');
|
||
const $insDur = document.getElementById('ed-inspect-dur');
|
||
const $canvasLb = document.getElementById('ed-canvas-label');
|
||
|
||
const TOTAL = 15;
|
||
let currentTime = 0;
|
||
let selectedEl = null;
|
||
let playing = false;
|
||
let rafId = null;
|
||
let lastTs = 0;
|
||
|
||
const PLAY_SVG = '<path d="M5 4l7 4-7 4z" fill="currentColor"/>';
|
||
const PAUSE_SVG = '<path d="M4 3h3v10H4zM9 3h3v10H9z" fill="currentColor"/>';
|
||
|
||
function fmtTime(s) {
|
||
s = Math.max(0, Math.min(TOTAL, s));
|
||
const m = Math.floor(s / 60);
|
||
const sec = s - m * 60;
|
||
return String(m).padStart(2,'0') + ':' + sec.toFixed(2).padStart(5,'0');
|
||
}
|
||
// 时间轴数据 · data-start(时间线起点) + data-dur(当前时长) + data-max(源最大) + data-in(源素材入点)
|
||
const MIN_DUR = 0.2;
|
||
const $laneB = document.querySelector('#ed-timeline .bgm-track .lane');
|
||
const lanes = { video: $laneV, subtitle: $laneS, bgm: $laneB };
|
||
const ALIGN_EPS = 0.12;
|
||
const $alignGuide = document.createElement('span');
|
||
$alignGuide.className = 'tl-align-guide';
|
||
$tl.appendChild($alignGuide);
|
||
const D = (c) => Number(c.dataset.dur || 1);
|
||
const M = (c) => Number(c.dataset.max || 1);
|
||
const IN = (c) => Number(c.dataset.in || 0);
|
||
function clipDur(c) { return D(c); }
|
||
function compactStart(c) {
|
||
let acc = 0;
|
||
for (const sib of c.parentElement.querySelectorAll('.clip')) {
|
||
if (sib === c) return acc;
|
||
acc += D(sib);
|
||
}
|
||
return 0;
|
||
}
|
||
function clipStart(c) {
|
||
const start = Number(c.dataset.start);
|
||
return Number.isFinite(start) ? start : compactStart(c);
|
||
}
|
||
function clipEnd(c) { return clipStart(c) + D(c); }
|
||
function sourceDur(c) { return Math.max(M(c), D(c), MIN_DUR); }
|
||
function clampSourceIn(c, sourceIn = IN(c)) {
|
||
const maxIn = Math.max(0, sourceDur(c) - D(c));
|
||
return Math.max(0, Math.min(maxIn, sourceIn));
|
||
}
|
||
function syncVideoPreview(c) {
|
||
if (!c.classList.contains('video')) return;
|
||
const dur = Math.max(D(c), MIN_DUR);
|
||
const srcDur = Math.max(sourceDur(c), dur);
|
||
const sourceIn = clampSourceIn(c);
|
||
c.dataset.in = String(sourceIn);
|
||
c.style.setProperty('--src-width', (srcDur / dur * 100) + '%');
|
||
c.style.setProperty('--src-offset', (srcDur ? (-(sourceIn / srcDur) * 100) : 0) + '%');
|
||
}
|
||
function freezeLaneStarts(lane) {
|
||
let acc = 0;
|
||
for (const clip of lane.querySelectorAll('.clip')) {
|
||
const explicit = Number(clip.dataset.start);
|
||
const start = Number.isFinite(explicit) ? explicit : acc;
|
||
clip.dataset.start = String(start);
|
||
acc = start + D(clip);
|
||
}
|
||
}
|
||
function compactLane(lane) {
|
||
let acc = 0;
|
||
for (const clip of lane.querySelectorAll('.clip')) {
|
||
clip.dataset.start = String(acc);
|
||
acc += D(clip);
|
||
}
|
||
layoutLane(lane);
|
||
}
|
||
function snapLane(lane) {
|
||
lane.classList.add('is-snapping');
|
||
compactLane(lane);
|
||
window.setTimeout(() => lane.classList.remove('is-snapping'), 180);
|
||
}
|
||
function settleLane(lane) {
|
||
if (lane === lanes.video) snapLane(lane);
|
||
else layoutLane(lane);
|
||
}
|
||
// 绝对布局:默认紧贴排列;trim 后允许保留被裁剪端的可见空隙
|
||
function layoutLane(lane) {
|
||
let acc = 0;
|
||
for (const clip of lane.querySelectorAll('.clip')) {
|
||
const dur = D(clip);
|
||
const explicit = Number(clip.dataset.start);
|
||
const start = Number.isFinite(explicit) ? explicit : acc;
|
||
clip.style.left = (start / TOTAL * 100) + '%';
|
||
clip.style.width = (dur / TOTAL * 100) + '%';
|
||
syncVideoPreview(clip);
|
||
acc = start + dur;
|
||
}
|
||
}
|
||
function videoBoundaries() {
|
||
const points = [];
|
||
$laneV.querySelectorAll('.clip.video').forEach(c => {
|
||
points.push(clipStart(c), clipEnd(c));
|
||
});
|
||
return points;
|
||
}
|
||
function nearestVideoBoundary(edges) {
|
||
let best = null;
|
||
const points = videoBoundaries();
|
||
edges.forEach((edge, edgeIndex) => {
|
||
points.forEach(time => {
|
||
const diff = Math.abs(edge - time);
|
||
if (diff <= ALIGN_EPS && (!best || diff < best.diff)) {
|
||
best = { edge, edgeIndex, time, diff };
|
||
}
|
||
});
|
||
});
|
||
return best;
|
||
}
|
||
function showAlignGuideAt(time) {
|
||
const laneRect = $laneV.getBoundingClientRect();
|
||
const tlRect = $tl.getBoundingClientRect();
|
||
const rulerRect = $ruler.getBoundingClientRect();
|
||
const lastLane = $laneB || $laneS || $laneV;
|
||
const lastRect = lastLane.getBoundingClientRect();
|
||
$alignGuide.style.left = (laneRect.left + (time / TOTAL) * laneRect.width - tlRect.left) + 'px';
|
||
$alignGuide.style.top = (rulerRect.top - tlRect.top) + 'px';
|
||
$alignGuide.style.height = (lastRect.bottom - rulerRect.top) + 'px';
|
||
$alignGuide.classList.add('show');
|
||
}
|
||
function hideAlignGuide() {
|
||
$alignGuide.classList.remove('show');
|
||
}
|
||
function guideEdgeToVideo(edge) {
|
||
const match = nearestVideoBoundary([edge]);
|
||
if (!match) {
|
||
hideAlignGuide();
|
||
return null;
|
||
}
|
||
showAlignGuideAt(match.time);
|
||
return match.time;
|
||
}
|
||
function guideClipToVideo(start, dur) {
|
||
const safeDur = Math.max(MIN_DUR, Math.min(dur, TOTAL));
|
||
const clampedStart = Math.max(0, Math.min(TOTAL - safeDur, start));
|
||
const match = nearestVideoBoundary([clampedStart, clampedStart + safeDur]);
|
||
if (!match) {
|
||
hideAlignGuide();
|
||
return clampedStart;
|
||
}
|
||
const nextStart = match.edgeIndex === 0 ? match.time : match.time - safeDur;
|
||
showAlignGuideAt(match.time);
|
||
return Math.max(0, Math.min(TOTAL - safeDur, nextStart));
|
||
}
|
||
function ensureDropGhost(lane) {
|
||
let ghost = lane.querySelector('.tl-insert-ghost');
|
||
if (!ghost) {
|
||
ghost = document.createElement('span');
|
||
ghost.className = 'tl-insert-ghost';
|
||
lane.appendChild(ghost);
|
||
}
|
||
return ghost;
|
||
}
|
||
function removeDropGhost(lane) {
|
||
lane.querySelector('.tl-insert-ghost')?.remove();
|
||
}
|
||
function placeDropGhost(ghost, start, dur) {
|
||
ghost.style.left = (start / TOTAL * 100) + '%';
|
||
ghost.style.width = (dur / TOTAL * 100) + '%';
|
||
}
|
||
function insertIndexForCenter(siblings, centerT) {
|
||
let acc = 0;
|
||
for (let i = 0; i < siblings.length; i++) {
|
||
const mid = acc + D(siblings[i]) / 2;
|
||
if (centerT < mid) return i;
|
||
acc += D(siblings[i]);
|
||
}
|
||
return siblings.length;
|
||
}
|
||
function previewReorder(lane, dragged, insertIndex) {
|
||
const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== dragged);
|
||
const ghost = ensureDropGhost(lane);
|
||
let acc = 0;
|
||
for (let i = 0; i <= siblings.length; i++) {
|
||
if (i === insertIndex) {
|
||
placeDropGhost(ghost, acc, D(dragged));
|
||
acc += D(dragged);
|
||
}
|
||
const sibling = siblings[i];
|
||
if (sibling) {
|
||
sibling.dataset.start = String(acc);
|
||
sibling.style.left = (acc / TOTAL * 100) + '%';
|
||
sibling.style.width = (D(sibling) / TOTAL * 100) + '%';
|
||
syncVideoPreview(sibling);
|
||
acc += D(sibling);
|
||
}
|
||
}
|
||
}
|
||
function commitReorder(lane, dragged, insertIndex) {
|
||
const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== dragged);
|
||
dragged.remove();
|
||
if (insertIndex >= siblings.length) lane.appendChild(dragged);
|
||
else lane.insertBefore(dragged, siblings[insertIndex]);
|
||
}
|
||
function compactAll() {
|
||
Object.values(lanes).forEach(l => l && compactLane(l));
|
||
}
|
||
function clipAtTimeOnTrack(track, t) {
|
||
const lane = lanes[track];
|
||
if (!lane) return null;
|
||
for (const c of lane.querySelectorAll('.clip')) {
|
||
if (t >= clipStart(c) && t < clipEnd(c)) return c;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function updateTimeUI() {
|
||
$curTime.textContent = fmtTime(currentTime);
|
||
$totalTime.textContent = fmtTime(TOTAL);
|
||
$playhead.style.left = (currentTime / TOTAL * 100) + '%';
|
||
const v = clipAtTimeOnTrack('video', currentTime);
|
||
if ($canvasLb) {
|
||
$canvasLb.textContent = v
|
||
? '9:16 预览 · 1080×1920 · ' + (v.dataset.label || '')
|
||
: '9:16 预览 · 1080×1920';
|
||
}
|
||
updateActionButtons();
|
||
}
|
||
|
||
function selectClip(clip) {
|
||
document.querySelectorAll('#ed-timeline .clip.selected').forEach(c => {
|
||
c.classList.remove('selected');
|
||
c.querySelectorAll('[data-trim]').forEach(t => t.remove());
|
||
});
|
||
selectedEl = clip;
|
||
if (clip) {
|
||
clip.classList.add('selected');
|
||
// 三轨皆可剪 · 任意选中片段都加 trim 把手
|
||
const l = document.createElement('span'); l.className = 'trim-l'; l.dataset.trim = 'l';
|
||
const r = document.createElement('span'); r.className = 'trim-r'; r.dataset.trim = 'r';
|
||
clip.prepend(l); clip.appendChild(r);
|
||
}
|
||
updateInspector();
|
||
updateActionButtons();
|
||
}
|
||
function updateInspector() {
|
||
if (!selectedEl) {
|
||
$insName.textContent = '未选';
|
||
$insStart.value = '—';
|
||
$insDur.value = '—';
|
||
return;
|
||
}
|
||
const t = selectedEl.dataset.track;
|
||
const num = selectedEl.querySelector('.num')?.textContent.trim();
|
||
const label = selectedEl.dataset.label || '';
|
||
$insName.textContent = (t === 'video' ? '镜 ' + num : t === 'subtitle' ? '字幕' : 'BGM')
|
||
+ (label ? ' · ' + label.slice(0, 12) : '');
|
||
$insStart.value = fmtTime(clipStart(selectedEl));
|
||
$insDur.value = clipDur(selectedEl).toFixed(2) + 's';
|
||
}
|
||
function updateActionButtons() {
|
||
const has = !!selectedEl;
|
||
$delBtn.disabled = !has;
|
||
$copyBtn.disabled = !has;
|
||
if (has) {
|
||
const s = clipStart(selectedEl), e = clipEnd(selectedEl);
|
||
$splitBtn.disabled = !(currentTime > s + 0.05 && currentTime < e - 0.05);
|
||
} else {
|
||
$splitBtn.disabled = true;
|
||
}
|
||
}
|
||
|
||
function bindClipClicks() {
|
||
document.querySelectorAll('#ed-timeline .clip').forEach(clip => {
|
||
if (clip.dataset.boundClick) return;
|
||
clip.dataset.boundClick = '1';
|
||
clip.addEventListener('click', (e) => {
|
||
if (e.target.closest('[data-trim]')) return;
|
||
if (_bodyDragMoved) return; // 拖动重排刚结束 · 抑制 click
|
||
e.stopPropagation();
|
||
selectClip(clip);
|
||
if (clip.dataset.track === 'video') {
|
||
currentTime = clipStart(clip);
|
||
updateTimeUI();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function laneSeek(lane, ev) {
|
||
if (ev.target.closest('.clip') || ev.target.closest('.playhead') || ev.target.closest('[data-trim]')) return;
|
||
const rect = lane.getBoundingClientRect();
|
||
const pct = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width));
|
||
currentTime = pct * TOTAL;
|
||
updateTimeUI();
|
||
}
|
||
$laneV.addEventListener('click', e => laneSeek($laneV, e));
|
||
|
||
$ruler.addEventListener('click', (e) => {
|
||
const rect = $ruler.getBoundingClientRect();
|
||
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
||
currentTime = pct * TOTAL;
|
||
updateTimeUI();
|
||
});
|
||
|
||
let dragging = false;
|
||
let playheadDragOffset = 0;
|
||
function playheadCenterX() {
|
||
const rect = $laneS.getBoundingClientRect();
|
||
return rect.left + (currentTime / TOTAL) * rect.width;
|
||
}
|
||
function seekPlayhead(clientX) {
|
||
const rect = $laneS.getBoundingClientRect();
|
||
const pct = Math.max(0, Math.min(1, (clientX - playheadDragOffset - rect.left) / rect.width));
|
||
currentTime = pct * TOTAL;
|
||
updateTimeUI();
|
||
}
|
||
function startPlayheadDrag(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
dragging = true;
|
||
playheadDragOffset = e.clientX - playheadCenterX();
|
||
if (playing) pause();
|
||
$playhead.classList.add('is-dragging');
|
||
$tl.classList.add('is-dragging-playhead');
|
||
}
|
||
$playhead.addEventListener('mousedown', startPlayheadDrag);
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!dragging) return;
|
||
seekPlayhead(e.clientX);
|
||
});
|
||
document.addEventListener('mouseup', () => {
|
||
if (!dragging) return;
|
||
dragging = false;
|
||
playheadDragOffset = 0;
|
||
$playhead.classList.remove('is-dragging');
|
||
$tl.classList.remove('is-dragging-playhead');
|
||
});
|
||
|
||
function play() {
|
||
if (playing) return;
|
||
if (currentTime >= TOTAL - 0.01) currentTime = 0;
|
||
playing = true;
|
||
$playIcon.innerHTML = PAUSE_SVG;
|
||
$playBtn.classList.add('is-playing');
|
||
lastTs = performance.now();
|
||
rafId = requestAnimationFrame(tick);
|
||
}
|
||
function pause() {
|
||
if (!playing) return;
|
||
playing = false;
|
||
$playIcon.innerHTML = PLAY_SVG;
|
||
$playBtn.classList.remove('is-playing');
|
||
if (rafId) cancelAnimationFrame(rafId);
|
||
rafId = null;
|
||
}
|
||
function tick(ts) {
|
||
if (!playing) return;
|
||
const dt = (ts - lastTs) / 1000;
|
||
lastTs = ts;
|
||
currentTime = Math.min(TOTAL, currentTime + dt);
|
||
updateTimeUI();
|
||
if (currentTime >= TOTAL) { pause(); return; }
|
||
rafId = requestAnimationFrame(tick);
|
||
}
|
||
$playBtn.addEventListener('click', () => playing ? pause() : play());
|
||
|
||
const FRAME = 1 / 30;
|
||
$prevBtn.addEventListener('click', () => {
|
||
pause();
|
||
currentTime = Math.max(0, currentTime - FRAME);
|
||
updateTimeUI();
|
||
});
|
||
$nextBtn.addEventListener('click', () => {
|
||
pause();
|
||
currentTime = Math.min(TOTAL, currentTime + FRAME);
|
||
updateTimeUI();
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
const stage5 = document.querySelector('section.stage.active[data-stage-pane="5"]');
|
||
if (!stage5) return;
|
||
if (e.target.matches('input, textarea, [contenteditable]')) return;
|
||
if (e.code === 'Space') { e.preventDefault(); playing ? pause() : play(); }
|
||
else if (e.key === 'ArrowLeft') { e.preventDefault(); $prevBtn.click(); }
|
||
else if (e.key === 'ArrowRight') { e.preventDefault(); $nextBtn.click(); }
|
||
else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||
if (selectedEl) { e.preventDefault(); deleteSelected(); }
|
||
}
|
||
});
|
||
|
||
$zoom.addEventListener('input', () => {
|
||
const pct = Number($zoom.value);
|
||
document.querySelectorAll('#ed-timeline .lane, #ed-timeline #ed-ruler').forEach(l => {
|
||
l.style.minWidth = (pct === 100 ? '' : (pct + '%'));
|
||
});
|
||
});
|
||
|
||
function deleteSelected() {
|
||
if (!selectedEl) return;
|
||
const lane = selectedEl.parentElement;
|
||
const next = selectedEl.nextElementSibling?.classList.contains('clip')
|
||
? selectedEl.nextElementSibling
|
||
: selectedEl.previousElementSibling?.classList.contains('clip')
|
||
? selectedEl.previousElementSibling
|
||
: null;
|
||
selectedEl.remove();
|
||
settleLane(lane);
|
||
if (next) selectClip(next);
|
||
else { selectedEl = null; updateInspector(); updateActionButtons(); }
|
||
renumberVideo();
|
||
updateTimeUI();
|
||
}
|
||
$delBtn.addEventListener('click', deleteSelected);
|
||
|
||
$copyBtn.addEventListener('click', () => {
|
||
if (!selectedEl) return;
|
||
const dup = selectedEl.cloneNode(true);
|
||
dup.classList.remove('selected');
|
||
dup.querySelectorAll('[data-trim]').forEach(t => t.remove());
|
||
delete dup.dataset.boundClick;
|
||
selectedEl.after(dup);
|
||
if (selectedEl.parentElement !== lanes.video) {
|
||
dup.dataset.start = String(Math.max(0, Math.min(TOTAL - D(dup), clipEnd(selectedEl))));
|
||
}
|
||
settleLane(selectedEl.parentElement);
|
||
bindClipClicks();
|
||
renumberVideo();
|
||
selectClip(dup);
|
||
});
|
||
|
||
$splitBtn.addEventListener('click', () => {
|
||
if (!selectedEl) return;
|
||
const s = clipStart(selectedEl), e = clipEnd(selectedEl);
|
||
if (!(currentTime > s + 0.05 && currentTime < e - 0.05)) return;
|
||
const cutOff = currentTime - s; // 距 clip 起始的偏移
|
||
const origDur = D(selectedEl);
|
||
const leftDur = cutOff;
|
||
const rightDur = origDur - cutOff;
|
||
// 左半:dur=max=leftDur(切开后双方各自独立 · 不能再恢复跨越切点)
|
||
selectedEl.dataset.dur = String(leftDur);
|
||
selectedEl.dataset.max = String(leftDur);
|
||
selectedEl.dataset.in = '0';
|
||
selectedEl.dataset.start = String(s);
|
||
const right = selectedEl.cloneNode(true);
|
||
right.classList.remove('selected');
|
||
right.querySelectorAll('[data-trim]').forEach(t => t.remove());
|
||
delete right.dataset.boundClick;
|
||
right.dataset.dur = String(rightDur);
|
||
right.dataset.max = String(rightDur);
|
||
right.dataset.in = '0';
|
||
right.dataset.start = String(s + leftDur);
|
||
// 右半若是视频片段 · 重新生成胶卷帧条数量(按时长成比例)
|
||
if (right.classList.contains('video')) {
|
||
const oldFrames = right.querySelector('.frames');
|
||
if (oldFrames) {
|
||
const n = Math.max(2, Math.round(rightDur * 1.5));
|
||
oldFrames.innerHTML = '<span class="fr"></span>'.repeat(n);
|
||
}
|
||
const oldFramesL = selectedEl.querySelector('.frames');
|
||
if (oldFramesL) {
|
||
const n = Math.max(2, Math.round(leftDur * 1.5));
|
||
oldFramesL.innerHTML = '<span class="fr"></span>'.repeat(n);
|
||
}
|
||
}
|
||
selectedEl.after(right);
|
||
layoutLane(selectedEl.parentElement);
|
||
bindClipClicks();
|
||
renumberVideo();
|
||
selectClip(right);
|
||
});
|
||
|
||
function renumberVideo() {
|
||
$laneV.querySelectorAll('.clip.video').forEach((c, i) => {
|
||
const num = c.querySelector('.num');
|
||
if (num) num.textContent = String(i + 1);
|
||
});
|
||
}
|
||
|
||
// Trim 把手 · 三轨通用 · 左右边界按真实裁剪方向反馈
|
||
document.addEventListener('mousedown', (e) => {
|
||
const handle = e.target.closest('[data-trim]');
|
||
if (!handle || !selectedEl) return;
|
||
e.preventDefault(); e.stopPropagation();
|
||
const side = handle.dataset.trim;
|
||
const lane = selectedEl.parentElement;
|
||
const isVideoTrack = lane === lanes.video;
|
||
freezeLaneStarts(lane);
|
||
const laneRect = lane.getBoundingClientRect();
|
||
const startMouseX = e.clientX;
|
||
const startStart = clipStart(selectedEl);
|
||
const startDur = D(selectedEl);
|
||
const startEnd = startStart + startDur;
|
||
const startIn = clampSourceIn(selectedEl);
|
||
const startSourceDur = sourceDur(selectedEl);
|
||
const startOut = Math.max(0, startSourceDur - startIn - startDur);
|
||
const prevClip = selectedEl.previousElementSibling?.classList.contains('clip') ? selectedEl.previousElementSibling : null;
|
||
const nextClip = selectedEl.nextElementSibling?.classList.contains('clip') ? selectedEl.nextElementSibling : null;
|
||
const prevEnd = prevClip ? clipEnd(prevClip) : 0;
|
||
const nextStart = nextClip ? clipStart(nextClip) : TOTAL;
|
||
let didTrim = false;
|
||
|
||
function onMove(ev) {
|
||
const dx = ev.clientX - startMouseX;
|
||
if (Math.abs(dx) > 1) didTrim = true;
|
||
const dt = (dx / laneRect.width) * TOTAL;
|
||
if (!isVideoTrack) {
|
||
if (side === 'l') {
|
||
const maxStart = startEnd - MIN_DUR;
|
||
let newStart = Math.max(0, Math.min(maxStart, startStart + dt));
|
||
const guidedStart = guideEdgeToVideo(newStart);
|
||
if (guidedStart !== null) newStart = Math.max(0, Math.min(maxStart, guidedStart));
|
||
selectedEl.dataset.start = String(newStart);
|
||
selectedEl.dataset.dur = String(Math.max(MIN_DUR, startEnd - newStart));
|
||
} else {
|
||
const minEnd = startStart + MIN_DUR;
|
||
let newEnd = Math.max(minEnd, Math.min(TOTAL, startEnd + dt));
|
||
const guidedEnd = guideEdgeToVideo(newEnd);
|
||
if (guidedEnd !== null) newEnd = Math.max(minEnd, Math.min(TOTAL, guidedEnd));
|
||
selectedEl.dataset.start = String(startStart);
|
||
selectedEl.dataset.dur = String(Math.max(MIN_DUR, newEnd - startStart));
|
||
}
|
||
layoutLane(lane);
|
||
updateInspector();
|
||
updateActionButtons();
|
||
return;
|
||
}
|
||
hideAlignGuide();
|
||
if (side === 'l') {
|
||
const minStart = Math.max(prevEnd, startStart - startIn);
|
||
const maxStart = startEnd - MIN_DUR;
|
||
const newStart = Math.max(minStart, Math.min(maxStart, startStart + dt));
|
||
const newDur = Math.max(MIN_DUR, startEnd - newStart);
|
||
const newIn = Math.max(0, Math.min(startSourceDur - newDur, startIn + (newStart - startStart)));
|
||
selectedEl.dataset.start = String(newStart);
|
||
selectedEl.dataset.dur = String(newDur);
|
||
selectedEl.dataset.in = String(newIn);
|
||
} else {
|
||
const minEnd = startStart + MIN_DUR;
|
||
const maxEnd = Math.min(nextStart, startEnd + startOut, TOTAL);
|
||
const newEnd = Math.max(minEnd, Math.min(maxEnd, startEnd + dt));
|
||
selectedEl.dataset.start = String(startStart);
|
||
selectedEl.dataset.dur = String(newEnd - startStart);
|
||
selectedEl.dataset.in = String(startIn);
|
||
}
|
||
layoutLane(lane);
|
||
updateInspector();
|
||
updateActionButtons();
|
||
}
|
||
function onUp() {
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
document.body.style.cursor = '';
|
||
hideAlignGuide();
|
||
if (isVideoTrack) {
|
||
snapLane(lane);
|
||
if (didTrim && selectedEl) {
|
||
currentTime = side === 'l' ? clipStart(selectedEl) : clipEnd(selectedEl);
|
||
}
|
||
updateInspector();
|
||
updateTimeUI();
|
||
} else {
|
||
layoutLane(lane);
|
||
updateInspector();
|
||
updateActionButtons();
|
||
}
|
||
}
|
||
document.body.style.cursor = 'ew-resize';
|
||
document.addEventListener('mousemove', onMove);
|
||
document.addEventListener('mouseup', onUp);
|
||
});
|
||
|
||
// 拖拽选中片段身体 · 在同轨内重排位置
|
||
let _bodyDragMoved = false;
|
||
document.addEventListener('mousedown', (e) => {
|
||
const clip = e.target.closest('#ed-timeline .clip');
|
||
if (!clip) return;
|
||
if (e.target.closest('[data-trim]')) return;
|
||
if (e.target.closest('.ph-grab')) return;
|
||
e.preventDefault();
|
||
_bodyDragMoved = false;
|
||
if (clip !== selectedEl) selectClip(clip);
|
||
const lane = clip.parentElement;
|
||
const isVideoTrack = lane === lanes.video;
|
||
freezeLaneStarts(lane);
|
||
const laneRect = lane.getBoundingClientRect();
|
||
const startMouseX = e.clientX;
|
||
const startStartT = clipStart(clip);
|
||
const clipDuration = D(clip);
|
||
let dropIndex = null;
|
||
|
||
function onMove(ev) {
|
||
const dx = ev.clientX - startMouseX;
|
||
if (!_bodyDragMoved && Math.abs(dx) < 5) return;
|
||
_bodyDragMoved = true;
|
||
clip.classList.add('dragging');
|
||
const dt = (dx / laneRect.width) * TOTAL;
|
||
if (!isVideoTrack) {
|
||
const previewStart = guideClipToVideo(startStartT + dt, clipDuration);
|
||
clip.dataset.start = String(previewStart);
|
||
clip.style.left = (previewStart / TOTAL * 100) + '%';
|
||
clip.style.width = (clipDuration / TOTAL * 100) + '%';
|
||
updateInspector();
|
||
updateActionButtons();
|
||
document.body.style.cursor = 'grabbing';
|
||
return;
|
||
}
|
||
hideAlignGuide();
|
||
lane.classList.add('is-reordering');
|
||
const newCenter = Math.max(clipDuration / 2, Math.min(TOTAL - clipDuration / 2, startStartT + clipDuration / 2 + dt));
|
||
const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip);
|
||
dropIndex = insertIndexForCenter(siblings, newCenter);
|
||
previewReorder(lane, clip, dropIndex);
|
||
const previewStart = Math.max(0, Math.min(TOTAL - clipDuration, newCenter - clipDuration / 2));
|
||
clip.dataset.start = String(previewStart);
|
||
clip.style.left = (previewStart / TOTAL * 100) + '%';
|
||
clip.style.width = (clipDuration / TOTAL * 100) + '%';
|
||
syncVideoPreview(clip);
|
||
document.body.style.cursor = 'grabbing';
|
||
}
|
||
function onUp(ev) {
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
removeDropGhost(lane);
|
||
lane.classList.remove('is-reordering');
|
||
clip.classList.remove('dragging');
|
||
document.body.style.cursor = '';
|
||
hideAlignGuide();
|
||
if (!_bodyDragMoved) return;
|
||
if (!isVideoTrack) {
|
||
layoutLane(lane);
|
||
selectClip(clip);
|
||
updateInspector();
|
||
updateActionButtons();
|
||
setTimeout(() => { _bodyDragMoved = false; }, 50);
|
||
return;
|
||
}
|
||
if (dropIndex === null) {
|
||
const dx = ev.clientX - startMouseX;
|
||
const dt = (dx / laneRect.width) * TOTAL;
|
||
const newCenter = Math.max(clipDuration / 2, Math.min(TOTAL - clipDuration / 2, startStartT + clipDuration / 2 + dt));
|
||
const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip);
|
||
dropIndex = insertIndexForCenter(siblings, newCenter);
|
||
}
|
||
commitReorder(lane, clip, dropIndex);
|
||
snapLane(lane);
|
||
if (lane === lanes.video) renumberVideo();
|
||
selectClip(clip);
|
||
updateTimeUI();
|
||
setTimeout(() => { _bodyDragMoved = false; }, 50);
|
||
}
|
||
document.addEventListener('mousemove', onMove);
|
||
document.addEventListener('mouseup', onUp);
|
||
});
|
||
|
||
// 初始化:磁吸布局 + 绑定
|
||
compactAll();
|
||
bindClipClicks();
|
||
updateTimeUI();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|