AirShelf/电商AI平台/pipeline.html
iye 04335f3269
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选
- 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 修复
2026-05-22 19:35:36 +08:00

2793 lines
165 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title 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', '好,请给我一句话主题(530 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>下面输入框直接打就行,我会按这句话扩成一稿镜头脚本。');
renderChat();
} else if (m === 'manual') {
pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框,我会按场自然切分并适配商品卖点。');
renderChat();
}
}
function init() {
renderChat();
renderShots();
document.getElementById('chat-clear-btn')?.addEventListener('click', () => {
chatMsgs = []; mode = null; shots = [];
renderChat(); renderShots();
});
document.getElementById('chat-regen-btn')?.addEventListener('click', () => {
Shell.toast('已请求整体重写', 'POST /script/regen');
});
const sendBtn = document.getElementById('chat-send-btn');
const ta = document.getElementById('chat-textarea');
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, '&lt;')}
<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, '&lt;')}</span>`).join('')}</div>`
: '';
pushMsg('user', fileTags + (v ? v.replace(/</g, '&lt;') : '<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>