All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索 - model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框 - model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除) - model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批) - model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃 - model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段 - image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通 - image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64 - asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列 - pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
2793 lines
165 KiB
HTML
2793 lines
165 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title id="page-title">流水线 · 流·Studio</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||
<style>
|
||
/* ─── Project header ─── */
|
||
.proj-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 22px; align-items: flex-start; }
|
||
.proj-head h1 { font-size: 20px; font-weight: 700; letter-spacing: -.012em; }
|
||
|
||
/* ─── Stepper ─── */
|
||
.stepper { display: flex; align-items: center; gap: 0; margin-bottom: 28px; padding: 14px 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; }
|
||
.stepper::before, .stepper::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
|
||
.stepper::before { top: -7px; left: -7px; }
|
||
.stepper::after { bottom: -7px; right: -7px; }
|
||
.stepper .corner-tr, .stepper .corner-bl { position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
|
||
.stepper .corner-tr { top: -7px; right: -7px; }
|
||
.stepper .corner-bl { bottom: -7px; left: -7px; }
|
||
.stage-step { display: flex; align-items: center; gap: 10px; padding: 6px 0; cursor: pointer; user-select: none; }
|
||
.stage-step .num { width: 26px; height: 26px; display: grid; place-items: center; font-family: var(--font-mono); font-size: 12px; font-weight: 600; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); color: var(--black-alpha-48); flex-shrink: 0; }
|
||
.stage-step.done .num { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }
|
||
.stage-step.active .num { background: var(--heat); border-color: var(--heat); color: var(--accent-white); }
|
||
.stage-step.locked { opacity: .5; cursor: not-allowed; }
|
||
.stage-step .lbl { font-size: 13px; font-weight: 500; color: var(--accent-black); }
|
||
.stage-step.active .lbl { color: var(--accent-black); font-weight: 600; }
|
||
.stage-step .st { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); margin-left: 4px; padding: 1px 6px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); letter-spacing: .04em; }
|
||
.stage-step.active .st { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
|
||
.stage-step.done .st { background: rgba(66,195,102,.12); color: var(--accent-forest); border-color: rgba(66,195,102,.3); }
|
||
.stage-step.done .lbl { color: var(--black-alpha-72); }
|
||
.stage-step:hover .num { border-color: var(--heat-40); }
|
||
.stage-step:hover .lbl { color: var(--heat); }
|
||
.stage-line { flex: 1; height: 1px; background: var(--border-faint); margin: 0 14px; min-width: 30px; }
|
||
.stage-line.done { background: var(--accent-black); }
|
||
|
||
/* ─── Stage panes ─── */
|
||
.stage { display: none; }
|
||
.stage.active { display: block; }
|
||
|
||
/* Common pane */
|
||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
|
||
.pane-h { display: flex; align-items: center; gap: 8px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); }
|
||
.pane-h strong { font-size: 14px; font-weight: 600; }
|
||
|
||
/* Stage foot */
|
||
.stage-foot { display: flex; justify-content: space-between; align-items: center; padding: 18px 0 0; margin-top: 18px; border-top: 1px solid var(--border-faint); }
|
||
.stage-foot .info { font-size: 12.5px; color: var(--black-alpha-56); }
|
||
.stage-foot .info .mono { font-family: var(--font-mono); color: var(--black-alpha-48); font-size: 11.5px; letter-spacing: .02em; }
|
||
|
||
/* === STAGE 1 · 脚本(镜头脚本 : 脚本助手 = 7 : 3,助手在 3:2 基础上再缩 1/4) === */
|
||
.stage-script { display: grid; grid-template-columns: 7fr 3fr; gap: 16px; min-height: 560px; }
|
||
|
||
.chat-pane { display: flex; flex-direction: column; }
|
||
.chat-body { padding: 16px 18px; flex: 1; overflow-y: auto; max-height: 460px; display: flex; flex-direction: column; gap: 14px; }
|
||
.msg .bubble { max-width: 90%; padding: 10px 14px; font-size: 13px; line-height: 1.6; border: 1px solid var(--border-faint); border-radius: var(--r-md); }
|
||
.msg.ai .bubble { background: var(--surface); }
|
||
.msg.user { display: flex; flex-direction: column; align-items: flex-end; }
|
||
.msg.user .bubble { background: var(--heat-12); color: var(--accent-black); border-color: var(--heat-20); }
|
||
.msg .time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 4px; letter-spacing: .02em; }
|
||
.msg .actions { display: flex; gap: 6px; margin-top: 6px; }
|
||
.ai-avatar { width: 26px; height: 26px; background: var(--heat); color: var(--accent-white); display: grid; place-items: center; font-size: 11px; font-weight: 700; border: 1px solid var(--heat); border-radius: 50%; }
|
||
.del { text-decoration: line-through; color: var(--black-alpha-48); }
|
||
.ins { background: var(--forest-bg); color: var(--accent-forest); padding: 0 3px; }
|
||
.chat-input { padding: 14px 18px 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 加分镜插槽 */
|
||
.shot-insert-gap { height: 14px; position: relative; display: flex; align-items: center; justify-content: center; }
|
||
.shot-insert-gap .add-shot-btn { opacity: 0; height: 22px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: 999px; font-size: 11.5px; font-family: var(--font-mono); cursor: pointer; box-shadow: var(--shadow-cta); transition: opacity var(--t-base); display: inline-flex; align-items: center; gap: 5px; pointer-events: none; }
|
||
.shot-insert-gap .add-shot-btn svg { width: 11px; height: 11px; }
|
||
.shot-insert-gap:hover .add-shot-btn { opacity: 1; pointer-events: auto; }
|
||
.shot-insert-gap::before { content: ''; position: absolute; left: 12px; right: 12px; top: 50%; height: 1px; background: transparent; transition: background var(--t-base); }
|
||
.shot-insert-gap:hover::before { background: var(--heat-20); }
|
||
|
||
/* 镜头脚本空缺省态 */
|
||
.shots-empty { padding: 36px 24px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 12px; color: var(--black-alpha-48); }
|
||
.shots-empty .empty-ico { width: 56px; height: 56px; border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--black-alpha-32); }
|
||
.shots-empty .empty-title { font-size: 14px; font-weight: 500; color: var(--accent-black); }
|
||
.shots-empty .empty-hint { font-size: 12px; color: var(--black-alpha-56); line-height: 1.55; max-width: 280px; font-family: var(--font-mono); letter-spacing: .02em; }
|
||
|
||
/* 对话空态三胶囊 */
|
||
.chat-empty { padding: 28px 18px 14px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||
.chat-empty .ce-title { font-size: 13.5px; color: var(--accent-black); font-weight: 500; }
|
||
.chat-empty .ce-hint { font-size: 11.5px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }
|
||
.chat-modes { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
|
||
.chat-mode { height: 30px; padding: 0 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 999px; font-size: 12.5px; color: var(--accent-black); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
|
||
.chat-mode:hover { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }
|
||
.chat-mode.primary { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }
|
||
.chat-mode svg { width: 13px; height: 13px; }
|
||
|
||
/* AI 思考态 typing indicator */
|
||
.ai-thinking .dots { display: inline-flex; gap: 3px; }
|
||
.ai-thinking .dots span { width: 6px; height: 6px; background: var(--black-alpha-32); border-radius: 50%; animation: thinking 1.2s ease-in-out infinite; }
|
||
.ai-thinking .dots span:nth-child(2) { animation-delay: .2s; }
|
||
.ai-thinking .dots span:nth-child(3) { animation-delay: .4s; }
|
||
@keyframes thinking { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
|
||
|
||
/* 视口锁定 · 只让主内容区滚动 (sidebar + topbar 固定,不随页面滚) */
|
||
html, body { height: 100%; overflow: hidden; max-width: 100vw; }
|
||
.app { height: 100vh; max-height: 100vh; overflow: hidden; }
|
||
.app > .sidebar { height: 100vh; overflow-y: auto; }
|
||
.app > main { height: 100vh; max-height: 100vh; overflow: hidden; display: flex; flex-direction: column; min-width: 0; }
|
||
.app > main > .topbar { flex-shrink: 0; }
|
||
.app > main > .content { flex: 1 1 0; min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden; }
|
||
|
||
/* === STAGE 2 · 基础资产 === */
|
||
.stage-assets { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; }
|
||
.stage-assets > div { min-width: 0; }
|
||
.asset-side { position: sticky; top: 16px; align-self: start; }
|
||
.asset-sec { min-width: 0; }
|
||
.asset-strip-wrap { min-width: 0; }
|
||
.asset-side .ttab { padding: 10px 12px; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 8px; border: 1px solid transparent; border-radius: var(--r-md); }
|
||
.asset-side .ttab:hover { background: var(--background-lighter); }
|
||
.asset-side .ttab.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
|
||
.asset-side .ttab .num { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); margin-left: auto; }
|
||
.asset-side .ttab.active .num { color: var(--heat); }
|
||
.asset-side .info { font-size: 12px; color: var(--black-alpha-48); padding: 14px 12px; line-height: 1.6; margin-top: 14px; border-top: 1px solid var(--border-faint); }
|
||
.asset-side .info strong { color: var(--black-alpha-56); display: block; }
|
||
.asset-side .info .mono { font-family: var(--font-mono); }
|
||
|
||
.asset-sec { scroll-margin-top: 16px; }
|
||
.asset-sec + .asset-sec { margin-top: 32px; }
|
||
.asset-sec .sec-h { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
||
.asset-sec .sec-h h3 { font-size: 15px; font-weight: 600; }
|
||
/* .pill-tip 主样式定义在下方 (heat 主色) */
|
||
|
||
/* 预设库横滑行(卡片尺寸与主区 .asset-card-2 一致) */
|
||
.asset-strip-wrap { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-faint); }
|
||
.asset-strip-wrap .strip-h { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase; }
|
||
.asset-strip { display: flex; gap: 14px; overflow-x: auto; overflow-y: hidden; padding: 2px 2px 14px; scrollbar-width: thin; }
|
||
.asset-strip::-webkit-scrollbar { height: 8px; }
|
||
.asset-strip::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }
|
||
.asset-strip .asset-card-2 { flex: 0 0 240px; min-width: 240px; max-width: 240px; }
|
||
|
||
/* 「去 XX 库」CTA 胶囊 · 主操作色,更显眼 */
|
||
.asset-sec .sec-h .pill-tip,
|
||
.asset-strip-wrap .strip-h .pill-tip {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
height: 28px; padding: 0 14px;
|
||
background: var(--heat-12);
|
||
border: 1px solid var(--heat-20);
|
||
border-radius: 999px;
|
||
font-size: 12px; color: var(--heat); font-weight: 500;
|
||
cursor: pointer; font-family: inherit;
|
||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||
}
|
||
.asset-sec .sec-h .pill-tip:hover,
|
||
.asset-strip-wrap .strip-h .pill-tip:hover {
|
||
background: var(--heat); color: var(--accent-white); border-color: var(--heat);
|
||
box-shadow: var(--shadow-cta);
|
||
}
|
||
.asset-sec .sec-h .pill-tip svg,
|
||
.asset-strip-wrap .strip-h .pill-tip svg { width: 12px; height: 12px; }
|
||
|
||
.asset-grid-2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
|
||
/* 商品行:左侧商品卡 + 右侧三视图预览(三视图是单张 16:9 图,不是 3 张) */
|
||
.prod-row { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
|
||
.prod-row > .asset-card-2 { flex: 0 0 240px; max-width: 240px; }
|
||
.prod-preview { flex: 0 0 360px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px; display: none; flex-direction: column; gap: 10px; }
|
||
.prod-preview.show { display: flex; }
|
||
.prod-preview-h { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .04em; text-transform: uppercase; }
|
||
.prod-preview-img { aspect-ratio: 16/9; }
|
||
.prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }
|
||
/* 三视图历史版本缩略图 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-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); }
|
||
.asset-card-2.prod-lib-card .prod-thumb { aspect-ratio: 1.4 / 1; position: relative; }
|
||
.asset-card-2.prod-lib-card .prod-body { padding: 14px 14px 12px; flex: 1; }
|
||
.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); }
|
||
.asset-detail-lead .ad-zoom-btn {
|
||
position: absolute; right: 10px; bottom: 10px;
|
||
height: 28px; padding: 0 12px;
|
||
background: rgba(21,20,15,.7); color: #fff;
|
||
border: 0; border-radius: var(--r-pill);
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
font-size: 11.5px; font-family: inherit;
|
||
cursor: pointer; backdrop-filter: blur(4px);
|
||
transition: background var(--t-base);
|
||
}
|
||
.asset-detail-lead .ad-zoom-btn:hover { background: rgba(21,20,15,.9); }
|
||
.asset-detail-lead .ad-zoom-btn svg { width: 12px; height: 12px; }
|
||
.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); }
|
||
.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; }
|
||
.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); }
|
||
|
||
/* ─── 添加来源 · 选择 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-stage-actions { display: flex; gap: 8px; margin-bottom: 12px; }
|
||
|
||
/* 故事板历史版本 */
|
||
.sb-history { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-faint); }
|
||
.sb-history-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; }
|
||
.sb-history-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }
|
||
.sb-history-row::-webkit-scrollbar { height: 6px; }
|
||
.sb-history-row::-webkit-scrollbar-thumb { background: var(--border-faint); }
|
||
.sb-history-thumb { flex: 0 0 80px; min-width: 80px; display: flex; flex-direction: column; gap: 4px; padding: 4px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; transition: border-color var(--t-base); }
|
||
.sb-history-thumb:hover { border-color: var(--heat); }
|
||
.sb-history-thumb.current { border-color: var(--heat); background: var(--heat-12); }
|
||
.sb-history-thumb .placeholder { aspect-ratio: 1; }
|
||
.sb-history-thumb .ts { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); text-align: center; }
|
||
.sb-history-thumb.current .ts { color: var(--heat); font-weight: 600; }
|
||
|
||
.pill-cta { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 14px; border-radius: 999px; font-size: 12.5px; cursor: pointer; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
|
||
.pill-cta.heat { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }
|
||
.pill-cta.heat:hover { box-shadow: var(--shadow-cta-hover); }
|
||
.pill-cta.ghost { background: var(--surface); color: var(--accent-black); border: 1px solid var(--border-faint); }
|
||
.pill-cta.ghost:hover { background: var(--background-lighter); border-color: var(--heat-20); color: var(--heat); }
|
||
.pill-cta svg { width: 13px; height: 13px; }
|
||
|
||
/* === STAGE 3 / 4 跳过条 === */
|
||
.skip-row { display: flex; justify-content: flex-end; margin-bottom: 12px; }
|
||
|
||
.sb-side .pane { padding: 18px; }
|
||
.prompt-edit { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.7; color: var(--black-alpha-56); white-space: pre-wrap; min-height: 200px; outline: none; letter-spacing: .01em; }
|
||
.prompt-edit:focus { border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }
|
||
.asset-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-pill); font-size: 11.5px; }
|
||
.asset-tag .dotc { width: 14px; height: 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 50%; }
|
||
|
||
/* === STAGE 4 · 视频片段 === */
|
||
.queue-bar { display: flex; align-items: center; gap: 16px; padding: 14px 18px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-bottom: 18px; }
|
||
.queue-bar .bar-wrap { flex: 1; height: 6px; background: var(--background-lighter); overflow: hidden; }
|
||
.queue-bar .bar-wrap > span { display: block; height: 100%; background: var(--heat); }
|
||
|
||
.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
|
||
.video-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base); }
|
||
.video-card:hover { border-color: var(--heat-40); }
|
||
.video-thumb { aspect-ratio: 9/16; max-height: 320px; position: relative; border-radius: var(--r-md) var(--r-md) 0 0; }
|
||
.video-thumb .play { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,0.05); cursor: pointer; opacity: 0; transition: opacity .15s; }
|
||
.video-thumb:hover .play { opacity: 1; }
|
||
.video-thumb .btn-play { width: 36px; height: 36px; background: rgba(0,0,0,.7); color: var(--accent-white); border-radius: 50%; display: grid; place-items: center; }
|
||
.video-card .body { padding: 10px 12px; }
|
||
|
||
/* 视频详情 modal 大视频 + 历史版本 */
|
||
.vd-main-wrap { display: flex; gap: 18px; align-items: flex-start; }
|
||
.vd-main { flex: 0 0 280px; aspect-ratio: 9/16; max-height: 460px; }
|
||
.vd-main .placeholder { aspect-ratio: 9/16; height: 100%; }
|
||
.vd-info { flex: 1; min-width: 0; }
|
||
.vd-history { margin-top: 16px; }
|
||
.vd-history-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
|
||
.vd-history-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }
|
||
.vd-history-thumb { flex: 0 0 64px; min-width: 64px; display: flex; flex-direction: column; gap: 4px; padding: 4px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; transition: border-color var(--t-base); position: relative; }
|
||
.vd-history-thumb:hover { border-color: var(--heat); }
|
||
.vd-history-thumb.current { border-color: var(--heat); background: var(--heat-12); }
|
||
.vd-history-thumb.adopted::after { content: ''; position: absolute; top: 2px; right: 2px; width: 14px; height: 14px; background: var(--heat); border-radius: 50%; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 9px 9px; }
|
||
.vd-history-thumb .placeholder { aspect-ratio: 9/16; }
|
||
.vd-history-thumb .ts { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); text-align: center; }
|
||
|
||
/* === STAGE 5 · 编辑器 === */
|
||
.editor { display: grid; grid-template-columns: 1fr 280px; grid-template-rows: 1fr auto; gap: 0; height: 580px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
|
||
.editor-preview { padding: 16px; border-right: 1px solid var(--border-faint); border-bottom: 1px solid var(--border-faint); display: flex; flex-direction: column; gap: 12px; }
|
||
.editor-preview .canvas { flex: 1; aspect-ratio: 9/16; max-height: 380px; margin: 0 auto; background:
|
||
repeating-linear-gradient(135deg, rgba(0,0,0,0.03) 0 1px, transparent 1px 12px),
|
||
var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
display: grid; place-items: center;
|
||
color: var(--black-alpha-48);
|
||
font-family: var(--font-mono);
|
||
font-size: 12px; }
|
||
.editor-preview .controls { display: flex; align-items: center; gap: 8px; justify-content: center; }
|
||
.ctl-btn { width: 36px; height: 36px; border: 1px solid var(--border-faint); background: var(--surface); color: var(--black-alpha-56); border-radius: var(--r-md); display: grid; place-items: center; cursor: pointer; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
|
||
.ctl-btn:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }
|
||
|
||
.editor-props { padding: 16px; border-bottom: 1px solid var(--border-faint); overflow-y: auto; }
|
||
.props-tabs { display: flex; gap: 0; margin-bottom: 14px; border-bottom: 1px solid var(--border-faint); }
|
||
.props-tabs > div { padding: 8px 12px; font-size: 12.5px; color: var(--black-alpha-56); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
||
.props-tabs > div.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; }
|
||
.style-swatch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
.swatch-card { padding: 10px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; }
|
||
.swatch-card:hover { background: var(--background-lighter); }
|
||
.swatch-card.selected { border-color: var(--heat); background: var(--heat-12); }
|
||
.swatch-card .demo { font-size: 12px; padding: 6px 8px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); margin-bottom: 4px; text-align: center; }
|
||
.swatch-card .demo.b { background: var(--accent-black); color: var(--accent-white); font-family: serif; }
|
||
.swatch-card .demo.c { color: var(--heat); -webkit-text-stroke: 0.5px var(--accent-black); }
|
||
.swatch-card .demo.d { background: var(--accent-honey); color: var(--accent-black); font-weight: 700; }
|
||
.swatch-card .nm { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }
|
||
.props-row { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; }
|
||
.props-row:last-child { border-bottom: 0; }
|
||
.props-row .k { color: var(--black-alpha-48); flex: 1; font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }
|
||
.input-mini { width: 90px; padding: 0 10px; height: 28px; font-size: 12px; border-radius: var(--r-md); background: var(--surface); border: 1px solid var(--black-alpha-12); }
|
||
|
||
.timeline { grid-column: 1 / -1; padding: 14px 16px; background: var(--background-base); }
|
||
.tl-toolbar { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border-faint); }
|
||
.tl-ruler { display: grid; grid-template-columns: 80px 1fr; align-items: center; padding: 4px 0; font-size: 10.5px; }
|
||
.tl-ruler .l { font-family: var(--font-mono); color: var(--black-alpha-48); padding-left: 4px; }
|
||
.tl-ruler .ticks { display: flex; justify-content: space-between; font-family: var(--font-mono); color: var(--black-alpha-48); padding: 0 4px; letter-spacing: .04em; }
|
||
.tl-track { display: grid; grid-template-columns: 80px 1fr; align-items: center; gap: 0; padding: 6px 0; }
|
||
.tl-track .label { font-size: 11.5px; color: var(--black-alpha-56); display: flex; align-items: center; gap: 6px; padding-left: 4px; }
|
||
.tl-track .label .dot { width: 8px; height: 8px; }
|
||
.tl-track .lane { display: flex; gap: 2px; height: 30px; position: relative; }
|
||
.clip { padding: 0 8px; font-size: 11px; display: flex; align-items: center; cursor: pointer; overflow: hidden; white-space: nowrap; user-select: none; }
|
||
.clip.video { background: var(--heat-12); border: 1px solid var(--heat-40); color: var(--heat); }
|
||
.clip.video.selected { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
|
||
.clip.subtitle { background: var(--forest-bg); border: 1px solid var(--forest-bd); color: var(--accent-forest); }
|
||
.clip.bgm { background: rgba(144, 97, 255, 0.10); border: 1px solid rgba(144, 97, 255, 0.30); color: var(--accent-amethyst); }
|
||
.clip .num { font-family: var(--font-mono); font-weight: 700; margin-right: 6px; opacity: .7; }
|
||
.playhead { position: absolute; top: -16px; bottom: -54px; width: 1px; background: var(--heat); pointer-events: none; }
|
||
.playhead::before { content: ''; position: absolute; top: -2px; left: -4px; width: 9px; height: 9px; background: var(--heat); transform: rotate(45deg); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="page">
|
||
|
||
<!-- Project header -->
|
||
<div class="proj-head">
|
||
<div style="display:flex; gap:14px; align-items:center;">
|
||
<div class="placeholder" style="width:42px;height:54px;"><span class="ph-frame">9:16</span></div>
|
||
<div>
|
||
<div style="display:flex; gap:8px; align-items:center;">
|
||
<h1 id="proj-h1">流水线项目</h1>
|
||
<span class="pill info"><span class="dot"></span>进行中</span>
|
||
</div>
|
||
<div class="muted-2 mono" style="font-size:11.5px; margin-top:4px; letter-spacing:.02em;">// <span id="proj-product">—</span> · AI 全生 · <span id="page-head-shots-meta">待生成镜头脚本</span> · 9:16</div>
|
||
</div>
|
||
</div>
|
||
<div class="hstack">
|
||
<button class="btn btn-ghost btn-sm" onclick="Shell.toast('分享', '/projects/p3/share')">分享</button>
|
||
<button class="btn btn-sm" id="proj-copy-btn">复制项目</button>
|
||
<button class="btn btn-sm" onclick="Shell.toast('归档项目', '/projects/p3/archive')">归档</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stage stepper -->
|
||
<div class="stepper">
|
||
<span class="corner-tr" aria-hidden></span><span class="corner-bl" aria-hidden></span>
|
||
<a class="stage-step active" data-stage="1" href="#stage-1"><div class="num">1</div><div class="lbl">脚本</div><div class="st">进行中</div></a>
|
||
<div class="stage-line"></div>
|
||
<a class="stage-step" data-stage="2" href="#stage-2"><div class="num">2</div><div class="lbl">基础资产</div><div class="st">待开始</div></a>
|
||
<div class="stage-line"></div>
|
||
<a class="stage-step" data-stage="3" href="#stage-3"><div class="num">3</div><div class="lbl">故事板</div><div class="st">待开始</div></a>
|
||
<div class="stage-line"></div>
|
||
<a class="stage-step" data-stage="4" href="#stage-4"><div class="num">4</div><div class="lbl">视频</div><div class="st">待开始</div></a>
|
||
<div class="stage-line"></div>
|
||
<a class="stage-step" data-stage="5" href="#stage-5"><div class="num">5</div><div class="lbl">拼接导出</div><div class="st">待开始</div></a>
|
||
</div>
|
||
|
||
<!-- ============= STAGE 1 · 脚本 ============= -->
|
||
<section class="stage active" data-stage-pane="1">
|
||
<div class="stage-script">
|
||
<div class="pane shot-list">
|
||
<div class="pane-h">
|
||
<strong>镜头脚本</strong>
|
||
<span class="muted-2 mono" id="shots-meta" style="font-size:11px;">· 空 · 待生成</span>
|
||
<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="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="2" 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="Quota.preflight({stage:'Stage 2 基础资产', est: 1.30, hash:'#stage-2'})">确认脚本,进入下一步 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg></button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============= STAGE 2 · 基础资产 ============= -->
|
||
<section class="stage" data-stage-pane="2">
|
||
<div class="stage-assets">
|
||
<div class="asset-side">
|
||
<div class="ttab active" data-jump="asset-sec-products"><span>商品</span><span class="num">3 张</span></div>
|
||
<div class="ttab" data-jump="asset-sec-characters"><span>人物</span><span class="num">2/2</span></div>
|
||
<div class="ttab" data-jump="asset-sec-scenes"><span>场景</span><span class="num">3/3</span></div>
|
||
<div class="info">
|
||
基础资产是后续故事板的素材。所有卡片同时展示,点左侧分类直接定位。
|
||
<br><br>
|
||
<strong class="mono">// 人物 +¥0.20/张</strong>
|
||
<strong class="mono">// 场景 +¥0.15/张</strong>
|
||
<span style="color:var(--black-alpha-48);">商品图无成本(直接复用商品库)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<!-- ===== 商品(项目内只有 1 个商品,从 URL ?product= 取)===== -->
|
||
<section class="asset-sec" id="asset-sec-products">
|
||
<div class="sec-h">
|
||
<h3>商品 · <span id="asset-prod-name">透真补水面膜</span></h3>
|
||
<span class="spacer"></span>
|
||
</div>
|
||
<div class="prod-row">
|
||
<div class="asset-card-2 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="2.4" 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">+ 新增人物</button>
|
||
</div>
|
||
|
||
<div class="asset-grid-2">
|
||
<div class="asset-card-2" data-asset-kind="character" data-asset-id="ch-linxi">
|
||
<div class="placeholder thumb-2"><span class="ph-frame">林夕 · 都市白领</span></div>
|
||
<div class="body-2">
|
||
<div class="hstack"><strong style="font-size:13.5px;">主角 · 林夕</strong><span class="spacer"></span></div>
|
||
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>25-30 岁都市白领,长发,穿宽松米色家居服,温柔但带点疲倦感。</div>
|
||
<div class="hstack" style="margin-top:10px;">
|
||
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-card-2" data-asset-kind="character" data-asset-id="ch-anan">
|
||
<div class="placeholder thumb-2">
|
||
<div style="display:flex; flex-direction:column; gap:8px; align-items:center;">
|
||
<div class="spinner"></div>
|
||
<span class="ph-frame">生成中 · 约 8s</span>
|
||
</div>
|
||
</div>
|
||
<div class="body-2">
|
||
<div class="hstack"><strong style="font-size:13.5px;">朋友/同事 · 阿楠</strong><span class="spacer"></span></div>
|
||
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>25-30 岁同龄女性,短发,穿白色衬衫,妆容精致皮肤好,作为对比。</div>
|
||
<div class="hstack" style="margin-top:10px;">
|
||
<button class="btn btn-ghost btn-sm" data-stop data-rerun disabled>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" data-stop data-replace disabled>替换</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- ===== 场景 ===== -->
|
||
<section class="asset-sec" id="asset-sec-scenes">
|
||
<div class="sec-h">
|
||
<h3>场景 · 3 个</h3>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-sm" id="asset-add-scene">+ 新增场景</button>
|
||
</div>
|
||
|
||
<div class="asset-grid-2">
|
||
<div class="asset-card-2" data-asset-kind="scene" data-asset-id="sc-desk">
|
||
<div class="placeholder thumb-2"><span class="ph-frame">深夜办公桌</span></div>
|
||
<div class="body-2">
|
||
<div class="hstack"><strong style="font-size:13.5px;">深夜办公桌</strong><span class="spacer"></span></div>
|
||
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>深夜居家办公环境,木质书桌,台灯暖光,电脑屏幕亮着。</div>
|
||
<div class="hstack" style="margin-top:10px;">
|
||
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-card-2" data-asset-kind="scene" data-asset-id="sc-bed">
|
||
<div class="placeholder thumb-2"><span class="ph-frame">床头特写</span></div>
|
||
<div class="body-2">
|
||
<div class="hstack"><strong style="font-size:13.5px;">卧室床头</strong><span class="spacer"></span></div>
|
||
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>米白色床品,木质床头柜,闹钟显示晚间时间。</div>
|
||
<div class="hstack" style="margin-top:10px;">
|
||
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-card-2" data-asset-kind="scene" data-asset-id="sc-subway">
|
||
<div class="placeholder thumb-2">
|
||
<div style="display:flex; flex-direction:column; gap:6px; align-items:center;">
|
||
<div class="fail-icon">!</div>
|
||
<span class="ph-frame">生成失败</span>
|
||
</div>
|
||
</div>
|
||
<div class="body-2">
|
||
<div class="hstack"><strong style="font-size:13.5px;">通勤地铁</strong><span class="spacer"></span><span class="pill err"><span class="dot"></span>失败</span></div>
|
||
<div class="prompt-box" contenteditable="true" spellcheck="false" data-stop>早高峰地铁车厢,光线偏冷,年轻通勤族,氛围紧张。</div>
|
||
<div class="hstack" style="margin-top:10px;">
|
||
<button class="btn btn-ghost btn-sm" data-stop data-rerun>重跑</button>
|
||
<span class="spacer"></span>
|
||
<button class="btn btn-ghost btn-sm" data-stop data-replace>替换</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stage-foot">
|
||
<div class="info"><span class="mono">[ 已确认 ¥0.85 · 待生成 ¥0.20 · 失败 ¥0(不扣) ]</span></div>
|
||
<div class="hstack">
|
||
<button class="btn" onclick="location.hash='#stage-1'">← 返回脚本</button>
|
||
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-3'">确认资产,进入故事板 →</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============= STAGE 3 · 故事板(按场分) ============= -->
|
||
<section class="stage" data-stage-pane="3">
|
||
<div class="skip-row">
|
||
<button class="btn btn-ghost btn-sm" onclick="location.hash='#stage-4'">跳过本步 →</button>
|
||
</div>
|
||
<div class="stage-storyboard">
|
||
<div class="sb-canvas">
|
||
<div class="sb-scenes-col" id="sb-scenes-row">
|
||
<!-- JS 注入 略缩图 (竖向) -->
|
||
</div>
|
||
<div class="placeholder sb-main-img" id="sb-main-img"><span class="ph-frame">未选择</span></div>
|
||
</div>
|
||
|
||
<div class="sb-side">
|
||
<div class="pane" style="padding:18px;">
|
||
<div class="hstack" style="margin-bottom:10px;">
|
||
<strong style="font-size:14px;">故事板 · <span id="sb-side-scene">场 1</span></strong>
|
||
<span class="spacer"></span>
|
||
<span class="pill ok"><span class="dot"></span>已生成</span>
|
||
</div>
|
||
<div class="muted-2" style="font-size:12px; line-height:1.55; margin-bottom:10px;">
|
||
整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。
|
||
</div>
|
||
<div style="display:flex; gap:8px; align-items:flex-start; padding:9px 10px; margin-bottom:14px; background:rgba(180,83,9,.08); border:1px solid rgba(180,83,9,.20); border-radius:var(--r-sm);">
|
||
<svg style="flex-shrink:0;margin-top:1px;" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#B45309" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
|
||
<div style="font-size:11.5px; color:#7C3A05; line-height:1.55; min-width:0;">
|
||
<strong style="color:#B45309;">仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href="#stage-1" style="color:#B45309;text-decoration:underline;">Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。
|
||
<div style="font-family:var(--font-mono); font-size:10.5px; color:rgba(180,83,9,.7); letter-spacing:.02em; margin-top:3px;">// PRD §5.2 · image-2 单次输出限制</div>
|
||
</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="Quota.preflight({stage:'Stage 4 视频片段', est: 1.35, 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>
|
||
</div>
|
||
|
||
<div class="video-grid" id="video-grid">
|
||
<div class="video-card" data-video-id="v1" data-duration="15">
|
||
<div class="placeholder video-thumb">
|
||
<span class="ph-frame">场 1 · 0-15s</span>
|
||
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="hstack"><strong style="font-size:13px;">场 1 · 深夜办公桌</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
|
||
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">15s · 1080×1920 · ¥0.45</div>
|
||
<div class="hstack" style="margin-top:8px;">
|
||
<button class="btn btn-ghost btn-sm" data-vstop>↻ 重跑</button>
|
||
<button class="btn btn-ghost btn-sm" data-vstop>⤓ 下载</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="video-card" data-video-id="v2" data-duration="12">
|
||
<div class="placeholder video-thumb">
|
||
<span class="ph-frame">场 2 · 15-27s</span>
|
||
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="hstack"><strong style="font-size:13px;">场 2 · 面膜包装/特写</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
|
||
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">12s · 1080×1920 · ¥0.45</div>
|
||
<div class="hstack" style="margin-top:8px;">
|
||
<button class="btn btn-ghost btn-sm" data-vstop>↻ 重跑</button>
|
||
<button class="btn btn-ghost btn-sm" data-vstop>⤓ 下载</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="video-card" data-video-id="v3" data-duration="13">
|
||
<div class="placeholder video-thumb">
|
||
<span class="ph-frame">场 3 · 27-40s</span>
|
||
<div class="play"><div class="btn-play"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M5 4l6 4-6 4z" fill="currentColor"/></svg></div></div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="hstack"><strong style="font-size:13px;">场 3 · 化妆台/产品定格</strong><span class="spacer"></span><span class="pill ok"><span class="dot"></span>完成</span></div>
|
||
<div class="muted-2 mono" style="font-size:11px; margin-top:4px;">13s · 1080×1920 · ¥0.45</div>
|
||
<div class="hstack" style="margin-top:8px;">
|
||
<button class="btn btn-ghost btn-sm" data-vstop>↻ 重跑</button>
|
||
<button class="btn btn-ghost btn-sm" data-vstop>⤓ 下载</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stage-foot">
|
||
<div class="info"><span class="mono">[ 已完成 3 场 · 累计 ¥1.35 · 总时长 <span id="seedance-total">40</span>s · 失败不扣 · 通过后扣 ]</span></div>
|
||
<div class="hstack">
|
||
<button class="btn" onclick="location.hash='#stage-3'">← 返回故事板</button>
|
||
<button class="btn btn-primary btn-lg" onclick="location.hash='#stage-5'">确认视频,进入拼接 →</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== Stage 4 · 视频详情 modal ===== -->
|
||
<div class="asset-modal-bg" id="video-detail-modal">
|
||
<div class="asset-modal" style="width: min(880px, 100%);">
|
||
<div class="asset-modal-h">
|
||
<h2 id="vd-title">视频详情</h2>
|
||
<span style="font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48);" id="vd-sub">// 场 1 · 15s</span>
|
||
<button class="x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
|
||
</div>
|
||
<div class="asset-modal-body">
|
||
<div class="vd-main-wrap">
|
||
<div class="vd-main">
|
||
<div class="placeholder" id="vd-main-img"><span class="ph-frame">大视频预览</span></div>
|
||
</div>
|
||
<div class="vd-info">
|
||
<div class="asset-detail-section-h">// 基础信息</div>
|
||
<div class="asset-detail-info" id="vd-info"></div>
|
||
<div class="vd-history">
|
||
<div class="vd-history-h">// 历史版本 · <span id="vd-history-ct">3</span> 版</div>
|
||
<div class="vd-history-row" id="vd-history-row"></div>
|
||
</div>
|
||
<div class="muted-2" style="font-size: 11.5px; color: var(--black-alpha-56); margin-top: 12px; font-family: var(--font-mono); letter-spacing: .02em;">// 点击历史略缩切换 · 「采用此版」标记当前展示版为最终采用</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-modal-f">
|
||
<button class="btn btn-ghost" type="button" data-modal-close>关闭</button>
|
||
<button class="btn" type="button" id="vd-regen-btn">↻ 重跑本场</button>
|
||
<button class="btn btn-primary" type="button" id="vd-adopt-btn">采用此版</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============= STAGE 5 · 拼接编辑器 ============= -->
|
||
<section class="stage" data-stage-pane="5">
|
||
<div class="editor">
|
||
<div class="editor-preview">
|
||
<div class="canvas">9:16 预览 · 1080×1920</div>
|
||
<div class="controls">
|
||
<button class="ctl-btn" title="上一帧"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M3 3v10l4-5zM9 3v10l4-5z" fill="currentColor"/></svg></button>
|
||
<button class="ctl-btn" title="播放" onclick="Shell.toast('播放', '00:08.42 / 00:15.00')"><svg width="16" height="16" viewBox="0 0 16 16"><path d="M5 4l7 4-7 4z" fill="currentColor"/></svg></button>
|
||
<button class="ctl-btn" title="下一帧"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M13 3v10l-4-5zM7 3v10l-4-5z" fill="currentColor"/></svg></button>
|
||
<span class="muted mono" style="font-size:12px; margin-left:8px;">00:08.42 / 00:15.00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="editor-props">
|
||
<div class="props-tabs">
|
||
<div class="active">字幕</div>
|
||
<div>转场</div>
|
||
<div>BGM</div>
|
||
</div>
|
||
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 字幕样式</div>
|
||
<div class="style-swatch">
|
||
<div class="swatch-card selected"><div class="demo">真实分享</div><div class="nm">朴素白底</div></div>
|
||
<div class="swatch-card"><div class="demo b">真实分享</div><div class="nm">影视黑底</div></div>
|
||
<div class="swatch-card"><div class="demo c">真实分享</div><div class="nm">手写描边</div></div>
|
||
<div class="swatch-card"><div class="demo d">真实分享</div><div class="nm">综艺暖黄</div></div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// 当前选中(镜 4)</div>
|
||
<div class="props-row"><span class="k">起始</span><input class="input-mini" value="00:08.00"></div>
|
||
<div class="props-row"><span class="k">时长</span><input class="input-mini" value="3.00s"></div>
|
||
<div class="props-row"><span class="k">音量</span><input class="input-mini" value="100"></div>
|
||
<div class="props-row"><span class="k">速度</span><input class="input-mini" value="1.0x"></div>
|
||
<div class="props-row"><span class="k">入场</span><span class="mono" style="font-size:11.5px;">交叉淡化</span></div>
|
||
|
||
<div class="divider"></div>
|
||
|
||
<div class="muted mono" style="font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;">// BGM</div>
|
||
<div class="props-row" style="border-bottom:0;">
|
||
<span style="font-size:12px; flex:1;">温柔治愈钢琴 · 0:42</span>
|
||
<button class="btn btn-ghost btn-sm">替换</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="timeline">
|
||
<div class="tl-toolbar">
|
||
<button class="btn btn-ghost btn-sm">↶</button>
|
||
<button class="btn btn-ghost btn-sm">↷</button>
|
||
<span class="muted-2" style="font-size:12px;">|</span>
|
||
<button class="btn btn-ghost btn-sm">分割</button>
|
||
<button class="btn btn-ghost btn-sm">复制</button>
|
||
<button class="btn btn-ghost btn-sm">删除</button>
|
||
<span class="spacer"></span>
|
||
<span class="muted mono" style="font-size:11px;">缩放</span>
|
||
<input type="range" min="50" max="200" value="100" style="width:120px;">
|
||
</div>
|
||
|
||
<div class="tl-ruler">
|
||
<div class="l">// time</div>
|
||
<div class="ticks">
|
||
<span>0s</span><span>2s</span><span>5s</span><span>8s</span><span>11s</span><span>13s</span><span>15s</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-track">
|
||
<div class="label"><span class="dot" style="background:var(--heat);"></span>视频</div>
|
||
<div class="lane">
|
||
<div class="clip video" style="flex:2;"><span class="num">1</span> 深夜办公桌</div>
|
||
<div class="clip video" style="flex:3;"><span class="num">2</span> 面膜包装</div>
|
||
<div class="clip video" style="flex:3;"><span class="num">3</span> 精华液微距</div>
|
||
<div class="clip video selected" style="flex:3;"><span class="num">4</span> 敷面膜平躺</div>
|
||
<div class="clip video" style="flex:2;"><span class="num">5</span> 化妆台</div>
|
||
<div class="clip video" style="flex:2;"><span class="num">6</span> 产品定格</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-track">
|
||
<div class="label"><span class="dot" style="background:var(--accent-forest);"></span>字幕</div>
|
||
<div class="lane" style="position:relative;">
|
||
<div class="clip subtitle" style="flex:2;">加班三天 脸已经不能看了…</div>
|
||
<div class="clip subtitle" style="flex:3;">还好我有这个 透真玻尿酸面膜</div>
|
||
<div class="clip subtitle" style="flex:3;">30g 精华 一片顶三片</div>
|
||
<div class="clip subtitle" style="flex:3;">敷完起来脸是软的</div>
|
||
<div class="clip subtitle" style="flex:2;">化妆都能看出来</div>
|
||
<div class="clip subtitle" style="flex:2;">5 片 ¥39.9 囤起来</div>
|
||
<div class="playhead" style="left:56%;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tl-track">
|
||
<div class="label"><span class="dot" style="background:var(--accent-amethyst);"></span>BGM</div>
|
||
<div class="lane">
|
||
<div class="clip bgm" style="flex:15;">温柔治愈钢琴 · 0:42(循环 1 次,淡入淡出)</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stage-foot">
|
||
<div class="info"><span class="mono">[ 合成预估 ~30s · 拼接 / 导出全程 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">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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.8" 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.8" 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.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
|
||
<button class="ai-gen-btn" type="button">AI 生成三视图</button>
|
||
</div>
|
||
</div>
|
||
<!-- 简介 -->
|
||
<div 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.8" 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.6" 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.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||
<span>没有三视图?上传立绘后用 AI 自动生成</span>
|
||
<button class="ai-gen-btn" type="button">AI 生成</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="asset-modal-f">
|
||
<button class="btn btn-ghost" type="button" data-modal-close>取消</button>
|
||
<button class="btn btn-primary" type="button" id="nc-save-btn">保存人物</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== Stage 2 · 演员库 / 场景库 全屏弹窗(共享 · kind 切换内容)===== -->
|
||
<div class="ml-modal-bg" id="ml-modal-bg">
|
||
<div class="ml-modal">
|
||
<div class="ml-modal-h">
|
||
<h2 id="ml-modal-title">演员库</h2>
|
||
<span class="ct" id="ml-modal-ct">// 共 0 个</span>
|
||
<button class="x" type="button" id="ml-close-btn" aria-label="关闭">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="ml-modal-body">
|
||
<aside class="ml-side" id="ml-side">
|
||
<!-- JS 注入 来源 -->
|
||
</aside>
|
||
<div class="ml-main">
|
||
<div class="ml-toolbar" id="ml-toolbar">
|
||
<!-- JS 注入 chips + 上传按钮 -->
|
||
</div>
|
||
<div class="ml-scroll">
|
||
<div class="ml-grid" id="ml-grid">
|
||
<!-- JS 注入 卡片 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 添加演员 / 场景 · 选择来源 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.7" 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.6" 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.6" 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/shell.js?v=202605211643"></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: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: PROJECT_TITLE }]
|
||
});
|
||
|
||
/* 渲染贯穿商品名 / 项目名 */
|
||
document.getElementById('page-title').textContent = PROJECT_TITLE + ' · 流水线 · 流·Studio';
|
||
document.getElementById('proj-h1').textContent = PROJECT_TITLE;
|
||
document.getElementById('proj-product').textContent = CURRENT_PRODUCT_NAME;
|
||
document.getElementById('proj-copy-btn').addEventListener('click', () => {
|
||
const nextV = 'v' + (parseInt(URL_PROJECT_VER.replace('v',''), 10) + 1);
|
||
Shell.toast('已复制项目', shortProductName(CURRENT_PRODUCT_NAME) + ' · ' + nextV);
|
||
});
|
||
|
||
// 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');
|
||
|
||
// tab 状态:< cur → done(已完成),= cur → active(进行中),> cur → 默认(待开始)
|
||
document.querySelectorAll('.stage-step').forEach(s => {
|
||
const i = +s.dataset.stage;
|
||
s.classList.remove('active', 'done');
|
||
const st = s.querySelector('.st');
|
||
if (i < cur) {
|
||
s.classList.add('done');
|
||
if (st) st.textContent = '已完成';
|
||
} else if (i === cur) {
|
||
s.classList.add('active');
|
||
if (st) st.textContent = '进行中';
|
||
} else {
|
||
if (st) st.textContent = '待开始';
|
||
}
|
||
});
|
||
// 连接线:i < cur 的线段标为 done
|
||
document.querySelectorAll('.stage-line').forEach((ln, idx) => {
|
||
// stage-line 在第 idx 个连接的是 stage(idx+1) → stage(idx+2),仅当 idx+1 < cur 才标 done
|
||
ln.classList.toggle('done', (idx + 1) < cur);
|
||
});
|
||
|
||
// 滚到顶部
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
function readHash() {
|
||
const m = location.hash.match(/stage-(\d)/);
|
||
if (m) activateStage(+m[1]);
|
||
// ?stage=N query 参数也接收
|
||
const q = new URLSearchParams(location.search);
|
||
const s = q.get('stage');
|
||
if (s) activateStage(+s);
|
||
}
|
||
window.addEventListener('hashchange', readHash);
|
||
readHash();
|
||
|
||
/* ============================================================
|
||
STAGE 1 · 脚本助手 + 镜头脚本 状态驱动
|
||
============================================================ */
|
||
const Stage1 = (function () {
|
||
let shots = []; // [{ id, painting, dialog, duration }]
|
||
let chatMsgs = []; // [{ role, html, time }]
|
||
let mode = null;
|
||
|
||
const $cb = () => document.getElementById('chat-body');
|
||
const $sb = () => document.getElementById('shots-body');
|
||
const $sm = () => document.getElementById('shots-meta');
|
||
|
||
function now() {
|
||
const d = new Date();
|
||
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
||
}
|
||
function pushMsg(role, html) { chatMsgs.push({ role, html, time: now() }); }
|
||
|
||
function renderChat() {
|
||
const body = $cb(); if (!body) return;
|
||
if (chatMsgs.length === 0 && !mode) {
|
||
body.innerHTML = `<div class="chat-empty">
|
||
<div class="ce-title">选择一种生成方式开始</div>
|
||
<div class="ce-hint">// 三种,由「最省事」到「最保真原意」</div>
|
||
<div class="chat-modes">
|
||
<button class="chat-mode primary" data-mode="ai"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/></svg>AI 全生</button>
|
||
<button class="chat-mode" data-mode="theme"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15 14a4.65 4.65 0 0 0 1.4-2.5A6 6 0 1 0 6 8c0 1 .23 2.23 1.5 3.5"/></svg>一句话主题</button>
|
||
<button class="chat-mode" data-mode="manual"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>自带脚本</button>
|
||
</div>
|
||
</div>`;
|
||
body.querySelectorAll('.chat-mode').forEach(btn => {
|
||
btn.addEventListener('click', () => pickMode(btn.dataset.mode));
|
||
});
|
||
return;
|
||
}
|
||
body.innerHTML = chatMsgs.map(msg => {
|
||
if (msg.role === 'ai') {
|
||
return `<div class="msg ai"><div style="display:flex; gap:10px; align-items:flex-start;"><div class="ai-avatar" style="margin-top:2px;">AI</div><div class="bubble">${msg.html}</div></div><div class="time" style="margin-left:36px;">${msg.time}</div></div>`;
|
||
}
|
||
return `<div class="msg user"><div class="bubble">${msg.html}</div><div class="time">${msg.time}</div></div>`;
|
||
}).join('');
|
||
body.scrollTop = body.scrollHeight;
|
||
}
|
||
|
||
function renderShots() {
|
||
const body = $sb(); if (!body) return;
|
||
const meta = $sm();
|
||
const phMeta = document.getElementById('page-head-shots-meta');
|
||
if (shots.length === 0) {
|
||
body.innerHTML = `<div class="shots-empty">
|
||
<div class="empty-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M9 5v14"/></svg></div>
|
||
<div class="empty-title">还没有镜头脚本</div>
|
||
<div class="empty-hint">// 跟左侧脚本助手对话<br>选择一种方式生成你的第一稿</div>
|
||
</div>`;
|
||
if (meta) meta.textContent = '· 空 · 待生成';
|
||
if (phMeta) phMeta.textContent = '待生成镜头脚本';
|
||
return;
|
||
}
|
||
let cum = 0;
|
||
let html = '';
|
||
shots.forEach((s, i) => {
|
||
const start = cum;
|
||
cum += (s.duration || 5);
|
||
const tlabel = start + '-' + cum + 's';
|
||
html += `<div class="shot-card" data-id="${s.id}">
|
||
<div class="shot-head">
|
||
<div class="shot-num">${i + 1}</div>
|
||
<div class="shot-time">${tlabel}</div>
|
||
<span class="spacer"></span>
|
||
<button class="icon-mini-btn" title="重写本场" data-act="regen" data-id="${s.id}">↻</button>
|
||
<button class="icon-mini-btn" title="删除本场" data-act="del" data-id="${s.id}">×</button>
|
||
</div>
|
||
<div class="shot-row"><span class="shot-k">画面</span><div class="shot-v" contenteditable="true" data-field="painting" data-placeholder="(画面必填)点击编辑"${s.painting ? '' : ' data-empty="true"'}>${s.painting || ''}</div></div>
|
||
<div class="shot-row"><span class="shot-k">对白</span><div class="shot-v" contenteditable="true" data-field="dialog" data-placeholder="(对白可空)点击编辑"${s.dialog ? '' : ' data-empty="true"'}>${s.dialog || ''}</div></div>
|
||
</div>`;
|
||
// 每张卡片后都跟一个 gap(包括最后一张),允许在任意位置 hover 加分镜
|
||
html += `<div class="shot-insert-gap" data-after="${s.id}"><button class="add-shot-btn" data-act="add-here" data-after="${s.id}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>添加分场</button></div>`;
|
||
});
|
||
body.innerHTML = html;
|
||
if (meta) meta.textContent = '· ' + shots.length + ' 镜 · 0-' + cum + 's';
|
||
if (phMeta) phMeta.textContent = shots.length + ' 镜 · 0-' + cum + 's';
|
||
body.querySelectorAll('.shot-v[contenteditable]').forEach(el => {
|
||
el.addEventListener('focus', () => { el.dataset.empty = 'false'; });
|
||
el.addEventListener('blur', () => {
|
||
const card = el.closest('.shot-card');
|
||
const id = card.dataset.id;
|
||
const field = el.dataset.field;
|
||
const v = el.textContent.trim();
|
||
const s = shots.find(x => x.id === id);
|
||
if (s) s[field] = v;
|
||
if (!v) el.dataset.empty = 'true';
|
||
});
|
||
});
|
||
body.querySelectorAll('[data-act]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const act = btn.dataset.act;
|
||
const id = btn.dataset.id;
|
||
const after = btn.dataset.after;
|
||
if (act === 'del') {
|
||
shots = shots.filter(x => x.id !== id);
|
||
renderShots();
|
||
} else if (act === 'regen') {
|
||
Shell.toast('已请求重写本场', '↻ shot-' + id);
|
||
} else if (act === 'add-here') {
|
||
const idx = shots.findIndex(x => x.id === after);
|
||
shots.splice(idx + 1, 0, { id: 'sh' + Date.now(), painting: '', dialog: '', duration: 5 });
|
||
renderShots();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function pickMode(m) {
|
||
mode = m;
|
||
if (m === 'ai') {
|
||
pushMsg('user', '帮我 AI 全自动生成一稿脚本');
|
||
renderChat();
|
||
setTimeout(() => {
|
||
pushMsg('ai', '<span class="ai-thinking">正在解析商品卖点与目标人群 <span class="dots"><span></span><span></span><span></span></span></span>');
|
||
renderChat();
|
||
}, 300);
|
||
// 7 镜 · 0-40s · 与 Stage 2 / 4 的 3 场切分对齐(场 1 深夜办公桌 0-15s / 场 2 面膜包装 15-27s / 场 3 化妆台定格 27-40s)
|
||
const draft = [
|
||
// ─ 场 1 · 深夜办公桌(15s)─
|
||
{ id: 'sh1', painting: '中景慢推 · 深夜居家书桌全景。屏幕仍亮着 PPT,女主背影瘫在椅子上,屏幕冷光 + 台灯暖光对比。字幕"凌晨 02:14"淡入。', dialog: '(无台词 · BGM 渐起)', duration: 5 },
|
||
{ id: 'sh2', painting: '近景 · 卫生间镜前。女主低头看脸,T 区起皮、暗沉特写,冷白灯偏惨。', dialog: '"做完这版稿又是凌晨两点……(叹气)脸已经不能看了。"', duration: 5 },
|
||
{ id: 'sh3', painting: '俯拍特写 · 回到书桌,拉开抽屉。囤好的透真补水面膜露半角,手伸进去抽出一片。', dialog: '"还好抽屉里囤了透真玻尿酸面膜。"', duration: 5 },
|
||
// ─ 场 2 · 面膜包装/特写(12s)─
|
||
{ id: 'sh4', painting: '桌面微距特写 · 撕开锡纸包装的瞬间。30g 厚精华液缓缓滴落,面膜布展开,质地拉丝可见。', dialog: '"30g 一片,精华液比普通面膜厚整整三倍。"', duration: 6 },
|
||
{ id: 'sh5', painting: '床头近景 · 女主敷好面膜闭眼躺下,台灯暖光打在脸侧。膜布贴合脸型,边缘服帖。', dialog: '"贴上去那一瞬间 —— 凉凉的,像把皮肤泡了一次澡。"', duration: 6 },
|
||
// ─ 场 3 · 化妆台/产品定格(13s)─
|
||
{ id: 'sh6', painting: '中景 · 第二天清晨化妆台。阳光透过窗帘,女主对镜上妆,皮肤透亮、粉底服帖。同事画外音"你最近用啥了"。', dialog: '"第二天脸是软的,粉底都不卡了。同事都跑来问。"', duration: 8 },
|
||
{ id: 'sh7', painting: '平铺俯拍 · 桌面五片装产品 + 单片包装。价格 "618 · 5 片 ¥39.9" 弹出,购物车图标右下角浮现。', dialog: '"618 五片 39.9,自用送人都合适。链接放评论区。"', duration: 5 },
|
||
];
|
||
let cur = 0;
|
||
const step = () => {
|
||
if (cur >= draft.length) {
|
||
// remove thinking msg
|
||
chatMsgs = chatMsgs.filter(x => !(x.role === 'ai' && /ai-thinking/.test(x.html)));
|
||
pushMsg('ai', '初稿完成。点击任意卡片文字可直接编辑;鼠标移到卡片之间会出现「+ 添加分场」。');
|
||
renderChat();
|
||
return;
|
||
}
|
||
shots.push(draft[cur++]);
|
||
renderShots();
|
||
setTimeout(step, 700);
|
||
};
|
||
setTimeout(step, 1100);
|
||
} else if (m === 'theme') {
|
||
pushMsg('ai', '好,请给我一句话主题(5–30 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>下面输入框直接打就行,我会按这句话扩成一稿镜头脚本。');
|
||
renderChat();
|
||
} else if (m === 'manual') {
|
||
pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框,我会按场自然切分并适配商品卖点。');
|
||
renderChat();
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
renderChat();
|
||
renderShots();
|
||
document.getElementById('chat-clear-btn')?.addEventListener('click', () => {
|
||
chatMsgs = []; mode = null; shots = [];
|
||
renderChat(); renderShots();
|
||
});
|
||
document.getElementById('chat-regen-btn')?.addEventListener('click', () => {
|
||
Shell.toast('已请求整体重写', 'POST /script/regen');
|
||
});
|
||
const sendBtn = document.getElementById('chat-send-btn');
|
||
const ta = document.getElementById('chat-textarea');
|
||
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="2" 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="2.4" 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>'));
|
||
ta.value = '';
|
||
attachments = []; renderAttach();
|
||
renderChat();
|
||
setTimeout(() => {
|
||
pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');
|
||
renderChat();
|
||
}, 400);
|
||
};
|
||
sendBtn.addEventListener('click', send);
|
||
ta.addEventListener('keydown', (e) => {
|
||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); send(); }
|
||
});
|
||
}
|
||
// 顶部「+ 追加一场」按钮已移除 — 添加分场改为卡片间 hover 时出现「+ 添加分场」
|
||
}
|
||
|
||
return { init };
|
||
})();
|
||
Stage1.init();
|
||
|
||
/* ============================================================
|
||
STAGE 2 · 基础资产 · 锚点 + 横滑预设 + 详情/库 modal
|
||
============================================================ */
|
||
const Stage2 = (function () {
|
||
const MODEL_LIB = [
|
||
{ id: 'm1', name: '清新短发女', sub: '通勤白领',
|
||
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 }
|
||
document.getElementById('asset-detail-title').textContent = payload.title;
|
||
document.getElementById('asset-detail-kind').textContent = '/ ' + payload.tagText;
|
||
document.getElementById('asset-detail-lead-img').innerHTML = `<span class="ph-frame">${payload.leadLabel || payload.title}</span>`;
|
||
|
||
// 缩略图 strip (默认 3 个版本占位)
|
||
const thumbs = payload.thumbs && payload.thumbs.length ? payload.thumbs : ['v1', 'v2', 'v3'];
|
||
const thumbsEl = document.getElementById('asset-detail-thumbs');
|
||
thumbsEl.innerHTML = thumbs.map((t, i) => `<div class="thumb placeholder${i === 0 ? ' active' : ''}"><span class="ph-frame">${t}</span></div>`).join('');
|
||
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
|
||
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
}));
|
||
|
||
// 三视图区
|
||
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');
|
||
if (payload.kind === 'scene') {
|
||
triSection.style.display = 'none';
|
||
} else if (payload.kind === 'actor') {
|
||
triSection.style.display = '';
|
||
tri.style.display = '';
|
||
tri.classList.remove('actor');
|
||
tri.innerHTML = `<div class="placeholder"><span class="ph-frame">${payload.title} · 三视图 (正/侧/背)</span></div>`;
|
||
ratioChip.textContent = '16:9';
|
||
tip.style.display = 'none';
|
||
} 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></div>`;
|
||
tip.style.display = 'none';
|
||
} else {
|
||
tri.innerHTML = `<div class="placeholder missing" data-tri="0">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||
<span>${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="2" 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_LIB 查完整属性
|
||
const actor = (kind === 'model') ? MODEL_LIB.find(x => x.name === name) : null;
|
||
if (kind === 'model') {
|
||
const a = actor || { gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '中等',
|
||
build: '标准', hairLen: '中发', hairColor: '黑色', vibe: '清新', feature: sub || '预设演员' };
|
||
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], ['创作人', '流·Studio'],
|
||
['身高', a.height], ['体格', a.build], ['文件大小', _fmtFileSize(name)],
|
||
['发型', a.hairLen + ' · ' + a.hairColor], ['来源', '平台预设'], ['发布时间', '2024-05-20'],
|
||
];
|
||
renderAssetDetail({
|
||
title: name, tagText: '人物 · 预设演员', leadLabel: name + ' · 立绘',
|
||
kind: 'actor', intro: a.feature || sub || '',
|
||
tags, props, applyLabel: '应用到当前项目',
|
||
});
|
||
} else {
|
||
// 场景
|
||
const props = [
|
||
['类别', '场景 · 预设'], ['标签', sub || '-'], ['作品ID', _fmtAssetId(name, 'scene')],
|
||
['来源', '场景库'], ['用途', '本项目场景资产'], ['创作人', '流·Studio'],
|
||
['镜头', '通用'], ['光线', '自然光'], ['文件大小', _fmtFileSize(name)],
|
||
];
|
||
renderAssetDetail({
|
||
title: name, tagText: '场景 · 预设', leadLabel: name + ' · 主图',
|
||
kind: 'scene', intro: sub || '场景资产',
|
||
tags: [sub].filter(Boolean), props, applyLabel: '应用到当前项目',
|
||
});
|
||
}
|
||
}
|
||
|
||
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(['创作人', '流·Studio']);
|
||
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.6" 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);
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ─── 添加演员/场景 · 选择来源 modal ─── */
|
||
function _openLibUploadChoice() {
|
||
const isModel = _curLibKind === 'model';
|
||
document.getElementById('ml-up-title').textContent = isModel ? '添加演员' : '添加场景';
|
||
document.getElementById('ml-up-ai-desc').textContent = isModel
|
||
? '描述外形 + 风格,AI 自动生成新演员形象与三视图'
|
||
: '描述类型 + 氛围,AI 自动生成新场景图与三视图';
|
||
document.getElementById('ml-up-local-desc').textContent = isModel
|
||
? '上传真人 / 既有演员素材,后续可生成三视图统一镜头'
|
||
: '上传商家自有场景图,后续可生成三视图统一镜头';
|
||
document.getElementById('ml-up-choice-bg').classList.add('show');
|
||
}
|
||
function _closeLibUploadChoice() {
|
||
document.getElementById('ml-up-choice-bg').classList.remove('show');
|
||
}
|
||
(function _bindLibUploadChoice() {
|
||
const bg = document.getElementById('ml-up-choice-bg');
|
||
if (!bg) return;
|
||
document.getElementById('ml-up-x').addEventListener('click', _closeLibUploadChoice);
|
||
bg.addEventListener('click', e => { if (e.target === bg) _closeLibUploadChoice(); });
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && bg.classList.contains('show')) _closeLibUploadChoice();
|
||
});
|
||
|
||
// AI 生成 → 待新增独立工作台,先 toast 不跳转
|
||
document.getElementById('ml-up-ai').addEventListener('click', () => {
|
||
const isModel = _curLibKind === 'model';
|
||
_closeLibUploadChoice();
|
||
Shell.toast(isModel ? 'AI 演员工作台' : 'AI 场景工作台', '页面待新增 · 暂未跳转');
|
||
});
|
||
|
||
// 本地上传 → 触发文件选择
|
||
const fileInput = document.getElementById('ml-up-file');
|
||
document.getElementById('ml-up-local').addEventListener('click', () => {
|
||
_closeLibUploadChoice();
|
||
fileInput.click();
|
||
});
|
||
fileInput.addEventListener('change', e => {
|
||
const files = [...(e.target.files || [])].filter(f => /^image\//.test(f.type));
|
||
if (!files.length) return;
|
||
const isModel = _curLibKind === 'model';
|
||
const ownsArr = isModel ? MODEL_OWN : SCENE_OWN;
|
||
files.forEach((f, i) => {
|
||
const baseName = (f.name || (isModel ? '我的演员' : '我的场景')).replace(/\.[^.]+$/, '').slice(0, 12);
|
||
ownsArr.unshift({
|
||
id: 'up-' + Date.now().toString(36) + i,
|
||
name: baseName,
|
||
sub: isModel ? '我的上传 · 待生成三视图' : '我的上传 · 待生成三视图',
|
||
source: 'own',
|
||
});
|
||
});
|
||
e.target.value = '';
|
||
// 切到「我的上传」让用户立即看到
|
||
_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 + (isModel ? MODEL_LIB.length : SCENE_LIB.length);
|
||
_renderLibGrid();
|
||
Shell.toast('已上传', `+ ${files.length} 张 · 来源 我的上传`);
|
||
});
|
||
})();
|
||
|
||
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;
|
||
|
||
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();
|
||
}
|
||
|
||
// 显式「采用」当前预览版本 · 同步商品资产 + 隐藏缺三视图徽标
|
||
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();
|
||
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 start() {
|
||
if (generating) return;
|
||
generating = true;
|
||
pane.classList.add('show');
|
||
renderLoading();
|
||
setTimeout(() => {
|
||
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() + ' · 预览中,满意请点「采用此版本」');
|
||
}
|
||
}, 1800);
|
||
}
|
||
|
||
// 主图点击 → 放大查看
|
||
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();
|
||
});
|
||
})();
|
||
}
|
||
return { init };
|
||
})();
|
||
Stage2.init();
|
||
|
||
/* ============================================================
|
||
STAGE 3 · 故事板 · 按场分 · 切换/重跑/应用/历史
|
||
============================================================ */
|
||
const Stage3 = (function () {
|
||
const scenes = [
|
||
{ id: 'sc1', name: '场 1 · 深夜办公桌', time: '0-15s', desc: '深夜居家办公环境,女主对镜叹气,皮肤干燥起皮特写。台灯暖光 + 屏幕冷光对比。', prompt: '中景 / 固定机位\n光线:台灯暖光 + 屏幕冷光\n演员:林夕(疲倦状态)\n关键道具:面膜盒(从抽屉露半角)\n氛围:午夜、安静、些许焦虑', adopted: 0, versions: [{ ts: '14:02', label: 'v1' }] },
|
||
{ id: 'sc2', name: '场 2 · 面膜包装/特写', time: '15-30s', desc: '女主从抽屉拿出补水面膜,包装盒微距特写 → 面膜布展开 → 30g 精华液滴落慢镜。', prompt: '特写 / 微距推镜\n光线:柔和暖光\n关键道具:面膜布、精华液\n节奏:慢镜头 + 水滴回弹', adopted: 0, versions: [{ ts: '14:08', label: 'v1' }, { ts: '14:21', label: 'v2' }] },
|
||
{ id: 'sc3', name: '场 3 · 化妆台/产品定格', time: '30-45s', desc: '第二天早上,女主对镜化妆,皮肤透亮。淡入产品定格大图 + 价格标签 ¥39.9。', prompt: '中景 / 定格\n光线:晨光 + 暖色滤镜\n演员:林夕(精致妆面)\n结尾:产品大图 + 价格 + 购物车浮动', adopted: 0, versions: [{ ts: '14:30', label: 'v1' }] },
|
||
];
|
||
let curId = scenes[0].id;
|
||
|
||
function renderRow() {
|
||
const row = document.getElementById('sb-scenes-row');
|
||
if (!row) return;
|
||
row.innerHTML = scenes.map(s => `<div class="sb-scene-thumb${s.id === curId ? ' selected' : ''}" data-sid="${s.id}">
|
||
<div class="placeholder"><span class="ph-frame">${s.name.split(' · ')[1] || s.name}</span></div>
|
||
<div class="nm">${s.name.split(' · ')[0]}</div>
|
||
<div class="sub">${s.time}</div>
|
||
</div>`).join('');
|
||
row.querySelectorAll('.sb-scene-thumb').forEach(t => {
|
||
t.addEventListener('click', () => { curId = t.dataset.sid; renderAll(); });
|
||
});
|
||
}
|
||
function renderMain() {
|
||
const s = scenes.find(x => x.id === curId); if (!s) return;
|
||
const v = s.versions[s.adopted];
|
||
document.getElementById('sb-main-img').innerHTML = `<span class="ph-frame">${s.name} · ${v.label}</span>`;
|
||
document.getElementById('sb-side-scene').textContent = s.name.split(' · ')[0];
|
||
document.getElementById('sb-prompt-edit').textContent = s.prompt;
|
||
// history
|
||
const ct = document.getElementById('sb-history-ct');
|
||
const hist = document.getElementById('sb-history-row');
|
||
ct.textContent = s.versions.length;
|
||
if (s.versions.length === 0) {
|
||
hist.innerHTML = '<div style="font-size: 11.5px; color: var(--black-alpha-48); padding: 12px 4px;">// 暂无历史版本</div>';
|
||
} else {
|
||
hist.innerHTML = s.versions.map((vv, i) => `<div class="sb-history-thumb${i === s.adopted ? ' current' : ''}" data-vi="${i}">
|
||
<div class="placeholder"><span class="ph-frame">${vv.label}</span></div>
|
||
<div class="ts">${vv.ts}</div>
|
||
</div>`).join('');
|
||
hist.querySelectorAll('.sb-history-thumb').forEach(t => {
|
||
t.addEventListener('click', () => {
|
||
s.adopted = +t.dataset.vi;
|
||
renderMain();
|
||
Shell.toast('已切换至 ' + s.versions[s.adopted].label, s.name);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
function renderAll() { renderRow(); renderMain(); }
|
||
|
||
function init() {
|
||
renderAll();
|
||
document.getElementById('sb-rerun-btn')?.addEventListener('click', () => {
|
||
const s = scenes.find(x => x.id === curId);
|
||
const v = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (s.versions.length + 1) };
|
||
s.versions.push(v);
|
||
s.adopted = s.versions.length - 1;
|
||
Shell.toast('整张重跑', s.name + ' · ' + v.label);
|
||
renderAll();
|
||
});
|
||
}
|
||
return { init };
|
||
})();
|
||
Stage3.init();
|
||
|
||
/* ============================================================
|
||
STAGE 4 · 视频 · 详情 modal(大图 + 历史 + 采用)
|
||
============================================================ */
|
||
const Stage4 = (function () {
|
||
const VIDEOS = {
|
||
'v1': { title: '场 1 · 深夜办公桌', time: '0-15s', info: [['场次', '场 1'], ['时长', '15.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:32', label: 'v1' }, { ts: '14:48', label: 'v2' }, { ts: '15:02', label: 'v3' }], adopted: 2 },
|
||
'v2': { title: '场 2 · 面膜包装/特写', time: '15-27s', info: [['场次', '场 2'], ['时长', '12.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:35', label: 'v1' }, { ts: '14:52', label: 'v2' }], adopted: 1 },
|
||
'v3': { title: '场 3 · 化妆台/产品定格', time: '27-40s', info: [['场次', '场 3'], ['时长', '13.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:40', label: 'v1' }], adopted: 0 },
|
||
};
|
||
let curVid = null;
|
||
|
||
function openDetail(id) {
|
||
const v = VIDEOS[id]; if (!v) return;
|
||
curVid = id;
|
||
document.getElementById('vd-title').textContent = v.title;
|
||
document.getElementById('vd-sub').textContent = '// ' + v.title + ' · ' + v.time;
|
||
const cur = v.versions[v.adopted];
|
||
document.getElementById('vd-main-img').innerHTML = `<span class="ph-frame">${v.title} · ${cur.label}</span>`;
|
||
document.getElementById('vd-info').innerHTML = v.info.map(([k, val]) => `<div class="row"><span class="k">${k}</span><span class="v">${val}</span></div>`).join('') + `<div class="row"><span class="k">当前展示</span><span class="v" style="color: var(--heat); font-weight:600;">${cur.label} · ${cur.ts}</span></div>`;
|
||
document.getElementById('vd-history-ct').textContent = v.versions.length;
|
||
const row = document.getElementById('vd-history-row');
|
||
row.innerHTML = v.versions.map((vv, i) => `<div class="vd-history-thumb${i === v.adopted ? ' current adopted' : ''}" data-vi="${i}">
|
||
<div class="placeholder"><span class="ph-frame">${vv.label}</span></div>
|
||
<div class="ts">${vv.ts}</div>
|
||
</div>`).join('');
|
||
row.querySelectorAll('.vd-history-thumb').forEach(t => {
|
||
t.addEventListener('click', () => {
|
||
v.adopted = +t.dataset.vi;
|
||
openDetail(id);
|
||
});
|
||
});
|
||
document.getElementById('video-detail-modal').classList.add('show');
|
||
}
|
||
|
||
function init() {
|
||
// 根据各场实际时长(data-duration)计算总时长 + 单场平均(用于「每场 Seedance 约 n 秒」)
|
||
const cards = document.querySelectorAll('.video-card[data-duration]');
|
||
if (cards.length) {
|
||
let total = 0;
|
||
cards.forEach(c => { total += Number(c.dataset.duration) || 0; });
|
||
const avg = Math.round(total / cards.length);
|
||
const avgEl = document.getElementById('seedance-avg');
|
||
const totalEl = document.getElementById('seedance-total');
|
||
if (avgEl) avgEl.textContent = String(avg);
|
||
if (totalEl) totalEl.textContent = String(total);
|
||
}
|
||
document.querySelectorAll('.video-card[data-video-id]').forEach(card => {
|
||
card.addEventListener('click', (e) => {
|
||
if (e.target.closest('[data-vstop]')) return;
|
||
openDetail(card.dataset.videoId);
|
||
});
|
||
});
|
||
document.getElementById('vd-adopt-btn')?.addEventListener('click', () => {
|
||
if (!curVid) return;
|
||
const v = VIDEOS[curVid];
|
||
Shell.toast('已采用 ' + v.versions[v.adopted].label, v.title + ' · 拼接将用此版');
|
||
document.getElementById('video-detail-modal').classList.remove('show');
|
||
});
|
||
document.getElementById('vd-regen-btn')?.addEventListener('click', () => {
|
||
if (!curVid) return;
|
||
const v = VIDEOS[curVid];
|
||
const nv = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (v.versions.length + 1) };
|
||
v.versions.push(nv);
|
||
v.adopted = v.versions.length - 1;
|
||
Shell.toast('重跑中', v.title + ' · 约 30s');
|
||
openDetail(curVid);
|
||
});
|
||
}
|
||
return { init };
|
||
})();
|
||
Stage4.init();
|
||
|
||
/* ============================================================
|
||
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="2" 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');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|