AirShelf/core/frontend/src/routes/exact-html.ts

27 lines
1.5 MiB
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.

/* This file is generated by scripts/generate-exact-html.mjs. Do not edit by hand. */
export type ExactHtmlKey = "account" | "assetFactory" | "dashboard" | "imageOptimize" | "library" | "login" | "messages" | "modelPhoto" | "modelPhotoDemoA" | "modelPhotoDemoB" | "pipeline" | "platformCover" | "productCreate" | "productCreateUpload" | "productDetail" | "products" | "projectWizard" | "projects" | "register" | "settings" | "team";
export const exactHtmlDocuments: Record<ExactHtmlKey, string> = {
"account": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"account.html\">\n<meta charset=\"utf-8\">\n<title>消费 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── 顶部:左右布局(余额 banner + 快速充值)─── */\n .top-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(480px, .85fr); gap: 16px; margin-bottom: 22px; align-items: stretch; }\n @media (max-width: 1120px) { .top-grid { grid-template-columns: 1fr; } }\n\n .balance-banner {\n background: var(--accent-black);\n color: var(--accent-white);\n padding: 26px 28px;\n position: relative;\n border: 1px solid var(--accent-black);\n border-radius: var(--r-md);\n display: flex;\n flex-direction: column;\n gap: 24px;\n min-width: 0;\n min-height: 246px;\n }\n .balance-banner::before, .balance-banner::after,\n .balance-banner > .corner-tr, .balance-banner > .corner-bl {\n content: ''; position: absolute; width: 14px; height: 14px;\n 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;\n background-size: contain; pointer-events: none;\n }\n .balance-banner::before { top: -7px; left: -7px; }\n .balance-banner::after { bottom: -7px; right: -7px; }\n .balance-banner > .corner-tr { top: -7px; right: -7px; }\n .balance-banner > .corner-bl { bottom: -7px; left: -7px; }\n\n /* 主余额 · 突出展示 */\n .balance-hero { display: flex; flex-direction: column; gap: 4px; }\n .balance-hero .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }\n .balance-hero .v { font-size: 38px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }\n .balance-hero .meta { font-size: 11.5px; color: rgba(255,255,255,.5); font-family: var(--font-mono); letter-spacing: .02em; margin-top: 4px; }\n\n /* 子统计 · 月限额 / 已用 */\n .balance-sub { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; padding: 16px 0 0; border-top: 1px solid rgba(255,255,255,.1); }\n .balance-sub .col { min-width: 0; }\n .balance-sub .lbl { font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,.5); letter-spacing: .06em; text-transform: uppercase; }\n .balance-sub .v { font-size: 18px; font-weight: 700; letter-spacing: -.01em; margin-top: 4px; font-variant-numeric: tabular-nums; }\n .balance-sub .meta { font-size: 10.5px; color: rgba(255,255,255,.42); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }\n\n .balance-foot { margin-top: auto; padding-top: 2px; }\n .balance-meter { height: 5px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); overflow: hidden; }\n .balance-meter > span { display: block; height: 100%; width: 5.4%; background: var(--heat); border-radius: inherit; }\n .balance-foot-meta { display: flex; justify-content: space-between; gap: 14px; margin-top: 8px; color: rgba(255,255,255,.46); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }\n\n /* ─── 快速充值 pane(右栏)─── */\n .pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }\n .pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }\n .pane .desc { font-size: 11.5px; color: var(--black-alpha-48); margin-bottom: 14px; font-family: var(--font-mono); letter-spacing: .02em; }\n .topup-pane { display: flex; flex-direction: column; padding: 20px 22px; margin-bottom: 0; min-height: 246px; }\n .topup-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 14px; }\n .topup-head h3 { margin-bottom: 5px; }\n .topup-head .desc { margin-bottom: 0; }\n .topup-selected { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); white-space: nowrap; padding-top: 2px; }\n\n .recharge-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }\n .recharge-card { min-height: 76px; border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px 8px; text-align: center; cursor: pointer; background: var(--surface); position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: background var(--t-base), border-color var(--t-base), box-shadow var(--t-base), transform var(--t-fast); }\n .recharge-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }\n .recharge-card:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--heat-40); }\n .recharge-card.selected { border-color: var(--heat); background: var(--heat-12); box-shadow: inset 0 0 0 1px var(--heat); }\n .recharge-card.selected::after { content: '✓'; position: absolute; top: 6px; right: 7px; width: 15px; height: 15px; border-radius: 50%; display: grid; place-items: center; background: var(--heat); color: var(--accent-white); font-size: 10px; line-height: 1; }\n .recharge-card .amt { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.15; }\n .recharge-card .gift { font-size: 10px; color: var(--black-alpha-48); margin-top: 4px; font-family: var(--font-mono); white-space: nowrap; }\n .recharge-card .gift.bonus { color: var(--accent-forest); font-weight: 600; }\n .recharge-card .ribbon { position: absolute; top: 6px; left: 7px; font-family: var(--font-mono); font-size: 9px; padding: 1px 5px; background: var(--heat); color: var(--accent-white); letter-spacing: .03em; font-weight: 600; border-radius: var(--r-sm); }\n .pay-row { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }\n .pay-title { font-size: 12px; font-weight: 600; color: var(--accent-black); line-height: 1.2; }\n .pay-row .input { width: 100%; box-sizing: border-box; height: 38px; }\n .pay-btn-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }\n .pay-method-btn { height: 38px; border-radius: var(--r-pill); display: inline-flex; justify-content: center; align-items: center; gap: 8px; background: var(--surface); color: var(--accent-black); border-color: var(--border-faint); font-weight: 500; }\n .pay-method-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }\n .pay-method-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--black-alpha-16); }\n .pay-logo { width: 18px; height: 18px; border-radius: 6px; display: inline-block; flex: 0 0 18px; overflow: hidden; }\n .pay-logo img { width: 100%; height: 100%; display: block; object-fit: cover; border-radius: inherit; }\n @media (max-width: 720px) {\n .recharge-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n }\n\n /* ─── Tab strip ─── */\n .billing-tabs { display: flex; align-items: flex-end; gap: 4px; border-bottom: 1px solid var(--border-faint); margin: 24px 0 18px; padding: 0 2px; overflow-x: auto; scrollbar-width: none; }\n .billing-tabs::-webkit-scrollbar { display: none; }\n .billing-tabs .tab { display: inline-flex; align-items: center; flex: 0 0 auto; gap: 6px; background: transparent; border: 0; border-bottom: 2px solid transparent; border-radius: var(--r-md) var(--r-md) 0 0; margin-bottom: -1px; padding: 10px 14px; font-size: 13px; font-weight: 500; color: var(--black-alpha-56); font-family: inherit; cursor: pointer; letter-spacing: 0; user-select: none; transition: color var(--t-base), background var(--t-base), border-color var(--t-base); }\n .billing-tabs .tab:hover { color: var(--accent-black); background: var(--black-alpha-4); }\n .billing-tabs .tab:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--heat-40); }\n .billing-tabs .tab.active { color: var(--accent-black); border-bottom-color: var(--heat); font-weight: 600; }\n .billing-tabs .tab .count { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); padding: 1px 7px; background: var(--black-alpha-4); border-radius: var(--r-sm); letter-spacing: .04em; }\n .billing-tabs .tab.active .count { background: var(--heat-12); color: var(--heat); }\n\n .tab-panel { display: none; }\n .tab-panel.active { display: block; }\n\n /* ─── 总览 · 趋势 + 阶段分布 (两栏等高) ─── */\n .overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: stretch; }\n\n .trend-pane { padding: 18px 20px 14px; display: flex; flex-direction: column; }\n .trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }\n .trend-head h3 { margin-bottom: 0; }\n .trend-head .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .trend-head .spacer { flex: 1; }\n .trend-head .chip { font-family: var(--font-mono); font-size: 10.5px; padding: 3px 8px; border: 1px solid var(--border-faint); border-radius: var(--r-pill); color: var(--black-alpha-56); cursor: pointer; }\n .trend-head .chip.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }\n\n .trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; min-height: 170px; flex: 1; padding: 6px 4px 2px; position: relative; }\n .trend-chart .bars { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; height: 100%; }\n .trend-chart .bar { background: var(--background-lighter); border-radius: 2px 2px 0 0; position: relative; transition: background var(--t-base); cursor: pointer; }\n .trend-chart .bar > span { display: block; width: 100%; background: var(--heat); border-radius: 2px 2px 0 0; }\n .trend-chart .bar:hover > span { background: var(--accent-black); }\n .trend-chart .bar.peak > span { background: var(--accent-black); }\n .trend-chart .x-axis { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-32); text-align: center; letter-spacing: .02em; }\n .trend-foot { display: flex; gap: 14px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-faint); font-size: 12px; }\n .trend-foot .item { display: flex; align-items: baseline; gap: 6px; }\n .trend-foot .item .k { color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }\n .trend-foot .item .v { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-black); }\n .trend-foot .item .v.warn { color: #B45309; }\n\n /* ─── 阶段分布 ─── */\n .stage-pane .usage-line { display: flex; justify-content: space-between; padding: 4px 0 4px; font-size: 12.5px; }\n .stage-pane .usage-line .k { color: var(--accent-black); }\n .stage-pane .usage-line .v { font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }\n .stage-pane .usage-bar { height: 4px; background: var(--background-lighter); border-radius: 2px; margin: 4px 0 10px; overflow: hidden; }\n .stage-pane .usage-bar > span { display: block; height: 100%; transition: width .3s ease; }\n .stage-pane .total { display: flex; justify-content: space-between; padding-top: 10px; margin-top: 6px; border-top: 1px solid var(--border-faint); font-size: 13px; font-weight: 600; }\n .stage-pane .total .v { font-variant-numeric: tabular-nums; }\n\n /* ─── 扣费规则 + 四层预检 ─── */\n .rule-pane .rule-list { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.7; }\n .rule-pane .rule-list strong { color: var(--accent-black); font-weight: 600; }\n .rule-pane .mono-acc { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; font-size: 11.5px; border-radius: var(--r-sm); }\n\n .quota-rules { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-faint); }\n .quota-rules .qr-head { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; margin-bottom: 10px; }\n .quota-rules .step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; align-items: baseline; margin-bottom: 6px; font-size: 12.5px; color: var(--accent-black); }\n .quota-rules .step .num { width: 20px; height: 20px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; }\n .quota-rules .step .formula { font-family: var(--font-mono); font-size: 11.5px; color: var(--heat); background: var(--heat-12); padding: 0 4px; border-radius: var(--r-sm); }\n\n /* ─── 表格通用 ─── */\n .billing-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-muted); border-radius: var(--r-md); overflow: hidden; }\n .billing-table th, .billing-table td { padding: 11px 14px; text-align: left; font-size: 12.5px; border-bottom: 0; }\n .billing-table thead th { background: var(--background-lighter); border-bottom: 1px solid var(--border-muted); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n .billing-table tbody tr:hover { background: var(--background-lighter); }\n .billing-table .ts { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .billing-table .neg { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-black); text-align: right; }\n .billing-table .pos { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--accent-forest); text-align: right; }\n .billing-table .zero { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--black-alpha-32); text-align: right; }\n .billing-table .muted { color: var(--black-alpha-56); font-size: 11.5px; }\n .billing-table .ref { color: var(--black-alpha-48); font-size: 10.5px; font-family: var(--font-mono); }\n .billing-table .who { display: inline-flex; align-items: center; gap: 8px; }\n .billing-table .who .av { width: 24px; height: 24px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: inline-grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--accent-black); }\n .billing-table .role-pill { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: var(--r-pill); font-size: 10.5px; font-weight: 500; }\n .billing-table .role-pill .dot { width: 5px; height: 5px; border-radius: 50%; }\n .billing-table .role-super { background: var(--heat-12); color: var(--heat); }\n .billing-table .role-super .dot { background: var(--heat); }\n .billing-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }\n .billing-table .role-admin .dot { background: #1E40AF; }\n .billing-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }\n .billing-table .role-member .dot { background: var(--black-alpha-56); }\n .billing-table .status-tag { font-family: var(--font-mono); font-size: 10px; padding: 1px 6px; border-radius: var(--r-sm); letter-spacing: .04em; }\n .billing-table .status-tag.ok { background: rgba(66,195,102,.12); color: var(--accent-forest); }\n .billing-table .status-tag.wip { background: var(--heat-12); color: var(--heat); }\n .billing-table .status-tag.fail { background: rgba(235,52,36,.10); color: var(--accent-crimson); }\n .billing-table .progress-mini { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; margin-left: 8px; }\n .billing-table .progress-mini > span { display: block; height: 100%; background: var(--heat); }\n\n /* ─── 流水筛选条 ─── */\n .filter-bar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }\n .filter-bar select, .filter-bar input { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 6px 10px; font-size: 12.5px; font-family: inherit; color: var(--accent-black); }\n .filter-bar select { padding-right: 24px; }\n .filter-bar .spacer { flex: 1; }\n .filter-bar .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n\n /* 充值 modal */\n .topup-modal { width: min(460px, 92vw); }\n .topup-modal .topup-qr { aspect-ratio: 1; max-width: 220px; margin: 4px auto 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; position: relative; overflow: hidden; }\n .topup-modal .topup-qr::before {\n content: ''; position: absolute; inset: 18px;\n background-image: linear-gradient(45deg, var(--accent-black) 25%, transparent 25%),\n linear-gradient(-45deg, var(--accent-black) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--accent-black) 75%),\n linear-gradient(-45deg, transparent 75%, var(--accent-black) 75%);\n background-size: 16px 16px;\n background-position: 0 0, 0 8px, 8px -8px, -8px 0px;\n opacity: .14;\n }\n .topup-modal .topup-qr .center { position: relative; z-index: 1; background: var(--surface); padding: 10px 12px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); letter-spacing: .02em; text-align: center; }\n .topup-modal .topup-info { text-align: center; font-size: 13px; color: var(--black-alpha-56); margin-bottom: 6px; }\n .topup-modal .topup-amt { text-align: center; font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--heat); margin-bottom: 6px; }\n .topup-modal .topup-note { text-align: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; margin-bottom: 4px; }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>消费</h1>\n <div class=\"sub\"><span class=\"mono\">// 余额 · 充值 · 4 维消费视图 + 账单流水</span></div>\n </div>\n</div>\n\n<!-- 顶部:余额 banner(左)+ 快速充值(右)左右布局 -->\n<div class=\"top-grid\">\n <!-- 左:余额 banner -->\n <div class=\"balance-banner\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"balance-hero\">\n <div class=\"lbl\">团队余额</div>\n <div class=\"v\">¥327.40</div>\n <div class=\"meta\">// 充值累加 · 不重置</div>\n </div>\n <div class=\"balance-sub\">\n <div class=\"col\">\n <div class=\"lbl\">本月限额</div>\n <div class=\"v\">¥3,000.00</div>\n <div class=\"meta\">// 按自然月重置</div>\n </div>\n <div class=\"col\">\n <div class=\"lbl\">当月已用</div>\n <div class=\"v\">¥162.60</div>\n <div class=\"meta\">// 占比 5.4% · 健康</div>\n </div>\n </div>\n <div class=\"balance-foot\">\n <div class=\"balance-meter\" aria-label=\"本月额度使用率 5.4%\"><span></span></div>\n <div class=\"balance-foot-meta\">\n <span>团队月剩余 ¥2,837.40</span>\n <span>使用率 5.4%</span>\n </div>\n </div>\n </div>\n\n <!-- 右:快速充值 -->\n <div class=\"pane topup-pane\">\n <div class=\"topup-head\">\n <div>\n <h3>快速充值</h3>\n <div class=\"desc\">// 充值后立刻到账,可开发票 · 仅超管可操作</div>\n </div>\n <div class=\"topup-selected\" id=\"topup-selected-label\">已选 ¥500</div>\n </div>\n <div class=\"recharge-row\">\n <div class=\"recharge-card\" data-amt=\"100\" role=\"button\" tabindex=\"0\" aria-pressed=\"false\"><div class=\"amt\">¥100</div><div class=\"gift\">无赠送</div></div>\n <div class=\"recharge-card selected\" data-amt=\"500\" role=\"button\" tabindex=\"0\" aria-pressed=\"true\"><span class=\"ribbon\">推荐</span><div class=\"amt\">¥500</div><div class=\"gift bonus\">+ ¥30 赠送</div></div>\n <div class=\"recharge-card\" data-amt=\"1000\" role=\"button\" tabindex=\"0\" aria-pressed=\"false\"><div class=\"amt\">¥1000</div><div class=\"gift bonus\">+ ¥80 赠送</div></div>\n <div class=\"recharge-card\" data-amt=\"3000\" role=\"button\" tabindex=\"0\" aria-pressed=\"false\"><div class=\"amt\">¥3000</div><div class=\"gift bonus\">+ ¥300 赠送</div></div>\n </div>\n <div class=\"pay-row\">\n <div class=\"pay-title\">自定义金额</div>\n <input class=\"input\" id=\"custom-amt\" placeholder=\"最低 ¥50,可输入任意金额\">\n <div class=\"pay-btn-row\">\n <button class=\"btn pay-method-btn pay-wechat\" onclick=\"openTopup('wechat')\" aria-label=\"微信支付\">\n <span class=\"pay-logo\" aria-hidden=\"true\"><img src=\"/exact/assets/pay-wechat.png\" alt=\"\"></span>\n 微信支付\n </button>\n <button class=\"btn pay-method-btn pay-alipay\" onclick=\"openTopup('alipay')\" aria-label=\"支付宝\">\n <span class=\"pay-logo\" aria-hidden=\"true\"><img src=\"/exact/assets/pay-alipay.png\" alt=\"\"></span>\n 支付宝\n </button>\n </div>\n </div>\n </div>\n</div>\n\n<!-- Tab strip -->\n<div class=\"billing-tabs\" role=\"tablist\">\n <button class=\"tab active\" type=\"button\" data-tab=\"overview\" role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-overview\">总览</button>\n <button class=\"tab\" type=\"button\" data-tab=\"by-project\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-by-project\">项目 <span class=\"count\">8</span></button>\n <button class=\"tab\" type=\"button\" data-tab=\"by-member\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-by-member\">成员 <span class=\"count\">5</span></button>\n <button class=\"tab\" type=\"button\" data-tab=\"bills\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-bills\">账单流水 <span class=\"count\">30天</span></button>\n</div>\n\n<!-- ===== Tab 1: 总览 ===== -->\n<div class=\"tab-panel active\" id=\"panel-overview\">\n <div class=\"overview-grid\">\n <div class=\"pane trend-pane\">\n <div class=\"trend-head\">\n <h3>消费趋势</h3>\n <span class=\"sub\" id=\"trend-sub\">// 近 14 天 · 单位 ¥</span>\n <span class=\"spacer\"></span>\n <button class=\"chip active\" data-grain=\"day\">日</button>\n <button class=\"chip\" data-grain=\"week\">周</button>\n <button class=\"chip\" data-grain=\"month\">月</button>\n </div>\n <div class=\"trend-chart\">\n <div class=\"bars\" id=\"trend-bars\">\n <!-- JS 注入 14 根柱 -->\n </div>\n <div class=\"x-axis\" id=\"trend-xaxis\">\n <!-- JS 注入日期 -->\n </div>\n </div>\n <div class=\"trend-foot\">\n <div class=\"item\"><span class=\"k\">14 天合计</span><span class=\"v\" id=\"trend-sum\">¥0.00</span></div>\n <div class=\"item\"><span class=\"k\">日均</span><span class=\"v\" id=\"trend-avg\">¥0.00</span></div>\n <div class=\"item\"><span class=\"k\">峰值</span><span class=\"v warn\" id=\"trend-peak\">¥0.00</span></div>\n </div>\n </div>\n\n <div class=\"pane stage-pane\">\n <h3>本月按阶段分布</h3>\n <div class=\"desc\">// PRD §5.3.5 扣费规则 · 仅确认后扣</div>\n <div class=\"usage-line\"><span class=\"k\">视频片段(Seedance)</span><span class=\"v\">¥98.40</span></div>\n <div class=\"usage-bar\"><span style=\"width:60%; background:var(--heat);\"></span></div>\n <div class=\"usage-line\"><span class=\"k\">故事板(image-2)</span><span class=\"v\">¥36.00</span></div>\n <div class=\"usage-bar\"><span style=\"width:22%; background:var(--accent-forest);\"></span></div>\n <div class=\"usage-line\"><span class=\"k\">基础资产</span><span class=\"v\">¥21.00</span></div>\n <div class=\"usage-bar\"><span style=\"width:13%; background:var(--black-alpha-56);\"></span></div>\n <div class=\"usage-line\"><span class=\"k\">脚本 LLM</span><span class=\"v\">¥7.20</span></div>\n <div class=\"usage-bar\"><span style=\"width:5%; background:var(--black-alpha-32);\"></span></div>\n <div class=\"total\"><span>合计</span><span class=\"v\">¥162.60</span></div>\n </div>\n </div>\n\n <div class=\"pane rule-pane\" style=\"margin-top: 16px;\">\n <h3>扣费 + 四层额度预检规则</h3>\n <div class=\"desc\">// PRD §5.3.5 + §10.3 · 对接团队请以此页为准</div>\n <div class=\"rule-list\">\n <strong>① 失败不扣</strong>:模型超时 / 内容审核拦截 / 生成异常一律不扣费。<br>\n <strong>② 用户重跑不扣首次</strong>:第一次重跑保留原扣费,第二次起按次结算。<br>\n <strong>③ 仅在你点击 <span class=\"mono-acc\">[ 确认通过 ]</span> 时入账</strong>。<br>\n <strong>④ 导出不再扣费</strong>,所有 token 已在过程中结算。\n </div>\n <div class=\"quota-rules\">\n <div class=\"qr-head\">// 任务确认前 · 四层额度预检(任一不通过即拦截)</div>\n <div class=\"step\"><span class=\"num\">1</span><span><strong>个人日剩余</strong> ≥ 任务预估 × <span class=\"formula\">1.2</span></span></div>\n <div class=\"step\"><span class=\"num\">2</span><span><strong>个人月剩余</strong> ≥ 同上</span></div>\n <div class=\"step\"><span class=\"num\">3</span><span><strong>团队月剩余</strong> ≥ 同上</span></div>\n <div class=\"step\"><span class=\"num\">4</span><span><strong>团队总余额</strong> ≥ 同上</span></div>\n </div>\n </div>\n</div>\n\n<!-- ===== Tab 2: 按项目 ===== -->\n<div class=\"tab-panel\" id=\"panel-by-project\">\n <div class=\"filter-bar\">\n <select id=\"proj-f-status\">\n <option value=\"all\">全部状态</option>\n <option value=\"wip\">进行中</option>\n <option value=\"ok\">已完成</option>\n <option value=\"fail\">失败 · 待重跑</option>\n </select>\n <select id=\"proj-f-range\">\n <option value=\"month\">本月</option>\n <option value=\"30d\">近 30 天</option>\n <option value=\"90d\">近 90 天</option>\n </select>\n <button class=\"chip\" id=\"proj-f-reset\" type=\"button\" style=\"height:30px;font-size:11px;display:none;\">清除筛选</button>\n <span class=\"spacer\"></span>\n <span class=\"ct\" id=\"proj-count\">共 <b style=\"color:var(--accent-black);\">0</b> 个项目 · 消耗 ¥0.00</span>\n </div>\n <table class=\"billing-table\">\n <thead>\n <tr>\n <th>项目</th>\n <th>商品</th>\n <th>所属成员</th>\n <th>当前阶段</th>\n <th>状态</th>\n <th style=\"text-align:right;\">消耗</th>\n </tr>\n </thead>\n <tbody id=\"proj-body\">\n <!-- JS 注入 -->\n </tbody>\n </table>\n <div id=\"proj-empty\" class=\"empty-state\" style=\"display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;\">\n // 当前筛选条件下没有项目 · 试试调整状态 / 时间范围\n </div>\n</div>\n\n<!-- ===== Tab 3: 按成员 ===== -->\n<div class=\"tab-panel\" id=\"panel-by-member\">\n <div class=\"filter-bar\">\n <select id=\"mem-f-role\">\n <option value=\"all\">全部角色</option>\n <option value=\"super\">超管</option>\n <option value=\"admin\">团管</option>\n <option value=\"member\">成员</option>\n </select>\n <select id=\"mem-f-range\">\n <option value=\"month\">本月</option>\n <option value=\"30d\">近 30 天</option>\n <option value=\"90d\">近 90 天</option>\n </select>\n <button class=\"chip\" id=\"mem-f-reset\" type=\"button\" style=\"height:30px;font-size:11px;display:none;\">清除筛选</button>\n <span class=\"spacer\"></span>\n <span class=\"ct\" id=\"mem-count\">共 <b style=\"color:var(--accent-black);\">0</b> 人 · 合计 ¥0.00</span>\n </div>\n <table class=\"billing-table\">\n <thead>\n <tr>\n <th>成员</th>\n <th>角色</th>\n <th>已完成项目</th>\n <th>已用 / 月度额度</th>\n <th>最近活跃</th>\n </tr>\n </thead>\n <tbody id=\"member-body\">\n <!-- JS 注入 -->\n </tbody>\n </table>\n <div id=\"mem-empty\" class=\"empty-state\" style=\"display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;\">\n // 当前筛选条件下没有成员 · 试试调整角色 / 时间范围\n </div>\n</div>\n\n<!-- ===== Tab 4: 账单流水 ===== -->\n<div class=\"tab-panel\" id=\"panel-bills\">\n <div class=\"filter-bar\">\n <select id=\"bills-f-stage\">\n <option value=\"all\">全部阶段</option>\n <option value=\"视频片段\">视频片段</option>\n <option value=\"故事板\">故事板</option>\n <option value=\"基础资产\">基础资产</option>\n <option value=\"脚本 LLM\">脚本 LLM</option>\n <option value=\"充值\">充值</option>\n <option value=\"导出\">导出</option>\n </select>\n <select id=\"bills-f-member\">\n <option value=\"all\">全部成员</option>\n <option value=\"李\">小李</option>\n <option value=\"张\">张运营</option>\n <option value=\"王\">王小姐</option>\n <option value=\"陈\">陈策划</option>\n </select>\n <select id=\"bills-f-range\">\n <option value=\"30d\">近 30 天</option>\n <option value=\"7d\">近 7 天</option>\n <option value=\"month\">本月</option>\n <option value=\"all\">全部</option>\n </select>\n <button class=\"chip\" id=\"bills-f-reset\" type=\"button\" style=\"height:30px;font-size:11px;display:none;\">清除筛选</button>\n <span class=\"spacer\"></span>\n <span class=\"ct\">共 <b id=\"bills-count\" style=\"color:var(--accent-black);\">0</b> 条</span>\n </div>\n <table class=\"billing-table\">\n <thead>\n <tr>\n <th>时间</th>\n <th>项目 / 类型</th>\n <th>详情</th>\n <th>成员</th>\n <th>状态</th>\n <th style=\"text-align:right;\">金额</th>\n </tr>\n </thead>\n <tbody id=\"bills-body\">\n <!-- JS 注入 -->\n </tbody>\n </table>\n <div id=\"bills-empty\" class=\"empty-state\" style=\"display:none; padding:48px 0; text-align:center; font-family:var(--font-mono); font-size:12px; color:var(--black-alpha-48); letter-spacing:.02em;\">\n // 当前筛选条件下没有账单 · 试试调整阶段 / 成员 / 时间范围\n </div>\n</div>\n\n<!-- 充值 modal -->\n<div class=\"modal-bg\" id=\"topup-bg\" onclick=\"if(event.target===this)Shell.closeModal('topup-bg')\">\n <div class=\"modal topup-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4\"/></svg>\n </div>\n <div class=\"ti\">扫码支付<span id=\"topup-channel-label\">// 微信支付</span></div>\n </div>\n <div class=\"modal-b\">\n <div class=\"topup-info\">支付金额</div>\n <div class=\"topup-amt\" id=\"topup-amt\">¥500.00</div>\n <div class=\"topup-note\" id=\"topup-bonus\">// 含 ¥30 赠送 · 实到账 ¥530</div>\n <div class=\"topup-qr\">\n <div class=\"center\" id=\"topup-channel-name\">微信扫码<br><span style=\"color:var(--black-alpha-32);\">/topup/wx/TX...</span></div>\n </div>\n <div style=\"text-align:center; font-family:var(--font-mono); font-size:11px; color:var(--black-alpha-48); letter-spacing:.02em;\">// 5 分钟内有效 · 到账后自动关闭</div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('topup-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" onclick=\"topupDone()\">已完成支付</button>\n </div>\n </div>\n</div>\n\n</div>\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({ active: 'account', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消费' }] });\n\n/* ============================================================\n Mock 数据\n ============================================================ */\nconst TREND_DAYS = [\n { d: '05.08', v: 12.40 }, { d: '05.09', v: 18.80 }, { d: '05.10', v: 6.20 }, { d: '05.11', v: 0 },\n { d: '05.12', v: 4.50 }, { d: '05.13', v: 22.10 }, { d: '05.14', v: 14.60 }, { d: '05.15', v: 9.30 },\n { d: '05.16', v: 28.40 }, { d: '05.17', v: 13.80 }, { d: '05.18', v: 8.20 }, { d: '05.19', v: 11.50 },\n { d: '05.20', v: 19.40 }, { d: '05.21', v: 7.80 },\n];\n\n// 8 周(以「W18~W21 + 前 4 周」做演示),按 7 天合计估算\nconst TREND_WEEKS = [\n { d: 'W14', v: 64.20 }, { d: 'W15', v: 92.80 }, { d: 'W16', v: 118.40 }, { d: 'W17', v: 78.60 },\n { d: 'W18', v: 102.30 }, { d: 'W19', v: 138.20 }, { d: 'W20', v: 86.40 }, { d: 'W21', v: 27.20 },\n];\n// 6 个月 demo,自然月聚合估算 + 当月真实合计 162.60\nconst TREND_MONTHS = [\n { d: '2025-12', v: 384.20 }, { d: '2026-01', v: 528.40 }, { d: '2026-02', v: 296.80 },\n { d: '2026-03', v: 412.00 }, { d: '2026-04', v: 348.60 }, { d: '2026-05', v: 162.60 },\n];\nconst TREND_DATA = { day: TREND_DAYS, week: TREND_WEEKS, month: TREND_MONTHS };\nconst TREND_LABEL = {\n day: { sub: '// 近 14 天 · 单位 ¥', sumLbl: '14 天合计', avgLbl: '日均' },\n week: { sub: '// 近 8 周 · 单位 ¥', sumLbl: '8 周合计', avgLbl: '周均' },\n month: { sub: '// 近 6 月 · 单位 ¥', sumLbl: '6 月合计', avgLbl: '月均' },\n};\nlet _trendGrain = 'day';\n\nconst PROJECTS_BILL = [\n { name: '补水面膜 · v3', product: '透真补水面膜', owner: '李', role: 'super', stage: 'Stage 3 故事板', stagePct: 60, status: 'wip', statusLabel: '进行中', amount: 48.20 },\n { name: '透真防晒 · 通勤对比', product: '透真防晒', owner: '李', role: 'super', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 32.60 },\n { name: '蓝牙耳机 · 开箱测评', product: 'Pro 4 蓝牙耳机', owner: '张', role: 'admin', stage: 'Stage 4 视频', stagePct: 80, status: 'wip', statusLabel: '进行中', amount: 28.40 },\n { name: '速食面 · 加班场景', product: '熊猫速食面', owner: '陈', role: 'member', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 12.80 },\n { name: '春日新品 · 立体口红', product: '凝彩立体口红', owner: '李', role: 'super', stage: 'Stage 2 资产', stagePct: 40, status: 'wip', statusLabel: '进行中', amount: 18.30 },\n { name: '咖啡冻干 · 早八', product: '冷萃咖啡冻干', owner: '王', role: 'member', stage: 'Stage 3 故事板', stagePct: 60, status: 'fail', statusLabel: '失败 · 待重跑', amount: 0 },\n { name: '瑜伽裤 · 通勤穿搭', product: '透气速干瑜伽裤', owner: '王', role: 'member', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 14.80 },\n { name: '保温杯 · 户外随行', product: '316 保温杯', owner: '张', role: 'admin', stage: 'Stage 1 脚本', stagePct: 20, status: 'wip', statusLabel: '进行中', amount: 7.50 },\n];\n\nconst MEMBERS_BILL = [\n { av: '李', name: '小李', role: 'super', projectsDone: 14, used: 162.60, monthly: 10000, lastActive: '15 分钟前' },\n { av: '张', name: '张运营', role: 'admin', projectsDone: 8, used: 98.40, monthly: 6000, lastActive: '10 分钟前' },\n { av: '王', name: '王小姐', role: 'member', projectsDone: 4, used: 45.20, monthly: 2000, lastActive: '28 分钟前' },\n { av: '陈', name: '陈策划', role: 'member', projectsDone: 1, used: 12.80, monthly: 2000, lastActive: '4 小时前' },\n { av: '林', name: '林新人', role: 'member', projectsDone: 0, used: 0.00, monthly: 2000, lastActive: '尚未激活' },\n];\n\nconst BILLS = [\n { ts: '05.21 14:32', proj: '补水面膜 · v3', type: '视频片段', detail: 'Seedance · 场 1 · 1 镜', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.45 },\n { ts: '05.21 14:08', proj: '补水面膜 · v3', type: '视频片段', detail: 'Seedance · 场 2 · 1 镜', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },\n { ts: '05.20 21:42', proj: '蓝牙耳机 · 开箱测评', type: '视频片段', detail: 'Seedance · 场 3 · 6 镜', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -1.20 },\n { ts: '05.20 18:21', proj: '透真防晒 · 通勤对比', type: '视频片段', detail: 'Seedance · 整段 · 6 镜', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -1.20 },\n { ts: '05.20 16:00', proj: '蓝牙耳机 · 开箱测评', type: '故事板', detail: 'image-2 · 整张重跑 · 场 2', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },\n { ts: '05.20 11:02', proj: '充值', type: '充值', detail: '微信支付 · TX2024052011021Z', who: '李', role: 'super', status: 'ok', statusLabel: '到账', amount: 500.00 },\n { ts: '05.19 18:08', proj: '速食面 · 加班场景', type: '故事板', detail: 'image-2 · 场 1', who: '陈', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.30 },\n { ts: '05.19 16:08', proj: '补水面膜 · v3', type: '故事板', detail: 'image-2 · 场 1', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.45 },\n { ts: '05.19 14:02', proj: '补水面膜 · v3', type: '脚本 LLM', detail: '2.4k tokens · AI 全生', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -0.04 },\n { ts: '05.19 13:38', proj: '补水面膜 · v3', type: '基础资产', detail: 'image-2 · 5 张', who: '李', role: 'super', status: 'ok', statusLabel: '通过', amount: -1.05 },\n { ts: '05.19 11:18', proj: '补水面膜 · v3', type: '故事板', detail: 'image-2 · 场 2', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.45 },\n { ts: '05.19 11:12', proj: '速食面 · 加班场景', type: '基础资产', detail: 'image-2 · 2 张', who: '陈', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.42 },\n { ts: '05.18 15:42', proj: '咖啡冻干 · 早八', type: '故事板', detail: 'image-2 · 场 3', who: '王', role: 'member', status: 'fail', statusLabel: '失败不扣', amount: 0 },\n { ts: '05.18 09:42', proj: '蓝牙耳机 · 开箱测评', type: '基础资产', detail: 'image-2 · 4 张', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.84 },\n { ts: '05.17 14:38', proj: '蓝牙耳机 · 开箱测评', type: '脚本 LLM', detail: '1.8k tokens · 自带粘贴', who: '张', role: 'admin', status: 'ok', statusLabel: '通过', amount: -0.03 },\n { ts: '05.17 10:30', proj: '瑜伽裤 · 通勤穿搭', type: '导出', detail: '1080×1920 · 9:16 · 38s', who: '王', role: 'member', status: 'ok', statusLabel: '免费', amount: 0 },\n { ts: '05.17 10:08', proj: '瑜伽裤 · 通勤穿搭', type: '视频片段', detail: 'Seedance · 整段 · 5 镜', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -3.20 },\n { ts: '05.16 19:38', proj: '透真防晒 · 通勤对比', type: '视频片段', detail: 'Seedance · 4 镜', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.80 },\n { ts: '05.16 11:42', proj: '透真防晒 · 通勤对比', type: '故事板', detail: 'image-2 · 场 2', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.45 },\n { ts: '05.15 16:08', proj: '透真防晒 · 通勤对比', type: '基础资产', detail: 'image-2 · 3 张', who: '王', role: 'member', status: 'ok', statusLabel: '通过', amount: -0.63 },\n];\n\nconst ROLE_META = {\n super: { label: '超管', cls: 'role-super' },\n admin: { label: '团管', cls: 'role-admin' },\n member: { label: '成员', cls: 'role-member' },\n};\n\nfunction fmtMoney(n) { return '¥' + Math.abs(n).toFixed(2).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); }\nfunction amtStr(n) {\n if (n === 0) return '¥0.00';\n return (n > 0 ? '+' : '-') + fmtMoney(n);\n}\nfunction amtCls(n) { return n > 0 ? 'pos' : (n === 0 ? 'zero' : 'neg'); }\n\n/* ─── 趋势柱 ─── */\nfunction renderTrend() {\n const data = TREND_DATA[_trendGrain];\n const meta = TREND_LABEL[_trendGrain];\n const bars = document.getElementById('trend-bars');\n const xax = document.getElementById('trend-xaxis');\n const max = Math.max(...data.map(d => d.v));\n // bars 的 grid-template-columns 默认是 repeat(14, 1fr),需要按数据长度动态调整\n bars.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;\n xax.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;\n bars.innerHTML = data.map(d => {\n const h = max > 0 ? (d.v / max * 100) : 0;\n const isPeak = d.v === max;\n return `<div class=\"bar${isPeak ? ' peak' : ''}\" title=\"${d.d} · ${fmtMoney(d.v)}\"><span style=\"height: ${h.toFixed(1)}%\"></span></div>`;\n }).join('');\n // x 轴标签:日 = 隔列显示 MM·DD 的 DD 部分;周/月 = 全显示\n if (_trendGrain === 'day') {\n xax.innerHTML = data.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');\n } else if (_trendGrain === 'week') {\n xax.innerHTML = data.map(d => `<span>${d.d}</span>`).join('');\n } else {\n xax.innerHTML = data.map(d => `<span>${d.d.slice(5)}</span>`).join('');\n }\n const sum = data.reduce((s, d) => s + d.v, 0);\n document.getElementById('trend-sub').textContent = meta.sub;\n document.getElementById('trend-sum').textContent = fmtMoney(sum);\n document.getElementById('trend-avg').textContent = fmtMoney(sum / data.length);\n document.getElementById('trend-peak').textContent = fmtMoney(max);\n // 旁标 label\n document.querySelectorAll('.trend-foot .item').forEach((it, idx) => {\n const k = it.querySelector('.k');\n if (idx === 0 && k) k.textContent = meta.sumLbl;\n else if (idx === 1 && k) k.textContent = meta.avgLbl;\n });\n}\n\n/* ─── 趋势 日/周/月 切换 ─── */\ndocument.querySelectorAll('.trend-head .chip[data-grain]').forEach(chip => {\n chip.addEventListener('click', () => {\n document.querySelectorAll('.trend-head .chip[data-grain]').forEach(c => c.classList.remove('active'));\n chip.classList.add('active');\n _trendGrain = chip.dataset.grain;\n renderTrend();\n });\n});\n\n/* ─── 按项目 表格 + 筛选 ─── */\nconst PROJ_FILTER = { status: 'all', range: 'month' };\nconst RANGE_MULT = { 'month': 1, '30d': 1, '90d': 2.6 }; // 演示用,30/90 天放大倍率(月 = 当月真实)\nconst RANGE_LBL = { 'month': '当月', '30d': '近 30 天', '90d': '近 90 天' };\nfunction renderProjects() {\n const tb = document.getElementById('proj-body');\n const mult = RANGE_MULT[PROJ_FILTER.range] || 1;\n const list = PROJECTS_BILL.filter(p => PROJ_FILTER.status === 'all' || p.status === PROJ_FILTER.status);\n tb.innerHTML = list.map(p => {\n const r = ROLE_META[p.role];\n const amt = p.amount * mult;\n return `\n <tr>\n <td><a href=\"pipeline.html?product=${encodeURIComponent(p.product)}\" style=\"color:var(--accent-black);text-decoration:none;font-weight:500;\">${p.name}</a></td>\n <td class=\"muted\">${p.product}</td>\n <td><span class=\"who\"><span class=\"av\">${p.owner}</span><span class=\"role-pill ${r.cls}\"><span class=\"dot\"></span>${r.label}</span></span></td>\n <td><span class=\"muted\">${p.stage}</span><span class=\"progress-mini\"><span style=\"width:${p.stagePct}%\"></span></span></td>\n <td><span class=\"status-tag ${p.status}\">${p.statusLabel}</span></td>\n <td class=\"${amtCls(-amt)}\">${amt === 0 ? '¥0.00' : '-' + fmtMoney(amt)}</td>\n </tr>\n `;\n }).join('');\n const sum = list.reduce((s, p) => s + p.amount * mult, 0);\n document.getElementById('proj-count').innerHTML =\n `共 <b style=\"color:var(--accent-black);\">${list.length}</b> 个项目 · ${RANGE_LBL[PROJ_FILTER.range]}消耗 ${fmtMoney(sum)}`;\n const empty = document.getElementById('proj-empty');\n const tbl = document.querySelector('#panel-by-project .billing-table');\n empty.style.display = list.length === 0 ? '' : 'none';\n tbl.style.display = list.length === 0 ? 'none' : '';\n const isDef = PROJ_FILTER.status === 'all' && PROJ_FILTER.range === 'month';\n document.getElementById('proj-f-reset').style.display = isDef ? 'none' : '';\n}\n\n['status', 'range'].forEach(key => {\n const sel = document.getElementById('proj-f-' + key);\n sel.addEventListener('change', () => { PROJ_FILTER[key] = sel.value; renderProjects(); });\n});\ndocument.getElementById('proj-f-reset').addEventListener('click', () => {\n PROJ_FILTER.status = 'all'; PROJ_FILTER.range = 'month';\n document.getElementById('proj-f-status').value = 'all';\n document.getElementById('proj-f-range').value = 'month';\n renderProjects();\n});\n\n/* ─── 按成员 表格 + 筛选 ─── */\nconst MEM_FILTER = { role: 'all', range: 'month' };\nfunction renderMembers() {\n const tb = document.getElementById('member-body');\n const mult = RANGE_MULT[MEM_FILTER.range] || 1;\n const list = MEMBERS_BILL.filter(m => MEM_FILTER.role === 'all' || m.role === MEM_FILTER.role);\n tb.innerHTML = list.map(m => {\n const r = ROLE_META[m.role];\n const used = m.used * mult;\n const pct = m.monthly > 0 ? (used / m.monthly * 100) : 0;\n return `\n <tr style=\"cursor:pointer;\">\n <td><span class=\"who\"><span class=\"av\">${m.av}</span><strong style=\"font-weight:500;\">${m.name}</strong></span></td>\n <td><span class=\"role-pill ${r.cls}\"><span class=\"dot\"></span>${r.label}</span></td>\n <td>${m.projectsDone}</td>\n <td>\n <strong style=\"font-variant-numeric:tabular-nums;font-weight:600;\">${fmtMoney(used)}</strong>\n <span class=\"muted\"> / ${fmtMoney(m.monthly)} · ${pct.toFixed(1)}%</span>\n <span class=\"progress-mini\"><span style=\"width:${Math.min(100, pct)}%; background:${pct >= 85 ? '#B45309' : 'var(--heat)'};\"></span></span>\n </td>\n <td><span class=\"ts\">${m.lastActive}</span></td>\n </tr>\n `;\n }).join('');\n const sum = list.reduce((s, m) => s + m.used * mult, 0);\n document.getElementById('mem-count').innerHTML =\n `共 <b style=\"color:var(--accent-black);\">${list.length}</b> 人 · ${RANGE_LBL[MEM_FILTER.range]}合计 ${fmtMoney(sum)}`;\n const empty = document.getElementById('mem-empty');\n const tbl = document.querySelector('#panel-by-member .billing-table');\n empty.style.display = list.length === 0 ? '' : 'none';\n tbl.style.display = list.length === 0 ? 'none' : '';\n const isDef = MEM_FILTER.role === 'all' && MEM_FILTER.range === 'month';\n document.getElementById('mem-f-reset').style.display = isDef ? 'none' : '';\n}\n\n['role', 'range'].forEach(key => {\n const sel = document.getElementById('mem-f-' + key);\n sel.addEventListener('change', () => { MEM_FILTER[key] = sel.value; renderMembers(); });\n});\ndocument.getElementById('mem-f-reset').addEventListener('click', () => {\n MEM_FILTER.role = 'all'; MEM_FILTER.range = 'month';\n document.getElementById('mem-f-role').value = 'all';\n document.getElementById('mem-f-range').value = 'month';\n renderMembers();\n});\n\n/* ─── 账单流水:筛选 + 表格渲染 ─── */\nconst BILLS_FILTER = { stage: 'all', member: 'all', range: '30d' };\n// demo \"今天\" = 05.21,所有 ts 都是 MM.DD 格式,这里基于这一假定算时间区间\nconst TODAY_MD = '05.21';\nfunction mdToDay(md) {\n // 把 \"MM.DD\" 当成 2026 年的日子换算成 1970 epoch ms,只用来比较先后\n const [m, d] = md.split('.').map(Number);\n return Date.UTC(2026, (m || 1) - 1, d || 1);\n}\nconst TODAY_MS = mdToDay(TODAY_MD);\n\nfunction passRange(ts, range) {\n if (range === 'all') return true;\n const md = ts.slice(0, 5); // \"05.21\"\n if (range === 'month') return md.startsWith(TODAY_MD.slice(0, 3));\n const diff = TODAY_MS - mdToDay(md);\n if (range === '7d') return diff <= 6 * 86400000;\n if (range === '30d') return diff <= 29 * 86400000;\n return true;\n}\n\nfunction getFilteredBills() {\n return BILLS.filter(b => {\n if (BILLS_FILTER.stage !== 'all' && b.type !== BILLS_FILTER.stage) return false;\n if (BILLS_FILTER.member !== 'all' && b.who !== BILLS_FILTER.member) return false;\n if (!passRange(b.ts, BILLS_FILTER.range)) return false;\n return true;\n });\n}\n\nfunction renderBills() {\n const tb = document.getElementById('bills-body');\n const list = getFilteredBills();\n tb.innerHTML = list.map(b => {\n const r = ROLE_META[b.role];\n return `\n <tr>\n <td class=\"ts\">${b.ts}</td>\n <td><strong style=\"font-weight:500;\">${b.proj}</strong><br><span class=\"muted\">${b.type}</span></td>\n <td class=\"muted\">${b.detail}</td>\n <td><span class=\"who\"><span class=\"av\">${b.who}</span></span></td>\n <td><span class=\"status-tag ${b.status}\">${b.statusLabel}</span></td>\n <td class=\"${amtCls(b.amount)}\">${b.amount === 0 ? '¥0.00' : (b.amount > 0 ? '+' + fmtMoney(b.amount) : '-' + fmtMoney(b.amount))}</td>\n </tr>\n `;\n }).join('');\n document.getElementById('bills-count').textContent = list.length;\n const empty = document.getElementById('bills-empty');\n const table = document.querySelector('#panel-bills .billing-table');\n if (list.length === 0) {\n empty.style.display = '';\n table.style.display = 'none';\n } else {\n empty.style.display = 'none';\n table.style.display = '';\n }\n // 「清除筛选」按钮:任何一个非默认就显示\n const isDefault = BILLS_FILTER.stage === 'all' && BILLS_FILTER.member === 'all' && BILLS_FILTER.range === '30d';\n document.getElementById('bills-f-reset').style.display = isDefault ? 'none' : '';\n}\n\n/* ─── 筛选绑定 ─── */\n['stage', 'member', 'range'].forEach(key => {\n const sel = document.getElementById('bills-f-' + key);\n sel.addEventListener('change', () => {\n BILLS_FILTER[key] = sel.value;\n renderBills();\n });\n});\ndocument.getElementById('bills-f-reset').addEventListener('click', () => {\n BILLS_FILTER.stage = 'all'; BILLS_FILTER.member = 'all'; BILLS_FILTER.range = '30d';\n document.getElementById('bills-f-stage').value = 'all';\n document.getElementById('bills-f-member').value = 'all';\n document.getElementById('bills-f-range').value = '30d';\n renderBills();\n});\n\n/* ─── Tab 切换 ─── */\ndocument.querySelectorAll('.billing-tabs .tab').forEach(tab => {\n tab.addEventListener('click', () => {\n const targetId = 'panel-' + tab.dataset.tab;\n document.querySelectorAll('.billing-tabs .tab').forEach(t => {\n const active = t === tab;\n t.classList.toggle('active', active);\n t.setAttribute('aria-selected', active ? 'true' : 'false');\n });\n document.querySelectorAll('.tab-panel').forEach(p => {\n p.classList.toggle('active', p.id === targetId);\n });\n });\n});\n\n/* ─── 快速充值卡选择 ─── */\nfunction setRechargeCard(card) {\n document.querySelectorAll('.recharge-card').forEach(c => {\n const active = c === card;\n c.classList.toggle('selected', active);\n c.setAttribute('aria-pressed', active ? 'true' : 'false');\n });\n const label = document.getElementById('topup-selected-label');\n if (label && card) label.textContent = '已选 ¥' + Number(card.dataset.amt).toLocaleString('zh-CN');\n}\ndocument.querySelectorAll('.recharge-card').forEach(card => {\n card.addEventListener('click', () => {\n setRechargeCard(card);\n document.getElementById('custom-amt').value = '';\n });\n card.addEventListener('keydown', e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setRechargeCard(card);\n document.getElementById('custom-amt').value = '';\n }\n });\n});\ndocument.getElementById('custom-amt').addEventListener('input', e => {\n const raw = e.target.value.trim();\n const label = document.getElementById('topup-selected-label');\n if (!raw) {\n const selected = document.querySelector('.recharge-card.selected');\n if (label && selected) label.textContent = '已选 ¥' + Number(selected.dataset.amt).toLocaleString('zh-CN');\n return;\n }\n document.querySelectorAll('.recharge-card').forEach(c => {\n c.classList.remove('selected');\n c.setAttribute('aria-pressed', 'false');\n });\n if (label) label.textContent = Number(raw) >= 50 ? '自定义 ¥' + Number(raw).toLocaleString('zh-CN') : '最低 ¥50';\n});\n\n/* ─── 充值 modal ─── */\nfunction openTopup(channel) {\n const selected = document.querySelector('.recharge-card.selected');\n const customRaw = document.getElementById('custom-amt').value.trim();\n const custom = Number(customRaw);\n let amt = 500, bonus = 30;\n if (custom >= 50) { amt = custom; bonus = 0; }\n else if (selected) {\n amt = Number(selected.dataset.amt);\n const bonusEl = selected.querySelector('.gift.bonus');\n bonus = bonusEl ? Number(bonusEl.textContent.replace(/\\D/g, '')) : 0;\n }\n document.getElementById('topup-amt').textContent = fmtMoney(amt);\n document.getElementById('topup-bonus').textContent = bonus > 0\n ? `// 含 ¥${bonus} 赠送 · 实到账 ¥${(amt + bonus).toFixed(2)}`\n : '// 无赠送';\n const isAlipay = channel === 'alipay';\n document.getElementById('topup-channel-label').textContent = isAlipay ? '// 支付宝' : '// 微信支付';\n document.getElementById('topup-channel-name').innerHTML = (isAlipay ? '支付宝扫码' : '微信扫码') +\n '<br><span style=\"color:var(--black-alpha-32);\">/topup/' + (isAlipay ? 'ali' : 'wx') + '/TX' + Date.now() + '</span>';\n Shell.openModal('topup-bg');\n}\nfunction topupDone() {\n Shell.closeModal('topup-bg');\n Shell.toast('充值成功', '余额已更新 · 可开发票');\n}\n\n/* ─── 初始化 ─── */\nrenderTrend();\nrenderProjects();\nrenderMembers();\nrenderBills();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"assetFactory": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"asset-factory.html\">\n<meta charset=\"utf-8\">\n<title>图片生成 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n #page-content { padding: 24px 28px 60px; }\n\n /* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片创作 · 等比)─── */\n .factory-hero {\n display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 16px; margin-bottom: 56px;\n }\n @media (max-width: 1400px) { .factory-hero { grid-template-columns: 1fr 1fr; } }\n @media (max-width: 1000px) { .factory-hero { grid-template-columns: 1fr; } }\n\n .factory-card {\n position: relative;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 28px 30px;\n overflow: hidden;\n }\n\n /* 卡片内 · 文上图下 单列(3 卡并排时保持视觉一致)*/\n .factory-body {\n display: flex; flex-direction: column;\n gap: 18px;\n height: 100%;\n }\n\n .factory-text { display: flex; flex-direction: column; min-width: 0; }\n .factory-tag {\n align-self: flex-start;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .06em;\n padding: 2px 8px;\n border: 1px solid var(--border-faint);\n background: var(--background-lighter);\n border-radius: var(--r-sm);\n margin-bottom: 14px;\n }\n .factory-title {\n font-size: 22px; font-weight: 600;\n letter-spacing: -.018em; line-height: 1.25;\n color: var(--accent-black);\n }\n .factory-desc {\n margin-top: 8px;\n font-size: 13.5px; color: var(--black-alpha-64); line-height: 1.55;\n }\n\n /* feature 列表 */\n .factory-features {\n list-style: none; padding: 0;\n margin: 22px 0 0;\n display: flex; flex-direction: column; gap: 11px;\n }\n .factory-features li {\n display: flex; align-items: center; gap: 10px;\n font-size: 13px; color: var(--black-alpha-72); font-weight: 500;\n }\n .factory-features .ff-ic {\n width: 26px; height: 26px;\n display: inline-flex; align-items: center; justify-content: center;\n background: var(--heat-12);\n color: var(--heat);\n border-radius: var(--r-md);\n flex-shrink: 0;\n }\n .factory-features .ff-ic svg { width: 14px; height: 14px; }\n\n /* 平台 chip 行 */\n .platform-row {\n display: flex; flex-wrap: wrap; gap: 8px;\n margin-top: 20px;\n }\n .platform-chip {\n display: inline-flex; align-items: center; gap: 7px;\n height: 30px; padding: 0 12px 0 8px;\n border: 1px solid var(--border-faint);\n background: var(--surface);\n border-radius: var(--r-pill);\n transition: background var(--t-base);\n cursor: pointer;\n }\n .platform-chip:hover { background: var(--black-alpha-4); }\n .platform-chip .code {\n display: inline-flex; align-items: center; justify-content: center;\n width: 20px; height: 20px;\n background: var(--accent-black); color: var(--accent-white);\n font-family: var(--font-mono); font-size: 8.5px; font-weight: 600;\n border-radius: var(--r-pill); letter-spacing: .04em;\n }\n .platform-chip .nm {\n font-size: 12px; color: var(--accent-black); font-weight: 500;\n }\n\n /* CTA 行:主按钮 + 价格 mono */\n .factory-cta {\n margin-top: auto; padding-top: 24px;\n display: flex; align-items: center; gap: 14px;\n }\n .factory-cta .cost {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n }\n\n /* 视觉占位 + feature 列表已隐藏 — CTA 卡片只保留标题 + 描述 + 按钮 */\n .factory-visual { display: none; }\n .factory-features { display: none; }\n .model-visual { grid-template-columns: repeat(4, 1fr); }\n .model-visual .main { aspect-ratio: 3 / 4; grid-column: span 1; }\n .model-visual .stack { display: contents; }\n .model-visual .stack .placeholder { aspect-ratio: 3 / 4; }\n .kit-visual { grid-template-columns: repeat(4, 1fr); }\n .kit-visual .placeholder { aspect-ratio: 1 / 1; }\n\n /* ─── 任务中心 · section header ─── */\n .section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }\n .section-h h2 { font-size: 18px; font-weight: 600; letter-spacing: -.01em; }\n .section-h .sub-mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n\n /* ─── 视图切换 (复用 projects.html · 图标 + 文字) ─── */\n .view-toggle { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }\n .view-toggle button { padding: 0 14px; background: var(--surface); color: var(--black-alpha-56); font-size: 13px; border: 0; border-right: 1px solid var(--border-faint); border-radius: 0; height: 36px; cursor: pointer; font-family: inherit; display: flex; align-items: center; gap: 6px; transition: background var(--t-base), color var(--t-base); }\n .view-toggle button:last-child { border-right: 0; }\n .view-toggle button:hover { background: var(--background-lighter); color: var(--accent-black); }\n .view-toggle button.active { background: var(--heat-12); color: var(--heat); font-weight: 600; }\n .view-toggle button svg { width: 13px; height: 13px; }\n\n /* ─── 网格视图 · 卡片(原 history-grid) ─── */\n .history-grid {\n display: grid; grid-template-columns: repeat(4, 1fr);\n gap: 16px;\n }\n @media (max-width: 1280px) { .history-grid { grid-template-columns: repeat(3, 1fr); } }\n @media (max-width: 960px) { .history-grid { grid-template-columns: repeat(2, 1fr); } }\n .history-grid[hidden] { display: none; }\n\n .history-card {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 12px;\n display: grid; grid-template-columns: 78px 1fr; gap: 14px;\n align-items: center;\n transition: background var(--t-base);\n cursor: pointer;\n position: relative;\n }\n .history-card:hover { background: var(--black-alpha-4); }\n .history-card:hover .card-del-btn { opacity: 1; }\n .history-card .placeholder { width: 78px; height: 78px; }\n\n /* ─── 列表视图 · 表格 ─── */\n #task-list-view {\n background: var(--surface);\n border: 1px solid var(--border-muted);\n border-radius: var(--r-md);\n overflow: hidden;\n }\n #task-list-view[hidden] { display: none; }\n #task-list-view table.t {\n border: 0;\n border-radius: 0;\n background: transparent;\n }\n #task-list-view table.t thead th {\n background: var(--background-lighter);\n border-bottom-color: var(--border-muted);\n }\n #task-list-view table.t tbody td {\n border-bottom: 0;\n }\n .task-name-cell { display: flex; align-items: center; gap: 12px; }\n .task-thumb { width: 40px; height: 40px; flex-shrink: 0; border-radius: var(--r-sm); }\n .task-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }\n .task-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }\n table.t .task-list-prog { display: flex; align-items: center; gap: 8px; min-width: 120px; }\n table.t .task-list-prog .bar { flex: 1; height: 4px; background: var(--black-alpha-7); border-radius: 2px; overflow: hidden; }\n table.t .task-list-prog .bar span { display: block; height: 100%; background: var(--heat); border-radius: 2px; animation: hp-pulse 1.4s ease-in-out infinite; }\n table.t .task-list-prog .pct { font-family: var(--font-mono); font-size: 10.5px; color: var(--heat); letter-spacing: .02em; white-space: nowrap; }\n\n /* ─── 行末 ⋯ 删除气泡 (复用 projects.html) ─── */\n .row-action { display: flex; gap: 4px; justify-content: flex-end; }\n table.t tbody tr .row-more { opacity: 0; transition: opacity .15s; }\n table.t tbody tr:hover .row-more { opacity: 1; }\n .row-more {\n position: relative; display: inline-flex;\n cursor: pointer; align-items: center;\n color: var(--black-alpha-56);\n padding: 4px;\n }\n .row-more:hover { color: var(--accent-black); }\n .row-more-tip {\n position: absolute; top: calc(100% + 6px); right: 0;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 4px 16px rgba(0,0,0,.08);\n padding: 4px; min-width: 110px;\n opacity: 0; pointer-events: none;\n transform: translateY(-2px);\n transition: opacity .15s, transform .15s;\n z-index: 12;\n }\n .row-more-tip::before {\n content: ''; position: absolute;\n top: -8px; left: 0; right: 0; height: 8px;\n }\n .row-more:hover .row-more-tip,\n .row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }\n .row-more-tip .mi {\n display: flex; align-items: center; gap: 6px;\n width: 100%; padding: 6px 10px;\n background: transparent; border: 0;\n border-radius: var(--r-sm); cursor: pointer;\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit; text-align: left;\n transition: background var(--t-base), color var(--t-base);\n }\n .row-more-tip .mi:hover {\n background: var(--crimson-bg, #fdebea);\n color: var(--accent-crimson, #c43d3d);\n }\n .row-more-tip .mi svg { width: 13px; height: 13px; }\n\n /* ─── 删除确认 modal · 复用 ─── */\n .modal-bg.show { display: flex; }\n .mono-acc { font-family: var(--font-mono); color: var(--heat); font-weight: 600; }\n\n .history-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }\n .history-name {\n font-size: 13.5px; font-weight: 600; color: var(--accent-black);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .history-type {\n font-size: 11.5px; color: var(--black-alpha-48);\n font-family: var(--font-mono); letter-spacing: .02em;\n }\n .history-foot {\n display: flex; align-items: center; justify-content: space-between;\n margin-top: 4px; gap: 6px; min-width: 0;\n }\n .history-foot .mono {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .history-foot .pill { padding: 2px 8px; font-size: 10.5px; }\n .history-foot .pill .dot { width: 5px; height: 5px; }\n\n /* 进度条(生成中状态) */\n .history-prog {\n margin-top: 6px;\n display: flex; align-items: center; gap: 8px;\n }\n .history-prog .bar {\n flex: 1; height: 4px;\n background: var(--black-alpha-7);\n border-radius: 2px; overflow: hidden;\n }\n .history-prog .bar span {\n display: block; height: 100%;\n background: var(--heat); border-radius: 2px;\n animation: hp-pulse 1.4s ease-in-out infinite;\n }\n @keyframes hp-pulse {\n 0%, 100% { opacity: 1; transform: scaleY(1); }\n 50% { opacity: .55; transform: scaleY(.7); }\n }\n .history-prog .pct {\n font-family: var(--font-mono); font-size: 10px;\n color: var(--heat); letter-spacing: .02em; white-space: nowrap;\n }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>图片生成</h1>\n <div class=\"sub\">\n <span class=\"mono\">// 一键生成</span>\n <span>·</span>\n <span>电商视觉素材,提升内容制作效率</span>\n </div>\n </div>\n</div>\n\n<!-- 双 Hero 卡片 -->\n<div class=\"factory-hero\">\n\n <!-- 卡片 A · 模特上身图 -->\n <div class=\"factory-card with-corners\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n\n <div class=\"factory-body\">\n <div class=\"factory-text\">\n <span class=\"factory-tag\">[ MODEL · TRY-ON ]</span>\n <div class=\"factory-title\">模特上身图</div>\n <div class=\"factory-desc\">选择模特,AI 生成商品模特上身效果图</div>\n\n <ul class=\"factory-features\">\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"9\" r=\"3\"/><circle cx=\"17\" cy=\"9\" r=\"2.5\"/><path d=\"M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5\"/></svg>\n </span>\n 支持多模特选择\n </li>\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"8\" height=\"8\"/><rect x=\"13\" y=\"3\" width=\"8\" height=\"8\"/><rect x=\"3\" y=\"13\" width=\"8\" height=\"8\"/><rect x=\"13\" y=\"13\" width=\"8\" height=\"8\"/></svg>\n </span>\n 一次生成 4 张\n </li>\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m3 7 9-4 9 4-9 4-9-4z\"/><path d=\"m3 12 9 4 9-4M3 17l9 4 9-4\"/></svg>\n </span>\n 支持多商品并行\n </li>\n </ul>\n\n <div class=\"factory-cta\">\n <a class=\"btn btn-primary btn-lg\" href=\"model-photo.html\">\n 开始生成\n <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>\n </a>\n <span class=\"cost\">[ ≈ ¥0.30 / 张 ]</span>\n </div>\n </div>\n\n <div class=\"factory-visual model-visual\">\n <div class=\"placeholder main\"><span class=\"ph-frame\">Ava · 9:16</span></div>\n <div class=\"stack\">\n <div class=\"placeholder\"><span class=\"ph-frame\">变体 01</span></div>\n <div class=\"placeholder\"><span class=\"ph-frame\">变体 02</span></div>\n <div class=\"placeholder\"><span class=\"ph-frame\">变体 03</span></div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- 卡片 B · 平台套图 -->\n <div class=\"factory-card with-corners\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n\n <div class=\"factory-body\">\n <div class=\"factory-text\">\n <span class=\"factory-tag\">[ PLATFORM · KIT ]</span>\n <div class=\"factory-title\">平台套图</div>\n <div class=\"factory-desc\">选择平台模板,AI 生成电商平台套图</div>\n\n <ul class=\"factory-features\">\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21V9M9 21V5M15 21v-8M21 21V11\"/></svg>\n </span>\n 覆盖主流电商平台\n </li>\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"8\" height=\"8\"/><rect x=\"13\" y=\"3\" width=\"8\" height=\"8\"/><rect x=\"3\" y=\"13\" width=\"8\" height=\"8\"/><rect x=\"13\" y=\"13\" width=\"8\" height=\"8\"/></svg>\n </span>\n 一键生成 4 张套图\n </li>\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z\"/></svg>\n </span>\n 智能排版设计\n </li>\n </ul>\n\n <div class=\"factory-cta\">\n <a class=\"btn btn-primary btn-lg\" href=\"platform-cover.html\">\n 开始生成\n <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>\n </a>\n <span class=\"cost\">[ ≈ ¥0.50 / 张 ]</span>\n </div>\n </div>\n\n <div class=\"factory-visual kit-visual\">\n <div class=\"placeholder\"><span class=\"ph-frame\">套图 / TB</span></div>\n <div class=\"placeholder\"><span class=\"ph-frame\">套图 / DY</span></div>\n <div class=\"placeholder\"><span class=\"ph-frame\">套图 / XHS</span></div>\n <div class=\"placeholder\"><span class=\"ph-frame\">套图 / PDD</span></div>\n </div>\n </div>\n </div>\n\n <!-- 卡片 C · 图片创作(自由创作 AI 图片工作台)-->\n <div class=\"factory-card with-corners\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n\n <div class=\"factory-body\">\n <div class=\"factory-text\">\n <span class=\"factory-tag\">[ IMAGE · STUDIO ]</span>\n <div class=\"factory-title\">图片创作</div>\n <div class=\"factory-desc\">自由创作 AI 图片,适用于详情图 / 海报 / 灵感速写</div>\n\n <ul class=\"factory-features\">\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"9\" r=\"3\"/><path d=\"M3 19c0-3 2.7-5 6-5s6 2 6 5\"/></svg>\n </span>\n 人物 · 商品 全支持\n </li>\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"6\" height=\"18\"/><rect x=\"9\" y=\"3\" width=\"6\" height=\"18\"/><rect x=\"15\" y=\"3\" width=\"6\" height=\"18\"/></svg>\n </span>\n 正面 / 侧面 / 背面 一次输出\n </li>\n <li>\n <span class=\"ff-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83\"/></svg>\n </span>\n 多镜头一致性保证\n </li>\n </ul>\n\n <div class=\"factory-cta\">\n <a class=\"btn btn-primary btn-lg\" href=\"image-optimize.html\">\n 开始生成\n <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>\n </a>\n <span class=\"cost\">[ ≈ ¥0.40 / 组 ]</span>\n </div>\n </div>\n\n <div class=\"factory-visual tri-visual\">\n <div class=\"placeholder\"><span class=\"ph-frame\">正 / 侧 / 背 · 三视图</span></div>\n </div>\n </div>\n </div>\n\n</div>\n\n<!-- ============= 任务中心 · 参考 projects.html 布局 ============= -->\n<div class=\"section-h\">\n <h2>任务中心</h2>\n <span class=\"sub-mono\">// <span id=\"tc-sub-total\">0</span> 个 · <span id=\"tc-sub-gen\">0</span> 生成中 · <span id=\"tc-sub-ok\">0</span> 已完成 · <span id=\"tc-sub-err\">0</span> 失败</span>\n</div>\n\n<!-- 状态 tabs (复用 .tabs) -->\n<div class=\"tabs\" id=\"tc-tabs\">\n <div class=\"tab active\" data-filter=\"all\">全部 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-filter=\"gen\">生成中 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-filter=\"ok\">已完成 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-filter=\"err\">失败 <span class=\"count\">0</span></div>\n</div>\n\n<!-- toolbar: search + 类型 chip + clear + view-toggle -->\n<div class=\"toolbar\">\n <div class=\"search-inline\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input class=\"input\" id=\"tc-search\" placeholder=\"搜索任务名\">\n </div>\n <div class=\"chip-wrap\" data-key=\"time\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">时间</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\">\n <div class=\"mi selected\" data-value=\"all\"><svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg><span>全部时间</span></div>\n <div class=\"mi-sep\"></div>\n <div class=\"mi\" data-value=\"today\"><svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg><span>今天</span></div>\n <div class=\"mi\" data-value=\"1h\"><svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg><span>1 小时内</span></div>\n <div class=\"mi\" data-value=\"10min\"><svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg><span>10 分钟内</span></div>\n </div>\n </div>\n <div class=\"chip-wrap\" data-key=\"type\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">任务类型</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <button class=\"clear-filters\" id=\"tc-clear\" type=\"button\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n 清空筛选\n </button>\n <span class=\"spacer\"></span>\n <div class=\"view-toggle\" id=\"tc-view-toggle\">\n <button type=\"button\" data-view=\"grid\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"/><rect x=\"9\" y=\"2\" width=\"5\" height=\"5\"/><rect x=\"2\" y=\"9\" width=\"5\" height=\"5\"/><rect x=\"9\" y=\"9\" width=\"5\" height=\"5\"/></svg>\n 网格\n </button>\n <button type=\"button\" class=\"active\" data-view=\"list\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg>\n 列表\n </button>\n </div>\n</div>\n\n<div class=\"result-meta\" id=\"tc-result-meta\">// 显示 <span class=\"count\">0</span> / 0 个任务</div>\n\n<!-- ============= LIST VIEW (默认) ============= -->\n<div id=\"task-list-view\">\n <table class=\"t\">\n <thead>\n <tr>\n <th style=\"width:42%\">任务</th>\n <th style=\"width:160px\">进度</th>\n <th>状态</th>\n <th style=\"width:120px\">创建于</th>\n <th style=\"width:48px\"></th>\n </tr>\n </thead>\n <tbody id=\"task-list-tbody\"><!-- JS 从卡片同步生成 --></tbody>\n </table>\n</div>\n\n<!-- ============= GRID VIEW (JS 从 localStorage 动态渲染) ============= -->\n<div class=\"history-grid\" id=\"task-grid\" hidden></div>\n\n<!-- 空态 -->\n<div id=\"tc-empty\" hidden style=\"padding:60px 20px;text-align:center;color:var(--black-alpha-48);font-size:13px;line-height:1.6\">\n <div style=\"font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;margin-bottom:6px;\">// NO TASKS YET</div>\n <div id=\"tc-empty-text\">还没有任务,去上方选一个工序开始生成吧</div>\n</div>\n\n</div>\n\n<!-- ===== 删除确认 modal (复用 projects.html 风格) ===== -->\n<div class=\"modal-bg\" id=\"tc-del-bg\">\n <div class=\"modal\" role=\"dialog\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </div>\n <div class=\"ti\">确认删除任务<span>// CONFIRM DELETE</span></div>\n </div>\n <div class=\"modal-b\" id=\"tc-del-body\">即将删除任务记录。</div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" id=\"tc-del-cancel\">取消</button>\n <button class=\"btn\" type=\"button\" id=\"tc-del-ok\" style=\"background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/></svg>\n 确认删除\n </button>\n </div>\n </div>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script src=\"/exact/assets/new-product-drawer.js?v=202605211643\"></script>\n<script>\nShell.render({\n active: 'asset-factory',\n crumbs: [{ label: '工作台', href: 'index.html' }, { label: '图片生成' }]\n});\n\n/* ============================================================\n 任务中心 · 从 localStorage 读取(与 model-photo / platform-cover 共享)\n ============================================================ */\n(function () {\n 'use strict';\n\n const TYPE_LABEL = { model: '模特上身图', platform: '平台套图', image: '图片创作' };\n const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };\n const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };\n const KEY_BY_TYPE = {\n model: 'fs-image-tasks-model',\n platform: 'fs-image-tasks-platform',\n image: 'fs-image-tasks-image',\n };\n const URL_BY_TYPE = {\n model: 'model-photo.html',\n platform: 'platform-cover.html',\n image: 'image-optimize.html',\n };\n\n const taskGrid = document.getElementById('task-grid');\n const listTbody = document.getElementById('task-list-tbody');\n const gridView = taskGrid;\n const listView = document.getElementById('task-list-view');\n\n let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)\n\n const state = { filter: 'all', type: 'all', time: 'all', search: '', view: 'list' };\n\n function _timeMatch(createdAt, key) {\n if (key === 'all' || !createdAt) return true;\n const now = Date.now();\n const diff = now - Number(createdAt);\n if (key === '10min') return diff <= 10 * 60 * 1000;\n if (key === '1h') return diff <= 60 * 60 * 1000;\n if (key === 'today') {\n const a = new Date(now); const b = new Date(Number(createdAt));\n return a.toDateString() === b.toDateString();\n }\n return true;\n }\n const TIME_LABEL = { all: '时间', today: '今天', '1h': '1 小时内', '10min': '10 分钟内' };\n\n function esc(s) { return String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c])); }\n\n /* ---------- localStorage 读写 ---------- */\n function loadType(type) {\n try { return JSON.parse(localStorage.getItem(KEY_BY_TYPE[type]) || '[]'); } catch (e) { return []; }\n }\n function saveType(type, arr) {\n try { localStorage.setItem(KEY_BY_TYPE[type], JSON.stringify(arr)); } catch (e) {}\n }\n function loadAllTasks() {\n const all = [];\n Object.keys(KEY_BY_TYPE).forEach(type => {\n loadType(type).forEach(t => all.push({ ...t, type })); // type 兜底\n });\n all.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));\n return all;\n }\n\n /* ---------- task → 渲染辅助 ---------- */\n function subTextOf(t) {\n const lbl = TYPE_LABEL[t.type] || t.type;\n const count = (t.snap && t.snap.count) || 4;\n return `${lbl} · ${count} 张`;\n }\n function thumbLabelOf(t) {\n // 取「商品 × 模特/平台」中右半作为缩略图占位文字\n const parts = (t.name || '').split(/\\s[×x]\\s/);\n return (parts[1] || parts[0] || '—').slice(0, 8);\n }\n\n /* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */\n function goToWorkbench(t) {\n const base = URL_BY_TYPE[t.type] || URL_BY_TYPE.model;\n location.href = base + '?taskId=' + encodeURIComponent(t.id);\n }\n\n /* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */\n function cardFor(t) {\n const card = document.createElement('div');\n card.className = 'task-card history-card';\n card.dataset.status = t.status;\n card.dataset.type = t.type;\n card.dataset.name = t.name;\n card.dataset.taskId = t.id;\n card.dataset.createdAt = String(t.createdAt || 0);\n card.style.cursor = 'pointer';\n card.innerHTML = `\n <button class=\"card-del-btn\" type=\"button\" title=\"删除任务\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </button>\n <div class=\"placeholder\"><span class=\"ph-frame\">${esc(thumbLabelOf(t))}</span></div>\n <div class=\"history-body\">\n <div class=\"history-name\">${esc(t.name)}</div>\n <div class=\"history-type\">${esc(subTextOf(t))}</div>\n <div class=\"history-foot\">\n <span class=\"mono\">// ${esc(t.time || '')}</span>\n <span class=\"pill ${STATUS_PILL[t.status] || 'info'}\"><span class=\"dot\"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span>\n </div>\n </div>\n `;\n return card;\n }\n\n function rowFor(t) {\n const tr = document.createElement('tr');\n tr.dataset.taskRow = '1';\n tr.dataset.name = t.name;\n tr.dataset.type = t.type;\n tr.dataset.status = t.status;\n tr.dataset.taskId = t.id;\n tr.addEventListener('click', () => goToWorkbench(t));\n tr.innerHTML = `\n <td>\n <div class=\"task-name-cell\">\n <div class=\"placeholder task-thumb\"><span class=\"ph-frame\">${esc(thumbLabelOf(t))}</span></div>\n <div>\n <div class=\"task-name\">${esc(t.name)}</div>\n <div class=\"task-sub\">${esc(subTextOf(t))}</div>\n </div>\n </div>\n </td>\n <td>${t.status === 'gen'\n ? `<div class=\"task-list-prog\"><div class=\"bar\"><span style=\"width:60%\"></span></div><span class=\"pct\">60%</span></div>`\n : (t.status === 'ok' ? '<span class=\"muted-2 mono\" style=\"font-size:11px;\">已完成</span>' : '<span class=\"muted-2 mono\" style=\"font-size:11px;\">—</span>')}</td>\n <td><span class=\"pill ${STATUS_PILL[t.status] || 'info'}\"><span class=\"dot\"></span>${esc(STATUS_LABEL[t.status] || t.status)}</span></td>\n <td class=\"muted-2\">${esc(t.time || '')}</td>\n <td>\n <div class=\"row-action\">\n <span class=\"row-more\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><circle cx=\"3\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"8\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"13\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/></svg>\n <div class=\"row-more-tip\"><button class=\"mi mi-del-task\" type=\"button\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>删除任务</button></div>\n </span>\n </div>\n </td>\n `;\n tr.querySelector('.row-more').addEventListener('click', e => e.stopPropagation());\n return tr;\n }\n\n /* ---------- 2. 全量重渲染(load → render → filter) ---------- */\n function renderAll() {\n // 清空 grid / list\n taskGrid.innerHTML = '';\n listTbody.innerHTML = '';\n cards = [];\n\n const tasks = loadAllTasks();\n tasks.forEach(t => {\n const card = cardFor(t);\n const row = rowFor(t);\n taskGrid.appendChild(card);\n listTbody.appendChild(row);\n card._listRow = row;\n card._task = t;\n row._card = card;\n cards.push(card);\n\n // 卡片点击 → 跳工作台\n card.addEventListener('click', e => {\n if (e.target.closest('.card-del-btn')) return;\n goToWorkbench(t);\n });\n // 卡片删除按钮\n card.querySelector('.card-del-btn').addEventListener('click', e => {\n e.stopPropagation();\n openDelConfirm(card);\n });\n });\n\n // 类型 chip 菜单(基于现有 task 的 type 集合,动态)\n rebuildTypeMenu();\n applyFilter();\n }\n\n /* ---------- 3. 构建类型 chip 菜单(基于当前 cards 的 type 集合) ---------- */\n const checkSvg = '<svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg>';\n const typeMenu = document.querySelector('.chip-wrap[data-key=\"type\"] .chip-menu');\n function rebuildTypeMenu() {\n const typeOptions = [...new Set(cards.map(c => c.dataset.type))];\n typeMenu.innerHTML = `<div class=\"mi selected\" data-value=\"all\">${checkSvg}<span>全部任务类型</span></div><div class=\"mi-sep\"></div>`\n + typeOptions.map(v => `<div class=\"mi\" data-value=\"${esc(v)}\">${checkSvg}<span>${esc(TYPE_LABEL[v] || v)}</span></div>`).join('');\n syncTypeChip();\n }\n\n function syncTypeChip() {\n const wrap = document.querySelector('.chip-wrap[data-key=\"type\"]');\n const label = wrap.querySelector('.chip-label');\n const chip = wrap.querySelector('.chip');\n if (state.type === 'all') {\n label.textContent = '任务类型';\n chip.classList.remove('active');\n } else {\n label.textContent = TYPE_LABEL[state.type] || state.type;\n chip.classList.add('active');\n }\n wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));\n }\n\n function syncTimeChip() {\n const wrap = document.querySelector('.chip-wrap[data-key=\"time\"]');\n if (!wrap) return;\n const label = wrap.querySelector('.chip-label');\n const chip = wrap.querySelector('.chip');\n if (state.time === 'all') {\n label.textContent = '时间';\n chip.classList.remove('active');\n } else {\n label.textContent = TIME_LABEL[state.time] || state.time;\n chip.classList.add('active');\n }\n wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.time));\n }\n\n /* ---------- 3. applyFilter ---------- */\n function applyFilter() {\n const q = state.search.toLowerCase();\n let visible = 0;\n cards.forEach(card => {\n const okStatus = state.filter === 'all' || card.dataset.status === state.filter;\n const okType = state.type === 'all' || card.dataset.type === state.type;\n const okTime = _timeMatch(card.dataset.createdAt, state.time);\n const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);\n const show = okStatus && okType && okTime && okSearch;\n card.style.display = show ? '' : 'none';\n if (card._listRow) card._listRow.style.display = show ? '' : 'none';\n if (show) visible++;\n });\n\n // 计数\n const counts = { all: cards.length, gen: 0, ok: 0, err: 0 };\n cards.forEach(c => { if (counts[c.dataset.status] !== undefined) counts[c.dataset.status]++; });\n document.querySelectorAll('#tc-tabs .tab').forEach(t => {\n const f = t.dataset.filter;\n t.querySelector('.count').textContent = f === 'all' ? counts.all : counts[f];\n });\n document.getElementById('tc-sub-total').textContent = counts.all;\n document.getElementById('tc-sub-gen').textContent = counts.gen;\n document.getElementById('tc-sub-ok').textContent = counts.ok;\n document.getElementById('tc-sub-err').textContent = counts.err;\n\n document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class=\"count\">${visible}</span> / ${cards.length} 个任务`;\n document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all' || state.time !== 'all');\n\n // 空态\n const emptyEl = document.getElementById('tc-empty');\n const emptyText = document.getElementById('tc-empty-text');\n if (cards.length === 0) {\n emptyEl.hidden = false;\n emptyText.textContent = '还没有任务,去上方选一个工序开始生成吧';\n listView.hidden = true; gridView.hidden = true;\n } else if (visible === 0) {\n emptyEl.hidden = false;\n emptyText.textContent = '没有符合筛选条件的任务';\n // 视图本身保留,空表头也保留\n } else {\n emptyEl.hidden = true;\n // 恢复 view 显示(可能在 length===0 分支被隐藏)\n if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }\n else { listView.hidden = true; gridView.hidden = false; }\n }\n }\n\n /* ---------- 4. 事件绑定 ---------- */\n // status tabs\n document.querySelectorAll('#tc-tabs .tab').forEach(t => {\n t.addEventListener('click', () => {\n document.querySelectorAll('#tc-tabs .tab').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n state.filter = t.dataset.filter;\n applyFilter();\n });\n });\n // search\n document.getElementById('tc-search').addEventListener('input', e => {\n state.search = e.target.value.trim();\n applyFilter();\n });\n // type chip\n const typeWrap = document.querySelector('.chip-wrap[data-key=\"type\"]');\n typeWrap.querySelector('.chip').addEventListener('click', e => {\n e.stopPropagation();\n const isOpen = typeWrap.classList.contains('open');\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) typeWrap.classList.add('open');\n });\n typeMenu.addEventListener('click', e => {\n const mi = e.target.closest('.mi');\n if (!mi) return;\n e.stopPropagation();\n state.type = mi.dataset.value;\n typeWrap.classList.remove('open');\n syncTypeChip();\n applyFilter();\n });\n // time chip\n const timeWrap = document.querySelector('.chip-wrap[data-key=\"time\"]');\n const timeMenu = timeWrap.querySelector('.chip-menu');\n timeWrap.querySelector('.chip').addEventListener('click', e => {\n e.stopPropagation();\n const isOpen = timeWrap.classList.contains('open');\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) timeWrap.classList.add('open');\n });\n timeMenu.addEventListener('click', e => {\n const mi = e.target.closest('.mi');\n if (!mi) return;\n e.stopPropagation();\n state.time = mi.dataset.value;\n timeWrap.classList.remove('open');\n syncTimeChip();\n applyFilter();\n });\n document.addEventListener('click', () => {\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n });\n // clear filters\n document.getElementById('tc-clear').addEventListener('click', () => {\n state.search = '';\n state.type = 'all';\n state.time = 'all';\n document.getElementById('tc-search').value = '';\n syncTypeChip();\n syncTimeChip();\n applyFilter();\n Shell.toast('已清空筛选');\n });\n // view toggle\n document.querySelectorAll('#tc-view-toggle button').forEach(b => {\n b.addEventListener('click', () => {\n document.querySelectorAll('#tc-view-toggle button').forEach(x => x.classList.remove('active'));\n b.classList.add('active');\n state.view = b.dataset.view;\n if (state.view === 'list') { listView.hidden = false; gridView.hidden = true; }\n else { listView.hidden = true; gridView.hidden = false; }\n });\n });\n\n /* ---------- 6. 删除 modal + 同步 localStorage ---------- */\n const delBg = document.getElementById('tc-del-bg');\n const delBody = document.getElementById('tc-del-body');\n const delCancel = document.getElementById('tc-del-cancel');\n const delOk = document.getElementById('tc-del-ok');\n let _delTarget = null;\n\n function openDelConfirm(target) {\n _delTarget = target;\n const name = target.dataset.name || '该任务';\n delBody.innerHTML = '即将删除任务 <span class=\"mono-acc\">' + esc(name) + '</span>。任务记录将清除,已入库的素材不受影响。';\n delBg.classList.add('show');\n }\n function closeDelConfirm() { delBg.classList.remove('show'); _delTarget = null; }\n delCancel.addEventListener('click', closeDelConfirm);\n delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });\n delOk.addEventListener('click', () => {\n if (!_delTarget) return;\n const card = _delTarget._card || _delTarget; // 可能传 row 或 card\n const taskId = card.dataset.taskId;\n const taskType = card.dataset.type;\n const name = card.dataset.name;\n // 从 localStorage 移除对应任务\n if (taskType && KEY_BY_TYPE[taskType]) {\n const arr = loadType(taskType).filter(t => t.id !== taskId);\n saveType(taskType, arr);\n }\n closeDelConfirm();\n Shell.toast('已删除', name);\n renderAll();\n });\n\n // 列表行 删除按钮(事件委托,因为是动态生成)\n listTbody.addEventListener('click', e => {\n const btn = e.target.closest('.mi-del-task');\n if (!btn) return;\n e.stopPropagation();\n const tr = btn.closest('tr');\n if (tr) openDelConfirm(tr);\n });\n\n /* ---------- 7. 初始化 ---------- */\n renderAll();\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"dashboard": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"index.html\">\n<meta charset=\"utf-8\">\n<title>工作台 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .dash-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 24px; align-items: start; }\n .recent-row { display: grid; grid-template-columns: 54px 1fr 110px 130px 60px; align-items: center; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); cursor: pointer; }\n .recent-row .prog, .recent-row .pill, .recent-row .btn { justify-self: start; }\n .recent-row:last-child { border-bottom: 0; }\n .recent-row:hover { background: var(--background-lighter); }\n .recent-row .thumb { width: 54px; height: 70px; border-radius: var(--r-md); }\n .recent-meta .name { font-weight: 600; font-size: 13.5px; color: var(--accent-black); }\n .recent-meta .sub { font-size: 12px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }\n .shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n .shortcut { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 16px; display: flex; align-items: flex-start; gap: 12px; cursor: pointer; transition: background var(--t-base); }\n .shortcut:hover { background: var(--black-alpha-4); }\n .shortcut .ic { width: 32px; height: 32px; background: var(--heat-12); color: var(--heat); display: grid; place-items: center; border-radius: var(--r-md); flex-shrink: 0; }\n .shortcut .ic svg { width: 16px; height: 16px; }\n .shortcut .t { font-size: 13px; font-weight: 600; }\n .shortcut .d { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .01em; }\n .tip { background: var(--surface); border: 1px dashed var(--border-faint); padding: 14px 16px; font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.6; border-radius: var(--r-md); }\n .tip strong { color: var(--accent-black); font-weight: 600; display: block; margin-bottom: 4px; }\n .tip .mono { font-family: var(--font-mono); color: var(--heat); background: var(--heat-12); padding: 1px 5px; border-radius: var(--r-sm); font-size: 11.5px; }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>欢迎回来,小李</h1>\n <div class=\"sub\">\n <span class=\"mono\">// 05.14 · 周三</span>\n <span>·</span>\n <span>你有 <b style=\"color:var(--accent-black)\">3 个项目</b> 正在进行中</span>\n </div>\n </div>\n <div class=\"actions\">\n <a class=\"btn btn-create\" href=\"javascript:void(0)\" onclick=\"event.preventDefault(); window.NewProductDrawer && NewProductDrawer.open();\">\n <span data-iconkit=\"productPlus\" data-icon-size=\"16\"></span>\n 新建商品\n </a>\n <a class=\"btn btn-primary btn-lg btn-create\" href=\"projects-new.html\">\n <span data-iconkit=\"clapperboard\" data-icon-size=\"16\"></span>\n 新建项目\n </a>\n </div>\n</div>\n\n<div class=\"stats with-corners\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <a class=\"stat\" href=\"projects.html\">\n <div class=\"lbl\">总项目 <span class=\"badge\">ALL</span></div>\n <div class=\"v\">8</div>\n <div class=\"delta up\"><span data-iconkit=\"arrowUp\" data-icon-size=\"14\"></span> 本月 +3</div>\n </a>\n <a class=\"stat\" href=\"projects.html?filter=wip\">\n <div class=\"lbl\">进行中 <span class=\"badge\">WIP</span></div>\n <div class=\"v\">3</div>\n <div class=\"delta\">2 个待审核</div>\n </a>\n <a class=\"stat\" href=\"projects.html?filter=done\">\n <div class=\"lbl\">本月成片 <span class=\"badge\">DONE</span></div>\n <div class=\"v\">3</div>\n <div class=\"delta up\"><span data-iconkit=\"arrowUp\" data-icon-size=\"14\"></span> 较上月 +33%</div>\n </a>\n <a class=\"stat\" href=\"account.html\">\n <div class=\"lbl\">余额 <span class=\"badge\">¥</span></div>\n <div class=\"v\">¥327<small>.40</small></div>\n <div class=\"bar\"><span style=\"width:33%\"></span></div>\n <div class=\"sub\">已用 ¥162.60 / ¥500</div>\n </a>\n</div>\n\n<div class=\"dash-grid\">\n <div>\n <div class=\"section-h\">\n <h2>最近项目</h2>\n <a class=\"more\" href=\"projects.html\">[ ALL · 8 ]</a>\n </div>\n <div class=\"card-hard\">\n <a class=\"recent-row\" href=\"pipeline.html?product=%E9%80%8F%E7%9C%9F%E8%A1%A5%E6%B0%B4%E9%9D%A2%E8%86%9C#stage-3\">\n <div class=\"placeholder thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div class=\"recent-meta\">\n <div class=\"name\">补水面膜 · 痛点种草 · v3</div>\n <div class=\"sub\">透真补水面膜 / AI 全生 / 7 镜</div>\n </div>\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"cur\"></span><span></span><span></span></div>\n <span class=\"pill info\"><span class=\"dot\"></span>故事板 待确认</span>\n <span class=\"btn btn-sm\">继续</span>\n </a>\n <a class=\"recent-row\" href=\"pipeline.html?product=%E9%80%8F%E7%9C%9F%E9%98%B2%E6%99%92#stage-5\">\n <div class=\"placeholder thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div class=\"recent-meta\">\n <div class=\"name\">透真防晒 · 通勤对比</div>\n <div class=\"sub\">透真防晒 / AI 全生 / 6 镜</div>\n </div>\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n <span class=\"btn btn-sm\">打开</span>\n </a>\n <a class=\"recent-row\" href=\"pipeline.html?product=Pro%204%20%E8%93%9D%E7%89%99%E8%80%B3%E6%9C%BA#stage-4\">\n <div class=\"placeholder thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div class=\"recent-meta\">\n <div class=\"name\">蓝牙耳机 · 开箱测评</div>\n <div class=\"sub\">Pro 4 蓝牙耳机 / 自带脚本 / 6 镜</div>\n </div>\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"cur\"></span><span></span></div>\n <span class=\"pill info\"><span class=\"dot\"></span>视频生成 4/6</span>\n <span class=\"btn btn-sm\">继续</span>\n </a>\n <a class=\"recent-row\" href=\"pipeline.html?product=%E5%87%9D%E5%BD%A9%E7%AB%8B%E4%BD%93%E5%8F%A3%E7%BA%A2#stage-2\">\n <div class=\"placeholder thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div class=\"recent-meta\">\n <div class=\"name\">春日新品 · 立体口红</div>\n <div class=\"sub\">凝彩立体口红 / 一句话 / 5 镜</div>\n </div>\n <div class=\"prog\"><span class=\"done\"></span><span class=\"cur\"></span><span></span><span></span><span></span></div>\n <span class=\"pill info\"><span class=\"dot\"></span>资产生成中</span>\n <span class=\"btn btn-sm\">继续</span>\n </a>\n <a class=\"recent-row\" href=\"pipeline.html?product=%E5%86%B7%E8%90%83%E5%92%96%E5%95%A1%E5%86%BB%E5%B9%B2#stage-3\">\n <div class=\"placeholder thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div class=\"recent-meta\">\n <div class=\"name\">咖啡冻干 · 早八</div>\n <div class=\"sub\">冷萃咖啡冻干 / 一句话 / 5 镜</div>\n </div>\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"fail\"></span><span></span><span></span></div>\n <span class=\"pill err\"><span class=\"dot\"></span>故事板失败</span>\n <span class=\"btn btn-sm\">查看</span>\n </a>\n </div>\n </div>\n\n <div style=\"display:flex;flex-direction:column;gap:24px\">\n <div>\n <div class=\"section-h\"><h2>快捷入口</h2><span class=\"more\">[ /shortcuts ]</span></div>\n <div class=\"shortcuts\">\n <a class=\"shortcut\" href=\"products.html\">\n <div class=\"ic\" data-iconkit=\"package\"></div>\n <div><div class=\"t\">商品库</div><div class=\"d\">7 SKU</div></div>\n </a>\n <a class=\"shortcut\" href=\"library.html\">\n <div class=\"ic\" data-iconkit=\"images\"></div>\n <div><div class=\"t\">资产库</div><div class=\"d\">人 8 · 景 14 · 片 8</div></div>\n </a>\n <a class=\"shortcut\" href=\"account.html\">\n <div class=\"ic\" data-iconkit=\"creditCard\"></div>\n <div><div class=\"t\">充值</div><div class=\"d\">¥327.40</div></div>\n </a>\n <a class=\"shortcut\" href=\"projects.html\">\n <div class=\"ic\" data-iconkit=\"clapperboard\"></div>\n <div><div class=\"t\">所有项目</div><div class=\"d\">8 个</div></div>\n </a>\n </div>\n </div>\n <div>\n <div class=\"section-h\"><h2>提示</h2><span class=\"more\">[ FAQ ]</span></div>\n <div class=\"tip\">\n <strong>扣费规则</strong>\n 生成失败、超时、用户重跑 — 均不扣费。仅在你点 <span class=\"mono\">[ 确认通过 ]</span> 时按 token 实际结算。\n </div>\n </div>\n </div>\n</div>\n\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script src=\"/exact/assets/new-product-drawer.js?v=202605211643\"></script>\n<script>\n document.querySelectorAll('[data-iconkit]').forEach(el => {\n el.innerHTML = IconKit.svg(el.dataset.iconkit, { size: Number(el.dataset.iconSize || 16) });\n });\n Shell.render({ active: 'dashboard', crumbs: [{ label: '工作台' }] });\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"imageOptimize": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"image-optimize.html\">\n<meta charset=\"utf-8\">\n<title>图片创作 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* viewport-fit · 工作台铺满 */\n .app { height: 100vh; overflow: hidden; }\n main { display: flex; flex-direction: column; min-height: 0; }\n #page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }\n\n /* ===== 整体两栏 · 左会话列表 + 中央对话流 ===== */\n .io-app {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 240px 1fr;\n transition: grid-template-columns var(--t-base);\n }\n .io-app.side-collapsed { grid-template-columns: 0 1fr; }\n @media (max-width: 1100px) {\n .io-app { grid-template-columns: 200px 1fr; }\n .io-app.side-collapsed { grid-template-columns: 0 1fr; }\n }\n\n /* ========== 左 · 会话栏 ========== */\n .io-side {\n border-right: 1px solid var(--border-faint);\n background: var(--surface);\n display: flex; flex-direction: column;\n min-height: 0; overflow: hidden;\n transition: opacity var(--t-base), transform var(--t-base);\n }\n .io-app.side-collapsed .io-side {\n opacity: 0;\n transform: translateX(-8px);\n pointer-events: none;\n }\n .io-side-h {\n display: flex; align-items: center; gap: 8px;\n padding: 14px 14px 10px;\n border-bottom: 1px solid var(--border-faint);\n }\n .io-side-h .ti { font-size: 13.5px; font-weight: 600; color: var(--accent-black); }\n .io-side-h .back-pill {\n display: inline-flex; align-items: center; gap: 6px;\n height: 34px; padding: 0 13px 0 11px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n color: var(--accent-black);\n font-size: 13px; font-weight: 500;\n font-family: inherit;\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .io-side-h .back-pill:hover {\n background: var(--black-alpha-4);\n border-color: var(--black-alpha-24);\n color: var(--accent-black);\n }\n .io-side-h .back-pill svg { width: 14px; height: 14px; }\n .io-side-h .fold {\n margin-left: auto;\n width: 26px; height: 26px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--black-alpha-48); cursor: pointer;\n transition: background var(--t-base), color var(--t-base);\n }\n .io-side-h .fold:hover { background: var(--black-alpha-4); color: var(--accent-black); }\n .io-side-h .fold svg { width: 14px; height: 14px; }\n .io-new-conv {\n margin: 10px 12px 0;\n height: 36px;\n display: inline-flex; align-items: center; gap: 8px;\n padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n color: var(--accent-black);\n font-size: 13px; font-weight: 500;\n font-family: inherit;\n cursor: pointer;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .io-new-conv:hover {\n border-color: var(--heat-20); background: var(--heat-12); color: var(--heat);\n }\n .io-new-conv svg { width: 13px; height: 13px; }\n .io-side-sec-h {\n margin: 16px 14px 6px;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48);\n letter-spacing: .08em;\n text-transform: uppercase;\n }\n .io-conv-list {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 0 6px 14px;\n display: flex; flex-direction: column; gap: 2px;\n }\n .io-conv-item {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 10px;\n border-radius: var(--r-sm);\n cursor: pointer;\n color: var(--accent-black);\n transition: background var(--t-base);\n position: relative;\n }\n .io-conv-item:hover { background: var(--background-lighter); }\n .io-conv-item.active { background: var(--heat-12); }\n .io-conv-item .thumb {\n flex-shrink: 0;\n width: 28px; height: 28px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n background-size: cover; background-position: center;\n display: grid; place-items: center;\n color: var(--black-alpha-32);\n }\n .io-conv-item .thumb svg { width: 13px; height: 13px; }\n .io-conv-item.default .thumb {\n background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black);\n }\n .io-conv-item .nm {\n flex: 1; min-width: 0;\n font-size: 12.5px;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .io-conv-item.active .nm { color: var(--heat); font-weight: 600; }\n .io-conv-item .del {\n display: none;\n width: 22px; height: 22px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n color: var(--black-alpha-48); cursor: pointer;\n align-items: center; justify-content: center;\n }\n .io-conv-item:hover .del { display: inline-flex; }\n .io-conv-item .del:hover { color: var(--accent-crimson); background: var(--crimson-bg, #fdebea); }\n .io-conv-item .del svg { width: 11px; height: 11px; }\n\n /* ========== 右 · 对话流 ========== */\n .io-main {\n display: flex; flex-direction: column;\n min-height: 0;\n position: relative;\n }\n .io-toolbar {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 10px;\n padding: 12px 28px;\n border-bottom: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .io-toolbar .spacer { flex: 1; }\n .io-toolbar .side-restore-btn {\n height: 32px;\n padding: 0 10px;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n }\n .io-toolbar .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }\n .io-toolbar .side-restore-btn[hidden] { display: none; }\n .io-toolbar .side-restore-btn svg { width: 14px; height: 14px; }\n .io-toolbar-search {\n position: relative;\n width: min(320px, 32vw);\n min-width: 220px;\n }\n .io-toolbar-search[hidden] { display: none; }\n .io-toolbar-search svg {\n position: absolute;\n left: 10px;\n top: 50%;\n transform: translateY(-50%);\n width: 13px;\n height: 13px;\n color: var(--black-alpha-48);\n pointer-events: none;\n }\n .io-toolbar-search input {\n width: 100%;\n height: 32px;\n padding: 0 32px 0 30px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--accent-black);\n font-family: inherit;\n font-size: 12.5px;\n outline: none;\n }\n .io-toolbar-search input:focus { border-color: var(--heat-40); background: var(--surface); }\n .io-toolbar-search .clear-search {\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n width: 24px;\n height: 24px;\n border: 0;\n border-radius: var(--r-sm);\n background: transparent;\n color: var(--black-alpha-48);\n cursor: pointer;\n display: grid;\n place-items: center;\n }\n .io-toolbar-search .clear-search:hover { background: var(--black-alpha-4); color: var(--accent-black); }\n .io-toolbar-search .clear-search svg {\n position: static;\n transform: none;\n width: 12px;\n height: 12px;\n }\n .io-toolbar .search-btn {\n width: 32px; height: 32px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n cursor: pointer;\n display: grid; place-items: center;\n }\n .io-toolbar .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }\n .io-toolbar .search-btn.active { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }\n .io-toolbar .search-btn svg { width: 14px; height: 14px; }\n .io-tool-filter { position: relative; display: inline-flex; }\n .io-toolbar .tb-chip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 32px; padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .io-toolbar .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }\n .io-toolbar .tb-chip.active { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }\n .io-toolbar .tb-chip svg { width: 10px; height: 10px; opacity: .6; }\n .io-tool-filter.open .tb-chip svg { transform: rotate(180deg); }\n .io-tool-menu {\n position: absolute;\n top: calc(100% + 4px);\n right: 0;\n min-width: 156px;\n display: none;\n padding: 4px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: var(--shadow-floating);\n z-index: 30;\n }\n .io-tool-filter.open .io-tool-menu { display: block; }\n .io-tool-menu .mi {\n width: 100%;\n min-height: 32px;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 0 10px;\n border: 0;\n border-radius: var(--r-sm);\n background: transparent;\n color: var(--accent-black);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n }\n .io-tool-menu .mi:hover { background: var(--black-alpha-4); }\n .io-tool-menu .mi.selected { background: var(--heat-12); color: var(--heat); font-weight: 500; }\n .io-tool-menu .mi svg {\n width: 12px;\n height: 12px;\n opacity: 0;\n color: var(--heat);\n }\n .io-tool-menu .mi.selected svg { opacity: 1; }\n\n /* 对话流主体 */\n .io-stream {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 28px 28px 220px; /* 底部留出输入框高度(避免被遮挡) */\n background: var(--background-base);\n }\n @media (max-width: 1100px) { .io-stream { padding: 22px 18px 220px; } }\n .io-stream-inner {\n max-width: 1180px; margin: 0 auto;\n display: flex; flex-direction: column; gap: 32px;\n }\n\n /* 单条对话(气泡式 · 提示词块 + 结果网格) */\n .io-msg { display: flex; flex-direction: column; gap: 14px; }\n .io-msg-prompt {\n display: flex; align-items: flex-start; gap: 12px;\n padding-left: 4px;\n }\n .io-msg-prompt .quote {\n flex-shrink: 0;\n width: 28px; height: 28px;\n border-radius: var(--r-sm);\n background: var(--surface);\n border: 1px solid var(--border-faint);\n color: var(--heat);\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 13px;\n }\n .io-msg-prompt .quote svg { width: 13px; height: 13px; }\n .io-msg-prompt .pt {\n flex: 1; min-width: 0;\n padding-top: 4px;\n }\n .io-msg-prompt .pt-text {\n font-size: 14px; color: var(--accent-black);\n line-height: 1.55;\n word-break: break-word;\n }\n .io-msg-prompt .pt-tags {\n margin-top: 8px;\n display: flex; flex-wrap: wrap; gap: 6px;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n align-items: center;\n }\n .io-msg-prompt .pt-tags .meta-chip {\n padding: 2px 8px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n }\n .io-msg-prompt .pt-tags .sep { color: var(--black-alpha-24); }\n\n /* 结果网格 — 4 张/行 */\n .io-msg-grid {\n display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;\n }\n @media (max-width: 1280px) { .io-msg-grid { grid-template-columns: repeat(3, 1fr); } }\n @media (max-width: 900px) { .io-msg-grid { grid-template-columns: repeat(2, 1fr); } }\n\n .io-cell {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow: hidden;\n cursor: pointer;\n transition: border-color var(--t-base);\n }\n .io-cell:hover { border-color: var(--black-alpha-32); }\n .io-cell.r-1-1 { aspect-ratio: 1 / 1; }\n .io-cell.r-16-9 { aspect-ratio: 16 / 9; }\n .io-cell.r-9-16 { aspect-ratio: 9 / 16; }\n .io-cell.r-4-3 { aspect-ratio: 4 / 3; }\n .io-cell.r-3-4 { aspect-ratio: 3 / 4; }\n .io-cell .ph-frame {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32); letter-spacing: .02em;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .io-cell.gen .ph-frame { animation: io-pulse 1.4s ease-in-out infinite; }\n @keyframes io-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: .55; }\n }\n .io-cell.err {\n border-color: var(--accent-crimson, #c43d3d);\n }\n .io-cell.err .ph-frame {\n color: var(--accent-crimson, #c43d3d);\n background: rgba(196, 61, 61, .05);\n }\n /* 右上 hover 操作组 */\n .io-cell .cell-ops {\n position: absolute; top: 6px; right: 6px;\n display: flex; gap: 4px;\n opacity: 0;\n transition: opacity var(--t-base);\n }\n .io-cell:hover .cell-ops { opacity: 1; }\n .io-cell .cell-ops button {\n width: 26px; height: 26px;\n background: rgba(255, 255, 255, .92);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--accent-black);\n cursor: pointer;\n display: grid; place-items: center;\n backdrop-filter: blur(4px);\n transition: border-color var(--t-base), color var(--t-base);\n }\n .io-cell .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }\n .io-cell .cell-ops button svg { width: 12px; height: 12px; }\n\n /* 更多 · 下拉气泡(删除 / 加入资产库) */\n .io-cell .cell-more-wrap { position: relative; }\n .io-cell .cell-more-menu {\n position: absolute; top: calc(100% + 4px); right: 0;\n min-width: 132px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .io-cell .cell-more-wrap.open .cell-more-menu { display: block; }\n .io-cell .cell-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n backdrop-filter: none !important;\n height: auto !important;\n justify-content: flex-start !important;\n }\n .io-cell .cell-more-menu button:hover {\n background: var(--background-lighter) !important;\n color: var(--heat) !important;\n }\n .io-cell .cell-more-menu button.danger:hover {\n color: var(--accent-crimson) !important;\n background: var(--crimson-bg, #fdebea) !important;\n }\n .io-cell .cell-more-menu button svg { width: 13px !important; height: 13px !important; }\n\n /* 操作行(重新编辑 / 再次生成 / ...) */\n .io-msg-ops {\n display: flex; gap: 8px;\n padding-left: 4px;\n }\n .io-msg-ops button {\n display: inline-flex; align-items: center; gap: 6px;\n height: 30px; padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n font-size: 12.5px;\n color: var(--accent-black);\n font-family: inherit;\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .io-msg-ops button:hover {\n border-color: var(--heat-20); color: var(--heat); background: var(--heat-12);\n }\n .io-msg-ops button.icon {\n width: 30px; padding: 0;\n justify-content: center;\n }\n .io-msg-ops button svg { width: 13px; height: 13px; }\n\n /* 操作行 · 更多气泡(全部加入资产库 / 删除该批结果) */\n .io-msg-ops .msg-more-wrap { position: relative; }\n .io-msg-ops .msg-more-menu {\n position: absolute; bottom: calc(100% + 6px); left: 0;\n min-width: 168px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .io-msg-ops .msg-more-wrap.open .msg-more-menu { display: block; }\n .io-msg-ops .msg-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n height: auto !important;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n text-align: left;\n justify-content: flex-start !important;\n }\n .io-msg-ops .msg-more-menu button:hover {\n background: var(--background-lighter) !important;\n color: var(--heat) !important;\n }\n .io-msg-ops .msg-more-menu button.danger:hover {\n color: var(--accent-crimson) !important;\n background: var(--crimson-bg, #fdebea) !important;\n }\n .io-msg-ops .msg-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }\n\n /* 重复加入资产库 · 确认弹窗 */\n .io-dup-modal-bg {\n position: fixed; inset: 0; z-index: 1200;\n background: rgba(21, 20, 15, .42);\n display: grid; place-items: center;\n padding: 16px;\n }\n .io-dup-modal-bg[hidden] { display: none; }\n .io-dup-modal {\n width: min(420px, 92vw);\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 16px 48px rgba(21, 20, 15, .18);\n overflow: hidden;\n }\n .io-dup-modal .dh {\n display: flex; align-items: center; gap: 12px;\n padding: 18px 20px 14px;\n }\n .io-dup-modal .dh .ic {\n width: 36px; height: 36px; flex-shrink: 0;\n border-radius: var(--r-md);\n background: var(--heat-12); color: var(--heat);\n display: grid; place-items: center;\n }\n .io-dup-modal .dh .ic svg { width: 18px; height: 18px; }\n .io-dup-modal .dh .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }\n .io-dup-modal .dh .ti strong { font-size: 14.5px; color: var(--accent-black); font-weight: 600; }\n .io-dup-modal .dh .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .io-dup-modal .df {\n display: flex; gap: 8px;\n padding: 0 20px 18px;\n justify-content: flex-end;\n }\n .io-dup-modal .df button {\n height: 32px; padding: 0 14px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit; cursor: pointer;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .io-dup-modal .df button:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }\n .io-dup-modal .df button.primary {\n background: var(--heat); border-color: var(--heat); color: var(--accent-white);\n }\n .io-dup-modal .df button.primary:hover { filter: brightness(1.05); color: var(--accent-white); }\n\n /* 空态 */\n .io-empty {\n flex: 1; min-height: 100%;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 16px;\n padding: 40px;\n color: var(--black-alpha-56);\n text-align: center;\n }\n .io-empty .badge {\n font-family: var(--font-mono); font-size: 11px;\n letter-spacing: .08em; color: var(--black-alpha-48);\n text-transform: uppercase;\n }\n .io-empty h2 {\n font-size: 22px; font-weight: 600;\n color: var(--accent-black);\n letter-spacing: -.015em;\n }\n .io-empty p { font-size: 13px; max-width: 460px; line-height: 1.6; }\n .io-empty .ic {\n width: 64px; height: 64px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--heat);\n }\n .io-empty .ic svg { width: 28px; height: 28px; }\n .io-empty .examples {\n margin-top: 10px;\n display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;\n max-width: 720px;\n }\n .io-empty .examples .ex {\n padding: 6px 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n font-size: 12px;\n color: var(--black-alpha-72);\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .io-empty .examples .ex:hover {\n border-color: var(--heat-20); color: var(--heat); background: var(--heat-12);\n }\n\n /* 浮动 \"回到底部\" 按钮 */\n .io-jump-bottom {\n position: absolute;\n bottom: 200px; left: 50%;\n transform: translateX(-50%) translateY(0);\n display: inline-flex; align-items: center; gap: 4px;\n padding: 6px 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n font-size: 12px;\n color: var(--black-alpha-72);\n cursor: pointer;\n box-shadow: 0 4px 16px rgba(0,0,0,.06);\n z-index: 6;\n opacity: 0; pointer-events: none;\n transition: opacity var(--t-base), transform var(--t-base);\n }\n .io-jump-bottom.show {\n opacity: 1; pointer-events: auto;\n transform: translateX(-50%) translateY(-6px);\n }\n .io-jump-bottom:hover { color: var(--heat); border-color: var(--heat-20); }\n .io-jump-bottom svg { width: 12px; height: 12px; }\n\n /* ========== 底部 · fixed 输入栏 ========== */\n .io-input-wrap {\n position: absolute; left: 0; right: 0; bottom: 0;\n padding: 14px 28px 22px;\n background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px);\n z-index: 5;\n }\n @media (max-width: 1100px) { .io-input-wrap { padding: 14px 18px 18px; } }\n .io-input {\n max-width: 1180px; margin: 0 auto;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: 18px;\n padding: 12px 14px 10px;\n display: flex; flex-direction: column; gap: 8px;\n box-shadow: 0 6px 24px rgba(0,0,0,.06);\n transition: border-color var(--t-base);\n }\n .io-input:focus-within { border-color: var(--heat-40); }\n /* 上行 · 参考图 + 加号按钮 (同一 flex 行, 视觉同尺寸) */\n .io-input-top {\n display: flex; align-items: center; gap: 8px;\n flex-wrap: wrap;\n }\n .io-input-top .add-btn {\n flex-shrink: 0;\n width: 64px; height: 64px;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--black-alpha-56);\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .io-input-top .add-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }\n .io-input-top .add-btn svg { width: 22px; height: 22px; }\n /* 中行 · textarea 满宽 */\n .io-input textarea#io-input-text {\n width: 100%;\n border: 0; outline: 0; resize: none;\n background: transparent;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.5;\n color: var(--accent-black);\n min-height: 44px; max-height: 220px;\n padding: 4px 2px;\n }\n .io-input textarea#io-input-text::placeholder { color: var(--black-alpha-48); }\n /* 发送按钮 (放底栏右端) */\n .io-input .send-btn {\n flex-shrink: 0;\n width: 32px; height: 32px;\n background: var(--heat); color: var(--accent-white);\n border: 0; border-radius: var(--r-md);\n cursor: pointer;\n display: grid; place-items: center;\n transition: opacity var(--t-base), filter var(--t-base);\n margin-left: 8px;\n }\n .io-input .send-btn:hover { filter: brightness(1.05); }\n .io-input .send-btn:disabled { opacity: .4; cursor: not-allowed; }\n .io-input .send-btn svg { width: 15px; height: 15px; }\n\n /* 参考图缩略 · 容器扁平化, 让子项直接参与 .io-input-top 的 flex 行 */\n .io-input-refs { display: contents; }\n .io-input-ref {\n position: relative;\n width: 64px; height: 64px;\n border-radius: var(--r-md);\n overflow: hidden;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n flex-shrink: 0;\n }\n .io-input-ref img { width: 100%; height: 100%; object-fit: cover; }\n .io-input-ref .x {\n position: absolute; top: 3px; right: 3px;\n width: 18px; height: 18px;\n background: rgba(0,0,0,.7); color: var(--accent-white);\n border: 0; border-radius: 50%;\n display: grid; place-items: center;\n cursor: pointer;\n }\n .io-input-ref .x svg { width: 10px; height: 10px; }\n\n /* 输入栏底部 · 参数胶囊行 (比例 / 模型 / 张数) */\n .io-input-bottom {\n display: flex; align-items: center; gap: 6px;\n flex-wrap: wrap;\n }\n .io-input-bottom .param {\n position: relative;\n display: inline-flex; align-items: center; gap: 4px;\n height: 26px; padding: 0 9px;\n background: var(--background-lighter);\n border: 1px solid transparent;\n border-radius: var(--r-pill);\n font-size: 11.5px; color: var(--black-alpha-72);\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .io-input-bottom .param[hidden] { display: none; }\n .io-input-bottom .param:hover { background: var(--surface); border-color: var(--border-faint); }\n .io-input-bottom .param.active { background: var(--heat-12); color: var(--heat); }\n .io-input-bottom .param .lbl-mono {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n margin-right: 1px;\n }\n .io-input-bottom .param.active .lbl-mono { color: var(--heat); }\n .io-input-bottom .param svg { width: 10px; height: 10px; opacity: .6; }\n .io-input-bottom .right-meta {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .io-input-bottom .right-meta .val { color: var(--accent-black); }\n\n /* 参数下拉气泡 */\n .io-param-menu {\n position: absolute; bottom: calc(100% + 6px); left: -2px;\n min-width: 140px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.08);\n padding: 4px;\n display: none;\n z-index: 30;\n }\n .io-input-bottom .param.open .io-param-menu { display: block; }\n .io-param-menu .mi {\n display: flex; align-items: center; gap: 8px;\n padding: 7px 10px;\n border-radius: var(--r-sm);\n font-size: 12.5px;\n color: var(--accent-black);\n cursor: pointer;\n }\n .io-param-menu .mi:hover { background: var(--background-lighter); }\n .io-param-menu .mi.selected { color: var(--heat); font-weight: 600; }\n .io-param-menu .mi .mi-sub {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .io-param-menu .mi .mi-check {\n width: 12px; height: 12px; opacity: 0;\n }\n .io-param-menu .mi.selected .mi-check { opacity: 1; }\n\n /* 隐藏 file input */\n #io-file-input { display: none; }\n</style>\n</head>\n<body>\n<div id=\"page\">\n <div class=\"io-app\">\n\n <!-- ===== 左 · 会话列表 ===== -->\n <aside class=\"io-side\">\n <div class=\"io-side-h\">\n <button class=\"back-pill\" type=\"button\" onclick=\"history.length > 1 ? history.back() : location.href='asset-factory.html'\" title=\"返回\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 18l-6-6 6-6\"/></svg>\n <span>返回</span>\n </button>\n <button class=\"fold\" type=\"button\" title=\"折叠侧栏\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M9 3v18\"/></svg>\n </button>\n </div>\n\n <button class=\"io-new-conv\" type=\"button\" id=\"io-new-conv\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z\"/></svg>\n 新对话\n </button>\n\n <div class=\"io-side-sec-h\">默认</div>\n <div class=\"io-conv-list\" style=\"flex: 0 0 auto;\">\n <div class=\"io-conv-item default active\" data-default>\n <div class=\"thumb\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M3 16l5-5 4 4 3-3 6 6\"/></svg>\n </div>\n <span class=\"nm\">默认创作</span>\n </div>\n </div>\n\n <div class=\"io-side-sec-h\">最近</div>\n <div class=\"io-conv-list\" id=\"io-conv-list\">\n <!-- JS 渲染最近会话 -->\n </div>\n </aside>\n\n <!-- ===== 右 · 对话流 ===== -->\n <section class=\"io-main\">\n\n <div class=\"io-toolbar\">\n <button class=\"side-restore-btn\" type=\"button\" id=\"io-side-restore\" hidden title=\"展开会话栏\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M9 3v18\"/></svg>\n 会话\n </button>\n <span class=\"spacer\"></span>\n <button class=\"search-btn\" type=\"button\" title=\"搜索\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </button>\n <div class=\"io-toolbar-search\" id=\"io-toolbar-search\" hidden>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input id=\"io-toolbar-search-input\" type=\"text\" placeholder=\"搜索当前对话结果\">\n <button class=\"clear-search\" type=\"button\" id=\"io-toolbar-search-clear\" title=\"清空搜索\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M18 6 6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"io-tool-filter\" data-filter=\"time\">\n <button class=\"tb-chip\" type=\"button\"><span data-filter-label>时间</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"io-tool-menu\" data-filter-menu></div>\n </div>\n <div class=\"io-tool-filter\" data-filter=\"mode\">\n <button class=\"tb-chip\" type=\"button\"><span data-filter-label>生成模式</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"io-tool-menu\" data-filter-menu></div>\n </div>\n <div class=\"io-tool-filter\" data-filter=\"action\">\n <button class=\"tb-chip\" type=\"button\"><span data-filter-label>操作类型</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"io-tool-menu\" data-filter-menu></div>\n </div>\n </div>\n\n <div class=\"io-stream\" id=\"io-stream\">\n <div class=\"io-stream-inner\" id=\"io-stream-inner\">\n <!-- JS 渲染对话流 / 空态 -->\n </div>\n </div>\n\n <button class=\"io-jump-bottom\" type=\"button\" id=\"io-jump-bottom\">\n 回到底部\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n\n <!-- 底部输入栏 -->\n <div class=\"io-input-wrap\">\n <div class=\"io-input\">\n <!-- 上行 · 参考图 + 加号按钮 (同一行, 64×64 同尺寸) -->\n <div class=\"io-input-top\">\n <div class=\"io-input-refs\" id=\"io-input-refs\"></div>\n <button class=\"add-btn\" type=\"button\" id=\"io-add-btn\" title=\"上传参考图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </button>\n <input type=\"file\" id=\"io-file-input\" accept=\"image/*\" multiple>\n </div>\n\n <!-- 中行 · textarea 满宽 -->\n <textarea id=\"io-input-text\" rows=\"1\" placeholder=\"输入想法、剧本或上传参考,支持 “/” 使用技能, @ 添加主体, 和 Agent 一起创作\"></textarea>\n\n <!-- 参数胶囊行 -->\n <div class=\"io-input-bottom\">\n\n <div class=\"param\" data-param=\"model\" tabindex=\"0\" hidden>\n <span class=\"lbl-mono\">模型</span>\n <span id=\"io-param-model-lbl\">Airshelf v2</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n <div class=\"io-param-menu\" id=\"io-menu-model\"></div>\n </div>\n\n <div class=\"param\" data-param=\"ratio\" tabindex=\"0\">\n <span class=\"lbl-mono\">比例</span>\n <span id=\"io-param-ratio-lbl\">1:1</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n <div class=\"io-param-menu\" id=\"io-menu-ratio\"></div>\n </div>\n\n <div class=\"param\" data-param=\"style\" tabindex=\"0\">\n <span class=\"lbl-mono\">风格</span>\n <span id=\"io-param-style-lbl\">默认</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n <div class=\"io-param-menu\" id=\"io-menu-style\"></div>\n </div>\n\n <div class=\"param\" data-param=\"count\" tabindex=\"0\">\n <span class=\"lbl-mono\">张数</span>\n <span id=\"io-param-count-lbl\">4</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n <div class=\"io-param-menu\" id=\"io-menu-count\"></div>\n </div>\n\n <span class=\"right-meta\">预估 <span class=\"val\" id=\"io-cost-val\">¥0.40</span> · 余额 <span class=\"val\">¥327.40</span></span>\n <button class=\"send-btn\" type=\"button\" id=\"io-send-btn\" disabled title=\"生成\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </button>\n </div>\n </div>\n </div>\n\n </section>\n </div>\n</div>\n\n<!-- ===== 重复加入资产库 · 确认弹窗 ===== -->\n<div class=\"io-dup-modal-bg\" id=\"io-dup-bg\" hidden>\n <div class=\"io-dup-modal\">\n <div class=\"dh\">\n <div class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/></svg>\n </div>\n <div class=\"ti\">\n <strong id=\"io-dup-title\">图片已在资产库</strong>\n <span id=\"io-dup-sub\" class=\"mono\">// 选择处理方式</span>\n </div>\n </div>\n <div class=\"df\">\n <button type=\"button\" data-act=\"cancel\">取消</button>\n <button type=\"button\" data-act=\"dup\">新增副本</button>\n <button type=\"button\" class=\"primary\" data-act=\"overwrite\">覆盖原图</button>\n </div>\n </div>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({\n active: 'asset-factory',\n crumbs: [\n { label: '工作台', href: 'index.html' },\n { label: '图片生成', href: 'asset-factory.html' },\n { label: '图片创作' }\n ]\n});\n</script>\n\n<script>\n/* ============================================================\n 图片创作 · 即梦风格 chat 工作台\n ----------------------------------------------------------\n 持久化:localStorage['fs-io-chat']\n 结构:[{ id, title, messages:[ {id, prompt, model, ratio, style, count,\n refImages:[{id,name}], results:[{id,status,label}], createdAt} ] }]\n ============================================================ */\n(function () {\n 'use strict';\n\n const STORAGE_KEY = 'fs-io-chat';\n const PRICE_PER = 0.10;\n\n const MODELS = [\n { id: 'studio-v2', label: 'Airshelf v2', sub: '通用 · 速度优' },\n { id: 'studio-v2-pro', label: 'Airshelf v2 Pro', sub: '细节 · 商用' },\n { id: 'realistic', label: '写实增强', sub: '商品 · 人像' },\n { id: 'anime', label: '国风动漫', sub: '二次元 · 海报' },\n ];\n const RATIOS = [\n { id: '1:1', label: '1:1', sub: '通用方图' },\n { id: '16:9', label: '16:9', sub: '横屏 · 横幅' },\n { id: '9:16', label: '9:16', sub: '竖屏 · 短视频' },\n { id: '4:3', label: '4:3', sub: '横向标准' },\n { id: '3:4', label: '3:4', sub: '纵向标准' },\n ];\n const STYLES = [\n { id: 'auto', label: '默认' },\n { id: 'realistic', label: '写实' },\n { id: 'cinematic', label: '电影感' },\n { id: 'anime', label: '动漫' },\n { id: 'oil', label: '油画' },\n { id: 'cn-ink', label: '国风水墨' },\n { id: 'cyber', label: '赛博' },\n ];\n const COUNTS = [\n { id: 1, label: '1' },\n { id: 2, label: '2' },\n { id: 4, label: '4' },\n ];\n\n const EXAMPLES = [\n '一只穿着宇航服的橘猫,漂浮在霓虹色星云中,赛博朋克风',\n '极简北欧风格的茶杯,白底,自然柔光,产品摄影',\n '国风水墨海报,主体一只白鹤立于水边,留白构图',\n '电影感都市夜景,街道湿漉漉反射霓虹,4K 高清',\n ];\n\n /* ---------- DOM ---------- */\n const $ = sel => document.querySelector(sel);\n const streamInner = $('#io-stream-inner');\n const stream = $('#io-stream');\n const inputText = $('#io-input-text');\n const sendBtn = $('#io-send-btn');\n const addBtn = $('#io-add-btn');\n const fileInput = $('#io-file-input');\n const inputRefs = $('#io-input-refs');\n const costVal = $('#io-cost-val');\n const convList = $('#io-conv-list');\n const newConvBtn = $('#io-new-conv');\n const jumpBtn = $('#io-jump-bottom');\n const ioApp = $('.io-app');\n const foldBtn = $('.io-side-h .fold');\n const sideRestore = $('#io-side-restore');\n const toolbarSearchBtn = $('.io-toolbar .search-btn');\n const toolbarSearch = $('#io-toolbar-search');\n const toolbarSearchInput = $('#io-toolbar-search-input');\n const toolbarSearchClear = $('#io-toolbar-search-clear');\n\n function esc(s) { return String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c])); }\n function uid() { return 'm-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }\n function relTime(ts) {\n const diff = (Date.now() - ts) / 1000;\n if (diff < 60) return '刚刚';\n if (diff < 3600) return Math.floor(diff / 60) + ' 分钟前';\n if (diff < 86400) return Math.floor(diff / 3600) + ' 小时前';\n if (diff < 86400 * 7) return Math.floor(diff / 86400) + ' 天前';\n const d = new Date(ts), z = n => (n < 10 ? '0' + n : '' + n);\n return d.getFullYear() + '-' + z(d.getMonth() + 1) + '-' + z(d.getDate());\n }\n\n /* ---------- state ---------- */\n const state = {\n convs: [], // 历史会话列表(元数据 id/title/thumb/updatedAt)\n activeConvId: 'default',\n messages: [], // 当前激活会话的对话流(从 convMessages 镜像出来)\n convMessages: { default: [] }, // 所有会话的 messages 按 convId 持久化\n refImages: [],\n toolbar: {\n q: '',\n time: 'all',\n mode: 'all',\n action: 'all',\n },\n param: {\n model: 'studio-v2',\n ratio: '1:1',\n style: 'auto',\n count: 4,\n },\n };\n\n const TOOL_FILTERS = {\n time: [\n { id: 'all', label: '时间' },\n { id: 'today', label: '今天' },\n { id: 'week', label: '近 7 天' },\n ],\n mode: [\n { id: 'all', label: '生成模式' },\n { id: 'studio-v2', label: 'Airshelf v2' },\n { id: 'studio-v2-pro', label: 'v2 Pro' },\n { id: 'realistic', label: '写实增强' },\n { id: 'anime', label: '国风动漫' },\n ],\n action: [\n { id: 'all', label: '操作类型' },\n { id: 'ok', label: '已完成' },\n { id: 'gen', label: '生成中' },\n { id: 'err', label: '失败' },\n ],\n };\n\n function slimMessages(msgs) {\n // dataUrl 体积大,持久化时剥离;只保留元数据\n return (msgs || []).map(m => ({\n ...m,\n refImages: (m.refImages || []).map(r => ({ id: r.id, name: r.name })),\n }));\n }\n\n function loadAll() {\n const fallback = { convs: [], activeConvId: 'default', convMessages: { default: [] } };\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return fallback;\n const data = JSON.parse(raw) || {};\n // 兼容旧结构 { convs, current: { id, messages } } → 迁移到 convMessages\n if (!data.convMessages && data.current) {\n data.convMessages = {};\n data.convMessages[data.current.id || 'default'] = data.current.messages || [];\n data.activeConvId = data.current.id || 'default';\n delete data.current;\n }\n if (!data.convMessages) data.convMessages = { default: [] };\n if (!data.convMessages.default) data.convMessages.default = [];\n if (!Array.isArray(data.convs)) data.convs = [];\n if (!data.activeConvId) data.activeConvId = 'default';\n return data;\n } catch (e) { return fallback; }\n }\n\n function saveAll() {\n try {\n // 当前对话内容同步进字典\n state.convMessages[state.activeConvId] = slimMessages(state.messages);\n const data = {\n convs: state.convs.map(c => ({ id: c.id, title: c.title, thumb: c.thumb, updatedAt: c.updatedAt })),\n activeConvId: state.activeConvId,\n convMessages: state.convMessages,\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(data));\n } catch (e) { /* quota */ }\n }\n\n /* ---------- 任务中心 · 同步图片创作任务到共享 localStorage ---------- */\n const IMG_TASK_KEY = 'fs-image-tasks-image';\n function _loadImgTasks() {\n try { return JSON.parse(localStorage.getItem(IMG_TASK_KEY) || '[]'); }\n catch (e) { return []; }\n }\n function _saveImgTasks(arr) {\n try { localStorage.setItem(IMG_TASK_KEY, JSON.stringify(arr)); } catch (e) {}\n }\n function _msgStatus(msg) {\n if (!msg.results || !msg.results.length) return 'gen';\n if (msg.results.some(r => r.status === 'gen')) return 'gen';\n if (msg.results.every(r => r.status === 'err')) return 'err';\n return 'ok';\n }\n function _timeNow(ts) {\n const d = new Date(ts || Date.now());\n return ('0'+(d.getMonth()+1)).slice(-2) + '.' + ('0'+d.getDate()).slice(-2)\n + ' ' + ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2);\n }\n function syncImageTask(msg) {\n if (!msg || !msg.id) return;\n const arr = _loadImgTasks();\n const promptShort = (msg.prompt || '').trim().replace(/\\s+/g, ' ').slice(0, 24);\n const ratio = msg.ratio || '';\n const taskRec = {\n id: 'img-' + msg.id,\n type: 'image',\n name: '图片创作 · ' + (promptShort || '未命名 prompt'),\n snap: {\n prompt: msg.prompt,\n count: msg.count || (msg.results ? msg.results.length : 1),\n ratio: ratio, style: msg.style, model: msg.model,\n },\n status: _msgStatus(msg),\n time: _timeNow(msg.createdAt),\n createdAt: msg.createdAt || Date.now(),\n };\n const idx = arr.findIndex(t => t.id === taskRec.id);\n if (idx >= 0) arr[idx] = taskRec;\n else arr.unshift(taskRec);\n // 限制最多保留 200 条\n if (arr.length > 200) arr.length = 200;\n _saveImgTasks(arr);\n }\n\n /* ---------- 渲染:左侧会话列表 ---------- */\n function renderSide() {\n document.querySelectorAll('.io-conv-item.default').forEach(d =>\n d.classList.toggle('active', state.activeConvId === 'default'));\n if (!state.convs.length) {\n convList.innerHTML = `<div style=\"padding:14px 12px;font-size:11.5px;color:var(--black-alpha-48);line-height:1.55;\">\n 还没有最近会话<br><span style=\"font-family:var(--font-mono);font-size:10.5px;letter-spacing:.02em;display:inline-block;margin-top:4px\">// NO HISTORY</span>\n </div>`;\n return;\n }\n convList.innerHTML = state.convs.map(c => `\n <div class=\"io-conv-item${state.activeConvId === c.id ? ' active' : ''}\" data-id=\"${c.id}\">\n <div class=\"thumb\"${c.thumb ? ` style=\"background-image:url(${c.thumb})\"` : ''}>\n ${c.thumb ? '' : '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"/><path d=\"M21 15l-5-5-9 9\"/></svg>'}\n </div>\n <span class=\"nm\">${esc(c.title)}</span>\n <button class=\"del\" type=\"button\" data-del=\"${c.id}\" title=\"删除\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </button>\n </div>\n `).join('');\n convList.querySelectorAll('.io-conv-item').forEach(it => {\n it.addEventListener('click', e => {\n if (e.target.closest('[data-del]')) return;\n switchConv(it.dataset.id);\n });\n });\n convList.querySelectorAll('[data-del]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n deleteConv(b.dataset.del);\n });\n });\n }\n\n document.querySelectorAll('.io-conv-item.default').forEach(d => {\n d.addEventListener('click', () => switchConv('default'));\n });\n\n function switchConv(id) {\n if (id === state.activeConvId) return;\n // 先把当前会话内容写回字典(slim),再切换\n state.convMessages[state.activeConvId] = slimMessages(state.messages);\n state.activeConvId = id;\n // 从字典读出目标会话的 messages(没有就空)\n state.messages = (state.convMessages[id] || []).slice();\n saveAll();\n renderSide();\n renderStream();\n setTimeout(() => stream.scrollTo({ top: stream.scrollHeight }), 30);\n }\n\n function deleteConv(id) {\n state.convs = state.convs.filter(c => c.id !== id);\n delete state.convMessages[id];\n if (state.activeConvId === id) {\n state.activeConvId = 'default';\n state.messages = (state.convMessages['default'] || []).slice();\n }\n saveAll();\n renderSide();\n renderStream();\n }\n\n /* ---------- 渲染:中央对话流 ---------- */\n function getVisibleMessages() {\n const t = state.toolbar;\n const q = (t.q || '').trim().toLowerCase();\n const now = Date.now();\n return state.messages.filter(m => {\n if (q) {\n const hay = [\n m.prompt,\n m.ratio,\n modelLabel(m.model),\n styleLabel(m.style),\n ...(m.results || []).map(r => r.label || r.status),\n ].join(' ').toLowerCase();\n if (!hay.includes(q)) return false;\n }\n if (t.time === 'today' && now - (m.createdAt || now) > 86400000) return false;\n if (t.time === 'week' && now - (m.createdAt || now) > 86400000 * 7) return false;\n if (t.mode !== 'all' && m.model !== t.mode) return false;\n if (t.action !== 'all' && !(m.results || []).some(r => r.status === t.action)) return false;\n return true;\n });\n }\n\n function renderStream() {\n if (!state.messages.length) {\n streamInner.innerHTML = `\n <div class=\"io-empty\">\n <div class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z\"/><path d=\"M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z\"/></svg>\n </div>\n <div class=\"badge\">// IMAGE STUDIO</div>\n <h2>开始你的创作</h2>\n <p>描述你想要的画面,AI 会按提示词 + 参考图 + 模型偏好,生成符合电商场景的视觉素材。<br>支持详情图、海报、灵感速写、3D 化等多场景。</p>\n <div class=\"examples\">\n ${EXAMPLES.map(e => `<button class=\"ex\" type=\"button\" data-ex=\"${esc(e)}\">${esc(e)}</button>`).join('')}\n </div>\n </div>`;\n // 示例点击\n streamInner.querySelectorAll('.ex').forEach(b => {\n b.addEventListener('click', () => {\n inputText.value = b.dataset.ex;\n syncSendDisabled();\n inputText.focus();\n autoResize();\n });\n });\n return;\n }\n const visible = getVisibleMessages();\n if (!visible.length) {\n streamInner.innerHTML = `\n <div class=\"io-empty\">\n <div class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </div>\n <div class=\"badge\">// NO MATCH</div>\n <h2>没有符合筛选的结果</h2>\n <p>当前对话里没有匹配的批次,可以清空搜索或切回全部筛选。</p>\n <div class=\"examples\"><button class=\"ex\" type=\"button\" id=\"io-clear-toolbar-filters\">清空筛选</button></div>\n </div>`;\n $('#io-clear-toolbar-filters')?.addEventListener('click', () => {\n state.toolbar = { q: '', time: 'all', mode: 'all', action: 'all' };\n if (toolbarSearchInput) toolbarSearchInput.value = '';\n syncToolbarFilters();\n renderStream();\n });\n return;\n }\n streamInner.innerHTML = visible.map(messageHTML).join('');\n bindMessageEvents();\n }\n\n function rClass(r) { return 'r-' + r.replace(':', '-'); }\n function modelLabel(id) { const m = MODELS.find(x => x.id === id); return m ? m.label : id; }\n function styleLabel(id) { const s = STYLES.find(x => x.id === id); return s ? s.label : id; }\n\n function messageHTML(m) {\n const cellsHTML = m.results.map(r => `\n <div class=\"io-cell ${rClass(m.ratio)} ${r.status}\" data-msg-id=\"${m.id}\" data-cell-id=\"${r.id}\">\n <div class=\"ph-frame\">${r.status === 'gen' ? '生成中…' : (r.status === 'err' ? '失败 · 点重新生成' : esc(r.label))}</div>\n ${r.status === 'ok' ? `\n <div class=\"cell-ops\">\n <button type=\"button\" data-act=\"cell-rerun\" title=\"再次生成\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>\n </button>\n <button type=\"button\" data-act=\"dl\" title=\"下载\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>\n </button>\n <div class=\"cell-more-wrap\">\n <button type=\"button\" data-act=\"more\" title=\"更多\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/></svg>\n </button>\n <div class=\"cell-more-menu\">\n <button type=\"button\" data-act=\"save-lib\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg>加入资产库</button>\n <button type=\"button\" class=\"danger\" data-act=\"cell-del\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>删除</button>\n </div>\n </div>\n </div>\n ` : ''}\n </div>\n `).join('');\n\n const tagsHTML = `\n <span class=\"meta-chip\">${esc(modelLabel(m.model))}</span>\n <span class=\"sep\">|</span>\n <span>${esc(m.ratio)}</span>\n <span class=\"sep\">|</span>\n <span>1K</span>\n ${m.style && m.style !== 'auto' ? `<span class=\"sep\">|</span><span>${esc(styleLabel(m.style))}</span>` : ''}\n <span class=\"sep\">·</span>\n <span>${esc(relTime(m.createdAt))}</span>\n `;\n\n return `<div class=\"io-msg\" data-id=\"${m.id}\">\n <div class=\"io-msg-prompt\">\n <div class=\"quote\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" stroke=\"none\"><path d=\"M3 21V11a8 8 0 0 1 8-8h1v3h-1a5 5 0 0 0-5 5h6v10H3zm12 0V11a8 8 0 0 1 8-8h1v3h-1a5 5 0 0 0-5 5h6v10h-9z\"/></svg>\n </div>\n <div class=\"pt\">\n <div class=\"pt-text\">${esc(m.prompt)}</div>\n <div class=\"pt-tags\">${tagsHTML}</div>\n </div>\n </div>\n <div class=\"io-msg-grid\">${cellsHTML}</div>\n <div class=\"io-msg-ops\">\n <button type=\"button\" data-act=\"edit\" data-id=\"${m.id}\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z\"/></svg>\n 重新编辑\n </button>\n <button type=\"button\" data-act=\"rerun\" data-id=\"${m.id}\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>\n 再次生成\n </button>\n <div class=\"msg-more-wrap\">\n <button type=\"button\" class=\"icon\" data-act=\"msg-more\" data-id=\"${m.id}\" title=\"更多\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/></svg>\n </button>\n <div class=\"msg-more-menu\" role=\"menu\">\n <button type=\"button\" data-act=\"msg-save-all\" data-id=\"${m.id}\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>\n 全部加入资产库\n </button>\n <button type=\"button\" class=\"danger\" data-act=\"msg-del\" data-id=\"${m.id}\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n 删除该批结果\n </button>\n </div>\n </div>\n </div>\n </div>`;\n }\n\n function bindMessageEvents() {\n streamInner.querySelectorAll('[data-act=\"edit\"]').forEach(b => {\n b.addEventListener('click', () => editMessage(b.dataset.id));\n });\n streamInner.querySelectorAll('[data-act=\"rerun\"]').forEach(b => {\n b.addEventListener('click', () => rerunMessage(b.dataset.id));\n });\n // 更多按钮 · 切换批次 menu\n streamInner.querySelectorAll('[data-act=\"msg-more\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.msg-more-wrap');\n const isOpen = wrap.classList.contains('open');\n document.querySelectorAll('.msg-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) wrap.classList.add('open');\n });\n });\n // 删除该批结果\n streamInner.querySelectorAll('[data-act=\"msg-del\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.msg-more-wrap');\n if (wrap) wrap.classList.remove('open');\n state.messages = state.messages.filter(m => m.id !== b.dataset.id);\n saveAll(); renderStream();\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('已删除该批结果');\n });\n });\n // 全部加入资产库\n streamInner.querySelectorAll('[data-act=\"msg-save-all\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.msg-more-wrap');\n if (wrap) wrap.classList.remove('open');\n saveBatchToLibrary(b.dataset.id);\n });\n });\n streamInner.querySelectorAll('[data-act=\"dl\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('已加入下载', '占位 · mock 演示');\n });\n });\n // 更多 → 切换菜单\n streamInner.querySelectorAll('[data-act=\"more\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n const isOpen = wrap.classList.contains('open');\n // 关闭其它所有 menu\n document.querySelectorAll('.cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) wrap.classList.add('open');\n });\n });\n // 加入资产库\n streamInner.querySelectorAll('[data-act=\"save-lib\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const cell = b.closest('.io-cell');\n const wrap = b.closest('.cell-more-wrap');\n if (wrap) wrap.classList.remove('open');\n saveToLibrary(cell.dataset.msgId, cell.dataset.cellId);\n });\n });\n // 单张删除\n streamInner.querySelectorAll('[data-act=\"cell-del\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const cell = b.closest('.io-cell');\n const wrap = b.closest('.cell-more-wrap');\n if (wrap) wrap.classList.remove('open');\n deleteCell(cell.dataset.msgId, cell.dataset.cellId);\n });\n });\n // 单张再次生成\n streamInner.querySelectorAll('[data-act=\"cell-rerun\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const cell = b.closest('.io-cell');\n rerunCell(cell.dataset.msgId, cell.dataset.cellId);\n });\n });\n }\n\n /* ---------- 单张再次生成 ---------- */\n function rerunCell(msgId, cellId) {\n const m = state.messages.find(x => x.id === msgId);\n if (!m) return;\n const r = (m.results || []).find(x => x.id === cellId);\n if (!r) return;\n r.status = 'gen';\n saveAll();\n renderStream();\n setTimeout(() => {\n r.status = Math.random() < 0.06 ? 'err' : 'ok';\n saveAll();\n renderStream();\n syncImageTask(m);\n }, 1100 + Math.random() * 800);\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('已重跑', '该图重新生成中');\n }\n\n /* ---------- 单张删除 / 加入资产库 ---------- */\n function deleteCell(msgId, cellId) {\n const m = state.messages.find(x => x.id === msgId);\n if (!m) return;\n m.results = (m.results || []).filter(r => r.id !== cellId);\n // 如果该 msg 下没有 result 了,顺手删掉整条\n if (!m.results.length) state.messages = state.messages.filter(x => x.id !== msgId);\n saveAll();\n renderStream();\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('已删除');\n }\n\n /* ---------- 资产库 IO ---------- */\n const LIB_KEY = 'fs-library-unclassified';\n function readLib() {\n let list;\n try { list = JSON.parse(localStorage.getItem(LIB_KEY) || '[]'); } catch (e) { list = []; }\n return Array.isArray(list) ? list : [];\n }\n function writeLib(list) {\n try { localStorage.setItem(LIB_KEY, JSON.stringify(list)); } catch (e) {}\n }\n function buildLibEntry(m, cellId) {\n return {\n id: 'lib-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5),\n cellId: cellId,\n name: (m.prompt || '未命名').slice(0, 18),\n prompt: m.prompt || '',\n ratio: m.ratio,\n model: m.model,\n style: m.style,\n source: '图片创作',\n kind: '未分类',\n addedAt: Date.now(),\n };\n }\n\n /* ---------- 重复确认弹窗 ---------- */\n function confirmDup({ count, isBatch }) {\n return new Promise(resolve => {\n const bg = document.getElementById('io-dup-bg');\n document.getElementById('io-dup-title').textContent = isBatch\n ? `${count} 张已在资产库`\n : '该图已在资产库';\n document.getElementById('io-dup-sub').textContent = isBatch\n ? '// 新增副本 = 各自独立 · 覆盖 = 更新时间到顶'\n : '// 新增副本 = 多一份独立条目 · 覆盖 = 更新时间到顶';\n bg.hidden = false;\n const buttons = bg.querySelectorAll('button[data-act]');\n function done(choice) {\n bg.hidden = true;\n buttons.forEach(b => b.onclick = null);\n bg.onclick = null;\n document.removeEventListener('keydown', escHandler);\n resolve(choice);\n }\n function escHandler(e) { if (e.key === 'Escape') done('cancel'); }\n buttons.forEach(b => b.onclick = () => done(b.dataset.act));\n bg.onclick = e => { if (e.target === bg) done('cancel'); };\n document.addEventListener('keydown', escHandler);\n });\n }\n\n /* ---------- 单张加入资产库 ---------- */\n async function saveToLibrary(msgId, cellId) {\n const m = state.messages.find(x => x.id === msgId);\n if (!m) return;\n const r = (m.results || []).find(x => x.id === cellId);\n if (!r) return;\n const list = readLib();\n const dupIdx = list.findIndex(x => x.cellId === cellId);\n if (dupIdx >= 0) {\n const choice = await confirmDup({ count: 1, isBatch: false });\n if (choice === 'cancel') return;\n if (choice === 'overwrite') {\n // 移除旧的,把新的放到最前\n const [old] = list.splice(dupIdx, 1);\n list.unshift({ ...old, addedAt: Date.now(), prompt: m.prompt || old.prompt, ratio: m.ratio, model: m.model, style: m.style });\n writeLib(list);\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('已覆盖原图', '资产库 → 未分类');\n return;\n }\n // dup: 新增副本 — 不去重,继续走 unshift\n }\n list.unshift(buildLibEntry(m, cellId));\n writeLib(list);\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('已加入资产库', '+ 1 张 · 资产库 → 未分类');\n }\n\n /* ---------- 整批加入资产库 ---------- */\n async function saveBatchToLibrary(msgId) {\n const m = state.messages.find(x => x.id === msgId);\n if (!m || !m.results || !m.results.length) {\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('无可加入的结果', '该批次为空');\n return;\n }\n const list = readLib();\n const existing = new Set(list.map(x => x.cellId));\n const dupCells = m.results.filter(r => existing.has(r.id));\n const newCells = m.results.filter(r => !existing.has(r.id));\n\n let dupAction = 'skip'; // 默认仅加入新的\n if (dupCells.length > 0) {\n const choice = await confirmDup({ count: dupCells.length, isBatch: true });\n if (choice === 'cancel') return;\n dupAction = choice; // 'dup' = 新增副本 / 'overwrite' = 覆盖\n }\n\n let added = 0, overwritten = 0;\n // 先处理新增的 cell\n newCells.forEach(r => {\n list.unshift(buildLibEntry(m, r.id));\n added++;\n });\n // 处理重复的\n if (dupAction === 'dup') {\n dupCells.forEach(r => { list.unshift(buildLibEntry(m, r.id)); added++; });\n } else if (dupAction === 'overwrite') {\n dupCells.forEach(r => {\n const idx = list.findIndex(x => x.cellId === r.id);\n if (idx >= 0) {\n const [old] = list.splice(idx, 1);\n list.unshift({ ...old, addedAt: Date.now(), prompt: m.prompt || old.prompt, ratio: m.ratio, model: m.model, style: m.style });\n overwritten++;\n }\n });\n }\n writeLib(list);\n if (typeof Shell !== 'undefined' && Shell.toast) {\n const parts = [];\n if (added > 0) parts.push(`+ ${added} 张`);\n if (overwritten > 0) parts.push(`覆盖 ${overwritten} 张`);\n Shell.toast(added > 0 || overwritten > 0 ? '已加入资产库' : '已取消', parts.length ? parts.join(' · ') + ' · 资产库 → 未分类' : '无变更');\n }\n }\n\n function editMessage(id) {\n const m = state.messages.find(x => x.id === id);\n if (!m) return;\n inputText.value = m.prompt;\n state.param = { model: m.model, ratio: m.ratio, style: m.style, count: m.count };\n syncParamLabels();\n syncSendDisabled();\n autoResize();\n inputText.focus();\n stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' });\n }\n\n function rerunMessage(id) {\n const m = state.messages.find(x => x.id === id);\n if (!m) return;\n const newMsg = createMessage(m.prompt, { model: m.model, ratio: m.ratio, style: m.style, count: m.count, refImages: m.refImages || [] });\n state.messages.push(newMsg);\n saveAll();\n renderStream();\n scheduleResults(newMsg);\n setTimeout(() => stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' }), 30);\n }\n\n /* ---------- 生成 ---------- */\n function createMessage(prompt, opts) {\n const p = opts || {};\n const count = p.count || state.param.count;\n return {\n id: uid(),\n prompt: prompt,\n model: p.model || state.param.model,\n ratio: p.ratio || state.param.ratio,\n style: p.style || state.param.style,\n count: count,\n refImages: p.refImages || state.refImages.slice(),\n results: Array.from({ length: count }, (_, i) => ({\n id: 'r-' + uid(), status: 'gen', label: (p.ratio || state.param.ratio) + ' · #' + (i + 1),\n })),\n createdAt: Date.now(),\n };\n }\n function scheduleResults(msg) {\n msg.results.forEach((r, idx) => {\n setTimeout(() => {\n r.status = Math.random() < 0.06 ? 'err' : 'ok';\n saveAll();\n renderStream();\n syncImageTask(msg);\n }, 1100 + idx * 300 + Math.random() * 600);\n });\n }\n\n function send() {\n const txt = (inputText.value || '').trim();\n if (!txt) return;\n const msg = createMessage(txt);\n state.messages.push(msg);\n inputText.value = '';\n state.refImages = [];\n renderRefs();\n syncSendDisabled();\n autoResize();\n saveAll();\n renderStream();\n scheduleResults(msg);\n syncImageTask(msg);\n setTimeout(() => stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' }), 30);\n }\n\n /* ---------- 输入栏:参考图 ---------- */\n function renderRefs() {\n if (!state.refImages.length) {\n inputRefs.classList.remove('show');\n inputRefs.innerHTML = '';\n return;\n }\n inputRefs.classList.add('show');\n inputRefs.innerHTML = state.refImages.map(r => `\n <div class=\"io-input-ref\" data-id=\"${r.id}\">\n <img src=\"${r.dataUrl}\" alt=\"\">\n <button class=\"x\" type=\"button\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg></button>\n </div>\n `).join('');\n inputRefs.querySelectorAll('.x').forEach(b => {\n b.onclick = e => {\n e.stopPropagation();\n const id = b.closest('.io-input-ref').dataset.id;\n state.refImages = state.refImages.filter(r => r.id !== id);\n renderRefs();\n };\n });\n }\n\n addBtn.addEventListener('click', () => fileInput.click());\n fileInput.addEventListener('change', e => {\n const fs = [...e.target.files].filter(f => f.type.startsWith('image/'));\n const room = 3 - state.refImages.length;\n if (room <= 0) { e.target.value = ''; return; }\n const incoming = fs.slice(0, room);\n let done = 0;\n incoming.forEach(f => {\n const reader = new FileReader();\n reader.onload = ev => {\n state.refImages.push({ id: 'rf-' + uid(), dataUrl: ev.target.result, name: f.name });\n if (++done === incoming.length) renderRefs();\n };\n reader.readAsDataURL(f);\n });\n e.target.value = '';\n });\n\n /* ---------- 输入框自适应高度 ---------- */\n function autoResize() {\n inputText.style.height = 'auto';\n inputText.style.height = Math.min(180, inputText.scrollHeight) + 'px';\n }\n inputText.addEventListener('input', () => {\n syncSendDisabled();\n autoResize();\n });\n function syncSendDisabled() {\n sendBtn.disabled = !(inputText.value || '').trim();\n }\n\n // 回车发送(Shift+Enter 换行)\n inputText.addEventListener('keydown', e => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n if (!sendBtn.disabled) send();\n }\n });\n sendBtn.addEventListener('click', send);\n\n /* ---------- 参数胶囊下拉 ---------- */\n function buildParamMenus() {\n function fill(menuEl, items, key, getLabel) {\n menuEl.innerHTML = items.map(it => {\n const v = it.id;\n return `<div class=\"mi${state.param[key] === v ? ' selected' : ''}\" data-val=\"${v}\">\n <svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg>\n <span>${esc(it.label)}</span>\n ${it.sub ? `<span class=\"mi-sub\">${esc(it.sub)}</span>` : ''}\n </div>`;\n }).join('');\n menuEl.addEventListener('click', e => {\n const mi = e.target.closest('.mi');\n if (!mi) return;\n e.stopPropagation();\n const val = key === 'count' ? parseInt(mi.dataset.val, 10) : mi.dataset.val;\n state.param[key] = val;\n menuEl.parentElement.classList.remove('open');\n syncParamLabels();\n updateCost();\n // 重新渲染当前菜单的 selected 状态\n menuEl.querySelectorAll('.mi').forEach(x => x.classList.toggle('selected', x.dataset.val == String(val)));\n });\n }\n fill($('#io-menu-model'), MODELS, 'model');\n fill($('#io-menu-ratio'), RATIOS, 'ratio');\n fill($('#io-menu-style'), STYLES, 'style');\n fill($('#io-menu-count'), COUNTS, 'count');\n }\n\n function syncParamLabels() {\n $('#io-param-model-lbl').textContent = (MODELS.find(m => m.id === state.param.model) || {}).label || state.param.model;\n $('#io-param-ratio-lbl').textContent = state.param.ratio;\n $('#io-param-style-lbl').textContent = (STYLES.find(s => s.id === state.param.style) || {}).label || state.param.style;\n $('#io-param-count-lbl').textContent = state.param.count;\n }\n function updateCost() {\n costVal.textContent = '¥' + (state.param.count * PRICE_PER).toFixed(2);\n }\n\n // 点击 chip 切换下拉\n document.querySelectorAll('.io-input-bottom .param').forEach(p => {\n p.addEventListener('click', e => {\n // 菜单内 click 已 stop,这里只处理 chip 本体\n if (e.target.closest('.io-param-menu')) return;\n const isOpen = p.classList.contains('open');\n document.querySelectorAll('.io-input-bottom .param.open').forEach(x => x.classList.remove('open'));\n if (!isOpen) p.classList.add('open');\n });\n });\n document.addEventListener('click', e => {\n if (!e.target.closest('.io-input-bottom .param')) {\n document.querySelectorAll('.io-input-bottom .param.open').forEach(x => x.classList.remove('open'));\n }\n if (!e.target.closest('.io-tool-filter')) {\n document.querySelectorAll('.io-tool-filter.open').forEach(x => x.classList.remove('open'));\n }\n if (!e.target.closest('.cell-more-wrap')) {\n document.querySelectorAll('.cell-more-wrap.open').forEach(x => x.classList.remove('open'));\n }\n if (!e.target.closest('.msg-more-wrap')) {\n document.querySelectorAll('.msg-more-wrap.open').forEach(x => x.classList.remove('open'));\n }\n });\n\n /* ---------- 顶部工具栏:折叠 / 搜索 / 筛选 ---------- */\n function setSideCollapsed(collapsed) {\n ioApp?.classList.toggle('side-collapsed', collapsed);\n if (sideRestore) sideRestore.hidden = !collapsed;\n try { localStorage.setItem('fs-io-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}\n }\n foldBtn?.addEventListener('click', () => setSideCollapsed(true));\n sideRestore?.addEventListener('click', () => setSideCollapsed(false));\n\n toolbarSearchBtn?.addEventListener('click', () => {\n const open = toolbarSearch?.hidden;\n if (toolbarSearch) toolbarSearch.hidden = !open;\n toolbarSearchBtn.classList.toggle('active', !!open || !!state.toolbar.q);\n if (open) requestAnimationFrame(() => toolbarSearchInput?.focus());\n });\n toolbarSearchInput?.addEventListener('input', e => {\n state.toolbar.q = e.target.value.trim();\n toolbarSearchBtn?.classList.toggle('active', !!state.toolbar.q || !toolbarSearch?.hidden);\n renderStream();\n });\n toolbarSearchClear?.addEventListener('click', () => {\n state.toolbar.q = '';\n if (toolbarSearchInput) toolbarSearchInput.value = '';\n toolbarSearchBtn?.classList.toggle('active', !toolbarSearch?.hidden);\n renderStream();\n toolbarSearchInput?.focus();\n });\n\n function syncToolbarFilters() {\n document.querySelectorAll('.io-tool-filter').forEach(wrap => {\n const key = wrap.dataset.filter;\n const value = state.toolbar[key] || 'all';\n const items = TOOL_FILTERS[key] || [];\n const selected = items.find(x => x.id === value) || items[0];\n wrap.querySelector('[data-filter-label]').textContent = selected?.label || '';\n wrap.querySelector('.tb-chip')?.classList.toggle('active', value !== 'all');\n const menu = wrap.querySelector('[data-filter-menu]');\n if (!menu) return;\n menu.innerHTML = items.map(it => `\n <button class=\"mi${it.id === value ? ' selected' : ''}\" type=\"button\" data-val=\"${esc(it.id)}\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg>\n <span>${esc(it.label)}</span>\n </button>\n `).join('');\n });\n }\n\n document.querySelectorAll('.io-tool-filter').forEach(wrap => {\n const btn = wrap.querySelector('.tb-chip');\n const menu = wrap.querySelector('[data-filter-menu]');\n btn?.addEventListener('click', e => {\n e.stopPropagation();\n const open = wrap.classList.contains('open');\n document.querySelectorAll('.io-tool-filter.open').forEach(x => x.classList.remove('open'));\n if (!open) wrap.classList.add('open');\n });\n menu?.addEventListener('click', e => {\n const item = e.target.closest('.mi');\n if (!item) return;\n e.stopPropagation();\n state.toolbar[wrap.dataset.filter] = item.dataset.val;\n wrap.classList.remove('open');\n syncToolbarFilters();\n renderStream();\n });\n });\n\n /* ---------- 新对话 ---------- */\n newConvBtn.addEventListener('click', () => {\n // 若 default 上有内容,把它归档到「最近」(新 id + 转存 messages),然后清空 default\n if (state.activeConvId === 'default' && state.messages.length) {\n const first = state.messages[0];\n const title = (first.prompt || '').slice(0, 18) || '未命名对话';\n const newId = 'c-' + uid();\n state.convMessages[newId] = slimMessages(state.messages);\n state.convs.unshift({\n id: newId,\n title,\n thumb: '',\n updatedAt: Date.now(),\n });\n // 限制 20 条 + 同步清掉超额会话的 messages\n if (state.convs.length > 20) {\n const dropped = state.convs.slice(20);\n state.convs = state.convs.slice(0, 20);\n dropped.forEach(c => { delete state.convMessages[c.id]; });\n }\n }\n // 清空 default 并切回 default\n state.convMessages['default'] = [];\n state.messages = [];\n state.activeConvId = 'default';\n saveAll();\n renderSide();\n renderStream();\n inputText.focus();\n });\n\n /* ---------- 浮动\"回到底部\" ---------- */\n stream.addEventListener('scroll', () => {\n const near = stream.scrollTop + stream.clientHeight >= stream.scrollHeight - 120;\n jumpBtn.classList.toggle('show', !near && state.messages.length > 0);\n });\n jumpBtn.addEventListener('click', () => {\n stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' });\n });\n\n /* ---------- 初始化 ---------- */\n (function init() {\n const data = loadAll();\n state.convs = data.convs || [];\n state.convMessages = data.convMessages || { default: [] };\n state.activeConvId = data.activeConvId || 'default';\n state.messages = (state.convMessages[state.activeConvId] || []).slice();\n // URL ?prompt= 预填\n try {\n const q = new URLSearchParams(location.search);\n const seed = q.get('prompt');\n if (seed) inputText.value = decodeURIComponent(seed);\n } catch (e) {}\n buildParamMenus();\n syncParamLabels();\n syncToolbarFilters();\n try { setSideCollapsed(localStorage.getItem('fs-io-side-collapsed') === '1'); } catch (e) {}\n updateCost();\n renderRefs();\n syncSendDisabled();\n autoResize();\n renderSide();\n renderStream();\n setTimeout(() => stream.scrollTo({ top: stream.scrollHeight }), 50);\n })();\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"library": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"library.html\">\n<meta charset=\"utf-8\">\n<title>资产库 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }\n .asset-grid.video-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }\n /* 修复:.asset-grid 的 display:grid 会盖过 [hidden] 的默认 display:none, 导致切 tab 时其它 tab 的卡片仍可见 */\n .asset-grid[hidden] { display: none; }\n\n /* ─── 底部分页 (吸底) ─── */\n .pagination {\n position: sticky;\n bottom: 0;\n z-index: 5;\n display: flex; align-items: center; gap: 16px;\n padding: 14px 28px;\n margin: 20px -28px 0;\n border-top: 1px solid var(--border-faint);\n background: var(--background-base);\n box-shadow: 0 -8px 24px -16px rgba(0, 0, 0, .08);\n font-size: 12.5px;\n color: var(--black-alpha-56);\n }\n .pagination[hidden] { display: none; }\n .pagination .total { font-family: var(--font-mono); letter-spacing: .02em; }\n .pagination .page-size {\n display: inline-flex; align-items: center; gap: 4px;\n height: 30px; padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-family: inherit; font-size: 12.5px;\n color: var(--black-alpha-72);\n transition: border-color var(--t-base), color var(--t-base);\n }\n .pagination .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pagination .page-size svg { width: 10px; height: 10px; opacity: .6; }\n .pagination .pages {\n display: inline-flex; gap: 4px;\n margin-left: auto;\n }\n .pagination .pages button {\n min-width: 30px; height: 30px;\n padding: 0 8px;\n border: 1px solid var(--border-faint);\n background: var(--surface);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-size: 12.5px;\n color: var(--black-alpha-72);\n font-family: inherit;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .pagination .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pagination .pages button.active {\n background: var(--heat);\n color: var(--accent-white);\n border-color: var(--heat);\n font-weight: 600;\n }\n .pagination .pages button:disabled { opacity: .4; cursor: not-allowed; }\n .pagination .pages .ellipsis {\n min-width: 22px; height: 30px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n }\n .pagination .jump {\n display: inline-flex; align-items: center; gap: 6px;\n color: var(--black-alpha-56);\n }\n .pagination .jump input {\n width: 44px; height: 30px;\n border: 1px solid var(--border-faint);\n background: var(--surface);\n border-radius: var(--r-sm);\n text-align: center;\n font-size: 12.5px;\n color: var(--accent-black);\n font-family: inherit;\n outline: none;\n transition: border-color var(--t-base);\n }\n .pagination .jump input:focus { border-color: var(--heat-40); }\n @media (max-width: 1100px) {\n .pagination { margin: 20px -24px 0; padding: 14px 24px; }\n }\n .asset-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; position: relative; }\n .asset-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }\n /* 下载按钮 · hover 卡片显示,与 card-del-btn 并列 · PRD §6.5 中间产物可下载 */\n .asset-card .card-dl-btn {\n position: absolute;\n top: 8px; right: 48px;\n width: 32px; height: 32px;\n background: rgba(255,255,255,.95);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--black-alpha-56);\n cursor: pointer;\n opacity: 0;\n transition: opacity var(--t-base), background var(--t-base), color var(--t-base);\n z-index: 4;\n }\n .asset-card .card-dl-btn svg { width: 14px; height: 14px; }\n .asset-card:hover .card-dl-btn { opacity: 1; }\n .asset-card .card-dl-btn:hover { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }\n body.edit-mode .asset-card .card-dl-btn { opacity: 0 !important; pointer-events: none !important; }\n /* 编辑模式 checkbox */\n .asset-card .card-check {\n position: absolute; top: 10px; left: 10px;\n width: 22px; height: 22px;\n border-radius: 50%;\n background: var(--surface);\n border: 2px solid var(--black-alpha-32);\n display: none;\n place-items: center;\n color: var(--accent-white);\n z-index: 5;\n pointer-events: none;\n }\n .asset-card .card-check svg { width: 11px; height: 11px; opacity: 0; }\n body.edit-mode .asset-card { cursor: pointer; }\n body.edit-mode .asset-card .card-check { display: grid; }\n body.edit-mode .asset-card.selected .card-check {\n background: var(--heat); border-color: var(--heat);\n }\n body.edit-mode .asset-card.selected .card-check svg { opacity: 1; }\n body.edit-mode .asset-card.selected { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; }\n body.edit-mode .asset-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; }\n /* edit-mode 下「管理资产」按钮变成「完成」 */\n .btn.active {\n background: var(--accent-black);\n color: var(--accent-white);\n border-color: var(--accent-black);\n }\n /* bulk-bar (浮动批量操作栏) */\n .bulk-bar {\n position: fixed;\n bottom: 24px; left: 50%;\n transform: translateX(-50%);\n background: var(--accent-black);\n color: var(--accent-white);\n border-radius: var(--r-md);\n padding: 10px 14px 10px 18px;\n display: none;\n align-items: center; gap: 16px;\n box-shadow: 0 8px 24px rgba(0,0,0,.18);\n z-index: 100;\n font-size: 13px;\n }\n body.edit-mode .bulk-bar { display: inline-flex; }\n .bulk-bar .ct { font-family: var(--font-mono); letter-spacing: .02em; }\n .bulk-bar .ct b { color: var(--heat); font-weight: 700; padding: 0 3px; }\n .bulk-bar .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); }\n .bulk-bar button {\n height: 30px; padding: 0 12px;\n background: transparent;\n border: 1px solid rgba(255,255,255,.24);\n border-radius: var(--r-sm);\n color: var(--accent-white);\n font-size: 12.5px;\n font-family: inherit;\n cursor: pointer;\n display: inline-flex; align-items: center; gap: 5px;\n transition: background var(--t-base);\n }\n .bulk-bar button:hover { background: rgba(255,255,255,.08); }\n .bulk-bar button.danger { background: var(--accent-crimson); border-color: var(--accent-crimson); }\n .bulk-bar button.danger:hover { filter: brightness(1.06); }\n .bulk-bar button svg { width: 12px; height: 12px; }\n .bulk-bar .clear-sel { color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer; background: none; border: 0; padding: 4px 6px; }\n .bulk-bar .clear-sel:hover { color: var(--accent-white); }\n /* 移动到 · 弹层菜单 (向上弹) */\n .bulk-bar .move-wrap { position: relative; display: inline-flex; }\n .bulk-bar .move-menu {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n min-width: 160px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 6px;\n box-shadow: 0 8px 24px rgba(0,0,0,.18);\n display: none;\n z-index: 2;\n }\n .bulk-bar .move-menu.show { display: block; }\n .bulk-bar .move-menu .mv-item {\n display: flex; align-items: center; gap: 8px;\n width: 100%; height: 32px; padding: 0 10px;\n background: transparent; border: 0;\n color: var(--accent-black);\n font-size: 13px; font-family: inherit;\n cursor: pointer; border-radius: var(--r-sm);\n text-align: left;\n }\n .bulk-bar .move-menu .mv-item:hover { background: var(--heat-12); color: var(--heat); }\n .bulk-bar .move-menu .mv-item svg { width: 12px; height: 12px; opacity: .7; }\n /* tab 作为拖拽目标 hover 态 */\n .tabs .tab.drag-over {\n background: var(--heat-12);\n color: var(--heat);\n border-radius: var(--r-sm);\n box-shadow: inset 0 0 0 1px var(--heat-40);\n }\n body.edit-mode .asset-card { cursor: grab; }\n body.edit-mode .asset-card.dragging { opacity: .4; }\n .asset-thumb { aspect-ratio: 1; }\n .asset-card.video .asset-thumb { aspect-ratio: 9/16; max-height: 280px; }\n .asset-body { padding: 12px 14px; }\n .asset-name { font-size: 13px; font-weight: 600; color: var(--accent-black); }\n .asset-meta { font-size: 11px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }\n .asset-badge { position: absolute; top: 8px; left: 8px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em; padding: 2px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-56); }\n .asset-card .placeholder { position: relative; }\n\n /* ─── Upload modal ─── */\n .upload-modal {\n max-width: 520px; width: 92%;\n max-height: calc(100vh - 80px);\n display: flex; flex-direction: column;\n }\n .upload-modal .modal-h {\n position: relative;\n flex-shrink: 0;\n }\n .upload-modal .modal-h .ti { flex: 1; min-width: 0; }\n .upload-modal .modal-h .modal-x {\n width: 32px; height: 32px; border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--black-alpha-56); cursor: pointer;\n background: transparent; border: 0; padding: 0;\n flex-shrink: 0;\n transition: background var(--t-base), color var(--t-base);\n }\n .upload-modal .modal-h .modal-x:hover { background: var(--black-alpha-4); color: var(--accent-crimson); }\n .upload-modal .modal-h .modal-x svg { width: 14px; height: 14px; }\n .upload-modal .modal-b {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 20px 24px;\n }\n .upload-modal .modal-f { flex-shrink: 0; }\n .upload-modal .field { margin-bottom: 16px; }\n .upload-modal .field:last-child { margin-bottom: 0; }\n /* 修复:.field 的 display:flex 会盖过 [hidden] 的默认 display:none */\n .upload-modal .modal-b .field[hidden] { display: none; }\n .upload-modal .upload-zone {\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n gap: 8px; padding: 24px; cursor: pointer;\n border: 1px dashed var(--border-faint); border-radius: var(--r-md);\n background: var(--background-lighter); color: var(--black-alpha-56);\n font-size: 13px; transition: border-color .15s, background .15s, color .15s;\n }\n .upload-modal .upload-zone:hover { border-color: var(--heat); background: var(--heat-8); color: var(--heat); }\n .upload-modal .upload-zone:hover .uz-ic { background: var(--heat); color: #fff; border-color: var(--heat); }\n .upload-modal .upload-zone .uz-ic {\n width: 40px; height: 40px; border-radius: var(--r-md);\n border: 1px solid var(--border-faint); background: var(--surface);\n display: grid; place-items: center; color: var(--black-alpha-56);\n transition: background .15s, color .15s, border-color .15s;\n }\n .upload-modal .upload-zone .uz-ic svg { width: 18px; height: 18px; }\n .upload-modal .upload-zone .uz-hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .upload-modal .upload-preview {\n position: relative;\n width: calc((100% - 32px) / 5);\n min-width: 80px; max-width: 110px;\n aspect-ratio: 1; border-radius: var(--r-md);\n overflow: hidden; border: 1px solid var(--border-faint); background: var(--background-lighter);\n }\n .upload-modal .upload-preview.video { aspect-ratio: 9/16; max-height: none; min-width: 80px; max-width: 110px; margin: 0; }\n .upload-modal .upload-preview img,\n .upload-modal .upload-preview video { width: 100%; height: 100%; object-fit: cover; display: block; }\n .upload-modal .upload-preview .preview-x {\n position: absolute; top: 8px; right: 8px;\n width: 24px; height: 24px; border-radius: 999px;\n background: rgba(21, 20, 15, .7); color: #fff;\n display: grid; place-items: center; cursor: pointer; border: 0;\n transition: background .15s, transform .15s;\n }\n .upload-modal .upload-preview .preview-x:hover { background: var(--accent-crimson); transform: scale(1.08); }\n .upload-modal .upload-preview .preview-x svg { width: 12px; height: 12px; }\n .upload-modal .modal-f { align-items: center; }\n .upload-modal .modal-f .modal-meta {\n flex: 1; font-family: var(--font-mono); font-size: 11.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .upload-modal .modal-f .modal-meta .accent { color: var(--heat); font-weight: 600; }\n .upload-modal .btn-primary:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>资产库</h1>\n <div class=\"sub\"><span class=\"mono\">// 跨项目复用 · <span id=\"sub-people\">0</span> 人 · <span id=\"sub-scenes\">0</span> 景 · <span id=\"sub-products\">0</span> 商 · <span id=\"sub-finals\">0</span> 片</span></div>\n </div>\n <div class=\"actions\">\n <button class=\"btn\" type=\"button\" id=\"lib-manage-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m3 7 2 2 4-4\"/><path d=\"m3 17 2 2 4-4\"/><path d=\"M13 6h8\"/><path d=\"M13 12h8\"/><path d=\"M13 18h8\"/></svg>\n <span class=\"lib-manage-label\">管理资产</span>\n </button>\n <button class=\"btn btn-primary\" id=\"open-upload-btn\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg>\n 上传资产\n </button>\n </div>\n</div>\n\n<div class=\"tabs\" id=\"asset-tabs\">\n <div class=\"tab active\" data-tab=\"people\">人物 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-tab=\"scenes\">场景 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-tab=\"products\">商品图 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-tab=\"finals\">成片 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-tab=\"uploads\">我的上传 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-tab=\"unclassified\">未分类 <span class=\"count\">0</span></div>\n</div>\n\n<div class=\"toolbar\">\n <div class=\"search-inline\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input class=\"input\" id=\"search-input\" placeholder=\"搜索资产名称、标签\">\n </div>\n\n <!-- ── 人物 tab 专属 ── -->\n <div class=\"chip-wrap\" data-key=\"gender\" data-tabs=\"people\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">性别</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <div class=\"chip-wrap\" data-key=\"age\" data-tabs=\"people\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">年龄段</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <div class=\"chip-wrap\" data-key=\"role\" data-tabs=\"people\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">角色标签</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n\n <!-- ── 场景 tab 专属 ── -->\n <div class=\"chip-wrap\" data-key=\"sceneType\" data-tabs=\"scenes\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">场景类型</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n\n <!-- ── 商品图 tab 专属 ── -->\n <div class=\"chip-wrap\" data-key=\"product\" data-tabs=\"products\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">关联商品</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n\n <!-- ── 成片 tab 专属 ── -->\n <div class=\"chip-wrap\" data-key=\"project\" data-tabs=\"finals\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">关联项目</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <div class=\"chip-wrap\" data-key=\"duration\" data-tabs=\"finals\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">时长</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n\n <!-- ── 我的上传 tab 专属 ── -->\n <div class=\"chip-wrap\" data-key=\"kind\" data-tabs=\"uploads\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">资产类型</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n\n <!-- ── 共用(人物 / 场景 / 商品图 / 我的上传)── -->\n <div class=\"chip-wrap\" data-key=\"source\" data-tabs=\"people scenes products uploads\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">来源</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n\n <button class=\"clear-filters\" id=\"clear-filters\" type=\"button\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n 清空筛选\n </button>\n <span class=\"spacer\"></span>\n\n <!-- ── 排序(所有 tab 共用)── -->\n <div class=\"chip-wrap\" data-key=\"sort\" data-tabs=\"all\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">最近使用</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu align-right\"></div>\n </div>\n</div>\n\n<div class=\"result-meta\" id=\"result-meta\">// 显示 <span class=\"count\">0</span> / 0 个资产</div>\n\n<!-- ============ 人物 (8) ============ -->\n<div class=\"asset-grid\" data-tab=\"people\" id=\"grid-people\">\n <div class=\"asset-card\" data-name=\"林夕\" data-gender=\"女\" data-age=\"青年\" data-role=\"都市白领\" data-source=\"AI 生成\" data-used=\"4\" data-added=\"20260513\" onclick=\"Shell.toast('查看资产', '林夕')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">林夕 · 都市白领</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">林夕</div>\n <div class=\"asset-meta\">女 · 青年 · 都市白领 · 用过 4 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"阿楠\" data-gender=\"女\" data-age=\"青年\" data-role=\"都市白领\" data-source=\"AI 生成\" data-used=\"2\" data-added=\"20260507\" onclick=\"Shell.toast('查看资产', '阿楠')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">阿楠 · 同事女</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">阿楠</div>\n <div class=\"asset-meta\">女 · 青年 · 都市白领 · 用过 2 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"小七\" data-gender=\"女\" data-age=\"青年\" data-role=\"学生\" data-source=\"AI 生成\" data-used=\"3\" data-added=\"20260512\" onclick=\"Shell.toast('查看资产', '小七')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">小七 · 学生女</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">小七</div>\n <div class=\"asset-meta\">女 · 青年 · 学生 · 用过 3 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"阿杰\" data-gender=\"男\" data-age=\"青年\" data-role=\"都市白领\" data-source=\"AI 生成\" data-used=\"2\" data-added=\"20260428\" onclick=\"Shell.toast('查看资产', '阿杰')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">阿杰 · 通勤男</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">阿杰</div>\n <div class=\"asset-meta\">男 · 青年 · 都市白领 · 用过 2 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"妈妈 · 王姐\" data-gender=\"女\" data-age=\"中年\" data-role=\"居家\" data-source=\"手动上传\" data-triview=\"0\" data-used=\"1\" data-added=\"20260415\" onclick=\"Shell.toast('查看资产', '王姐')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\">\n <span class=\"tri-missing-badge\" tabindex=\"0\" role=\"button\" aria-label=\"缺三视图,查看说明\">\n <span class=\"ico\" aria-hidden=\"true\"></span>\n <span class=\"lbl-mono\">缺三视图</span>\n <span class=\"tri-missing-pop\" role=\"tooltip\">\n <span class=\"pop-h\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01\"/><path d=\"M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/></svg>\n MISSING TRI-VIEW\n </span>\n <span class=\"pop-body\">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>\n <span class=\"pop-tip\">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>\n </span>\n </span>\n <span class=\"ph-frame\">妈妈 · 居家</span>\n </div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">妈妈 · 王姐</div>\n <div class=\"asset-meta\">女 · 中年 · 居家 · 用过 1 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"阿强\" data-gender=\"男\" data-age=\"青年\" data-role=\"健身\" data-source=\"AI 生成\" data-used=\"2\" data-added=\"20260508\" onclick=\"Shell.toast('查看资产', '阿强')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">阿强 · 健身男</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">阿强</div>\n <div class=\"asset-meta\">男 · 青年 · 健身 · 用过 2 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"小苏\" data-gender=\"女\" data-age=\"青年\" data-role=\"文艺\" data-source=\"AI 生成\" data-used=\"1\" data-added=\"20260420\" onclick=\"Shell.toast('查看资产', '小苏')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">小苏 · 文艺女</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">小苏</div>\n <div class=\"asset-meta\">女 · 青年 · 文艺 · 用过 1 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"闺蜜组合\" data-gender=\"女\" data-age=\"青年\" data-role=\"都市白领\" data-source=\"AI 生成\" data-used=\"1\" data-added=\"20260511\" onclick=\"Shell.toast('查看资产', '闺蜜组合')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">闺蜜组合 · 双人</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">闺蜜组合</div>\n <div class=\"asset-meta\">女 · 青年 · 都市白领 · 用过 1 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"豆豆\" data-gender=\"女\" data-age=\"幼年\" data-role=\"居家\" data-source=\"AI 生成\" data-used=\"2\" data-added=\"20260509\" onclick=\"Shell.toast('查看资产', '豆豆')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">豆豆 · 幼儿</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">豆豆</div>\n <div class=\"asset-meta\">女 · 幼年 · 居家 · 用过 2 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"小宇\" data-gender=\"男\" data-age=\"少年\" data-role=\"学生\" data-source=\"AI 生成\" data-used=\"1\" data-added=\"20260502\" onclick=\"Shell.toast('查看资产', '小宇')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">小宇 · 中学生</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">小宇</div>\n <div class=\"asset-meta\">男 · 少年 · 学生 · 用过 1 次</div>\n </div>\n </div>\n <div class=\"asset-card\" data-name=\"李爷爷\" data-gender=\"男\" data-age=\"老年\" data-role=\"居家\" data-source=\"手动上传\" data-triview=\"0\" data-used=\"1\" data-added=\"20260418\" onclick=\"Shell.toast('查看资产', '李爷爷')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\">\n <span class=\"tri-missing-badge\" tabindex=\"0\" role=\"button\" aria-label=\"缺三视图,查看说明\">\n <span class=\"ico\" aria-hidden=\"true\"></span>\n <span class=\"lbl-mono\">缺三视图</span>\n <span class=\"tri-missing-pop\" role=\"tooltip\">\n <span class=\"pop-h\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01\"/><path d=\"M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/></svg>\n MISSING TRI-VIEW\n </span>\n <span class=\"pop-body\">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>\n <span class=\"pop-tip\">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>\n </span>\n </span>\n <span class=\"ph-frame\">李爷爷 · 居家</span>\n </div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">李爷爷</div>\n <div class=\"asset-meta\">男 · 老年 · 居家 · 用过 1 次</div>\n </div>\n </div>\n</div>\n\n<!-- ============ 场景 (12) ============ -->\n<div class=\"asset-grid\" data-tab=\"scenes\" id=\"grid-scenes\" hidden>\n <div class=\"asset-card\" data-name=\"卧室·暖光\" data-scene-type=\"卧室\" data-source=\"AI 生成\" data-used=\"6\" data-added=\"20260513\" onclick=\"Shell.toast('查看资产', '卧室·暖光')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">卧室 · 暖光</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">卧室·暖光</div><div class=\"asset-meta\">卧室 · AI 生成 · 用过 6 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"卧室·冷调\" data-scene-type=\"卧室\" data-source=\"AI 生成\" data-used=\"3\" data-added=\"20260507\" onclick=\"Shell.toast('查看资产', '卧室·冷调')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">卧室 · 冷调</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">卧室·冷调</div><div class=\"asset-meta\">卧室 · AI 生成 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"浴室·梳妆台\" data-scene-type=\"浴室\" data-source=\"AI 生成\" data-used=\"4\" data-added=\"20260510\" onclick=\"Shell.toast('查看资产', '浴室·梳妆台')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">浴室 · 梳妆台</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">浴室·梳妆台</div><div class=\"asset-meta\">浴室 · AI 生成 · 用过 4 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"客厅·北欧\" data-scene-type=\"客厅\" data-source=\"AI 生成\" data-used=\"5\" data-added=\"20260512\" onclick=\"Shell.toast('查看资产', '客厅·北欧')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">客厅 · 北欧</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">客厅·北欧</div><div class=\"asset-meta\">客厅 · AI 生成 · 用过 5 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"客厅·中古\" data-scene-type=\"客厅\" data-source=\"手动上传\" data-used=\"1\" data-added=\"20260418\" onclick=\"Shell.toast('查看资产', '客厅·中古')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">客厅 · 中古</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">客厅·中古</div><div class=\"asset-meta\">客厅 · 手动上传 · 用过 1 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"厨房·中岛\" data-scene-type=\"厨房\" data-source=\"AI 生成\" data-used=\"3\" data-added=\"20260509\" onclick=\"Shell.toast('查看资产', '厨房·中岛')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">厨房 · 中岛</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">厨房·中岛</div><div class=\"asset-meta\">厨房 · AI 生成 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"办公室·开放\" data-scene-type=\"办公室\" data-source=\"AI 生成\" data-used=\"2\" data-added=\"20260506\" onclick=\"Shell.toast('查看资产', '办公室·开放')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">办公室 · 开放</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">办公室·开放</div><div class=\"asset-meta\">办公室 · AI 生成 · 用过 2 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"办公室·会议室\" data-scene-type=\"办公室\" data-source=\"AI 生成\" data-used=\"1\" data-added=\"20260425\" onclick=\"Shell.toast('查看资产', '办公室·会议室')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">办公室 · 会议室</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">办公室·会议室</div><div class=\"asset-meta\">办公室 · AI 生成 · 用过 1 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"咖啡店·窗边\" data-scene-type=\"咖啡店\" data-source=\"AI 生成\" data-used=\"4\" data-added=\"20260511\" onclick=\"Shell.toast('查看资产', '咖啡店·窗边')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">咖啡店 · 窗边</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">咖啡店·窗边</div><div class=\"asset-meta\">咖啡店 · AI 生成 · 用过 4 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"街景·夜\" data-scene-type=\"街景\" data-source=\"AI 生成\" data-used=\"2\" data-added=\"20260430\" onclick=\"Shell.toast('查看资产', '街景·夜')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">街景 · 夜</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">街景·夜</div><div class=\"asset-meta\">街景 · AI 生成 · 用过 2 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"健身房·器械\" data-scene-type=\"健身房\" data-source=\"AI 生成\" data-used=\"3\" data-added=\"20260508\" onclick=\"Shell.toast('查看资产', '健身房·器械')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">健身房 · 器械</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">健身房·器械</div><div class=\"asset-meta\">健身房 · AI 生成 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"街景·日\" data-scene-type=\"街景\" data-source=\"手动上传\" data-used=\"1\" data-added=\"20260422\" onclick=\"Shell.toast('查看资产', '街景·日')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">街景 · 日</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">街景·日</div><div class=\"asset-meta\">街景 · 手动上传 · 用过 1 次</div></div>\n </div>\n</div>\n\n<!-- ============ 商品图 (12) ============ -->\n<div class=\"asset-grid\" data-tab=\"products\" id=\"grid-products\" hidden>\n <div class=\"asset-card\" data-name=\"补水面膜 · 主图\" data-product=\"透真补水面膜\" data-source=\"商品库引用\" data-used=\"5\" data-added=\"20260513\" onclick=\"Shell.toast('查看资产', '补水面膜 主图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">补水面膜 · 主图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">补水面膜 · 主图</div><div class=\"asset-meta\">透真补水面膜 · 库引用 · 用过 5 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"补水面膜 · AI 优化\" data-product=\"透真补水面膜\" data-source=\"AI 优化\" data-used=\"3\" data-added=\"20260513\" onclick=\"Shell.toast('查看资产', '补水面膜 AI 优化')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">补水面膜 · AI 优化</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">补水面膜 · AI 优化</div><div class=\"asset-meta\">透真补水面膜 · AI 优化 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"蓝牙耳机 · 主图\" data-product=\"南卡 Lite Pro\" data-source=\"商品库引用\" data-used=\"4\" data-added=\"20260507\" onclick=\"Shell.toast('查看资产', '蓝牙耳机 主图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">蓝牙耳机 · 主图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">蓝牙耳机 · 主图</div><div class=\"asset-meta\">南卡 Lite Pro · 库引用 · 用过 4 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"蓝牙耳机 · 场景图\" data-product=\"南卡 Lite Pro\" data-source=\"手动上传\" data-used=\"1\" data-added=\"20260507\" onclick=\"Shell.toast('查看资产', '蓝牙耳机 场景')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">蓝牙耳机 · 场景</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">蓝牙耳机 · 场景图</div><div class=\"asset-meta\">南卡 Lite Pro · 手动上传 · 用过 1 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"速食面 · 主图\" data-product=\"滋啦速食\" data-source=\"商品库引用\" data-used=\"3\" data-added=\"20260512\" onclick=\"Shell.toast('查看资产', '速食面 主图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">速食面 · 主图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">速食面 · 主图</div><div class=\"asset-meta\">滋啦速食 · 库引用 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"速食面 · 加汤\" data-product=\"滋啦速食\" data-source=\"AI 优化\" data-used=\"2\" data-added=\"20260512\" onclick=\"Shell.toast('查看资产', '速食面 加汤')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">速食面 · 加汤</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">速食面 · 加汤</div><div class=\"asset-meta\">滋啦速食 · AI 优化 · 用过 2 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"防晒霜 · 主图\" data-product=\"透真防晒霜\" data-source=\"商品库引用\" data-used=\"4\" data-added=\"20260510\" onclick=\"Shell.toast('查看资产', '防晒霜 主图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">防晒霜 · 主图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">防晒霜 · 主图</div><div class=\"asset-meta\">透真防晒霜 · 库引用 · 用过 4 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"防晒霜 · AI 优化\" data-product=\"透真防晒霜\" data-source=\"AI 优化\" data-used=\"3\" data-added=\"20260510\" onclick=\"Shell.toast('查看资产', '防晒霜 优化')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">防晒霜 · AI 优化</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">防晒霜 · AI 优化</div><div class=\"asset-meta\">透真防晒霜 · AI 优化 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"咖啡冻干 · 主图\" data-product=\"三顿半同款\" data-source=\"商品库引用\" data-used=\"3\" data-added=\"20260509\" onclick=\"Shell.toast('查看资产', '咖啡冻干 主图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">咖啡冻干 · 主图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">咖啡冻干 · 主图</div><div class=\"asset-meta\">三顿半同款 · 库引用 · 用过 3 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"咖啡冻干 · 24 颗\" data-product=\"三顿半同款\" data-source=\"商品库引用\" data-used=\"2\" data-added=\"20260509\" onclick=\"Shell.toast('查看资产', '咖啡冻干 24 颗')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">咖啡冻干 · 24 颗</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">咖啡冻干 · 24 颗</div><div class=\"asset-meta\">三顿半同款 · 库引用 · 用过 2 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"空气炸锅 · 主图\" data-product=\"小熊 4L 空气炸锅\" data-source=\"商品库引用\" data-used=\"2\" data-added=\"20260504\" onclick=\"Shell.toast('查看资产', '空气炸锅 主图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">空气炸锅 · 主图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">空气炸锅 · 主图</div><div class=\"asset-meta\">小熊 4L · 库引用 · 用过 2 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"瑜伽裤 · 模特图\" data-product=\"露露同款瑜伽裤\" data-source=\"手动上传\" data-used=\"3\" data-added=\"20260506\" onclick=\"Shell.toast('查看资产', '瑜伽裤 模特')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">瑜伽裤 · 模特</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">瑜伽裤 · 模特图</div><div class=\"asset-meta\">露露同款瑜伽裤 · 手动上传 · 用过 3 次</div></div>\n </div>\n</div>\n\n<!-- ============ 成片 (8) ============ -->\n<div class=\"asset-grid video-grid\" data-tab=\"finals\" id=\"grid-finals\" hidden>\n <div class=\"asset-card video\" data-name=\"蓝牙耳机 · 开箱测评\" data-project=\"蓝牙耳机 · 开箱测评\" data-duration=\"60s\" data-used=\"3\" data-added=\"20260507\" onclick=\"Shell.toast('打开成片', '蓝牙耳机 · 开箱测评')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 60s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">蓝牙耳机 · 开箱测评</div><div class=\"asset-meta\">南卡 Lite Pro · 60s · 5 月 7 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"瑜伽裤 · 通勤穿搭\" data-project=\"瑜伽裤 · 通勤穿搭\" data-duration=\"45s\" data-used=\"2\" data-added=\"20260506\" onclick=\"Shell.toast('打开成片', '瑜伽裤 · 通勤穿搭')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 45s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">瑜伽裤 · 通勤穿搭</div><div class=\"asset-meta\">露露同款 · 45s · 5 月 6 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"空气炸锅 · 小户型\" data-project=\"空气炸锅 · 小户型\" data-duration=\"30s\" data-used=\"2\" data-added=\"20260504\" onclick=\"Shell.toast('打开成片', '空气炸锅 · 小户型')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 30s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">空气炸锅 · 小户型</div><div class=\"asset-meta\">小熊 4L · 30s · 5 月 4 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"补水面膜 · 痛点种草 v1\" data-project=\"补水面膜 · 痛点种草 v1\" data-duration=\"60s\" data-used=\"2\" data-added=\"20260428\" onclick=\"Shell.toast('打开成片', '补水面膜 v1')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 60s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">补水面膜 · 痛点种草 v1</div><div class=\"asset-meta\">透真补水面膜 · 60s · 4 月 28 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"防晒霜 · 通勤对比\" data-project=\"防晒霜 · 通勤对比\" data-duration=\"60s\" data-used=\"1\" data-added=\"20260425\" onclick=\"Shell.toast('打开成片', '防晒霜 · 通勤对比')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 60s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">防晒霜 · 通勤对比</div><div class=\"asset-meta\">透真防晒霜 · 60s · 4 月 25 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"速食面 · 加班治愈\" data-project=\"速食面 · 加班治愈\" data-duration=\"30s\" data-used=\"1\" data-added=\"20260420\" onclick=\"Shell.toast('打开成片', '速食面 · 加班治愈')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 30s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">速食面 · 加班治愈</div><div class=\"asset-meta\">滋啦速食 · 30s · 4 月 20 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"咖啡 · 早八剧情\" data-project=\"咖啡 · 早八剧情\" data-duration=\"45s\" data-used=\"2\" data-added=\"20260418\" onclick=\"Shell.toast('打开成片', '咖啡 · 早八剧情')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 45s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">咖啡 · 早八剧情</div><div class=\"asset-meta\">三顿半同款 · 45s · 4 月 18 日</div></div>\n </div>\n <div class=\"asset-card video\" data-name=\"收纳 · 北欧\" data-project=\"收纳 · 北欧\" data-duration=\"15s\" data-used=\"1\" data-added=\"20260410\" onclick=\"Shell.toast('打开成片', '收纳 · 北欧')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">9:16 · 15s</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">收纳 · 北欧</div><div class=\"asset-meta\">家居好物 · 15s · 4 月 10 日</div></div>\n </div>\n</div>\n\n<!-- ============ 我的上传 (3) ============ -->\n<div class=\"asset-grid\" data-tab=\"uploads\" id=\"grid-uploads\" hidden>\n <div class=\"asset-card\" data-name=\"林夕 · 主播照\" data-kind=\"人物\" data-source=\"手动上传\" data-used=\"4\" data-added=\"20260513\" onclick=\"Shell.toast('查看资产', '林夕 · 主播照')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">林夕 · 主播照</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">林夕 · 主播照</div><div class=\"asset-meta\">人物 · 手动上传 · 用过 4 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"卧室 · 实拍\" data-kind=\"场景\" data-source=\"手动上传\" data-used=\"2\" data-added=\"20260510\" onclick=\"Shell.toast('查看资产', '卧室 · 实拍')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">卧室 · 实拍</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">卧室 · 实拍</div><div class=\"asset-meta\">场景 · 手动上传 · 用过 2 次</div></div>\n </div>\n <div class=\"asset-card\" data-name=\"防晒霜 · 官方图\" data-kind=\"商品\" data-source=\"手动上传\" data-used=\"3\" data-added=\"20260507\" onclick=\"Shell.toast('查看资产', '防晒霜 · 官方图')\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">防晒霜 · 官方图</span></div>\n <div class=\"asset-body\"><div class=\"asset-name\">防晒霜 · 官方图</div><div class=\"asset-meta\">商品 · 手动上传 · 用过 3 次</div></div>\n </div>\n</div>\n\n<!-- ============ 未分类(由图片优化\"加入资产库\"持久化进来) ============ -->\n<div class=\"asset-grid\" data-tab=\"unclassified\" id=\"grid-unclassified\" hidden></div>\n\n<div class=\"empty-state\" id=\"empty\">\n <div class=\"ic-empty\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </div>\n <h3>没有匹配的资产</h3>\n <p>// 试试切换 tab 或修改搜索词</p>\n</div>\n\n<!-- ============ 分页 (吸底) ============ -->\n<div class=\"pagination\" id=\"pagination\" hidden>\n <span class=\"total\">共 <b id=\"page-total\">0</b> 条</span>\n <button class=\"page-size\" type=\"button\" id=\"page-size-btn\" title=\"切换每页条数\">\n <span id=\"page-size-label\">12 条/页</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <span class=\"pages\" id=\"page-list\"></span>\n <span class=\"jump\">跳至 <input type=\"number\" min=\"1\" value=\"1\" id=\"page-jump\"> 页</span>\n</div>\n\n<!-- ============ 上传资产 Modal ============ -->\n<div class=\"modal-bg\" id=\"upload-modal-bg\" onclick=\"if(event.target===this)Shell.closeModal('upload-modal-bg')\">\n <div class=\"modal upload-modal\">\n <span class=\"corner-tr\"></span>\n <span class=\"corner-bl\"></span>\n\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg>\n </div>\n <div class=\"ti\">上传资产<span>// 跨项目共享 · 不消耗 token</span></div>\n <button class=\"modal-x\" type=\"button\" onclick=\"Shell.closeModal('upload-modal-bg')\" aria-label=\"关闭\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n\n <div class=\"modal-b\">\n <div class=\"field\">\n <label class=\"field-label\">资产类型<span class=\"req\">*</span></label>\n <select class=\"select\" id=\"upload-kind\">\n <option value=\"people\">人物</option>\n <option value=\"scenes\">场景</option>\n <option value=\"products\">商品图</option>\n <option value=\"finals\">成片(视频)</option>\n </select>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\">资产文件<span class=\"req\">*</span></label>\n <input type=\"file\" id=\"upload-file\" accept=\"image/*\" hidden>\n <div class=\"upload-zone\" id=\"upload-zone\">\n <span class=\"uz-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg>\n </span>\n <span id=\"upload-zone-text\">点击或拖拽上传图片</span>\n <span class=\"uz-hint\" id=\"upload-zone-hint\">// JPG / PNG / WEBP · 单文件</span>\n </div>\n <div class=\"upload-preview\" id=\"upload-preview\" hidden>\n <img id=\"upload-preview-img\" alt=\"预览\">\n <video id=\"upload-preview-video\" controls hidden></video>\n <button class=\"preview-x\" type=\"button\" id=\"upload-preview-x\" aria-label=\"移除\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n </button>\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\">资产名称<span class=\"req\">*</span></label>\n <input class=\"input\" id=\"upload-name\" placeholder=\"例: 林夕 · 都市白领\">\n </div>\n\n <!-- 人物 字段 -->\n <div class=\"field\" data-fields=\"people\">\n <label class=\"field-label\">性别</label>\n <select class=\"select\" id=\"upload-gender\">\n <option value=\"女\">女</option>\n <option value=\"男\">男</option>\n </select>\n </div>\n <div class=\"field\" data-fields=\"people\">\n <label class=\"field-label\">年龄段</label>\n <select class=\"select\" id=\"upload-age\">\n <option value=\"幼年\">幼年</option>\n <option value=\"少年\">少年</option>\n <option value=\"青年\" selected>青年</option>\n <option value=\"中年\">中年</option>\n <option value=\"老年\">老年</option>\n </select>\n </div>\n <div class=\"field\" data-fields=\"people\">\n <label class=\"field-label\">角色标签</label>\n <input class=\"input\" id=\"upload-role\" placeholder=\"例: 都市白领 / 学生 / 居家\">\n </div>\n\n <!-- 场景 字段 -->\n <div class=\"field\" data-fields=\"scenes\">\n <label class=\"field-label\">场景类型<span class=\"req\">*</span></label>\n <input class=\"input\" id=\"upload-scene-type\" placeholder=\"例: 卧室 / 客厅 / 办公室\">\n </div>\n\n <!-- 商品图 字段 -->\n <div class=\"field\" data-fields=\"products\">\n <label class=\"field-label\">关联商品<span class=\"req\">*</span></label>\n <select class=\"select\" id=\"upload-product\">\n <option value=\"\">— 选择商品 —</option>\n <option>透真补水面膜</option>\n <option>南卡 Lite Pro</option>\n <option>滋啦速食</option>\n <option>透真防晒霜</option>\n <option>三顿半同款</option>\n <option>小熊 4L 空气炸锅</option>\n <option>露露同款瑜伽裤</option>\n </select>\n </div>\n\n <!-- 成片 字段 -->\n <div class=\"field\" data-fields=\"finals\">\n <label class=\"field-label\">关联项目</label>\n <input class=\"input\" id=\"upload-project\" placeholder=\"例: 蓝牙耳机 · 开箱测评\">\n </div>\n <div class=\"field\" data-fields=\"finals\">\n <label class=\"field-label\">时长</label>\n <select class=\"select\" id=\"upload-duration\">\n <option value=\"15s\">15 秒</option>\n <option value=\"30s\">30 秒</option>\n <option value=\"45s\">45 秒</option>\n <option value=\"60s\" selected>60 秒</option>\n </select>\n </div>\n </div>\n\n <div class=\"modal-f\">\n <span class=\"modal-meta\">// 跨项目共享 · <span class=\"accent\">不消耗 token</span></span>\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('upload-modal-bg')\">取消</button>\n <button class=\"btn btn-primary\" id=\"upload-submit\" type=\"button\" disabled>\n <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 12l5 5L20 6\"/></svg>\n 上传到资产库\n </button>\n </div>\n </div>\n</div>\n\n</div>\n\n<!-- ===== 删除确认 modal ===== -->\n<div class=\"modal-bg\" id=\"del-confirm-bg\">\n <div class=\"modal\" role=\"dialog\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </div>\n <div class=\"ti\">确认删除资产<span>// CONFIRM DELETE</span></div>\n </div>\n <div class=\"modal-b\" id=\"del-confirm-body\">即将删除该资产。</div>\n <div class=\"modal-f\" id=\"del-confirm-foot\">\n <button class=\"btn\" type=\"button\" id=\"del-confirm-cancel\">取消</button>\n <button class=\"btn\" type=\"button\" id=\"del-confirm-ok\" style=\"background:var(--accent-crimson,#c43d3d);color:var(--accent-white);border-color:var(--accent-crimson,#c43d3d)\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/></svg>\n 确认删除\n </button>\n </div>\n </div>\n</div>\n\n<!-- ===== bulk-bar ===== -->\n<div class=\"bulk-bar\" id=\"bulk-bar\">\n <span class=\"ct\">已选 <b id=\"bulk-count\">0</b> 项</span>\n <button class=\"clear-sel\" type=\"button\" id=\"bulk-clear\">清空</button>\n <span class=\"sep\"></span>\n <button class=\"danger\" type=\"button\" id=\"bulk-del\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n 删除所选\n </button>\n <div class=\"move-wrap\">\n <button type=\"button\" id=\"bulk-move\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 18l6-6-6-6\"/><path d=\"M3 12h12\"/></svg>\n 移动到\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"margin-left:2px\"><path d=\"M4 10l4-4 4 4\"/></svg>\n </button>\n <div class=\"move-menu\" id=\"bulk-move-menu\"></div>\n </div>\n <button type=\"button\" id=\"bulk-exit\">完成</button>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '资产库' }] });\n\n/* ─── 给所有资产卡注入下载按钮 · PRD §6.5 所有中间产物可下载 ─── */\n(function injectDownloadBtns() {\n const dlSvg = '<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-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>';\n document.querySelectorAll('.asset-card').forEach(card => {\n if (card.querySelector('.card-dl-btn')) return;\n const btn = document.createElement('button');\n btn.className = 'card-dl-btn';\n btn.type = 'button';\n btn.title = '下载资产';\n btn.innerHTML = dlSvg;\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const name = card.dataset.name || '资产';\n // 推测卡片类型用作 mono 后缀\n const grid = card.closest('.asset-grid');\n const kind = grid ? grid.dataset.tab : '';\n const kindLabel = { people: '人物 · PNG', scenes: '场景 · PNG', products: '商品 · PNG', finals: '成片 · MP4 1080p', uploads: '原始素材' }[kind] || '资产';\n Shell.toast('下载中', name + ' · ' + kindLabel);\n });\n card.appendChild(btn);\n });\n})();\n\n// ============== State ==============\nconst TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads', 'unclassified'];\n\n/* ============== 加载图片优化\"加入资产库\"持久化数据 ==============\n image-optimize.html 把图保存到 localStorage['fs-library-unclassified']\n 这里读出后注入到 #grid-unclassified ============== */\n(function loadUnclassified() {\n let list;\n try { list = JSON.parse(localStorage.getItem('fs-library-unclassified') || '[]'); } catch (e) { list = []; }\n if (!Array.isArray(list) || !list.length) return;\n const grid = document.getElementById('grid-unclassified');\n if (!grid) return;\n function esc(s) { return String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c])); }\n function fmtDate(ts) {\n if (!ts) return '';\n const d = new Date(ts), z = n => (n < 10 ? '0' + n : '' + n);\n return d.getFullYear() + z(d.getMonth() + 1) + z(d.getDate());\n }\n list.forEach(it => {\n const card = document.createElement('div');\n card.className = 'asset-card';\n card.dataset.name = it.name || '未命名';\n card.dataset.kind = '未分类';\n card.dataset.source = it.source || '图片优化';\n card.dataset.used = '0';\n card.dataset.added = fmtDate(it.addedAt);\n card.dataset.libId = it.id || '';\n card.innerHTML = `\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除资产\" onclick=\"event.stopPropagation();\" data-action=\"delete-asset\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder asset-thumb\"><span class=\"ph-frame\">${esc(it.name || '未命名')}</span></div>\n <div class=\"asset-body\">\n <div class=\"asset-name\">${esc(it.name || '未命名')}</div>\n <div class=\"asset-meta\">未分类 · ${esc(it.source || '图片优化')} · ${esc(it.ratio || '')}</div>\n </div>\n `;\n card.addEventListener('click', () => {\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('查看资产', it.name || '未分类素材');\n });\n grid.appendChild(card);\n });\n})();\nconst PAGE_SIZES = [12, 24, 48, 96];\nconst state = {\n tab: 'people',\n search: '',\n // 人物\n gender: 'all', age: 'all', role: 'all',\n // 场景\n sceneType: 'all',\n // 商品图\n product: 'all',\n // 成片\n project: 'all', duration: 'all',\n // 我的上传\n kind: 'all',\n // 通用\n source: 'all',\n sort: 'used-desc',\n // 分页\n page: 1,\n pageSize: 12,\n};\n\nconst CHIP_DEFAULT_LABEL = {\n gender: '性别', age: '年龄段', role: '角色标签',\n sceneType: '场景类型',\n product: '关联商品',\n project: '关联项目', duration: '时长',\n kind: '资产类型',\n source: '来源',\n};\n\nconst SORT_LABEL = {\n 'used-desc': '最近使用',\n 'added-desc': '最近添加',\n 'ref-desc': '引用次数',\n};\n\nconst checkSvg = '<svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg>';\n\n// ============== Card pools per tab ==============\nconst cardsByTab = {};\nTAB_KEYS.forEach(t => {\n cardsByTab[t] = [...document.querySelectorAll(`#grid-${t} .asset-card`)];\n});\n\n// 同步 tab count + 副标题计数\nTAB_KEYS.forEach(t => {\n const tab = document.querySelector(`.tab[data-tab=\"${t}\"] .count`);\n if (tab) tab.textContent = cardsByTab[t].length;\n});\ndocument.getElementById('sub-people').textContent = cardsByTab.people.length;\ndocument.getElementById('sub-scenes').textContent = cardsByTab.scenes.length;\ndocument.getElementById('sub-products').textContent = cardsByTab.products.length;\ndocument.getElementById('sub-finals').textContent = cardsByTab.finals.length;\n\n// 同步 sidebar 徽章(资产总数)\nconst sidebarBadge = document.querySelector('aside.sidebar a[href=\"library.html\"] .pill-mini');\nif (sidebarBadge) {\n const total = TAB_KEYS.reduce((s, t) => s + cardsByTab[t].length, 0);\n sidebarBadge.textContent = total;\n}\n\n// ============== 构建下拉菜单 ==============\nfunction uniqueValues(tab, attr) {\n const set = new Set();\n cardsByTab[tab].forEach(c => {\n const v = c.dataset[attr];\n if (v) set.add(v);\n });\n return [...set];\n}\n\nfunction buildMenu(key, options, defaultLabel) {\n const wrap = document.querySelector(`.chip-wrap[data-key=\"${key}\"]`);\n if (!wrap) return;\n const menu = wrap.querySelector('.chip-menu');\n const all = `<div class=\"mi selected\" data-value=\"all\">${checkSvg}<span>${defaultLabel}</span></div><div class=\"mi-sep\"></div>`;\n const items = options.map(o => `<div class=\"mi\" data-value=\"${o.value}\">${checkSvg}<span>${o.label}</span></div>`).join('');\n menu.innerHTML = all + items;\n}\n\n// 人物\nbuildMenu('gender', [\n { value: '女', label: '女' },\n { value: '男', label: '男' },\n], '全部性别');\nbuildMenu('age', [\n { value: '幼年', label: '幼年' },\n { value: '少年', label: '少年' },\n { value: '青年', label: '青年' },\n { value: '中年', label: '中年' },\n { value: '老年', label: '老年' },\n], '全部年龄段');\nbuildMenu('role', uniqueValues('people', 'role').map(v => ({ value: v, label: v })), '全部角色');\n\n// 场景\nbuildMenu('sceneType', uniqueValues('scenes', 'sceneType').map(v => ({ value: v, label: v })), '全部场景');\n\n// 商品图\nbuildMenu('product', uniqueValues('products', 'product').map(v => ({ value: v, label: v })), '全部商品');\n\n// 成片\nbuildMenu('project', uniqueValues('finals', 'project').map(v => ({ value: v, label: v })), '全部项目');\nbuildMenu('duration', [\n { value: '15s', label: '15 秒' },\n { value: '30s', label: '30 秒' },\n { value: '45s', label: '45 秒' },\n { value: '60s', label: '60 秒' },\n], '全部时长');\n\n// 我的上传\nbuildMenu('kind', [\n { value: '人物', label: '人物' },\n { value: '场景', label: '场景' },\n { value: '商品', label: '商品' },\n], '全部类型');\n\n// 通用·来源(按当前 tab 的实际数据动态构建)\nfunction rebuildSourceMenu() {\n const sources = uniqueValues(state.tab, 'source');\n buildMenu('source', sources.map(v => ({ value: v, label: v })), '全部来源');\n syncChipUI('source');\n}\nrebuildSourceMenu();\n\n// 排序(无\"全部\",默认第一项选中)\nconst sortWrap = document.querySelector('.chip-wrap[data-key=\"sort\"]');\nsortWrap.querySelector('.chip-menu').innerHTML = Object.entries(SORT_LABEL).map(([v, l], i) =>\n `<div class=\"mi${i === 0 ? ' selected' : ''}\" data-value=\"${v}\">${checkSvg}<span>${l}</span></div>`\n).join('');\n\n// ============== Apply ==============\nfunction applyFilter() {\n // 网格切换\n TAB_KEYS.forEach(t => {\n const g = document.getElementById(`grid-${t}`);\n if (!g) return;\n g.hidden = t !== state.tab;\n });\n\n // chip-wrap 按当前 tab 显隐\n document.querySelectorAll('.chip-wrap').forEach(wrap => {\n const tabs = (wrap.dataset.tabs || '').split(' ');\n const show = tabs.includes('all') || tabs.includes(state.tab);\n wrap.style.display = show ? '' : 'none';\n });\n\n // 过滤\n const cards = cardsByTab[state.tab];\n const q = state.search.toLowerCase();\n let visible = [];\n cards.forEach(c => {\n let show = true;\n if (q) {\n const hay = `${c.dataset.name || ''} ${c.dataset.role || ''} ${c.dataset.sceneType || ''} ${c.dataset.product || ''} ${c.dataset.project || ''} ${c.dataset.source || ''}`.toLowerCase();\n if (!hay.includes(q)) show = false;\n }\n // 通用 source\n if (show && state.source !== 'all' && c.dataset.source !== state.source) show = false;\n\n if (state.tab === 'people') {\n if (show && state.gender !== 'all' && c.dataset.gender !== state.gender) show = false;\n if (show && state.age !== 'all' && c.dataset.age !== state.age) show = false;\n if (show && state.role !== 'all' && c.dataset.role !== state.role) show = false;\n } else if (state.tab === 'scenes') {\n if (show && state.sceneType !== 'all' && c.dataset.sceneType !== state.sceneType) show = false;\n } else if (state.tab === 'products') {\n if (show && state.product !== 'all' && c.dataset.product !== state.product) show = false;\n } else if (state.tab === 'finals') {\n if (show && state.project !== 'all' && c.dataset.project !== state.project) show = false;\n if (show && state.duration !== 'all' && c.dataset.duration !== state.duration) show = false;\n } else if (state.tab === 'uploads') {\n if (show && state.kind !== 'all' && c.dataset.kind !== state.kind) show = false;\n }\n\n c.style.display = show ? '' : 'none';\n if (show) visible.push(c);\n });\n\n // 排序\n const sorters = {\n 'used-desc': (a, b) => +b.dataset.used - +a.dataset.used,\n 'added-desc': (a, b) => +b.dataset.added - +a.dataset.added,\n 'ref-desc': (a, b) => +b.dataset.used - +a.dataset.used,\n };\n visible.sort(sorters[state.sort] || sorters['used-desc']);\n const grid = document.getElementById(`grid-${state.tab}`);\n visible.forEach(c => grid.appendChild(c));\n\n // 分页 · 在排序之后裁页, 把非当前页的卡片隐藏\n const totalVisible = visible.length;\n const totalPages = Math.max(1, Math.ceil(totalVisible / state.pageSize));\n if (state.page > totalPages) state.page = totalPages;\n if (state.page < 1) state.page = 1;\n const start = (state.page - 1) * state.pageSize;\n const end = start + state.pageSize;\n visible.forEach((c, i) => {\n if (i < start || i >= end) c.style.display = 'none';\n });\n\n // 计数 + 空状态\n const total = cards.length;\n document.getElementById('result-meta').innerHTML = `// 显示 <span class=\"count\">${totalVisible}</span> / ${total} 个资产`;\n const empty = document.getElementById('empty');\n if (totalVisible === 0) {\n empty.classList.add('show');\n grid.style.display = 'none';\n } else {\n empty.classList.remove('show');\n grid.style.display = '';\n }\n\n // 渲染分页器\n renderPagination(totalVisible, totalPages);\n\n // 是否有任意筛选生效 → 显示\"清空筛选\"\n const activeKeys = visibleFilterKeys();\n const hasFilter = state.search || activeKeys.some(k => state[k] !== 'all');\n document.getElementById('clear-filters').hidden = !hasFilter;\n}\n\n// ============== 分页器渲染 ==============\nfunction pageNumberList(cur, total) {\n // 智能省略号 · 始终显示首末页 + cur 前后各 1 页\n if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);\n const pages = new Set([1, total, cur, cur - 1, cur + 1]);\n if (cur <= 4) [2, 3, 4, 5].forEach(p => pages.add(p));\n if (cur >= total - 3) [total - 4, total - 3, total - 2, total - 1].forEach(p => pages.add(p));\n const sorted = [...pages].filter(p => p >= 1 && p <= total).sort((a, b) => a - b);\n const out = [];\n for (let i = 0; i < sorted.length; i++) {\n if (i > 0 && sorted[i] - sorted[i - 1] > 1) out.push('…');\n out.push(sorted[i]);\n }\n return out;\n}\n\nfunction renderPagination(totalVisible, totalPages) {\n const root = document.getElementById('pagination');\n if (!root) return;\n // 空结果或只有一页且 ≤ pageSize → 不显示(数据不够分页时没必要占视觉)\n if (totalVisible === 0 || (totalPages <= 1 && totalVisible <= state.pageSize)) {\n root.hidden = true;\n return;\n }\n root.hidden = false;\n document.getElementById('page-total').textContent = totalVisible;\n document.getElementById('page-size-label').textContent = `${state.pageSize} 条/页`;\n document.getElementById('page-jump').value = state.page;\n document.getElementById('page-jump').max = totalPages;\n\n const list = document.getElementById('page-list');\n const items = pageNumberList(state.page, totalPages);\n let html = `<button type=\"button\" data-page=\"prev\" ${state.page <= 1 ? 'disabled' : ''} aria-label=\"上一页\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 12L6 8l4-4\"/></svg>\n </button>`;\n items.forEach(p => {\n if (p === '…') html += `<span class=\"ellipsis\">…</span>`;\n else html += `<button type=\"button\" data-page=\"${p}\" ${p === state.page ? 'class=\"active\"' : ''}>${p}</button>`;\n });\n html += `<button type=\"button\" data-page=\"next\" ${state.page >= totalPages ? 'disabled' : ''} aria-label=\"下一页\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4l4 4-4 4\"/></svg>\n </button>`;\n list.innerHTML = html;\n}\n\nfunction visibleFilterKeys() {\n return [...document.querySelectorAll('.chip-wrap')]\n .filter(w => {\n const tabs = (w.dataset.tabs || '').split(' ');\n return (tabs.includes('all') || tabs.includes(state.tab)) && w.dataset.key !== 'sort';\n })\n .map(w => w.dataset.key);\n}\n\n// ============== Sync chip UI ==============\nfunction syncChipUI(key) {\n const wrap = document.querySelector(`.chip-wrap[data-key=\"${key}\"]`);\n if (!wrap) return;\n const label = wrap.querySelector('.chip-label');\n const chip = wrap.querySelector('.chip');\n const v = state[key];\n if (key === 'sort') {\n label.textContent = SORT_LABEL[v];\n } else if (v === 'all') {\n label.textContent = CHIP_DEFAULT_LABEL[key];\n chip.classList.remove('active');\n } else {\n // 找到对应 mi 的 label 文字(优先用菜单里的展示文本)\n const mi = wrap.querySelector(`.mi[data-value=\"${CSS.escape(v)}\"] span`);\n label.textContent = mi ? mi.textContent : v;\n chip.classList.add('active');\n }\n wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === v));\n}\n\n// ============== Bind ==============\ndocument.querySelectorAll('.chip-wrap').forEach(wrap => {\n const key = wrap.dataset.key;\n wrap.querySelector('.chip').addEventListener('click', e => {\n e.stopPropagation();\n const isOpen = wrap.classList.contains('open');\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) wrap.classList.add('open');\n });\n wrap.addEventListener('click', e => {\n const mi = e.target.closest('.mi');\n if (!mi) return;\n e.stopPropagation();\n state[key] = mi.dataset.value;\n state.page = 1;\n wrap.classList.remove('open');\n syncChipUI(key);\n applyFilter();\n });\n});\n\ndocument.addEventListener('click', () => {\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n});\n\n// Tab clicks\ndocument.querySelectorAll('#asset-tabs .tab').forEach(t => {\n t.addEventListener('click', () => {\n document.querySelectorAll('#asset-tabs .tab').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n state.tab = t.dataset.tab;\n state.page = 1;\n // 切 tab 时,清空本 tab 不再可见的筛选\n ['gender', 'age', 'role', 'sceneType', 'product', 'project', 'duration', 'kind', 'source'].forEach(k => {\n const wrap = document.querySelector(`.chip-wrap[data-key=\"${k}\"]`);\n if (!wrap) return;\n const tabs = (wrap.dataset.tabs || '').split(' ');\n const visible = tabs.includes('all') || tabs.includes(state.tab);\n if (!visible) { state[k] = 'all'; syncChipUI(k); }\n });\n // 来源选项随 tab 数据重新构建\n state.source = 'all';\n rebuildSourceMenu();\n applyFilter();\n });\n});\n\n// 搜索\ndocument.getElementById('search-input').addEventListener('input', e => {\n state.search = e.target.value.trim();\n state.page = 1;\n applyFilter();\n});\n\n// 清空筛选\ndocument.getElementById('clear-filters').addEventListener('click', () => {\n state.search = '';\n document.getElementById('search-input').value = '';\n ['gender', 'age', 'role', 'sceneType', 'product', 'project', 'duration', 'kind', 'source'].forEach(k => {\n state[k] = 'all';\n syncChipUI(k);\n });\n state.page = 1;\n applyFilter();\n Shell.toast('已清空筛选');\n});\n\n// 分页器 · 翻页按钮(事件委托)\ndocument.getElementById('page-list').addEventListener('click', e => {\n const btn = e.target.closest('button[data-page]');\n if (!btn || btn.disabled) return;\n const v = btn.dataset.page;\n const totalPages = +document.getElementById('page-jump').max || 1;\n if (v === 'prev') state.page = Math.max(1, state.page - 1);\n else if (v === 'next') state.page = Math.min(totalPages, state.page + 1);\n else state.page = +v;\n applyFilter();\n // 翻页后滚到顶部, 让首张卡片立刻可见\n window.scrollTo({ top: 0, behavior: 'smooth' });\n});\n\n// 分页器 · 每页条数(循环切换 12 → 24 → 48 → 96 → 12)\ndocument.getElementById('page-size-btn').addEventListener('click', () => {\n const i = PAGE_SIZES.indexOf(state.pageSize);\n state.pageSize = PAGE_SIZES[(i + 1) % PAGE_SIZES.length];\n state.page = 1;\n applyFilter();\n});\n\n// 分页器 · 跳转\nconst _jumpEl = document.getElementById('page-jump');\nfunction _doJump() {\n let v = parseInt(_jumpEl.value, 10);\n const max = +_jumpEl.max || 1;\n if (!Number.isFinite(v)) v = 1;\n v = Math.max(1, Math.min(max, v));\n state.page = v;\n applyFilter();\n}\n_jumpEl.addEventListener('change', _doJump);\n_jumpEl.addEventListener('blur', _doJump);\n_jumpEl.addEventListener('keydown', e => {\n if (e.key === 'Enter') { e.preventDefault(); _doJump(); _jumpEl.blur(); }\n});\n\napplyFilter();\n\n// ============== 上传资产 Modal ==============\n// 修复:把 modal 提到 <body> 末尾,脱离 main 的 stacking context 干扰\n// (main 是 position:relative + overflow:hidden, topbar z-index:2 会盖在 fixed modal-bg 之上)\nconst _modalRoot = document.getElementById('upload-modal-bg');\nif (_modalRoot && _modalRoot.parentElement !== document.body) {\n document.body.appendChild(_modalRoot);\n}\n\nconst uploadState = { kind: 'people', file: null, dataUrl: '', mime: '' };\n\nconst KIND_LABEL = { people: '人物', scenes: '场景', products: '商品图', finals: '成片' };\nconst DEFAULT_THUMB_TEXT = {\n people: '新资产', scenes: '新场景', products: '新商品图', finals: '9:16 · 新成片'\n};\nconst FILE_HINT = {\n image: '// JPG / PNG / WEBP · 单文件',\n video: '// MP4 / WEBM · 9:16 · ≤ 60 秒',\n};\n\nconst $ = (id) => document.getElementById(id);\nconst modalBg = $('upload-modal-bg');\nconst kindSel = $('upload-kind');\nconst zone = $('upload-zone');\nconst zoneText = $('upload-zone-text');\nconst zoneHint = $('upload-zone-hint');\nconst fileInput = $('upload-file');\nconst preview = $('upload-preview');\nconst previewImg = $('upload-preview-img');\nconst previewVideo = $('upload-preview-video');\nconst previewX = $('upload-preview-x');\nconst nameInput = $('upload-name');\nconst submitBtn = $('upload-submit');\n\nfunction syncKindFields() {\n document.querySelectorAll('.upload-modal .field[data-fields]').forEach(f => {\n f.hidden = f.dataset.fields !== uploadState.kind;\n });\n const isVideo = uploadState.kind === 'finals';\n fileInput.accept = isVideo ? 'video/*' : 'image/*';\n zoneText.textContent = isVideo ? '点击或拖拽上传视频' : '点击或拖拽上传图片';\n zoneHint.textContent = isVideo ? FILE_HINT.video : FILE_HINT.image;\n preview.classList.toggle('video', isVideo);\n}\n\nfunction syncSubmit() {\n let ok = !!uploadState.file && nameInput.value.trim().length > 0;\n if (ok && uploadState.kind === 'scenes' && !$('upload-scene-type').value.trim()) ok = false;\n if (ok && uploadState.kind === 'products' && !$('upload-product').value) ok = false;\n submitBtn.disabled = !ok;\n}\n\nfunction resetUploadModal() {\n uploadState.file = null;\n uploadState.dataUrl = '';\n uploadState.mime = '';\n fileInput.value = '';\n preview.hidden = true;\n previewImg.removeAttribute('src');\n previewVideo.removeAttribute('src');\n previewVideo.hidden = true;\n previewImg.hidden = false;\n zone.hidden = false;\n nameInput.value = '';\n $('upload-role').value = '';\n $('upload-scene-type').value = '';\n $('upload-product').selectedIndex = 0;\n $('upload-project').value = '';\n $('upload-gender').selectedIndex = 0;\n $('upload-age').value = '青年';\n $('upload-duration').value = '60s';\n syncSubmit();\n}\n\nfunction handleFile(file) {\n if (!file) return;\n const isVideo = uploadState.kind === 'finals';\n if (isVideo && !file.type.startsWith('video/')) {\n Shell.toast('请上传视频文件', file.type || 'unknown');\n return;\n }\n if (!isVideo && !file.type.startsWith('image/')) {\n Shell.toast('请上传图片文件', file.type || 'unknown');\n return;\n }\n uploadState.file = file;\n uploadState.mime = file.type;\n const reader = new FileReader();\n reader.onload = (e) => {\n uploadState.dataUrl = e.target.result;\n if (isVideo) {\n previewVideo.src = uploadState.dataUrl;\n previewVideo.hidden = false;\n previewImg.hidden = true;\n } else {\n previewImg.src = uploadState.dataUrl;\n previewImg.hidden = false;\n previewVideo.hidden = true;\n }\n preview.hidden = false;\n zone.hidden = true;\n // 自动填名称(去后缀)\n if (!nameInput.value) nameInput.value = file.name.replace(/\\.[^.]+$/, '');\n syncSubmit();\n };\n reader.readAsDataURL(file);\n}\n\nkindSel.addEventListener('change', () => {\n uploadState.kind = kindSel.value;\n resetUploadModal();\n syncKindFields();\n});\n\nzone.addEventListener('click', () => fileInput.click());\nzone.addEventListener('dragover', e => { e.preventDefault(); zone.style.borderColor = 'var(--heat)'; });\nzone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });\nzone.addEventListener('drop', e => {\n e.preventDefault();\n zone.style.borderColor = '';\n if (e.dataTransfer?.files?.length) handleFile(e.dataTransfer.files[0]);\n});\nfileInput.addEventListener('change', e => {\n if (e.target.files?.length) handleFile(e.target.files[0]);\n});\n\npreviewX.addEventListener('click', () => {\n uploadState.file = null;\n uploadState.dataUrl = '';\n fileInput.value = '';\n preview.hidden = true;\n zone.hidden = false;\n previewImg.removeAttribute('src');\n previewVideo.removeAttribute('src');\n syncSubmit();\n});\n\nnameInput.addEventListener('input', syncSubmit);\n$('upload-scene-type').addEventListener('input', syncSubmit);\n$('upload-product').addEventListener('change', syncSubmit);\n\n// 提交 → 构造卡片插入到对应 grid 首位\nsubmitBtn.addEventListener('click', () => {\n if (submitBtn.disabled) return;\n const kind = uploadState.kind;\n const name = nameInput.value.trim();\n const today = new Date();\n const stamp = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n\n const card = document.createElement('div');\n card.className = 'asset-card' + (kind === 'finals' ? ' video' : '');\n card.dataset.name = name;\n card.dataset.source = '手动上传';\n card.dataset.used = '0';\n card.dataset.added = stamp;\n card.setAttribute('onclick', `Shell.toast('查看资产', ${JSON.stringify(name)})`);\n\n let metaText = '';\n if (kind === 'people') {\n const gender = $('upload-gender').value;\n const age = $('upload-age').value;\n const role = $('upload-role').value.trim() || '—';\n card.dataset.gender = gender;\n card.dataset.age = age;\n card.dataset.role = role;\n metaText = `${gender} · ${age} · ${role} · 手动上传`;\n } else if (kind === 'scenes') {\n const sceneType = $('upload-scene-type').value.trim();\n card.dataset.sceneType = sceneType;\n metaText = `${sceneType} · 手动上传 · 用过 0 次`;\n } else if (kind === 'products') {\n const product = $('upload-product').value;\n card.dataset.product = product;\n metaText = `${product} · 手动上传 · 用过 0 次`;\n } else if (kind === 'finals') {\n const project = $('upload-project').value.trim() || name;\n const duration = $('upload-duration').value;\n card.dataset.project = project;\n card.dataset.duration = duration;\n metaText = `${project} · ${duration} · 手动上传`;\n }\n\n // 缩略图:有 dataUrl 用真实预览,否则占位\n const thumb = document.createElement('div');\n thumb.className = 'placeholder asset-thumb';\n if (uploadState.dataUrl) {\n if (kind === 'finals') {\n thumb.innerHTML = `<video src=\"${uploadState.dataUrl}\" muted playsinline style=\"width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit;\"></video>`;\n } else {\n thumb.style.backgroundImage = `url(\"${uploadState.dataUrl}\")`;\n thumb.style.backgroundSize = 'cover';\n thumb.style.backgroundPosition = 'center';\n thumb.innerHTML = '';\n }\n } else {\n thumb.innerHTML = `<span class=\"ph-frame\">${DEFAULT_THUMB_TEXT[kind]}</span>`;\n }\n\n const body = document.createElement('div');\n body.className = 'asset-body';\n body.innerHTML = `<div class=\"asset-name\">${name}</div><div class=\"asset-meta\">${metaText}</div>`;\n card.appendChild(thumb);\n card.appendChild(body);\n\n // 插入 grid 首位 + 更新 cardsByTab + 计数 + 切到该 tab\n const targetGrid = $(`grid-${kind}`);\n targetGrid.prepend(card);\n cardsByTab[kind].unshift(card);\n\n // 刷新 tab count + 副标题计数\n const tabCount = document.querySelector(`.tab[data-tab=\"${kind}\"] .count`);\n if (tabCount) tabCount.textContent = cardsByTab[kind].length;\n if (kind === 'people') $('sub-people').textContent = cardsByTab.people.length;\n if (kind === 'scenes') $('sub-scenes').textContent = cardsByTab.scenes.length;\n if (kind === 'products') $('sub-products').textContent = cardsByTab.products.length;\n if (kind === 'finals') $('sub-finals').textContent = cardsByTab.finals.length;\n if (sidebarBadge) {\n const total = TAB_KEYS.reduce((s, t) => s + cardsByTab[t].length, 0);\n sidebarBadge.textContent = total;\n }\n\n // 角色标签来源新值 → 重建角色菜单(可能新增)\n if (kind === 'people') {\n buildMenu('role', uniqueValues('people', 'role').map(v => ({ value: v, label: v })), '全部角色');\n syncChipUI('role');\n }\n if (kind === 'scenes') {\n buildMenu('sceneType', uniqueValues('scenes', 'sceneType').map(v => ({ value: v, label: v })), '全部场景');\n syncChipUI('sceneType');\n }\n if (kind === 'products') {\n buildMenu('product', uniqueValues('products', 'product').map(v => ({ value: v, label: v })), '全部商品');\n syncChipUI('product');\n }\n if (kind === 'finals') {\n buildMenu('project', uniqueValues('finals', 'project').map(v => ({ value: v, label: v })), '全部项目');\n syncChipUI('project');\n }\n\n // 切到目标 tab + 清空筛选\n document.querySelectorAll('#asset-tabs .tab').forEach(t =>\n t.classList.toggle('active', t.dataset.tab === kind)\n );\n state.tab = kind;\n state.search = ''; $('search-input').value = '';\n ['gender', 'age', 'role', 'sceneType', 'product', 'project', 'duration', 'source'].forEach(k => {\n state[k] = 'all';\n syncChipUI(k);\n });\n state.source = 'all';\n state.page = 1;\n rebuildSourceMenu();\n applyFilter();\n\n Shell.closeModal('upload-modal-bg');\n Shell.toast(`已上传到${KIND_LABEL[kind]}`, `+ ${name}`);\n});\n\n// 打开按钮\n$('open-upload-btn').addEventListener('click', () => {\n resetUploadModal();\n kindSel.value = state.tab in KIND_LABEL ? state.tab : 'people';\n uploadState.kind = kindSel.value;\n syncKindFields();\n Shell.openModal('upload-modal-bg');\n setTimeout(() => nameInput.focus(), 100);\n});\n\n// ESC 关闭\ndocument.addEventListener('keydown', e => {\n if (e.key === 'Escape' && modalBg.classList.contains('show')) {\n Shell.closeModal('upload-modal-bg');\n }\n});\n\n// 初始化\nsyncKindFields();\n\n// ============================================================\n// 资产删除 + 批量管理 (PRD §6.3 软删除/引用检查)\n// ============================================================\n// 模拟引用记录: 某些资产被项目引用 (实际从后台获取)\nconst ASSET_REFS = {\n '林夕': ['夏日水嫩计划', '春装新品'],\n '小七': ['学生季推广'],\n '卧室·暖光': ['补水面膜 v1', '面膜对比'],\n '客厅·北欧': ['咖啡早八剧情'],\n '蓝牙耳机 · 开箱测评': ['南卡 Lite 推广']\n};\nfunction getAssetRefs(card) {\n const name = card.dataset.name || '';\n return ASSET_REFS[name] || [];\n}\n\nconst delBg = document.getElementById('del-confirm-bg');\nconst delBody = document.getElementById('del-confirm-body');\nconst delFoot = document.getElementById('del-confirm-foot');\nconst delCancel = document.getElementById('del-confirm-cancel');\nconst delOk = document.getElementById('del-confirm-ok');\nlet _delQueue = [];\n\nfunction setFootDeletable() {\n delFoot.innerHTML = '';\n delFoot.appendChild(delCancel);\n delFoot.appendChild(delOk);\n}\nfunction setFootBlocked() {\n delFoot.innerHTML = '';\n const okBtn = document.createElement('button');\n okBtn.className = 'btn btn-primary';\n okBtn.type = 'button';\n okBtn.textContent = '我知道了';\n okBtn.addEventListener('click', closeDelConfirm);\n delFoot.appendChild(okBtn);\n}\nfunction openDelConfirm(targets) {\n const blocked = targets.filter(c => getAssetRefs(c).length > 0);\n const deletable = targets.filter(c => getAssetRefs(c).length === 0);\n if (deletable.length === 0 && blocked.length > 0) {\n const c = blocked[0];\n const refs = getAssetRefs(c);\n if (blocked.length === 1) {\n delBody.innerHTML = `<span class=\"mono-acc\">${c.dataset.name}</span> 当前被 <b>${refs.length}</b> 个项目使用,无法直接删除。请先在以下项目中解除引用:<br><br>` +\n refs.map(r => `<span class=\"mono-acc\" style=\"margin-right:6px\">${r}</span>`).join('');\n } else {\n delBody.innerHTML = `所选 <b>${blocked.length}</b> 个资产均被项目引用,无法直接删除。`;\n }\n setFootBlocked();\n delBg.classList.add('show');\n _delQueue = [];\n return;\n }\n _delQueue = deletable;\n if (deletable.length === 1 && blocked.length === 0) {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + deletable[0].dataset.name + '</span>,此操作无法撤销。';\n } else if (blocked.length > 0) {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + deletable.length + ' 个资产</span>,其中 <b>' + blocked.length + '</b> 个被项目引用已跳过。';\n } else {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + deletable.length + ' 个资产</span>,此操作无法撤销。';\n }\n setFootDeletable();\n delBg.classList.add('show');\n}\nfunction closeDelConfirm() { delBg.classList.remove('show'); _delQueue = []; }\ndelCancel.addEventListener('click', closeDelConfirm);\ndelBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });\ndelOk.addEventListener('click', () => {\n const n = _delQueue.length;\n // 收集被删除中、source 是\"未分类\"的 libId,同步从 localStorage 移除\n const removedLibIds = _delQueue\n .filter(c => c.dataset.libId)\n .map(c => c.dataset.libId);\n _delQueue.forEach(card => card.remove());\n if (removedLibIds.length) {\n try {\n const LIB_KEY = 'fs-library-unclassified';\n const list = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');\n const next = (Array.isArray(list) ? list : []).filter(x => !removedLibIds.includes(x.id));\n localStorage.setItem(LIB_KEY, JSON.stringify(next));\n } catch (e) { /* ignore */ }\n }\n closeDelConfirm();\n Shell.toast('已删除', n === 1 ? '资产已移除' : '已删除 ' + n + ' 个资产');\n updateBulkBar();\n});\n\n// 单卡片删除按钮\ndocument.querySelectorAll('.asset-card .card-del-btn').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const card = btn.closest('.asset-card');\n if (!card) return;\n openDelConfirm([card]);\n });\n});\n\n// 管理资产模式\nconst libManageBtn = document.getElementById('lib-manage-btn');\nconst libManageLabel = libManageBtn.querySelector('.lib-manage-label');\nconst bulkBar = document.getElementById('bulk-bar');\nconst bulkCount = document.getElementById('bulk-count');\nconst bulkClear = document.getElementById('bulk-clear');\nconst bulkDel = document.getElementById('bulk-del');\nconst bulkExit = document.getElementById('bulk-exit');\n\nfunction getSelected() { return [...document.querySelectorAll('.asset-card.selected')]; }\nfunction updateBulkBar() {\n const sel = getSelected();\n bulkCount.textContent = sel.length;\n bulkDel.disabled = sel.length === 0;\n bulkDel.style.opacity = sel.length === 0 ? '.4' : '1';\n}\nfunction enterEditMode() {\n document.body.classList.add('edit-mode');\n libManageBtn.classList.add('active');\n libManageLabel.textContent = '完成';\n applyDraggableToCards(true);\n updateBulkBar();\n}\nfunction exitEditMode() {\n document.body.classList.remove('edit-mode');\n libManageBtn.classList.remove('active');\n libManageLabel.textContent = '管理资产';\n document.querySelectorAll('.asset-card.selected').forEach(c => c.classList.remove('selected'));\n applyDraggableToCards(false);\n if (bulkMoveMenu) bulkMoveMenu.classList.remove('show');\n}\nlibManageBtn.addEventListener('click', () => {\n if (document.body.classList.contains('edit-mode')) exitEditMode();\n else enterEditMode();\n});\nbulkExit.addEventListener('click', exitEditMode);\nbulkClear.addEventListener('click', () => {\n document.querySelectorAll('.asset-card.selected').forEach(c => c.classList.remove('selected'));\n updateBulkBar();\n});\nbulkDel.addEventListener('click', () => {\n const sel = getSelected();\n if (!sel.length) return;\n openDelConfirm(sel);\n});\n\n// ── 移动到 · 菜单 + 拖拽 ──\nconst TAB_NAMES = {\n people: '人物', scenes: '场景', products: '商品图',\n finals: '成片', uploads: '我的上传', unclassified: '未分类'\n};\nconst bulkMove = document.getElementById('bulk-move');\nconst bulkMoveMenu = document.getElementById('bulk-move-menu');\n\nfunction getCurrentTab() {\n const active = document.querySelector('#asset-tabs .tab.active');\n return active ? active.dataset.tab : TAB_KEYS[0];\n}\nfunction refreshAllTabCounts() {\n TAB_KEYS.forEach(t => {\n const tab = document.querySelector(`.tab[data-tab=\"${t}\"] .count`);\n if (tab && cardsByTab[t]) tab.textContent = cardsByTab[t].length;\n });\n const total = TAB_KEYS.reduce((s, t) => s + (cardsByTab[t] ? cardsByTab[t].length : 0), 0);\n const sidebarBadge = document.querySelector('aside.sidebar a[href=\"library.html\"] .pill-mini');\n if (sidebarBadge) sidebarBadge.textContent = total;\n const subMap = { people:'sub-people', scenes:'sub-scenes', products:'sub-products', finals:'sub-finals' };\n Object.keys(subMap).forEach(k => {\n const el = document.getElementById(subMap[k]);\n if (el && cardsByTab[k]) el.textContent = cardsByTab[k].length;\n });\n}\nfunction moveSelectedTo(targetTab) {\n const sel = getSelected();\n if (!sel.length) { Shell.toast('请先选中资产'); return; }\n const targetGrid = document.getElementById(`grid-${targetTab}`);\n if (!targetGrid) return;\n let movedCount = 0;\n sel.forEach(card => {\n // 找出 card 当前所在 tab (DOM-based,跨多 tab 的 selected 也能正确移动)\n const curGrid = card.closest('.asset-grid');\n const curTab = curGrid ? curGrid.dataset.tab : getCurrentTab();\n if (curTab === targetTab) return; // 同分类跳过\n // 更新内存\n if (cardsByTab[curTab]) {\n const idx = cardsByTab[curTab].indexOf(card);\n if (idx >= 0) cardsByTab[curTab].splice(idx, 1);\n }\n if (cardsByTab[targetTab]) cardsByTab[targetTab].push(card);\n // 更新 DOM\n card.classList.remove('selected');\n card.dataset.kind = TAB_NAMES[targetTab];\n targetGrid.appendChild(card);\n movedCount += 1;\n });\n refreshAllTabCounts();\n updateBulkBar();\n if (movedCount > 0) {\n Shell.toast('已移动', `${movedCount} 个资产 → 「${TAB_NAMES[targetTab]}」`);\n } else {\n Shell.toast('未移动', '所选资产已在该分类');\n }\n}\nfunction renderMoveMenu() {\n const cur = getCurrentTab();\n bulkMoveMenu.innerHTML = TAB_KEYS\n .filter(t => t !== cur)\n .map(t => `<button class=\"mv-item\" type=\"button\" data-target=\"${t}\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"/></svg>\n 移到「${TAB_NAMES[t]}」\n </button>`).join('');\n bulkMoveMenu.querySelectorAll('.mv-item').forEach(btn => {\n btn.addEventListener('click', () => {\n moveSelectedTo(btn.dataset.target);\n bulkMoveMenu.classList.remove('show');\n });\n });\n}\nbulkMove.addEventListener('click', e => {\n e.stopPropagation();\n if (!getSelected().length) { Shell.toast('请先选中资产'); return; }\n renderMoveMenu();\n bulkMoveMenu.classList.toggle('show');\n});\ndocument.addEventListener('click', e => {\n if (!bulkMove.contains(e.target) && !bulkMoveMenu.contains(e.target)) {\n bulkMoveMenu.classList.remove('show');\n }\n});\n\n// ── 拖拽到 tab 移动 (edit-mode 下生效) ──\nfunction applyDraggableToCards(on) {\n document.querySelectorAll('.asset-card').forEach(c => {\n if (on) c.setAttribute('draggable', 'true');\n else c.removeAttribute('draggable');\n });\n}\ndocument.addEventListener('dragstart', e => {\n const card = e.target.closest('.asset-card');\n if (!card || !document.body.classList.contains('edit-mode')) return;\n // 没选中就当前 card 也算 (允许直接拖单张)\n if (!card.classList.contains('selected')) {\n card.classList.add('selected');\n updateBulkBar();\n }\n card.classList.add('dragging');\n e.dataTransfer.effectAllowed = 'move';\n try { e.dataTransfer.setData('text/plain', 'move-asset'); } catch (err) {}\n});\ndocument.addEventListener('dragend', e => {\n const card = e.target.closest('.asset-card');\n if (card) card.classList.remove('dragging');\n document.querySelectorAll('#asset-tabs .tab.drag-over').forEach(t => t.classList.remove('drag-over'));\n});\ndocument.querySelectorAll('#asset-tabs .tab').forEach(tab => {\n tab.addEventListener('dragover', e => {\n if (!document.body.classList.contains('edit-mode')) return;\n if (tab.dataset.tab === getCurrentTab()) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n tab.classList.add('drag-over');\n });\n tab.addEventListener('dragleave', () => tab.classList.remove('drag-over'));\n tab.addEventListener('drop', e => {\n if (!document.body.classList.contains('edit-mode')) return;\n e.preventDefault();\n tab.classList.remove('drag-over');\n if (tab.dataset.tab === getCurrentTab()) return;\n moveSelectedTo(tab.dataset.tab);\n });\n});\n\n// 编辑模式下,卡片点击切换 selected (不再 toast / 打开)\ndocument.querySelectorAll('.asset-card').forEach(card => {\n card.addEventListener('click', e => {\n if (!document.body.classList.contains('edit-mode')) return;\n e.stopImmediatePropagation();\n e.preventDefault();\n card.classList.toggle('selected');\n updateBulkBar();\n }, true);\n});\n\n/* ============================================================\n 资产详情 modal · 与 pipeline.html 共用参考布局 v2\n ============================================================ */\n(function () {\n // 注入 modal CSS (与 pipeline.html 保持一致的 .asset-* 命名)\n const css = `\n .asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }\n .asset-modal-bg.show { display: flex; }\n .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); }\n .asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }\n .asset-modal-h h2 { font-size: 15px; font-weight: 600; }\n .asset-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .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; }\n .asset-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }\n .asset-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }\n .asset-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }\n .asset-detail-lead { display: flex; flex-direction: column; gap: 10px; }\n .asset-detail-lead .ad-lead-wrap { position: relative; }\n .asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); }\n /* 查看大图 icon · 悬浮容器才显示 · 32×32 icon-only */\n .ad-zoom-btn { position: absolute; right: 8px; bottom: 8px; width: 32px; height: 32px; padding: 0; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-sm); display: grid; place-items: center; cursor: pointer; backdrop-filter: blur(4px); opacity: 0; transition: opacity var(--t-base), background var(--t-base); z-index: 3; }\n .ad-zoom-btn:hover { background: rgba(21,20,15,.92); }\n .ad-zoom-btn svg { width: 14px; height: 14px; }\n .asset-detail-lead .ad-lead-wrap:hover .ad-zoom-btn,\n .asset-detail-tri-row .placeholder:hover .ad-zoom-btn { opacity: 1; }\n .asset-detail-tri-row .placeholder { position: relative; }\n .asset-detail-lead .ad-thumbs { display: flex; gap: 8px; }\n .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; }\n .asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }\n .asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }\n .asset-detail-right .ad-section + .ad-section { margin-top: 18px; }\n .asset-detail-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }\n .asset-detail-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }\n .asset-detail-section-h .ic svg { width: 14px; height: 14px; }\n .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); }\n .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; }\n .asset-detail-section-h .ad-icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }\n .asset-detail-section-h .ad-icon-btn svg { width: 12px; height: 12px; }\n .asset-detail-tri-row .placeholder { aspect-ratio: 16/9; border-radius: var(--r-md); }\n .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; gap: 8px; }\n .asset-detail-tri-row .placeholder.missing:hover { border-color: var(--heat); color: var(--heat); }\n .ad-intro { font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); margin: 0 0 12px; }\n .ad-tags { display: flex; flex-wrap: wrap; gap: 8px; }\n .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); }\n .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; }\n .ad-tags .ad-tag-add:hover { border-color: var(--heat); color: var(--heat); }\n .ad-tags .ad-tag-add svg { width: 12px; height: 12px; }\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; }\n .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; }\n .ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }\n .ad-props .ad-prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }\n .ad-props .ad-prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }\n .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; }\n .asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }\n .asset-detail-tip .ai-gen-btn { margin-left: auto; height: 26px; padding: 0 10px; background: var(--heat); color: #fff; border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; flex-shrink: 0; display: inline-flex; align-items: center; }\n .asset-detail-tip .ai-gen-btn:disabled { opacity: .55; cursor: not-allowed; }\n .asset-detail-tip.is-loading svg { animation: ad-spin 1s linear infinite; }\n @keyframes ad-spin { to { transform: rotate(360deg); } }\n .asset-detail-history { margin-top: 10px; padding: 10px 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }\n .asset-detail-history .adh-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); margin-bottom: 8px; letter-spacing: .02em; }\n .asset-detail-history .adh-h .adh-cur { color: var(--heat); }\n .asset-detail-history .adh-row { display: flex; gap: 8px; flex-wrap: wrap; }\n .asset-detail-history .adh-thumb { width: 64px; height: 36px; border-radius: var(--r-sm); background: var(--background-base); border: 1px solid var(--border-faint); display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-72); cursor: pointer; position: relative; transition: border-color var(--t-base), color var(--t-base); }\n .asset-detail-history .adh-thumb:hover { border-color: var(--heat-40); color: var(--heat); }\n .asset-detail-history .adh-thumb.active { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .asset-detail-history .adh-thumb.active::after { content: \"\"; position: absolute; top: -3px; right: -3px; width: 8px; height: 8px; background: var(--heat); border: 2px solid var(--surface); border-radius: 50%; }\n .asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }\n .asset-modal-f .ad-foot-stats { display: flex; gap: 6px; margin-right: auto; }\n .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; }\n .asset-modal-f .ad-stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }\n .asset-modal-f .ad-stat-btn svg { width: 13px; height: 13px; }\n .asset-modal-f .ad-stat-btn b { color: var(--accent-black); font-weight: 600; }\n /* ── 缺保存 · 二次确认弹窗(模仿 model-photo .mc-leave) ── */\n .lib-confirm-bg { position: fixed; inset: 0; z-index: 1300; background: rgba(21,20,15,.42); display: none; align-items: center; justify-content: center; padding: 40px; }\n .lib-confirm-bg.show { display: flex; }\n .lib-confirm { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: 440px; max-width: 92vw; box-shadow: 0 24px 64px rgba(0,0,0,.16); overflow: hidden; }\n .lib-confirm .lc-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px 10px; }\n .lib-confirm .lc-h .ic { width: 28px; height: 28px; display: grid; place-items: center; background: var(--heat-12); color: var(--heat); border-radius: var(--r-sm); flex-shrink: 0; }\n .lib-confirm .lc-h .ic svg { width: 16px; height: 16px; }\n .lib-confirm .lc-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }\n .lib-confirm .lc-h .mono { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .lib-confirm .lc-b { padding: 4px 20px 18px; font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); }\n .lib-confirm .lc-b b { color: var(--accent-black); font-weight: 600; }\n .lib-confirm .lc-f { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--border-faint); background: var(--background-lighter); }\n .lib-confirm .lc-f .spacer { flex: 1; }\n .lib-confirm .lc-f .btn { height: 34px; padding: 0 14px; font-size: 13px; }\n `;\n const style = document.createElement('style');\n style.textContent = css;\n document.head.appendChild(style);\n\n const modalHTML = `\n <div class=\"asset-modal-bg\" id=\"lib-detail-bg\">\n <div class=\"asset-modal\">\n <div class=\"asset-modal-h\">\n <h2 id=\"lib-detail-title\">资产详情</h2>\n <span class=\"ad-tag\" id=\"lib-detail-kind\">/ kind</span>\n <button class=\"x\" id=\"lib-detail-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>\n </div>\n <div class=\"asset-modal-body\">\n <div class=\"asset-detail-grid\">\n <div class=\"asset-detail-lead\">\n <div class=\"ad-lead-wrap\">\n <div class=\"placeholder ad-lead-img\" id=\"lib-detail-lead-img\"><span class=\"ph-frame\">立绘 / 主图</span></div>\n <button class=\"ad-zoom-btn\" type=\"button\" id=\"lib-detail-lead-zoom\" aria-label=\"查看大图\" title=\"查看大图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg>\n </button>\n </div>\n <div class=\"ad-thumbs\" id=\"lib-detail-thumbs\"></div>\n </div>\n <div class=\"asset-detail-right\">\n <div class=\"ad-section\" id=\"lib-detail-tri-section\">\n <div class=\"asset-detail-section-h\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/></svg></span>\n <span class=\"t\">三视图</span>\n <span class=\"ad-ratio-chip\" id=\"lib-detail-ratio\">16:9</span>\n <button class=\"ad-icon-btn\" type=\"button\" title=\"下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n </div>\n <div class=\"asset-detail-tri-row\" id=\"lib-detail-tri\">\n <div class=\"placeholder\"><span class=\"ph-frame\">正 / 侧 / 背 · 三视图</span></div>\n </div>\n <div class=\"asset-detail-tip\" id=\"lib-detail-tip\" style=\"display:none;\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/></svg>\n <span id=\"lib-detail-tip-text\">暂无三视图,建议用 AI 生成以保证多角度一致性</span>\n <button class=\"ai-gen-btn\" type=\"button\" id=\"lib-detail-aigen\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:11px;height:11px;display:inline-block;vertical-align:-1px;margin-right:3px;\"><path d=\"M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z\"/></svg>\n <span id=\"lib-detail-aigen-label\">AI 生成三视图</span>\n </button>\n </div>\n <div class=\"asset-detail-history\" id=\"lib-detail-history\" style=\"display:none;\">\n <div class=\"adh-h\">// 三视图版本 · <span class=\"adh-ct\" id=\"lib-detail-history-count\">0</span> 版 · <span class=\"adh-cur\" id=\"lib-detail-history-cur\">v1</span></div>\n <div class=\"adh-row\" id=\"lib-detail-history-row\"></div>\n </div>\n </div>\n <div class=\"ad-section\">\n <div class=\"asset-detail-section-h\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6h16M4 12h16M4 18h10\"/></svg></span>\n <span class=\"t\">简介</span>\n </div>\n <p class=\"ad-intro\" id=\"lib-detail-intro\"></p>\n <div class=\"ad-tags\" id=\"lib-detail-tags\"></div>\n </div>\n <div class=\"ad-props\" id=\"lib-detail-props\"></div>\n </div>\n </div>\n </div>\n <div class=\"asset-modal-f\">\n <div class=\"ad-foot-stats\">\n <button class=\"ad-stat-btn\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>\n 下载\n </button>\n </div>\n <button class=\"btn btn-primary\" type=\"button\" id=\"lib-detail-apply\">保存</button>\n </div>\n </div>\n </div>\n <div class=\"lib-confirm-bg\" id=\"lib-confirm-bg\" aria-hidden=\"true\">\n <div class=\"lib-confirm\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"lib-confirm-title\">\n <div class=\"lc-h\">\n <span class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z\"/></svg>\n </span>\n <h3 id=\"lib-confirm-title\">三视图尚未保存</h3>\n <span class=\"mono\">// UNSAVED</span>\n </div>\n <div class=\"lc-b\" id=\"lib-confirm-body\">\n 已生成 <b id=\"lib-confirm-count\">1</b> 版三视图但<b>尚未保存</b>。直接退出会丢失这些版本,且当前资产仍标记为「<b>缺三视图</b>」。\n </div>\n <div class=\"lc-f\">\n <span class=\"spacer\"></span>\n <button class=\"btn\" type=\"button\" id=\"lib-confirm-discard\">退出</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"lib-confirm-save\">保存并退出</button>\n </div>\n </div>\n </div>`;\n document.body.insertAdjacentHTML('beforeend', modalHTML);\n\n const bg = document.getElementById('lib-detail-bg');\n const titleEl = document.getElementById('lib-detail-title');\n const kindEl = document.getElementById('lib-detail-kind');\n const leadImg = document.getElementById('lib-detail-lead-img');\n const thumbsEl = document.getElementById('lib-detail-thumbs');\n const triSection = document.getElementById('lib-detail-tri-section');\n const triEl = document.getElementById('lib-detail-tri');\n const ratioChip = document.getElementById('lib-detail-ratio');\n const introEl = document.getElementById('lib-detail-intro');\n const tagsEl = document.getElementById('lib-detail-tags');\n const propsEl = document.getElementById('lib-detail-props');\n const tipEl = document.getElementById('lib-detail-tip');\n const tipTextEl = document.getElementById('lib-detail-tip-text');\n const aigenBtn = document.getElementById('lib-detail-aigen');\n const aigenLabel = document.getElementById('lib-detail-aigen-label');\n const historyEl = document.getElementById('lib-detail-history');\n const historyRowEl = document.getElementById('lib-detail-history-row');\n const historyCountEl = document.getElementById('lib-detail-history-count');\n const historyCurEl = document.getElementById('lib-detail-history-cur');\n const applyBtn = document.getElementById('lib-detail-apply');\n\n // 当前打开资产的状态(仅 isActor + missing tri 时启用)\n let _curCard = null;\n let _curName = '';\n let _versions = []; // [{ ts, label }]\n let _curIdx = -1;\n let _dirty = false; // 已生成但未保存\n let _generating = false;\n let _allowGen = false; // 是否启用生成入口(missing tri-view 才启用)\n\n function _renderHistory() {\n if (!_versions.length) { historyEl.style.display = 'none'; return; }\n historyEl.style.display = 'block';\n historyCountEl.textContent = _versions.length;\n historyCurEl.textContent = _versions[_curIdx]?.label || '';\n historyRowEl.innerHTML = _versions.map((v, i) =>\n '<div class=\"adh-thumb' + (i === _curIdx ? ' active' : '') + '\" data-idx=\"' + i + '\" title=\"' + v.label + ' · ' + v.ts + '\">' + v.label + '</div>'\n ).join('');\n historyRowEl.querySelectorAll('.adh-thumb').forEach(el => {\n el.addEventListener('click', () => {\n const i = Number(el.dataset.idx);\n if (i === _curIdx) return;\n _curIdx = i;\n _renderTriPreview();\n _renderHistory();\n });\n });\n }\n\n function _renderTriPreview() {\n if (_curIdx < 0) return;\n const ver = _versions[_curIdx];\n triEl.innerHTML = '<div class=\"placeholder\"><span class=\"ph-frame\">' + _curName + ' · 三视图(正/侧/背)· ' + ver.label + '</span><button class=\"ad-zoom-btn\" type=\"button\" data-zoom-tri aria-label=\"查看大图\" title=\"查看大图\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg></button></div>';\n triEl.querySelector('[data-zoom-tri]')?.addEventListener('click', e => {\n e.stopPropagation();\n if (window.Shell?._openLightbox) Shell._openLightbox('', _curName + ' · 三视图 · ' + ver.label);\n });\n }\n\n function _renderTriLoading() {\n triEl.innerHTML = '<div class=\"placeholder\" style=\"display:grid;place-items:center;gap:6px;\"><div class=\"spinner\" style=\"width:22px;height:22px;border:2px solid var(--border-faint);border-top-color:var(--heat);border-radius:50%;animation:ad-spin 1s linear infinite;\"></div><span class=\"ph-frame\" style=\"font-size:10.5px;\">生成中 · 约 12s</span></div>';\n }\n\n function _setAigenLabel(text, loading) {\n aigenLabel.textContent = text;\n aigenBtn.disabled = !!loading;\n tipEl.classList.toggle('is-loading', !!loading);\n }\n\n function _startGenerate() {\n if (!_allowGen || _generating) return;\n _generating = true;\n _setAigenLabel(_versions.length ? '生成中…' : '生成中…', true);\n _renderTriLoading();\n setTimeout(() => {\n _generating = false;\n const now = new Date();\n const ts = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');\n _versions.push({ ts, label: 'v' + (_versions.length + 1) });\n _curIdx = _versions.length - 1;\n _dirty = true;\n _renderTriPreview();\n _renderHistory();\n // 第一次生成后,按钮文案 → 再次生成;并隐藏 tip 文案,只留按钮在右侧\n tipEl.style.display = 'flex';\n tipTextEl.textContent = '已生成 ' + _versions.length + ' 版三视图 · 不满意可重跑,保存后写入资产';\n _setAigenLabel('再次生成', false);\n if (window.Shell?.toast) {\n Shell.toast('三视图已生成', _curName + ' · ' + _versions[_curIdx].label + ' · 满意请点「保存」');\n }\n }, 1500);\n }\n\n function _hash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }\n function _fmtAssetId(name, k) { return 'ASSET-20240520-' + (k === 'person' ? 'M' : k === 'scene' ? 'S' : 'P') + String(_hash(name) % 1000).padStart(3, '0'); }\n function _fmtSize(name) { return (4 + (_hash(name) % 100) / 10).toFixed(1) + 'MB'; }\n function _fmtFav(name) { return String(8 + _hash(name) % 80); }\n function _fmtDl(name) { const n = 200 + _hash(name) % 1800; return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }\n\n function open(card) {\n // 重置本次打开的状态\n _curCard = card;\n _versions = [];\n _curIdx = -1;\n _dirty = false;\n _generating = false;\n _allowGen = false;\n historyEl.style.display = 'none';\n tipEl.classList.remove('is-loading');\n _setAigenLabel('AI 生成三视图', false);\n\n const name = card.dataset.name || '资产';\n _curName = name;\n const used = card.dataset.used || '0';\n const source = card.dataset.source || '平台预设';\n let tagText = 'AI 素材', intro = '', tags = [], props = [], hasTri = false, isActor = false;\n\n if (card.dataset.gender) {\n tagText = '人物 · 模特';\n isActor = true; hasTri = true;\n intro = (card.dataset.role || '人物模特') + ' · 可用于本项目人物资产生成';\n tags = [card.dataset.gender, card.dataset.age, card.dataset.role].filter(Boolean);\n props = [\n ['性别', card.dataset.gender || '-'], ['种族', '东亚'], ['作品ID', _fmtAssetId(name, 'person')],\n ['年龄段', card.dataset.age || '-'], ['角色', card.dataset.role || '-'], ['创作人', 'Airshelf'],\n ['身高', '中等'], ['来源', source], ['文件大小', _fmtSize(name)],\n ['使用次数', used + ' 次'], ['授权', '商用'], ['发布时间', '2024-05-20'],\n ];\n } else if (card.dataset.sceneType) {\n tagText = '场景 · ' + card.dataset.sceneType;\n isActor = false; hasTri = false;\n intro = card.dataset.sceneType + ' 场景资产';\n tags = [card.dataset.sceneType, source].filter(Boolean);\n props = [\n ['场景类型', card.dataset.sceneType], ['来源', source], ['作品ID', _fmtAssetId(name, 'scene')],\n ['镜头', '通用'], ['光线', '自然光'], ['创作人', 'Airshelf'],\n ['用途', '本项目场景资产'], ['使用次数', used + ' 次'], ['文件大小', _fmtSize(name)],\n ];\n } else if (card.dataset.product) {\n tagText = '商品资产';\n isActor = false;\n intro = '关联商品: ' + card.dataset.product;\n tags = ['商品', source].filter(Boolean);\n props = [\n ['关联商品', card.dataset.product], ['来源', source], ['作品ID', _fmtAssetId(name, 'product')],\n ['用途', '商品资产'], ['使用次数', used + ' 次'], ['创作人', 'Airshelf'],\n ['授权', '商用'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],\n ];\n } else {\n tagText = 'AI 素材';\n isActor = false;\n intro = name;\n tags = [source].filter(Boolean);\n props = [\n ['名称', name], ['来源', source], ['作品ID', _fmtAssetId(name, 'asset')],\n ['创作人', 'Airshelf'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],\n ];\n }\n\n titleEl.textContent = name;\n kindEl.textContent = '/ ' + tagText;\n leadImg.innerHTML = '<span class=\"ph-frame\">' + name + '</span>';\n // 立绘 zoom 按钮(单次绑定 · 通过 name 闭包始终读最新 _curName)\n const _leadZoomBtn = document.getElementById('lib-detail-lead-zoom');\n if (_leadZoomBtn && !_leadZoomBtn.dataset.bound) {\n _leadZoomBtn.dataset.bound = '1';\n _leadZoomBtn.addEventListener('click', e => {\n e.stopPropagation();\n if (window.Shell?._openLightbox) Shell._openLightbox('', _curName || titleEl.textContent);\n });\n }\n\n // 平台预设资产 · 仅 1 张缩略图(用户上传多张的场景在 pipeline 工作台详情中处理)\n const _thumbLabel = card.dataset.sceneType ? '场景' : (isActor ? '立绘' : '主图');\n thumbsEl.innerHTML = `<div class=\"thumb placeholder active\"><span class=\"ph-frame\">${_thumbLabel}</span></div>`;\n\n // 卡片是否标注「缺三视图」(data-triview=\"0\")\n const cardMissingTri = card.dataset.triview === '0';\n\n if (card.dataset.sceneType) {\n triSection.style.display = 'none';\n } else if (isActor) {\n triSection.style.display = '';\n triEl.classList.remove('actor');\n ratioChip.textContent = '16:9';\n if (cardMissingTri) {\n // 人物 · 缺三视图 → 显示生成入口\n _allowGen = true;\n triEl.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>暂未生成三视图 · 点击右侧按钮 AI 生成</span></div>';\n tipTextEl.textContent = '暂无三视图,建议用 AI 生成以保证多角度一致性';\n _setAigenLabel('AI 生成三视图', false);\n tipEl.style.display = 'flex';\n } else {\n triEl.innerHTML = '<div class=\"placeholder\"><span class=\"ph-frame\">' + name + ' · 三视图 (正/侧/背)</span><button class=\"ad-zoom-btn\" type=\"button\" data-zoom-tri aria-label=\"查看大图\" title=\"查看大图\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg></button></div>';\n tipEl.style.display = 'none';\n }\n } else {\n triSection.style.display = '';\n triEl.classList.remove('actor');\n ratioChip.textContent = '16:9';\n if (hasTri && !cardMissingTri) {\n triEl.innerHTML = '<div class=\"placeholder\"><span class=\"ph-frame\">' + name + ' · 三视图</span><button class=\"ad-zoom-btn\" type=\"button\" data-zoom-tri aria-label=\"查看大图\" title=\"查看大图\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg></button></div>';\n tipEl.style.display = 'none';\n } else {\n // 商品/其他 · 缺三视图 → 同样启用生成入口\n _allowGen = true;\n triEl.innerHTML = '<div class=\"placeholder missing\" data-tri=\"0\"><svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/></svg><span>暂未生成三视图(16:9 单图)</span></div>';\n tipTextEl.textContent = '暂无三视图,建议用 AI 生成以保证多角度一致性';\n _setAigenLabel('AI 生成三视图', false);\n tipEl.style.display = 'flex';\n }\n }\n\n // 三视图 zoom 按钮 click\n triEl.querySelector('[data-zoom-tri]')?.addEventListener('click', e => {\n e.stopPropagation();\n if (window.Shell?._openLightbox) Shell._openLightbox('', name + ' · 三视图');\n });\n\n introEl.textContent = intro || '暂无简介';\n tagsEl.innerHTML = tags.map(t => '<span class=\"ad-tag-chip\">' + t + '</span>').join('') +\n '<button class=\"ad-tag-add\" type=\"button\" title=\"添加标签\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M8 3v10M3 8h10\"/></svg></button>';\n const tagAddBtn = tagsEl.querySelector('.ad-tag-add');\n tagAddBtn?.addEventListener('click', e => {\n e.stopPropagation();\n const existing = [...tagsEl.querySelectorAll('.ad-tag-chip')].map(el => el.textContent);\n const next = ['精选素材', '常用资产', '待复用', '已核准'].find(t => !existing.includes(t)) || '新标签';\n const chip = document.createElement('span');\n chip.className = 'ad-tag-chip';\n chip.textContent = next;\n tagsEl.insertBefore(chip, tagAddBtn);\n Shell.toast('已添加标签', next);\n });\n propsEl.innerHTML = props.map(([k, v]) => '<div class=\"ad-prop\"><span class=\"k\">' + k + '</span><span class=\"v\">' + v + '</span></div>').join('');\n\n bg.classList.add('show');\n }\n // ── 二次确认弹窗 ──\n const confirmBg = document.getElementById('lib-confirm-bg');\n const confirmCountEl = document.getElementById('lib-confirm-count');\n const confirmSaveBtn = document.getElementById('lib-confirm-save');\n const confirmDiscardBtn = document.getElementById('lib-confirm-discard');\n function _openConfirm() {\n confirmCountEl.textContent = _versions.length;\n confirmBg.classList.add('show');\n confirmBg.setAttribute('aria-hidden', 'false');\n }\n function _closeConfirm() {\n confirmBg.classList.remove('show');\n confirmBg.setAttribute('aria-hidden', 'true');\n }\n confirmBg.addEventListener('click', e => { if (e.target === confirmBg) _closeConfirm(); });\n\n function _doSave() {\n if (_curCard) {\n const badge = _curCard.querySelector('.tri-missing-badge');\n if (badge) badge.remove();\n _curCard.dataset.triview = '1';\n }\n _dirty = false;\n Shell.toast('已保存', _curName + ' · 三视图(' + _versions[_curIdx].label + ')已写入资产');\n }\n\n function close(force) {\n if (!force && _dirty) {\n _openConfirm();\n return;\n }\n bg.classList.remove('show');\n _dirty = false;\n }\n\n bg.addEventListener('click', e => { if (e.target === bg) close(); });\n document.getElementById('lib-detail-x').addEventListener('click', () => close());\n\n applyBtn.addEventListener('click', () => {\n if (_allowGen && _versions.length) {\n _doSave();\n close(true);\n return;\n }\n if (_allowGen && !_versions.length) {\n Shell.toast('请先生成三视图', '点击「AI 生成三视图」开始');\n return;\n }\n Shell.toast('已保存', _curName);\n close(true);\n });\n\n bg.querySelectorAll('.ad-icon-btn, .ad-stat-btn').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n Shell.toast('已加入下载', (_curName || '资产') + ' · 原图 / 三视图');\n });\n });\n\n // 弹窗按钮 · 主按钮「保存并退出」/ 次按钮「退出」\n confirmSaveBtn.addEventListener('click', () => {\n if (_versions.length) _doSave();\n _closeConfirm();\n close(true);\n });\n confirmDiscardBtn.addEventListener('click', () => {\n _dirty = false;\n _closeConfirm();\n close(true);\n });\n\n aigenBtn.addEventListener('click', _startGenerate);\n\n // Esc · 若确认弹窗开着,先关确认;否则尝试关详情(经过 dirty 检查)\n document.addEventListener('keydown', e => {\n if (e.key !== 'Escape') return;\n if (confirmBg.classList.contains('show')) { _closeConfirm(); return; }\n if (!bg.classList.contains('show')) return;\n close();\n });\n\n // 把所有 .asset-card 的旧 onclick=\"Shell.toast(...)\" 清掉,改成 open(card)\n document.querySelectorAll('.asset-card').forEach(card => {\n if (card.onclick) card.onclick = null;\n card.removeAttribute('onclick');\n card.style.cursor = 'pointer';\n card.addEventListener('click', e => {\n if (document.body.classList.contains('edit-mode')) return; // 编辑模式由更早 capture handler 处理\n if (e.target.closest('.card-del-btn')) return;\n open(card);\n });\n });\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"login": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"login.html\">\n<meta charset=\"utf-8\">\n<title>登录 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n body { margin: 0; min-height: 100vh; background: var(--background-base); display: grid; place-items: center; padding: 32px 24px; }\n\n .auth-wrap { width: 100%; max-width: 980px; display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; overflow: hidden; min-height: 560px; }\n\n /* 4 装订线 */\n .auth-wrap::before, .auth-wrap::after,\n .auth-wrap > .corner-tr, .auth-wrap > .corner-bl {\n content: ''; position: absolute; width: 14px; height: 14px;\n 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;\n background-size: contain; pointer-events: none; z-index: 2;\n }\n .auth-wrap::before { top: -7px; left: -7px; }\n .auth-wrap::after { bottom: -7px; right: -7px; }\n .auth-wrap > .corner-tr { top: -7px; right: -7px; }\n .auth-wrap > .corner-bl { bottom: -7px; left: -7px; }\n\n /* 左侧品牌区 · 深色 */\n .auth-brand { background: var(--accent-black); color: var(--accent-white); padding: 40px 44px; display: flex; flex-direction: column; position: relative; overflow: hidden; }\n .auth-brand .logo { display: flex; align-items: center; height: 56px; }\n .auth-brand .logo-img { display: block; width: 196px; height: auto; margin: -14px 0 -10px -14px; object-fit: contain; }\n .auth-brand .tag { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.5); letter-spacing: .08em; text-transform: uppercase; margin-top: 4px; }\n\n .auth-brand .hero { margin-top: auto; margin-bottom: auto; }\n .auth-brand .hero h1 { font-size: 30px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; margin: 0 0 14px; }\n .auth-brand .hero h1 .h { color: var(--heat); }\n .auth-brand .hero p { font-size: 13.5px; color: rgba(255,255,255,.62); line-height: 1.7; max-width: 320px; margin: 0; }\n\n /* ASCII 装饰行 */\n .auth-brand .ascii { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.32); letter-spacing: .04em; line-height: 1.8; margin-top: 28px; }\n .auth-brand .ascii .ln .k { color: rgba(255,255,255,.5); }\n .auth-brand .ascii .ln .v { color: var(--heat); }\n\n .auth-brand .foot { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.32); letter-spacing: .04em; margin-top: 22px; display: flex; gap: 14px; }\n .auth-brand .foot a { color: rgba(255,255,255,.5); text-decoration: none; }\n .auth-brand .foot a:hover { color: var(--heat); }\n\n /* 右侧表单区 */\n .auth-form { padding: 44px 44px 36px; display: flex; flex-direction: column; }\n .auth-form .h-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }\n .auth-form h2 { font-size: 22px; font-weight: 600; letter-spacing: -.012em; margin: 0; }\n .auth-form .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; }\n .auth-form .lead { font-size: 13px; color: var(--black-alpha-56); margin: 0 0 28px; }\n\n .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }\n .field-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }\n .field-label .req { color: var(--accent-crimson); }\n .field-input-wrap { position: relative; }\n .field-input-wrap .ic-l { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--black-alpha-32); width: 14px; height: 14px; pointer-events: none; }\n .field input { width: 100%; box-sizing: border-box; padding: 11px 12px 11px 36px; font-size: 13.5px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-family: inherit; color: var(--accent-black); transition: border-color var(--t-base), box-shadow var(--t-base); }\n .field input:focus { outline: none; border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }\n .field input::placeholder { color: var(--black-alpha-32); }\n .field .toggle-pwd { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--black-alpha-48); cursor: pointer; background: none; border: 0; padding: 0; display: grid; place-items: center; }\n .field .toggle-pwd:hover { color: var(--accent-black); }\n\n .row-between { display: flex; align-items: center; justify-content: space-between; margin: 4px 0 22px; font-size: 12.5px; }\n .row-between label { display: inline-flex; align-items: center; gap: 6px; color: var(--black-alpha-56); cursor: pointer; user-select: none; }\n .row-between label input { width: 13px; height: 13px; accent-color: var(--heat); }\n .row-between a { color: var(--heat); text-decoration: none; font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .02em; }\n .row-between a:hover { text-decoration: underline; }\n\n .btn-cta { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); padding: 12px 14px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: box-shadow var(--t-base), transform var(--t-base); display: inline-flex; align-items: center; justify-content: center; gap: 8px; }\n .btn-cta:hover { box-shadow: 0 4px 14px rgba(250,93,25,.28); }\n .btn-cta:active { transform: translateY(1px); }\n .btn-cta svg { width: 15px; height: 15px; }\n\n .divider { display: flex; align-items: center; gap: 10px; margin: 22px 0 18px; }\n .divider .line { flex: 1; height: 1px; background: var(--border-faint); }\n .divider .txt { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-32); letter-spacing: .08em; text-transform: uppercase; }\n\n .sso-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }\n .sso-btn { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px 12px; font-size: 12.5px; color: var(--accent-black); cursor: pointer; font-family: inherit; display: inline-flex; align-items: center; justify-content: center; gap: 8px; transition: border-color var(--t-base); }\n .sso-btn:hover { border-color: var(--black-alpha-32); }\n .sso-btn svg { width: 14px; height: 14px; }\n\n .switch-row { margin-top: auto; padding-top: 28px; text-align: center; font-size: 12.5px; color: var(--black-alpha-56); }\n .switch-row a { color: var(--heat); text-decoration: none; font-weight: 500; }\n .switch-row a:hover { text-decoration: underline; }\n\n /* 响应式 */\n @media (max-width: 820px) {\n .auth-wrap { grid-template-columns: 1fr; min-height: 0; }\n .auth-brand { padding: 32px 28px; }\n .auth-brand .ascii { display: none; }\n .auth-form { padding: 32px 28px; }\n }\n\n /* 顶部小返回链(可选) */\n .top-back { position: fixed; top: 20px; left: 24px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); text-decoration: none; letter-spacing: .04em; }\n .top-back:hover { color: var(--heat); }\n</style>\n</head>\n<body>\n<a class=\"top-back\" href=\"index.html\">← 返回工作台</a>\n\n<div class=\"auth-wrap\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n\n <!-- 左:品牌 -->\n <aside class=\"auth-brand\">\n <div class=\"logo\">\n <img class=\"logo-img\" src=\"/exact/assets/logo-dark.png\" alt=\"Airshelf\">\n </div>\n <div class=\"tag\">// SHORT-VIDEO COMMERCE PLATFORM</div>\n\n <div class=\"hero\">\n <h1>AI 全流程<br><span class=\"h\">短剧化</span>带货生成</h1>\n <p>商品 → AI 脚本 → 故事板 → Seedance 视频片段 → 一键导出 9:16 ≤60s 成片。</p>\n </div>\n\n <div class=\"ascii\">\n <div class=\"ln\"><span class=\"k\">// step·1</span> &nbsp; 脚本生成 &nbsp; &nbsp; <span class=\"v\">●●●●●</span></div>\n <div class=\"ln\"><span class=\"k\">// step·2</span> &nbsp; 基础资产 &nbsp; &nbsp; <span class=\"v\">●●●●○</span></div>\n <div class=\"ln\"><span class=\"k\">// step·3</span> &nbsp; 故事板 &nbsp; &nbsp; &nbsp; <span class=\"v\">●●●○○</span></div>\n <div class=\"ln\"><span class=\"k\">// step·4</span> &nbsp; 视频片段 &nbsp; &nbsp; <span class=\"v\">●●○○○</span></div>\n <div class=\"ln\"><span class=\"k\">// step·5</span> &nbsp; 拼接导出 &nbsp; &nbsp; <span class=\"v\">●○○○○</span></div>\n </div>\n\n <div class=\"foot\">\n <a href=\"#\">关于</a>\n <a href=\"#\">定价</a>\n <a href=\"#\">联系</a>\n <a href=\"#\">隐私</a>\n </div>\n </aside>\n\n <!-- 右:表单 -->\n <main class=\"auth-form\">\n <div class=\"h-row\">\n <h2>登录</h2>\n <span class=\"sub\">// /auth/login</span>\n </div>\n <p class=\"lead\">使用团队邀请邮箱登录,接受邀请后自动加入对应团队。</p>\n\n <form id=\"login-form\" autocomplete=\"off\" onsubmit=\"event.preventDefault(); doLogin();\">\n <div class=\"field\">\n <label class=\"field-label\" for=\"auth-email\">邮箱 <span class=\"req\">*</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z\"/><path d=\"m22 6-10 7L2 6\"/></svg>\n <input type=\"email\" id=\"auth-email\" placeholder=\"name@company.com\" value=\"li@shop.com\" required>\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\" for=\"auth-pwd\">密码 <span class=\"req\">*</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></svg>\n <input type=\"password\" id=\"auth-pwd\" placeholder=\"••••••••\" value=\"demo-1234\" required>\n <button type=\"button\" class=\"toggle-pwd\" aria-label=\"切换密码可见\" onclick=\"togglePwd()\">\n <svg id=\"pwd-eye\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n </button>\n </div>\n </div>\n\n <div class=\"row-between\">\n <label><input type=\"checkbox\" checked> 记住我 7 天</label>\n <a href=\"#\" onclick=\"event.preventDefault();_loginToast('已发送重置邮件','请到 li@shop.com 收件箱查看 · 链接 30 分钟有效');\">忘记密码?</a>\n </div>\n\n <button class=\"btn-cta\" type=\"submit\">\n 登录\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </button>\n\n <div class=\"divider\"><span class=\"line\"></span><span class=\"txt\">OR</span><span class=\"line\"></span></div>\n\n <div class=\"sso-row\">\n <button type=\"button\" class=\"sso-btn\" onclick=\"_loginToast('微信扫码','请在 60s 内用微信扫一扫完成授权 · 内测中,以邮箱登录为准');\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M9 4C5 4 2 6.5 2 9.5c0 1.7 1 3.2 2.5 4.2L4 16l2.2-1.1c.7.2 1.5.3 2.3.3-.3-.5-.5-1.1-.5-1.7 0-2.8 2.9-5 6.5-5h.5C14.6 6 12 4 9 4zm-2 3.5c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zm4 0c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zM15 9c-3.3 0-6 2.2-6 5s2.7 5 6 5c.7 0 1.4-.1 2-.3L19 20l-.4-1.7c1.4-.9 2.4-2.3 2.4-3.8 0-2.8-2.7-5-6-5zm-2 2.5c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1zm4 0c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1 .4-1 1-1z\"/></svg>\n 微信扫码\n </button>\n <button type=\"button\" class=\"sso-btn\" onclick=\"_loginToast('飞书 SSO','即将打开企业飞书登录授权页 · 内测中,以邮箱登录为准');\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"3\"/></svg>\n 飞书 SSO\n </button>\n </div>\n\n <div class=\"switch-row\">\n 还没账号? <a href=\"register.html\">注册团队 →</a>\n </div>\n </form>\n </main>\n</div>\n\n<script>\n function togglePwd() {\n const inp = document.getElementById('auth-pwd');\n inp.type = inp.type === 'password' ? 'text' : 'password';\n }\n function doLogin() {\n const btn = document.querySelector('.btn-cta');\n btn.innerHTML = '<span style=\"font-family:var(--font-mono);font-size:12px;letter-spacing:.04em;\">// 验证中...</span>';\n btn.disabled = true;\n setTimeout(() => { location.href = 'index.html'; }, 700);\n }\n // 轻量 toast(不依赖 shell.js,免登录页拉重)\n function _loginToast(t, sub) {\n let el = document.getElementById('__login-toast');\n if (!el) {\n el = document.createElement('div');\n el.id = '__login-toast';\n el.style.cssText = 'position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(20px);background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:12px 18px;box-shadow:0 8px 24px rgba(0,0,0,.12);display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .2s,transform .2s;z-index:9999;font-family:inherit;max-width:360px;';\n document.body.appendChild(el);\n }\n el.innerHTML = '<div style=\"font-size:13.5px;font-weight:600;color:#262626;\">' + t + '</div>' + (sub ? '<div style=\"font-size:11.5px;color:rgba(0,0,0,.56);font-family:\\'Inter\\',system-ui,sans-serif;letter-spacing:.02em;\">// ' + sub + '</div>' : '');\n requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%) translateY(0)'; });\n clearTimeout(el._t);\n el._t = setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(-50%) translateY(20px)'; }, 2800);\n }\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"messages": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"messages.html\">\n<meta charset=\"utf-8\">\n<title>消息中心 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .msg-page {\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n .msg-page .page-head {\n margin-bottom: 0;\n }\n .msg-head-actions {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n }\n .msg-workbench {\n display: grid;\n grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);\n min-height: 640px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow: hidden;\n }\n .msg-panel {\n min-width: 0;\n background: transparent;\n border: 0;\n border-radius: 0;\n overflow: hidden;\n }\n .msg-inbox,\n .msg-detail {\n display: flex;\n flex-direction: column;\n min-height: 0;\n }\n .msg-inbox { border-right: 1px solid var(--border-faint); }\n .msg-panel-h {\n min-height: 58px;\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 14px 16px;\n border-bottom: 1px solid var(--border-faint);\n }\n .msg-panel-h .ti {\n font-size: 13px;\n font-weight: 600;\n color: var(--accent-black);\n }\n .msg-panel-h .mono {\n margin-left: auto;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n }\n .msg-filters {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--border-faint);\n }\n .msg-filter {\n height: 30px;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 0 10px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n background: var(--surface);\n color: var(--black-alpha-56);\n font-family: inherit;\n font-size: 12px;\n cursor: pointer;\n }\n .msg-filter:hover {\n border-color: var(--black-alpha-24);\n color: var(--accent-black);\n background: var(--black-alpha-4);\n }\n .msg-filter.active {\n border-color: var(--heat-20);\n background: var(--heat-12);\n color: var(--heat);\n font-weight: 600;\n }\n .msg-filter .ct {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: .02em;\n }\n .msg-search {\n position: relative;\n padding: 0 14px 12px;\n border-bottom: 1px solid var(--border-faint);\n }\n .msg-search svg {\n position: absolute;\n left: 26px;\n top: 10px;\n width: 13px;\n height: 13px;\n color: var(--black-alpha-48);\n pointer-events: none;\n }\n .msg-search input {\n width: 100%;\n height: 34px;\n padding: 0 12px 0 32px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n background: var(--background-lighter);\n color: var(--accent-black);\n font-family: inherit;\n font-size: 13px;\n outline: none;\n }\n .msg-search input:focus {\n background: var(--surface);\n border-color: var(--heat-40);\n box-shadow: inset 0 0 0 1px var(--heat-40);\n }\n .msg-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n .msg-item {\n position: relative;\n width: 100%;\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr);\n gap: 10px;\n padding: 14px 16px;\n border: 0;\n border-bottom: 1px solid var(--border-faint);\n background: transparent;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n }\n .msg-item:hover { background: var(--black-alpha-4); }\n .msg-item.active { background: var(--heat-12); }\n .msg-item.active::before {\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 3px;\n background: var(--heat);\n }\n .msg-item.read .msg-item-title { color: var(--black-alpha-56); font-weight: 500; }\n .msg-type-ic {\n width: 30px;\n height: 30px;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n background: var(--background-lighter);\n color: var(--black-alpha-72);\n }\n .msg-type-ic svg { width: 14px; height: 14px; }\n .msg-type-ic.task { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }\n .msg-type-ic.team { background: var(--black-alpha-4); color: var(--accent-black); }\n .msg-type-ic.billing { background: var(--honey-bg); border-color: var(--honey-bd); color: var(--accent-honey); }\n .msg-type-ic.system { background: var(--black-alpha-7); color: var(--black-alpha-72); }\n .msg-item-main { min-width: 0; }\n .msg-item-row {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .msg-dot {\n width: 7px;\n height: 7px;\n border-radius: var(--r-pill);\n background: var(--heat);\n flex-shrink: 0;\n }\n .msg-item.read .msg-dot { display: none; }\n .msg-item-title {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--accent-black);\n font-size: 13px;\n font-weight: 600;\n }\n .msg-time {\n flex-shrink: 0;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .02em;\n }\n .msg-brief {\n margin-top: 4px;\n color: var(--black-alpha-56);\n font-size: 12px;\n line-height: 1.55;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n }\n .msg-item-foot {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 8px;\n }\n .msg-priority {\n display: inline-flex;\n align-items: center;\n height: 20px;\n padding: 0 7px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n background: var(--background-lighter);\n color: var(--black-alpha-56);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: .02em;\n }\n .msg-priority.ok { background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest); }\n .msg-priority.warn { background: var(--honey-bg); border-color: var(--honey-bd); color: var(--accent-honey); }\n .msg-priority.err { background: var(--crimson-bg); border-color: var(--crimson-bd); color: var(--accent-crimson); }\n .msg-priority.info { background: var(--heat-12); border-color: var(--heat-20); color: var(--heat); }\n .msg-empty {\n min-height: 320px;\n display: grid;\n place-items: center;\n gap: 8px;\n padding: 24px;\n color: var(--black-alpha-48);\n font-size: 13px;\n text-align: center;\n }\n .msg-empty svg { width: 24px; height: 24px; color: var(--black-alpha-40); }\n .msg-detail-empty {\n flex: 1;\n min-height: 520px;\n display: grid;\n place-items: center;\n gap: 8px;\n color: var(--black-alpha-48);\n text-align: center;\n }\n .msg-detail-empty .ic {\n width: 46px;\n height: 46px;\n display: grid;\n place-items: center;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n background: var(--background-lighter);\n }\n .msg-detail-empty svg { width: 21px; height: 21px; }\n .msg-detail-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 22px 24px 24px;\n }\n .msg-detail-top {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding-bottom: 18px;\n border-bottom: 1px solid var(--border-faint);\n }\n .msg-detail-title {\n min-width: 0;\n flex: 1;\n }\n .msg-detail-title h2 {\n margin: 0;\n font-size: 20px;\n line-height: 1.35;\n font-weight: 600;\n letter-spacing: -.012em;\n color: var(--accent-black);\n }\n .msg-detail-title .meta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 8px;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .04em;\n }\n .msg-body-text {\n margin: 18px 0 0;\n color: var(--accent-black);\n font-size: 14px;\n line-height: 1.75;\n }\n .msg-props {\n display: grid;\n grid-template-columns: 110px 1fr;\n gap: 10px 16px;\n margin-top: 18px;\n padding: 14px 16px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n background: var(--background-lighter);\n }\n .msg-props .k {\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .04em;\n }\n .msg-props .v {\n min-width: 0;\n color: var(--accent-black);\n font-size: 13px;\n }\n .msg-props .v a { color: var(--heat); }\n .msg-timeline {\n margin-top: 18px;\n padding: 16px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n }\n .msg-timeline-h {\n margin-bottom: 12px;\n color: var(--accent-black);\n font-size: 13px;\n font-weight: 600;\n }\n .msg-step {\n display: grid;\n grid-template-columns: 68px 1fr;\n gap: 12px;\n padding: 10px 0;\n border-top: 1px solid var(--border-faint);\n }\n .msg-step:first-of-type { border-top: 0; padding-top: 0; }\n .msg-step .t {\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .02em;\n }\n .msg-step .d {\n color: var(--black-alpha-72);\n font-size: 12.5px;\n line-height: 1.55;\n }\n .msg-log {\n margin-top: 14px;\n padding: 12px 14px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n background: var(--accent-black);\n color: var(--accent-white);\n font-family: var(--font-mono);\n font-size: 11px;\n line-height: 1.7;\n letter-spacing: .02em;\n white-space: pre-wrap;\n }\n .msg-detail-f {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 14px 16px;\n border-top: 1px solid var(--border-faint);\n background: var(--background-lighter);\n }\n .msg-detail-f .spacer { flex: 1; }\n .msg-foot-note {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .04em;\n }\n .msg-foot-note a { color: var(--heat); }\n @media (max-width: 1280px) {\n .msg-workbench { grid-template-columns: minmax(300px, 340px) minmax(0, 1fr); }\n }\n @media (max-width: 860px) {\n .msg-workbench { grid-template-columns: 1fr; }\n .msg-inbox { border-right: 0; border-bottom: 1px solid var(--border-faint); }\n .msg-list { max-height: 360px; }\n }\n</style>\n</head>\n<body>\n<div id=\"page\">\n <div class=\"msg-page\">\n <div class=\"page-head\">\n <div>\n <h1>消息中心</h1>\n <div class=\"sub\"><span class=\"mono\" id=\"msg-head-sub\">// INBOX</span> 任务提醒 · 团队协作 · 计费与系统公告</div>\n </div>\n <div class=\"msg-head-actions\">\n <button class=\"btn\" type=\"button\" id=\"msg-mark-all\">全部标已读</button>\n <button class=\"btn\" type=\"button\" id=\"msg-settings\">通知设置</button>\n </div>\n </div>\n\n <div class=\"msg-workbench\">\n <section class=\"msg-panel msg-inbox\">\n <div class=\"msg-panel-h\">\n <span class=\"ti\">收件箱</span>\n <span class=\"mono\" id=\"msg-list-count\">// 0 条</span>\n </div>\n <div class=\"msg-filters\" id=\"msg-filters\"></div>\n <div class=\"msg-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input id=\"msg-search\" type=\"text\" placeholder=\"搜索项目、来源、内容\">\n </div>\n <div class=\"msg-list\" id=\"msg-list\"></div>\n </section>\n\n <section class=\"msg-panel msg-detail\" id=\"msg-detail\"></section>\n </div>\n\n <div class=\"msg-foot-note\">\n <span>// 消息保留 90 天 · 高风险任务会同时进入工作台队列</span>\n <a href=\"settings.html#sec-notify\">管理通知策略 →</a>\n </div>\n </div>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({ active: '', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '消息中心' }] });\n\n(function () {\n const NOW = new Date('2026-05-28T10:30:00');\n const $ = sel => document.querySelector(sel);\n const icon = name => window.IconKit ? window.IconKit.svg(name) : '';\n const esc = s => String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '\"':'&quot;', \"'\":'&#39;' }[c]));\n const mins = n => new Date(NOW.getTime() - n * 60000);\n const zhType = { all: '全部', unread: '未读', task: '任务', team: '团队', billing: '计费', system: '系统' };\n const typeIcon = { task: 'clapperboard', team: 'users', billing: 'creditCard', system: 'info' };\n const priLabel = { ok: '已完成', warn: '需关注', err: '风险', info: '更新' };\n\n function fmtTime(d) {\n const diff = Math.round((NOW - d) / 60000);\n if (diff < 60) return diff + 'm';\n if (diff < 1440) return Math.floor(diff / 60) + 'h';\n return Math.floor(diff / 1440) + 'd';\n }\n function fmtFull(d) {\n const z = n => String(n).padStart(2, '0');\n return d.getFullYear() + '-' + z(d.getMonth() + 1) + '-' + z(d.getDate()) + ' ' + z(d.getHours()) + ':' + z(d.getMinutes());\n }\n\n const messages = [\n {\n id: 'task-video-ready', type: 'task', priority: 'ok', unread: true,\n title: '补水面膜 · 痛点种草 v3 成片已完成',\n brief: '7 镜 · 40 秒 · ¥18.40 已结算,可确认进入投放阶段。',\n body: 'Seedance 视频生成全部完成,本次输出 3 条 9:16 成片。系统检测到镜头 3 的停留率预测最高,建议优先作为首版投放素材。',\n source: '视频生成队列', project: '补水面膜', stage: 'Stage 5 · 拼接导出', owner: '李', cost: '¥18.40',\n time: mins(12), href: 'pipeline.html?product=' + encodeURIComponent('补水面膜'),\n log: 'render_id=vid-20260528-1030\\nstatus=ok\\nclips=7\\nratio=9:16\\ncharge=18.40',\n timeline: [['10:06', '脚本与镜头配置锁定'], ['10:14', '3 条视频完成生成'], ['10:26', '字幕/BGM 合成完成'], ['10:30', '成片已入库,等待确认投放']],\n actions: [{ id: 'goto', label: '进入项目', primary: true }, { id: 'preview', label: '查看成片' }, { id: 'rerun', label: '重跑视频' }],\n },\n {\n id: 'task-script-failed', type: 'task', priority: 'err', unread: true,\n title: '脚本生成失败 · 618 大促',\n brief: 'Prompt 超过服务限制,本次失败未扣费,可一键重试。',\n body: '脚本助手收到的商品卖点和参考文案过长,超过当前脚本模型单次上下文限制。系统已保存失败日志,并建议先压缩参考内容再重跑。',\n source: '脚本助手', project: '618 大促', stage: 'Stage 1 · 脚本', owner: '张', cost: '¥0.00',\n time: mins(64), href: 'pipeline.html?product=' + encodeURIComponent('618 大促'),\n log: 'request_id=script-413\\nerror=context_limit\\nprompt_tokens=8124\\nmax_tokens=8000\\ncharged=false',\n timeline: [['09:12', '提交脚本生成'], ['09:13', '模型返回 context_limit'], ['09:13', '费用回滚完成']],\n actions: [{ id: 'rerun', label: '重试生成', primary: true }, { id: 'log', label: '查看日志' }],\n },\n {\n id: 'task-asset-done', type: 'task', priority: 'ok', unread: true,\n title: '4 张模特上身图已加入资产库',\n brief: '祛痘精华 · 通勤白领 · 3:4,可直接进入视频项目。',\n body: '模特上身图通过基础审核,已同步到资产库「未分类」。建议把其中 2 张加入祛痘精华项目作为 Stage 3 镜头参考。',\n source: '图片生成', project: '祛痘精华', stage: '图片生成 · 模特上身图', owner: '陈', cost: '¥3.20',\n time: mins(140), href: 'library.html',\n timeline: [['08:05', '收到 2 张参考图'], ['08:08', '生成 4 张候选'], ['08:11', '通过筛选并写入资产库']],\n actions: [{ id: 'goto', label: '查看素材', primary: true }, { id: 'attach', label: '加入项目' }],\n },\n {\n id: 'team-edit-lock', type: 'team', priority: 'info', unread: true,\n title: '@刘 正在编辑「补水面膜」项目',\n brief: '你暂时以只读方式查看该项目,避免两人同时改动覆盖。',\n body: '系统检测到同项目正在被团队成员编辑。为避免脚本、镜头或资产配置被互相覆盖,后进入者会进入只读查看状态。编辑结束后会自动恢复操作权限。',\n source: '协作锁定', project: '补水面膜', stage: 'Stage 3 · 镜头', owner: '刘', cost: '-',\n time: mins(210), href: 'pipeline.html?product=' + encodeURIComponent('补水面膜') + '#stage-3',\n timeline: [['07:00', '刘进入项目编辑'], ['07:01', '系统为后进入成员开启只读状态']],\n actions: [{ id: 'goto', label: '查看项目', primary: true }, { id: 'ack', label: '我已了解' }],\n },\n {\n id: 'team-member', type: 'team', priority: 'info', unread: false,\n title: '@王芳 已加入团队',\n brief: '角色为运营,月限额 ¥800,可读写团队资产。',\n body: '新成员已完成邮箱验证。她可以查看并编辑团队项目,但不能调整计费、安全与团队角色设置。',\n source: '@李(超管)', project: '团队', stage: '成员管理', owner: '王', cost: '-',\n time: mins(320), href: 'team.html',\n timeline: [['05:02', '邀请邮件送达'], ['05:18', '成员完成验证'], ['05:19', '角色权限生效']],\n actions: [{ id: 'goto', label: '查看团队', primary: true }, { id: 'settings', label: '调整权限' }],\n },\n {\n id: 'billing-low', type: 'billing', priority: 'warn', unread: false,\n title: '团队余额低于预警线',\n brief: '当前余额 ¥87.20,按近 7 天速度预计可支撑 4 个视频项目。',\n body: '余额预警阈值为 ¥100。若今天继续批量生成视频,建议先充值或临时降低成员月限额。',\n source: '计费中心', project: '消费', stage: '余额监控', owner: '系统', cost: '¥87.20',\n time: mins(460), href: 'account.html',\n timeline: [['02:50', '余额低于 ¥100'], ['02:51', '已发送站内通知']],\n actions: [{ id: 'goto', label: '前往充值', primary: true }, { id: 'settings', label: '预警设置' }],\n },\n {\n id: 'system-maintenance', type: 'system', priority: 'info', unread: false,\n title: '视频生成服务今晚例行维护',\n brief: '23:00 - 01:00 期间提交任务会自动排队。',\n body: '维护期间已进入生成中的任务不受影响。新提交的视频生成、三视图生成会进入队列,维护结束后按提交时间继续处理。',\n source: 'Airshelf 运维', project: '系统', stage: '服务公告', owner: '系统', cost: '-',\n time: mins(720), href: '',\n timeline: [['昨天', '发布维护窗口'], ['今晚', '队列自动延后处理']],\n actions: [{ id: 'ack', label: '我已了解', primary: true }],\n },\n {\n id: 'system-review', type: 'system', priority: 'ok', unread: false,\n title: '祛痘精华首版素材通过平台审核',\n brief: '4 张图 / 2 条视频审核通过,可进入投放。',\n body: '素材合规审核耗时 1 小时 12 分钟。系统未发现夸大功效、违禁词或画面遮挡问题。',\n source: '审核中台', project: '祛痘精华', stage: 'Stage 5 · 投放', owner: '系统', cost: '-',\n time: mins(1120), href: 'pipeline.html?product=' + encodeURIComponent('祛痘精华'),\n timeline: [['昨天 16:40', '提交平台审核'], ['昨天 17:52', '审核通过'], ['昨天 17:53', '写入项目状态']],\n actions: [{ id: 'goto', label: '进入投放', primary: true }],\n },\n ];\n\n const state = { tab: 'all', q: '', selectedId: messages[0].id, showLogId: null };\n\n function counts() {\n return {\n unread: messages.filter(m => m.unread).length,\n task: messages.filter(m => m.type === 'task').length,\n team: messages.filter(m => m.type === 'team').length,\n billing: messages.filter(m => m.type === 'billing').length,\n system: messages.filter(m => m.type === 'system').length,\n risk: messages.filter(m => m.priority === 'err' || m.priority === 'warn').length,\n };\n }\n function visibleMessages() {\n const q = state.q.toLowerCase();\n return messages.filter(m => {\n if (state.tab === 'unread' && !m.unread) return false;\n if (!['all', 'unread'].includes(state.tab) && m.type !== state.tab) return false;\n if (q && ![m.title, m.brief, m.body, m.source, m.project, m.stage].join(' ').toLowerCase().includes(q)) return false;\n return true;\n });\n }\n\n function renderSummary() {\n const c = counts();\n $('#msg-head-sub').textContent = '// ' + c.unread + ' 条未读 · ' + messages.length + ' 条总计';\n }\n\n function renderFilters() {\n const c = counts();\n const filters = [\n ['all', '全部', messages.length],\n ['unread', '未读', c.unread],\n ['task', '任务', c.task],\n ['team', '团队', c.team],\n ['billing', '计费', c.billing],\n ['system', '系统', c.system],\n ];\n $('#msg-filters').innerHTML = filters.map(([id, label, ct]) => `\n <button class=\"msg-filter${state.tab === id ? ' active' : ''}\" type=\"button\" data-tab=\"${id}\">\n ${label}<span class=\"ct\">${ct}</span>\n </button>\n `).join('');\n $('#msg-filters').querySelectorAll('[data-tab]').forEach(btn => {\n btn.addEventListener('click', () => {\n state.tab = btn.dataset.tab;\n render();\n });\n });\n }\n\n function renderList() {\n const list = visibleMessages();\n $('#msg-list-count').textContent = '// 显示 ' + list.length + ' 条';\n if (!list.length) {\n $('#msg-list').innerHTML = `<div class=\"msg-empty\">${icon('search')}<span>没有符合条件的消息</span></div>`;\n return;\n }\n $('#msg-list').innerHTML = list.map(m => `\n <button class=\"msg-item${state.selectedId === m.id ? ' active' : ''}${m.unread ? '' : ' read'}\" type=\"button\" data-id=\"${esc(m.id)}\">\n <span class=\"msg-type-ic ${esc(m.type)}\">${icon(typeIcon[m.type] || 'info')}</span>\n <span class=\"msg-item-main\">\n <span class=\"msg-item-row\">\n <span class=\"msg-dot\"></span>\n <span class=\"msg-item-title\">${esc(m.title)}</span>\n <span class=\"msg-time\">${fmtTime(m.time)}</span>\n </span>\n <span class=\"msg-brief\">${esc(m.brief)}</span>\n <span class=\"msg-item-foot\">\n <span class=\"msg-priority ${esc(m.priority)}\">${esc(priLabel[m.priority] || '更新')}</span>\n <span class=\"msg-priority\">${esc(m.project)}</span>\n </span>\n </span>\n </button>\n `).join('');\n $('#msg-list').querySelectorAll('[data-id]').forEach(btn => {\n btn.addEventListener('click', () => {\n state.selectedId = btn.dataset.id;\n const msg = messages.find(m => m.id === state.selectedId);\n if (msg) msg.unread = false;\n render();\n });\n });\n }\n\n function renderDetail() {\n const m = messages.find(x => x.id === state.selectedId) || visibleMessages()[0] || messages[0];\n if (!m) {\n $('#msg-detail').innerHTML = `<div class=\"msg-detail-empty\"><div class=\"ic\">${icon('bell')}</div><div>暂无消息</div></div>`;\n return;\n }\n state.selectedId = m.id;\n const props = [\n ['来源', m.source],\n ['类别', zhType[m.type]],\n ['项目', m.project],\n ['阶段', m.stage],\n ['负责人', m.owner],\n ['费用', m.cost],\n ['时间', fmtFull(m.time)],\n ['关联资源', m.href ? `<a href=\"${esc(m.href)}\">${esc(m.project)} →</a>` : '无'],\n ].map(([k, v]) => `<span class=\"k\">${k}</span><span class=\"v\">${v}</span>`).join('');\n const actions = m.actions.map(a => `<button class=\"btn${a.primary ? ' btn-primary' : ''}\" type=\"button\" data-action=\"${esc(a.id)}\">${esc(a.label)}</button>`).join('');\n $('#msg-detail').innerHTML = `\n <div class=\"msg-detail-body\">\n <div class=\"msg-detail-top\">\n <span class=\"msg-type-ic ${esc(m.type)}\">${icon(typeIcon[m.type] || 'info')}</span>\n <div class=\"msg-detail-title\">\n <h2>${esc(m.title)}</h2>\n <div class=\"meta\"><span>${esc(m.source)}</span><span>// ${esc(zhType[m.type])}</span><span>${fmtFull(m.time)}</span></div>\n </div>\n <span class=\"msg-priority ${esc(m.priority)}\">${esc(priLabel[m.priority] || '更新')}</span>\n </div>\n <p class=\"msg-body-text\">${esc(m.body)}</p>\n <div class=\"msg-props\">${props}</div>\n <div class=\"msg-timeline\">\n <div class=\"msg-timeline-h\">处理记录</div>\n ${m.timeline.map(([t, d]) => `<div class=\"msg-step\"><span class=\"t\">${esc(t)}</span><span class=\"d\">${esc(d)}</span></div>`).join('')}\n </div>\n ${state.showLogId === m.id && m.log ? `<pre class=\"msg-log\">${esc(m.log)}</pre>` : ''}\n </div>\n <div class=\"msg-detail-f\">\n <button class=\"btn btn-ghost\" type=\"button\" data-action=\"archive\">归档</button>\n <button class=\"btn btn-ghost\" type=\"button\" data-action=\"mute\">静音同类</button>\n <span class=\"spacer\"></span>\n ${actions}\n </div>\n `;\n $('#msg-detail').querySelectorAll('[data-action]').forEach(btn => {\n btn.addEventListener('click', () => runAction(m, btn.dataset.action));\n });\n }\n\n function runAction(m, act) {\n if (!m) return;\n if (act === 'goto') {\n if (m.href) location.href = m.href;\n else location.href = 'settings.html#sec-notify';\n return;\n }\n if (act === 'preview') { Shell.toast('打开视频预览', m.project + ' · 成片'); return; }\n if (act === 'rerun') {\n m.priority = 'info';\n m.brief = '任务已重新排队,完成后会再次提醒。';\n Shell.toast('已重新排队', m.project + ' · ' + m.stage);\n render();\n return;\n }\n if (act === 'log') { state.showLogId = state.showLogId === m.id ? null : m.id; renderDetail(); return; }\n if (act === 'attach') { location.href = 'projects-new.html'; return; }\n if (act === 'settings') { location.href = 'settings.html#sec-notify'; return; }\n if (act === 'ack') { m.unread = false; Shell.toast('已确认', m.title); render(); return; }\n if (act === 'archive') {\n const i = messages.findIndex(x => x.id === m.id);\n if (i >= 0) messages.splice(i, 1);\n state.selectedId = messages[0]?.id || null;\n Shell.toast('已归档', m.title);\n render();\n return;\n }\n if (act === 'mute') { Shell.toast('已静音同类', zhType[m.type] + ' 类提醒可在设置恢复'); return; }\n }\n\n function render() {\n renderSummary();\n renderFilters();\n renderList();\n renderDetail();\n }\n\n $('#msg-search').addEventListener('input', e => {\n state.q = e.target.value.trim();\n renderList();\n });\n $('#msg-mark-all').addEventListener('click', () => {\n messages.forEach(m => { m.unread = false; });\n Shell.toast('已全部标为已读', messages.length + ' 条');\n render();\n });\n $('#msg-settings').addEventListener('click', () => { location.href = 'settings.html#sec-notify'; });\n\n render();\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"modelPhoto": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"model-photo.html\">\n<meta charset=\"utf-8\">\n<title>模特上身图 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* viewport-fit · 工作台铺满 (跟 demo-a 一致 · padding:0) */\n .app { height: 100vh; overflow: hidden; }\n main { display: flex; flex-direction: column; min-height: 0; }\n #page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }\n\n .mp-layout {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 260px 1fr;\n transition: grid-template-columns var(--t-base);\n }\n .mp-layout.side-collapsed { grid-template-columns: 0 1fr; }\n @media (max-width: 1280px) {\n .mp-layout { grid-template-columns: 240px 1fr; }\n .mp-layout.side-collapsed { grid-template-columns: 0 1fr; }\n }\n\n /* ─── 主区: flat 双区 (头部 + body) · 无 card · 用 border 分隔 ─── */\n .mp-main {\n display: flex; flex-direction: column;\n min-height: 0;\n overflow: hidden;\n }\n /* 主区头部 · toolbar 风格 (跟 image-optimize 一致) */\n .mp-main-h {\n flex-shrink: 0;\n position: relative; z-index: 20;\n display: flex; align-items: center; gap: 10px;\n padding: 12px 28px;\n border-bottom: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .mp-main-h .cur-title {\n display: flex; align-items: baseline; gap: 8px;\n min-width: 0; max-width: 50%;\n }\n .mp-main-h .cur-title .crumb {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n flex-shrink: 0;\n }\n .mp-main-h .cur-title .nm {\n font-size: 15px; font-weight: 600;\n color: var(--accent-black);\n letter-spacing: -.005em;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .mp-main-h .cur-title .nm.placeholder {\n font-weight: 400; font-size: 13px;\n color: var(--black-alpha-48);\n }\n .mp-main-h .spacer { flex: 1; }\n .mp-main-h .side-restore-btn {\n height: 32px;\n padding: 0 10px;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n }\n .mp-main-h .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }\n .mp-main-h .side-restore-btn[hidden] { display: none; }\n .mp-main-h .side-restore-btn svg { width: 14px; height: 14px; }\n .mp-main-h .search-btn {\n width: 32px; height: 32px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n cursor: pointer;\n display: grid; place-items: center;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .mp-main-h .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }\n .mp-main-h .search-btn svg { width: 14px; height: 14px; }\n .mp-main-h .tb-chip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 32px; padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .mp-main-h .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }\n .mp-main-h .tb-chip svg { width: 10px; height: 10px; opacity: .6; }\n .mp-main-h .tb-chip.active {\n background: var(--heat-12);\n border-color: var(--heat-40);\n color: var(--heat);\n }\n .mp-main-h .tb-chip.active svg { opacity: .8; }\n .mp-main-h .tb-chip.active .lbl::after {\n content: ':';\n margin: 0 2px 0 0;\n }\n\n /* search · 折叠图标态 + 展开输入框 */\n .mp-main-h .tb-search-wrap { display: flex; align-items: center; }\n .mp-main-h .tb-search-input {\n width: 0; height: 32px;\n padding: 0; margin: 0;\n border: 1px solid transparent;\n background: transparent;\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit; outline: none;\n transition: width var(--t-base), padding var(--t-base), border-color var(--t-base), margin var(--t-base), background var(--t-base);\n }\n .mp-main-h .tb-search-wrap.expanded .tb-search-input {\n width: 220px;\n padding: 0 10px;\n margin-left: 6px;\n background: var(--surface);\n border-color: var(--border-faint);\n }\n .mp-main-h .tb-search-wrap.expanded .tb-search-input:focus { border-color: var(--heat-40); }\n .mp-main-h .tb-search-wrap.expanded .search-btn { border-color: var(--heat-40); color: var(--heat); }\n\n /* dropdown · chip 下拉菜单 */\n .mp-main-h .tb-menu-wrap { position: relative; }\n .mp-main-h .tb-menu {\n position: absolute;\n top: calc(100% + 6px); right: 0;\n min-width: 160px; max-width: 260px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n box-shadow: 0 8px 24px rgba(0,0,0,.08);\n padding: 4px;\n z-index: 50;\n display: none;\n max-height: 320px; overflow-y: auto;\n }\n .mp-main-h .tb-menu-wrap.open .tb-menu { display: block; }\n .mp-main-h .tb-menu-item {\n display: flex; align-items: center; gap: 8px;\n width: 100%; padding: 7px 10px;\n background: transparent; border: 0;\n border-radius: 4px;\n font-size: 12.5px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer; text-align: left;\n transition: background var(--t-base), color var(--t-base);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .mp-main-h .tb-menu-item:hover { background: var(--background-lighter); color: var(--accent-black); }\n .mp-main-h .tb-menu-item.active {\n background: var(--heat-12); color: var(--heat); font-weight: 500;\n }\n .mp-main-h .tb-menu-empty {\n padding: 10px;\n font-size: 11.5px; color: var(--black-alpha-48);\n font-family: var(--font-mono); letter-spacing: .02em;\n text-align: center;\n }\n\n /* 批次被筛选隐藏 */\n .mp-result-batch[data-hidden=\"1\"] { display: none !important; }\n .mp-main-body {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 320px 1fr;\n }\n @media (max-width: 1280px) {\n .mp-main-body { grid-template-columns: 300px 1fr; }\n }\n /* 内部 form / preview 不再带 border + radius, 由 layout 用 border-right 分隔 */\n .mp-main-body > .mp-form {\n border: 0; border-radius: 0;\n border-right: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .mp-main-body > .mp-preview {\n border: 0; border-radius: 0;\n background: var(--background-base);\n }\n\n /* ─── 商品空间 (最左 · 单选 · 当前商品决定结果区批次) ─── */\n .mp-prod-space {\n background: var(--surface);\n border-right: 1px solid var(--border-faint);\n display: flex; flex-direction: column;\n min-height: 0;\n overflow: hidden;\n transition: opacity var(--t-base), transform var(--t-base);\n }\n .mp-layout.side-collapsed .mp-prod-space {\n opacity: 0;\n transform: translateX(-8px);\n pointer-events: none;\n }\n /* 顶部工具栏: 返回 + 折叠 (跟 image-optimize 视觉一致) */\n .mp-side-top {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 8px;\n padding: 14px 14px 10px;\n border-bottom: 1px solid var(--border-faint);\n }\n .mp-side-top .back-pill {\n display: inline-flex; align-items: center; gap: 6px;\n height: 34px; padding: 0 13px 0 11px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n color: var(--accent-black);\n font-size: 13px; font-weight: 500;\n font-family: inherit;\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .mp-side-top .back-pill:hover {\n background: var(--black-alpha-4);\n border-color: var(--black-alpha-24);\n color: var(--accent-black);\n }\n .mp-side-top .back-pill svg { width: 14px; height: 14px; }\n .mp-side-top .fold {\n margin-left: auto;\n width: 26px; height: 26px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--black-alpha-48); cursor: pointer;\n transition: background var(--t-base), color var(--t-base);\n }\n .mp-side-top .fold:hover { background: var(--black-alpha-4); color: var(--accent-black); }\n .mp-side-top .fold svg { width: 14px; height: 14px; }\n .mp-ps-h {\n flex-shrink: 0;\n padding: 12px 14px 10px;\n }\n /* 商品列表 头部 (// 商品空间 + 新建商品 主 CTA · 右上显眼橙色) */\n .mp-list-h {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 8px;\n padding: 4px 14px 10px;\n }\n .mp-list-h .mono {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .06em;\n text-transform: uppercase;\n }\n .mp-list-h .new-prod {\n margin-left: auto;\n height: 28px; padding: 0 12px 0 10px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--heat); color: #fff;\n border: 1px solid var(--heat);\n border-radius: var(--r-sm);\n font-size: 12px; font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n box-shadow:\n inset 0 -2px 4px rgba(250, 93, 25, 0.20),\n 0 1px 1px rgba(250, 93, 25, 0.12),\n 0 2px 4px rgba(250, 93, 25, 0.10);\n transition: filter var(--t-base), transform var(--t-fast), box-shadow var(--t-base);\n }\n .mp-list-h .new-prod:hover {\n filter: brightness(.96);\n box-shadow:\n inset 0 -2px 4px rgba(250, 93, 25, 0.20),\n 0 1px 1px rgba(250, 93, 25, 0.16),\n 0 4px 8px rgba(250, 93, 25, 0.20);\n }\n .mp-list-h .new-prod:active { transform: scale(.98); }\n .mp-list-h .new-prod svg { width: 12px; height: 12px; }\n /* 搜索框 (spec §10.3 · icon z-index:2) */\n .mp-ps-search {\n position: relative;\n height: 32px;\n }\n .mp-ps-search svg {\n position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n width: 13px; height: 13px;\n color: var(--black-alpha-48);\n z-index: 2;\n pointer-events: none;\n }\n .mp-ps-search input {\n width: 100%; height: 100%;\n padding: 0 10px 0 30px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit;\n outline: none;\n transition: border-color var(--t-base), background var(--t-base);\n }\n .mp-ps-search input:focus { border-color: var(--heat-40); background: var(--surface); }\n .mp-ps-search input::placeholder { color: var(--black-alpha-48); }\n /* 商品列表 (flex:1 占据中部所有剩余空间) */\n .mp-ps-list {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 4px 10px 10px;\n display: flex; flex-direction: column; gap: 4px;\n }\n .mp-ps-empty {\n padding: 24px 14px;\n text-align: center;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n line-height: 1.7;\n }\n .mp-prod-item {\n display: flex; align-items: center; gap: 10px;\n padding: 8px;\n border: 1px solid transparent;\n border-radius: var(--r-sm);\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .mp-prod-item:hover { background: var(--black-alpha-4); }\n .mp-prod-item.active { background: var(--heat-12); border-color: var(--heat-20); }\n .mp-prod-item .thumb {\n flex-shrink: 0;\n width: 36px; height: 36px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden;\n }\n .mp-prod-item.active .thumb { border-color: var(--heat); }\n .mp-prod-item .body { flex: 1; min-width: 0; }\n .mp-prod-item .nm {\n font-size: 12.5px;\n color: var(--accent-black); font-weight: 500;\n line-height: 1.3;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .mp-prod-item.active .nm { color: var(--heat); font-weight: 600; }\n .mp-prod-item .sub {\n margin-top: 2px;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n /* 全部商品入口 (贴底) */\n .mp-ps-all {\n flex-shrink: 0;\n margin: 0 10px 12px;\n padding: 9px 12px;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n display: flex; align-items: center; gap: 8px;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .mp-ps-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .mp-ps-all .ct {\n margin-left: auto;\n color: var(--black-alpha-48);\n font-family: var(--font-mono); font-size: 10.5px;\n }\n .mp-ps-all:hover .ct { color: var(--heat); }\n .mp-ps-all svg { width: 12px; height: 12px; }\n\n /* ─── 左栏 · 表单 ─── */\n .mp-form {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow-y: auto;\n padding: 18px 20px;\n display: flex; flex-direction: column;\n }\n .mp-step { margin-bottom: 22px; }\n .mp-step:last-child { margin-bottom: 0; }\n .mp-step-h {\n display: flex; align-items: center; gap: 8px;\n margin-bottom: 12px;\n }\n .mp-step-h .num {\n width: 22px; height: 22px;\n border-radius: 50%;\n background: var(--heat-12); color: var(--heat);\n font-family: var(--font-mono); font-size: 11px;\n font-weight: 700;\n display: grid; place-items: center;\n flex-shrink: 0;\n }\n .mp-step-h .title { font-size: 14px; font-weight: 600; color: var(--accent-black); }\n .mp-step-h .right { margin-left: auto; font-size: 12px; color: var(--heat); cursor: pointer; }\n .mp-step-h .right:hover { text-decoration: underline; }\n\n /* 商品选择器 · 已选 chip 列表 */\n .prod-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 6px; }\n .prod-row {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 10px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n transition: background var(--t-base), border-color var(--t-base);\n }\n .prod-row .thumb {\n width: 28px; height: 28px;\n flex-shrink: 0;\n border-radius: var(--r-sm);\n }\n .prod-row .info { flex: 1; min-width: 0; }\n .prod-row .nm {\n font-size: 12.5px; color: var(--accent-black); font-weight: 500;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .prod-row .meta {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n margin-top: 2px;\n }\n .prod-row .x {\n width: 22px; height: 22px;\n display: grid; place-items: center;\n background: transparent; border: 0;\n border-radius: var(--r-sm);\n cursor: pointer;\n color: var(--black-alpha-48);\n flex-shrink: 0;\n transition: background var(--t-base), color var(--t-base);\n }\n .prod-row .x:hover { background: var(--black-alpha-8); color: var(--accent-crimson); }\n .prod-row .x svg { width: 12px; height: 12px; }\n .prod-row .swap {\n width: 22px; height: 22px;\n display: grid; place-items: center;\n background: transparent; border: 0;\n border-radius: var(--r-sm);\n cursor: pointer;\n color: var(--black-alpha-48);\n flex-shrink: 0;\n transition: background var(--t-base), color var(--t-base);\n }\n .prod-row .swap:hover { background: var(--heat-12); color: var(--heat); }\n .prod-row .swap svg { width: 13px; height: 13px; }\n .prod-empty {\n padding: 14px 10px;\n text-align: center;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-48);\n font-size: 12px;\n margin-bottom: 6px;\n }\n .prod-empty .mono {\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .02em;\n margin-top: 4px;\n }\n .prod-add {\n display: flex; align-items: center; justify-content: center; gap: 6px;\n padding: 8px 10px;\n background: transparent;\n border: 1px dashed var(--heat-40);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-size: 12.5px;\n color: var(--heat);\n font-family: inherit;\n transition: background var(--t-base);\n width: 100%;\n }\n .prod-add:hover { background: var(--heat-12); }\n .prod-add svg { width: 12px; height: 12px; }\n .prod-add[hidden] { display: none; }\n\n /* 商品库全屏弹窗 (复用 ml-modal-bg 结构) */\n .pl-modal-bg {\n position: fixed; inset: 0;\n background: var(--surface);\n z-index: 998;\n display: none;\n }\n .pl-modal-bg.show { display: flex; }\n .pl-modal {\n margin: 0;\n flex: 1;\n background: var(--surface);\n overflow: hidden;\n display: flex; flex-direction: column;\n }\n .pl-modal-h {\n display: flex; align-items: center; gap: 14px;\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n flex-shrink: 0;\n }\n .pl-modal-h h2 { font-size: 16px; font-weight: 600; }\n .pl-modal-h .ct {\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n }\n .pl-modal-h .actions { margin-left: auto; display: flex; gap: 10px; }\n .pl-modal-body {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 200px 1fr;\n }\n .pl-side {\n border-right: 1px solid var(--border-faint);\n padding: 18px 0;\n overflow-y: auto;\n }\n .pl-side .pl-side-h {\n padding: 0 20px 8px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .06em;\n }\n .pl-side .pl-side-item {\n display: flex; align-items: center; gap: 8px;\n padding: 9px 20px;\n cursor: pointer;\n color: var(--black-alpha-72);\n font-size: 13px;\n border-left: 3px solid transparent;\n transition: background var(--t-base), color var(--t-base);\n }\n .pl-side .pl-side-item:hover { background: var(--black-alpha-4); }\n .pl-side .pl-side-item.active {\n background: var(--heat-12);\n color: var(--accent-black);\n border-left-color: var(--heat);\n font-weight: 600;\n }\n .pl-side .pl-side-item .ct {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48);\n }\n .pl-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; }\n .pl-toolbar {\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n display: flex; align-items: center; gap: 12px;\n flex-shrink: 0;\n }\n .pl-toolbar .search {\n position: relative; flex: 1; max-width: 360px;\n }\n .pl-toolbar .search input {\n width: 100%; height: 32px;\n padding: 0 10px 0 32px;\n background: var(--background-lighter);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n font-size: 12.5px;\n font-family: inherit;\n color: var(--accent-black);\n outline: none;\n }\n .pl-toolbar .search svg {\n position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n width: 14px; height: 14px; color: var(--black-alpha-48);\n }\n .pl-toolbar .btn-new {\n height: 32px;\n padding: 0 14px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--surface);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n color: var(--accent-black);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n margin-left: auto;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pl-toolbar .btn-new:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }\n .pl-toolbar .btn-new svg { width: 13px; height: 13px; }\n .pl-scroll {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 20px 28px 28px;\n }\n .pl-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: 12px;\n }\n .pl-card {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 10px;\n cursor: pointer;\n display: flex; flex-direction: column; gap: 6px;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pl-card:hover { background: var(--surface); }\n .pl-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .pl-card .pl-thumb {\n aspect-ratio: 1;\n border-radius: var(--r-sm);\n }\n .pl-card .pl-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }\n .pl-card .pl-meta {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .pl-card .pl-check {\n position: absolute; top: 16px; right: 16px;\n width: 22px; height: 22px;\n background: rgba(255,255,255,.95);\n border: 1.5px solid var(--black-alpha-24);\n border-radius: 50%;\n display: grid; place-items: center;\n z-index: 2;\n color: var(--accent-white);\n }\n .pl-card .pl-check svg { width: 11px; height: 11px; opacity: 0; }\n .pl-card.selected .pl-check { background: var(--heat); border-color: var(--heat); }\n .pl-card.selected .pl-check svg { opacity: 1; }\n /* 卡片右上 actions: 编辑 + 删除 (hover 显示, check 左侧) */\n .pl-card .pl-card-actions {\n position: absolute;\n top: 14px; right: 44px;\n display: flex; gap: 4px;\n z-index: 2;\n opacity: 0;\n transition: opacity var(--t-base);\n }\n .pl-card:hover .pl-card-actions { opacity: 1; }\n .pl-card .pl-act {\n width: 26px; height: 26px;\n background: rgba(255,255,255,.95);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n cursor: pointer;\n color: var(--black-alpha-72);\n transition: color var(--t-base), border-color var(--t-base), background var(--t-base);\n }\n .pl-card .pl-act:hover { color: var(--heat); border-color: var(--heat); background: var(--surface); }\n .pl-card .pl-act.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }\n .pl-card .pl-act svg { width: 12px; height: 12px; }\n .pl-modal-f {\n padding: 14px 28px;\n border-top: 1px solid var(--border-faint);\n display: flex; justify-content: flex-end; align-items: center; gap: 10px;\n flex-shrink: 0;\n }\n .pl-modal-f .summary {\n margin-right: auto;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--black-alpha-56);\n letter-spacing: .02em;\n }\n .pl-modal-f .summary b { color: var(--heat); font-weight: 700; }\n\n /* 模特选择 · 矩形卡 多选 */\n .model-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 8px;\n }\n .model-card {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 8px;\n cursor: pointer;\n display: flex; flex-direction: column; gap: 6px;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .model-card:hover { background: var(--surface); }\n .model-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .model-card .m-thumb {\n aspect-ratio: 3/4;\n border-radius: var(--r-sm);\n cursor: pointer;\n position: relative;\n }\n .model-card .m-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }\n .model-card .m-tag {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .model-card .m-check {\n position: absolute; top: 14px; right: 14px;\n width: 22px; height: 22px;\n background: rgba(255,255,255,.95);\n border: 1.5px solid var(--black-alpha-24);\n border-radius: 50%;\n display: grid; place-items: center;\n color: var(--accent-white);\n z-index: 2;\n }\n .model-card .m-check svg { width: 11px; height: 11px; opacity: 0; }\n .model-card.selected .m-check { background: var(--heat); border-color: var(--heat); }\n .model-card.selected .m-check svg { opacity: 1; }\n .model-upload {\n grid-column: 1 / -1;\n aspect-ratio: 2/1;\n border: 1.5px dashed var(--heat-40);\n background: var(--heat-12);\n border-radius: var(--r-md);\n display: flex; align-items: center; justify-content: center; gap: 6px;\n cursor: pointer;\n color: var(--heat);\n font-size: 12.5px;\n transition: background var(--t-base);\n }\n .model-upload:hover { background: var(--heat-20); }\n .model-upload svg { width: 14px; height: 14px; }\n\n /* 单选 pill */\n .pill-row { display: flex; gap: 6px; }\n .pill-row .opt {\n flex: 1;\n height: 32px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12.5px;\n cursor: pointer;\n font-family: inherit;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .pill-row .opt:hover { color: var(--accent-black); }\n .pill-row .opt.active {\n background: var(--heat-12);\n color: var(--heat);\n border-color: var(--heat-40);\n font-weight: 600;\n }\n\n /* 设置 · 子项 */\n .mp-sub-h {\n font-size: 12px; color: var(--black-alpha-48);\n margin-bottom: 6px;\n font-family: var(--font-mono); letter-spacing: .02em;\n }\n .mp-sub { margin-bottom: 12px; }\n .mp-sub:last-child { margin-bottom: 0; }\n\n /* 左栏底部 · 立即生成 */\n .mp-cta {\n margin-top: auto;\n padding-top: 14px;\n }\n .mp-cta .btn-gen {\n width: 100%;\n justify-content: center;\n padding: 12px;\n font-size: 14px;\n font-weight: 600;\n }\n .mp-cta .btn-gen:disabled,\n .mp-cta .btn-gen.disabled {\n opacity: .45; cursor: not-allowed; pointer-events: none;\n }\n .mp-cta-hint {\n margin-top: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n text-align: center;\n line-height: 1.5;\n }\n\n /* ─── 右栏 · 预览 ─── */\n .mp-preview {\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 18px 22px;\n display: flex; flex-direction: column;\n overflow-y: auto;\n }\n /* prompt-style summary 卡片 (引号 icon + 灰底 + 右上 meta) */\n .mp-pv-h {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 14px 18px 14px 44px;\n margin-bottom: 14px;\n }\n .mp-pv-h .quote-icon {\n position: absolute;\n top: 13px; left: 16px;\n width: 18px; height: 18px;\n color: var(--black-alpha-24);\n }\n .mp-pv-h .pv-meta {\n float: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n line-height: 1.5;\n }\n .mp-pv-h .pv-meta b { color: var(--accent-black); font-weight: 600; }\n .mp-pv-h .pv-line {\n font-size: 13px;\n color: var(--accent-black);\n line-height: 1.6;\n display: flex; align-items: center;\n }\n .mp-pv-h .pv-line .k {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n margin-right: 8px;\n min-width: 36px;\n }\n .mp-pv-h .pv-line .v {\n font-weight: 500;\n }\n .mp-pv-h .pv-line .swap {\n margin-left: 10px;\n font-size: 11.5px; color: var(--heat);\n cursor: pointer;\n }\n .mp-pv-h .pv-line .swap:hover { text-decoration: underline; }\n\n .mp-pv-grid {\n flex: 1;\n display: flex; flex-direction: column;\n gap: 18px;\n overflow-y: auto;\n padding-right: 4px;\n align-content: start;\n }\n .mp-result-batch { display: flex; flex-direction: column; gap: 10px; }\n .mp-batch-head {\n display: flex; align-items: center; gap: 8px;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-56); letter-spacing: .04em;\n }\n .mp-batch-head .lab {\n font-family: var(--font-mono); font-size: 10.5px;\n padding: 2px 8px; border-radius: var(--r-pill);\n background: var(--heat-12); color: var(--heat); font-weight: 600;\n }\n .mp-batch-head .lab.gen { background: var(--background-lighter); color: var(--black-alpha-56); }\n .mp-batch-head .lab.rerun { background: var(--heat-12); color: var(--heat); }\n .mp-batch-head .sep { color: var(--black-alpha-32); }\n .mp-result-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 12px;\n align-content: start;\n }\n @media (max-width: 1400px) {\n .mp-result-grid { gap: 10px; }\n }\n /* 结果卡片 · 与图片创作 .io-cell 视觉一致 */\n .mp-result {\n position: relative;\n aspect-ratio: 3/4;\n border-radius: var(--r-md);\n overflow: hidden;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n cursor: pointer;\n transition: border-color var(--t-base);\n }\n .mp-result:hover { border-color: var(--black-alpha-32); }\n .mp-result .mp-r-thumb {\n position: absolute; inset: 0;\n }\n .mp-result .mp-r-thumb .ph-frame {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32); letter-spacing: .02em;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .mp-result.gen .mp-r-thumb .ph-frame { animation: mp-pulse 1.4s ease-in-out infinite; }\n @keyframes mp-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: .55; }\n }\n .mp-result.err { border-color: var(--accent-crimson, #c43d3d); }\n .mp-result.err .mp-r-thumb .ph-frame {\n color: var(--accent-crimson, #c43d3d);\n background: rgba(196, 61, 61, .05);\n }\n /* 右上 hover 操作组 · 同 spec §4.18 .gen-image-actions (整体容器 + 透明按钮) */\n .mp-result .cell-ops {\n position: absolute; top: 8px; right: 8px;\n display: flex; gap: 2px;\n padding: 2px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 2px 8px rgba(0,0,0,.08);\n opacity: 0;\n transition: opacity var(--t-base);\n z-index: 2;\n }\n .mp-result:hover .cell-ops { opacity: 1; }\n .mp-result .cell-ops button {\n width: 28px; height: 28px;\n background: transparent;\n border: 0;\n border-radius: 6px;\n color: var(--black-alpha-56);\n cursor: pointer;\n display: grid; place-items: center;\n transition: background var(--t-base), color var(--t-base);\n }\n .mp-result .cell-ops button:hover {\n background: var(--black-alpha-4);\n color: var(--accent-black);\n }\n .mp-result .cell-ops button svg { width: 14px; height: 14px; }\n /* 中央采用反馈 · 同 spec §4.18 .gen-image-feedback */\n .mp-result .cell-feedback {\n position: absolute; inset: 0;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;\n background: rgba(38, 38, 38, .88);\n color: var(--accent-white);\n border-radius: var(--r-md);\n opacity: 0;\n pointer-events: none;\n transition: opacity .2s var(--t-base, ease);\n z-index: 3;\n }\n .mp-result.show-feedback .cell-feedback { opacity: 1; }\n .mp-result .cell-feedback svg { width: 20px; height: 20px; }\n .mp-result .cell-feedback span { font-size: 12.5px; font-weight: 500; letter-spacing: .02em; }\n /* 更多 · 下拉气泡 */\n .mp-result .cell-more-wrap { position: relative; }\n .mp-result .cell-more-menu {\n position: absolute; top: calc(100% + 4px); right: 0;\n min-width: 132px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .mp-result .cell-more-wrap.open .cell-more-menu { display: block; }\n .mp-result .cell-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n backdrop-filter: none !important;\n height: auto !important;\n justify-content: flex-start !important;\n }\n .mp-result .cell-more-menu button:hover {\n background: var(--background-lighter) !important;\n color: var(--heat) !important;\n }\n .mp-result .cell-more-menu button.danger:hover {\n color: var(--accent-crimson) !important;\n background: var(--crimson-bg, #fdebea) !important;\n }\n .mp-result .cell-more-menu button svg { width: 13px !important; height: 13px !important; }\n /* 已采用角标 (保留 · 仅状态指示, hover overlay 仍可见) */\n .mp-result .adopt-badge {\n position: absolute; top: 8px; left: 8px;\n background: var(--accent-forest, #42c366);\n color: #fff;\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n letter-spacing: .04em;\n padding: 2px 8px;\n border-radius: var(--r-pill);\n z-index: 3;\n display: none;\n }\n .mp-result.adopted .adopt-badge { display: inline-block; }\n\n /* 每个批次下方的批量操作 (胶囊按钮组 · 左对齐) */\n .mp-pv-batch {\n margin-top: 4px;\n display: flex; gap: 10px; align-items: center; justify-content: flex-start;\n }\n .mp-pv-batch[hidden] { display: none; }\n .mp-result-batch .mp-pv-batch.batch-foot { margin-top: 0; }\n\n /* 预览区空态 (新任务且未生成) */\n .mp-pv-empty {\n flex: 1;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n text-align: center;\n padding: 40px 24px;\n gap: 6px;\n }\n .mp-pv-empty[hidden] { display: none; }\n .mp-pv-empty .mono {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .06em;\n margin-bottom: 4px;\n }\n .mp-pv-empty .title {\n font-size: 14px;\n font-weight: 600;\n color: var(--accent-black);\n }\n .mp-pv-empty .hint {\n font-size: 12.5px;\n color: var(--black-alpha-48);\n line-height: 1.6;\n max-width: 320px;\n }\n .mp-pv-empty .hint b { color: var(--heat); font-weight: 600; }\n /* 预览区 hidden 时收起所有内容元素 */\n #pv-summary[hidden], #pv-grid[hidden], #pv-foot[hidden] { display: none; }\n .mp-pv-batch .summary {\n margin-right: auto;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .mp-pv-batch .summary b { color: var(--heat); font-weight: 700; }\n /* 底栏按钮 · 同 spec §4.18 + image-optimize .io-msg-ops button */\n .mp-pv-batch .pill-btn {\n height: 30px;\n padding: 0 12px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n color: var(--accent-black);\n font-family: inherit; font-size: 12.5px;\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .mp-pv-batch .pill-btn:hover {\n border-color: var(--heat-20); color: var(--heat); background: var(--heat-12);\n }\n .mp-pv-batch .pill-btn svg { width: 13px; height: 13px; }\n .mp-pv-batch .pill-btn.icon { width: 30px; padding: 0; justify-content: center; }\n /* 批次更多气泡 (全部加入资产库 / 删除该批结果) */\n .mp-pv-batch .batch-more-wrap { position: relative; display: inline-flex; }\n .mp-pv-batch .batch-more-menu {\n position: absolute; bottom: calc(100% + 6px); right: 0;\n min-width: 168px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .mp-pv-batch .batch-more-wrap.open .batch-more-menu { display: block; }\n .mp-pv-batch .batch-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n height: auto !important;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n text-align: left;\n justify-content: flex-start !important;\n cursor: pointer; font-family: inherit;\n }\n .mp-pv-batch .batch-more-menu button:hover {\n background: var(--background-lighter) !important;\n color: var(--heat) !important;\n }\n .mp-pv-batch .batch-more-menu button.danger:hover {\n color: var(--accent-crimson) !important;\n background: var(--crimson-bg, #fdebea) !important;\n }\n .mp-pv-batch .batch-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }\n\n .mp-pv-foot {\n margin-top: 14px;\n padding-top: 12px;\n border-top: 1px solid var(--border-faint);\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--black-alpha-56);\n letter-spacing: .02em;\n line-height: 1.6;\n }\n .mp-pv-foot a { color: var(--heat); }\n\n @media (max-width: 1100px) {\n .mp-layout { grid-template-columns: 1fr; }\n }\n\n /* ─── 模特库全屏弹窗 (无遮罩,自适应铺满) ─── */\n .ml-modal-bg {\n position: fixed; inset: 0;\n background: var(--surface);\n z-index: 999;\n display: none;\n }\n .ml-modal-bg.show { display: flex; }\n .ml-modal {\n margin: 0;\n flex: 1;\n background: var(--surface);\n border-radius: 0;\n overflow: hidden;\n display: flex; flex-direction: column;\n }\n .ml-modal-h {\n display: flex; align-items: center;\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n flex-shrink: 0;\n }\n .ml-modal-h h2 { font-size: 16px; font-weight: 600; }\n .ml-modal-h .x {\n margin-left: auto;\n width: 32px; height: 32px;\n display: grid; place-items: center;\n background: transparent;\n border: 0; border-radius: var(--r-sm);\n cursor: pointer;\n color: var(--black-alpha-56);\n transition: background var(--t-base), color var(--t-base);\n }\n .ml-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }\n .ml-modal-h .x svg { width: 16px; height: 16px; }\n .ml-modal-body {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 200px 1fr;\n }\n .ml-side {\n border-right: 1px solid var(--border-faint);\n padding: 18px 0;\n overflow-y: auto;\n }\n .ml-side .ml-side-h {\n padding: 0 20px 8px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .06em;\n }\n .ml-side .ml-side-item {\n display: flex; align-items: center; gap: 8px;\n padding: 9px 20px;\n cursor: pointer;\n color: var(--black-alpha-72);\n font-size: 13px;\n border-left: 3px solid transparent;\n transition: background var(--t-base), color var(--t-base);\n }\n .ml-side .ml-side-item:hover { background: var(--black-alpha-4); }\n .ml-side .ml-side-item.active {\n background: var(--heat-12);\n color: var(--accent-black);\n border-left-color: var(--heat);\n font-weight: 600;\n }\n .ml-side .ml-side-item .ct {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48);\n }\n .ml-main {\n overflow-y: auto;\n padding: 0;\n display: flex; flex-direction: column;\n position: relative; /* anchor for .ml-canvas overlay */\n }\n\n /* 添加模特 · 工作台画布 · 覆盖 .ml-main 的整个区域,自带展开/收起动效 */\n .ml-canvas {\n position: absolute; inset: 0;\n z-index: 10;\n background: var(--background-base);\n display: flex; flex-direction: column;\n opacity: 0; visibility: hidden;\n transform: scale(.94);\n transform-origin: 32px 80px; /* 从左上「添加模特」卡片的视觉中心展开 */\n transition: opacity .28s ease, transform .32s cubic-bezier(.18,.72,.28,1), visibility .32s;\n pointer-events: none;\n }\n .ml-canvas.show {\n opacity: 1; visibility: visible;\n transform: scale(1);\n pointer-events: auto;\n }\n .ml-canvas-h {\n display: flex; align-items: center; gap: 12px;\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n flex-shrink: 0;\n background: var(--surface);\n }\n .ml-canvas-h .back-btn {\n display: inline-flex; align-items: center; gap: 4px;\n height: 28px; padding: 0 10px;\n background: transparent; border: 1px solid var(--border-faint);\n border-radius: var(--r-sm); color: var(--black-alpha-72);\n font-family: inherit; font-size: 12px; cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .ml-canvas-h .back-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }\n .ml-canvas-h .back-btn svg { width: 12px; height: 12px; }\n .ml-canvas-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }\n .ml-canvas-h .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n .ml-canvas-h .x {\n width: 28px; height: 28px; display: grid; place-items: center;\n background: transparent; border: none;\n border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer;\n transition: background var(--t-base), color var(--t-base);\n }\n .ml-canvas-h .x:hover { background: var(--background-lighter); color: var(--accent-black); }\n .ml-canvas-h .x svg { width: 14px; height: 14px; }\n\n /* 画布双栏:左 AI 生成(主视觉),右 本地上传(副视觉) */\n .ml-canvas-body {\n flex: 1; min-height: 0;\n display: grid; grid-template-columns: 2fr 1fr;\n overflow: hidden;\n }\n .mc-ai { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--background-base); }\n .mc-up { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--surface); border-left: 1px solid var(--border-faint); }\n\n /* ── 左:AI 生成 · 完全照搬 image-optimize .io-* 输入对话框样式 ── */\n .mc-stream {\n flex: 1; min-height: 0;\n overflow-y: auto;\n /* 左右 28px · 与 .ml-canvas-h(padding: 14px 28px)版心对齐 */\n padding: 28px 28px 220px;\n background: var(--background-base);\n }\n .mc-stream-inner {\n /* 取消 max-width 限制 · 让结果网格在固定画布宽度下吃满可用空间,并与头部 28px 版心对齐 */\n width: 100%; margin: 0 auto;\n display: flex; flex-direction: column; gap: 28px;\n }\n\n /* 空态 · 同 .io-empty */\n .mc-empty {\n flex: 1; min-height: 100%;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 16px;\n padding: 40px;\n color: var(--black-alpha-56);\n text-align: center;\n }\n .mc-empty .badge {\n font-family: var(--font-mono); font-size: 11px;\n letter-spacing: .08em; color: var(--black-alpha-48);\n text-transform: uppercase;\n }\n .mc-empty h2 {\n font-size: 22px; font-weight: 600;\n color: var(--accent-black);\n letter-spacing: -.015em;\n margin: 0;\n }\n .mc-empty p { font-size: 13px; max-width: 460px; line-height: 1.6; margin: 0; }\n .mc-empty .ic {\n width: 64px; height: 64px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--heat);\n }\n .mc-empty .ic svg { width: 28px; height: 28px; }\n .mc-empty .examples {\n margin-top: 10px;\n display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;\n max-width: 720px;\n }\n .mc-empty .examples .ex {\n padding: 6px 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n font-size: 12px;\n color: var(--black-alpha-72);\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .mc-empty .examples .ex:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }\n\n /* 单条对话气泡 · 同 .io-msg(去掉 padding-left,让消息贴齐外层 28px 版心) */\n .mc-msg { display: flex; flex-direction: column; gap: 14px; }\n .mc-msg-prompt { display: flex; align-items: flex-start; gap: 12px; }\n .mc-msg-prompt .quote {\n flex-shrink: 0; width: 28px; height: 28px;\n border-radius: var(--r-sm);\n background: var(--surface); border: 1px solid var(--border-faint);\n color: var(--heat); display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 13px;\n }\n .mc-msg-prompt .quote svg { width: 13px; height: 13px; }\n .mc-msg-prompt .pt { flex: 1; min-width: 0; padding-top: 4px; }\n .mc-msg-prompt .pt-text {\n font-size: 14px; color: var(--accent-black);\n line-height: 1.55; word-break: break-word;\n }\n .mc-msg-prompt .pt-tags {\n margin-top: 8px;\n display: flex; flex-wrap: wrap; gap: 6px;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n align-items: center;\n }\n .mc-msg-prompt .pt-tags .meta-chip {\n padding: 2px 8px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n }\n .mc-msg-prompt .pt-tags .sep { color: var(--black-alpha-24); }\n .mc-msg-grid {\n display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;\n }\n @media (max-width: 1280px) { .mc-msg-grid { grid-template-columns: repeat(3, 1fr); } }\n .mc-cell {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow: hidden;\n aspect-ratio: 3/4;\n }\n .mc-cell .ph-frame {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32); letter-spacing: .02em;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .mc-cell.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }\n @keyframes mc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }\n .mc-cell:hover { border-color: var(--black-alpha-32); }\n .mc-cell.err { border-color: var(--accent-crimson, #c43d3d); }\n .mc-cell.err .ph-frame { color: var(--accent-crimson, #c43d3d); background: rgba(196, 61, 61, .05); }\n\n /* 单图右上 hover 操作组 · 同 .io-cell .cell-ops */\n .mc-cell .cell-ops {\n position: absolute; top: 6px; right: 6px;\n display: flex; gap: 4px;\n opacity: 0;\n transition: opacity var(--t-base);\n z-index: 2;\n }\n .mc-cell:hover .cell-ops { opacity: 1; }\n .mc-cell .cell-ops button {\n width: 26px; height: 26px;\n background: rgba(255, 255, 255, .92);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--accent-black);\n cursor: pointer;\n display: grid; place-items: center;\n backdrop-filter: blur(4px);\n transition: border-color var(--t-base), color var(--t-base);\n }\n .mc-cell .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }\n .mc-cell .cell-ops button svg { width: 12px; height: 12px; }\n .mc-cell .cell-more-wrap { position: relative; }\n .mc-cell .cell-more-menu {\n position: absolute; top: calc(100% + 4px); right: 0;\n min-width: 140px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .mc-cell .cell-more-wrap.open .cell-more-menu { display: block; }\n .mc-cell .cell-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n height: auto !important;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n backdrop-filter: none !important;\n justify-content: flex-start !important;\n }\n .mc-cell .cell-more-menu button:hover { background: var(--background-lighter) !important; color: var(--heat) !important; }\n .mc-cell .cell-more-menu button.danger:hover { color: var(--accent-crimson) !important; background: var(--crimson-bg, #fdebea) !important; }\n .mc-cell .cell-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }\n\n /* 操作行 · 同 .io-msg-ops(去掉 padding-left,与版心对齐) */\n .mc-msg-ops { display: flex; gap: 8px; }\n .mc-msg-ops button {\n display: inline-flex; align-items: center; gap: 6px;\n height: 30px; padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n font-size: 12.5px;\n color: var(--accent-black);\n font-family: inherit;\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .mc-msg-ops button:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }\n .mc-msg-ops button.icon { width: 30px; padding: 0; justify-content: center; }\n .mc-msg-ops button svg { width: 13px; height: 13px; }\n /* 批次更多气泡 · 同 .io-msg-ops .msg-more-wrap */\n .mc-msg-ops .msg-more-wrap { position: relative; }\n .mc-msg-ops .msg-more-menu {\n position: absolute; bottom: calc(100% + 6px); left: 0;\n min-width: 168px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .mc-msg-ops .msg-more-wrap.open .msg-more-menu { display: block; }\n .mc-msg-ops .msg-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n height: auto !important;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n text-align: left;\n justify-content: flex-start !important;\n cursor: pointer; font-family: inherit;\n }\n .mc-msg-ops .msg-more-menu button:hover { background: var(--background-lighter) !important; color: var(--heat) !important; }\n .mc-msg-ops .msg-more-menu button.danger:hover { color: var(--accent-crimson) !important; background: var(--crimson-bg, #fdebea) !important; }\n .mc-msg-ops .msg-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }\n\n /* 底部输入栏 · 完全照搬 .io-input 各项尺寸 */\n .mc-input-wrap {\n position: absolute; left: 0; right: 0; bottom: 0;\n padding: 14px 28px 22px;\n background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px);\n z-index: 5;\n }\n .mc-input {\n max-width: 720px; margin: 0 auto;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: 18px;\n padding: 12px 14px 10px;\n display: flex; flex-direction: column; gap: 8px;\n box-shadow: 0 6px 24px rgba(0,0,0,.06);\n transition: border-color var(--t-base);\n }\n .mc-input:focus-within { border-color: var(--heat-40); }\n\n /* 上行 · 加号 + 参考图(同一 flex 行,均 64×64,refs 容器用 display:contents 让子项直接参与上行排列) */\n .mc-input-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n .mc-input-top .add-btn {\n flex-shrink: 0; width: 64px; height: 64px;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--black-alpha-56); cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .mc-input-top .add-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }\n .mc-input-top .add-btn svg { width: 22px; height: 22px; }\n\n .mc-input-refs { display: contents; }\n .mc-input-ref {\n position: relative; width: 64px; height: 64px;\n border-radius: var(--r-md); overflow: hidden;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n flex-shrink: 0;\n }\n .mc-input-ref img { width: 100%; height: 100%; object-fit: cover; }\n .mc-input-ref .x {\n position: absolute; top: 3px; right: 3px;\n width: 18px; height: 18px;\n background: rgba(0,0,0,.7); color: var(--accent-white);\n border: 0; border-radius: 50%;\n display: grid; place-items: center;\n cursor: pointer;\n }\n .mc-input-ref .x svg { width: 10px; height: 10px; }\n\n /* 中行 · textarea 满宽 · 直接作为 .mc-input 的子级,自成一行 */\n .mc-input textarea#mc-input-text {\n width: 100%;\n border: 0; outline: 0; resize: none;\n background: transparent;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.5;\n color: var(--accent-black);\n min-height: 44px; max-height: 220px;\n padding: 4px 2px;\n }\n .mc-input textarea#mc-input-text::placeholder { color: var(--black-alpha-48); }\n\n /* 下行 · 参数胶囊 + 右 meta + 发送按钮(32×32 放在最右,margin-left:8) */\n .mc-input-bottom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n .mc-input-bottom .param {\n position: relative; display: inline-flex; align-items: center; gap: 4px;\n height: 26px; padding: 0 9px;\n background: var(--background-lighter);\n border: 1px solid transparent;\n border-radius: var(--r-pill);\n font-size: 11.5px; color: var(--black-alpha-72);\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .mc-input-bottom .param:hover { background: var(--surface); border-color: var(--border-faint); }\n .mc-input-bottom .param .lbl-mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-right: 1px; }\n .mc-input-bottom .param svg { width: 10px; height: 10px; opacity: .6; }\n .mc-input-bottom .right-meta { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .mc-input-bottom .right-meta .val { color: var(--accent-black); }\n .mc-input .send-btn {\n flex-shrink: 0; width: 32px; height: 32px;\n background: var(--heat); color: var(--accent-white);\n border: 0; border-radius: var(--r-md); cursor: pointer;\n display: grid; place-items: center;\n transition: opacity var(--t-base), filter var(--t-base);\n margin-left: 8px;\n }\n .mc-input .send-btn:hover { filter: brightness(1.05); }\n .mc-input .send-btn:disabled { opacity: .4; cursor: not-allowed; }\n .mc-input .send-btn svg { width: 15px; height: 15px; }\n\n /* ── 右:副视觉 · 顶部 tab 切换(AI 生成 / 本地上传)+ 3 模块(姓名/立绘/三视图) ── */\n .mc-up-tabs { display: flex; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; background: var(--surface); }\n .mc-up-tab {\n flex: 1; height: 44px;\n background: transparent; border: 0;\n border-bottom: 2px solid transparent;\n font-family: inherit; font-size: 13px; font-weight: 500;\n color: var(--black-alpha-56); cursor: pointer;\n transition: color var(--t-base), border-color var(--t-base), background var(--t-base);\n }\n .mc-up-tab:hover { color: var(--accent-black); background: var(--background-lighter); }\n .mc-up-tab.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; background: var(--surface); }\n\n .mc-up-body { flex: 1; min-height: 0; padding: 18px 20px 14px; display: flex; flex-direction: column; gap: 18px; overflow-y: auto; }\n .mc-up-section { display: flex; flex-direction: column; gap: 8px; }\n .mc-up-sec-h { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n\n /* 模特姓名 输入 */\n .mc-up-name {\n width: 100%; height: 36px;\n padding: 0 12px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n font-family: inherit; font-size: 13.5px; color: var(--accent-black);\n outline: none;\n transition: border-color var(--t-base), background var(--t-base);\n }\n .mc-up-name:focus { border-color: var(--heat-40); background: var(--surface); }\n .mc-up-name::placeholder { color: var(--black-alpha-40); }\n\n /* 立绘模块 · AI 选中态 */\n .mc-portrait-ai .empty {\n aspect-ratio: 3/4; max-height: 220px;\n border: 1.5px dashed var(--black-alpha-24);\n border-radius: var(--r-md);\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;\n background: var(--background-lighter);\n text-align: center; padding: 14px;\n }\n .mc-portrait-ai .empty[hidden] { display: none; }\n .mc-portrait-ai .picked[hidden] { display: none; }\n .mc-portrait-ai .empty .ic { width: 38px; height: 38px; border-radius: 50%; background: var(--surface); border: 1px solid var(--border-faint); display: grid; place-items: center; color: var(--black-alpha-48); }\n .mc-portrait-ai .empty .ic svg { width: 16px; height: 16px; }\n .mc-portrait-ai .empty .desc { font-size: 12.5px; color: var(--black-alpha-72); line-height: 1.55; }\n .mc-portrait-ai .empty .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .mc-portrait-ai .picked {\n position: relative; aspect-ratio: 3/4; max-height: 280px;\n background: var(--background-lighter); border: 1.5px solid var(--heat);\n border-radius: var(--r-md); overflow: hidden;\n }\n .mc-portrait-ai .picked .ph-frame {\n position: absolute; inset: 0; display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32); letter-spacing: .02em;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .mc-portrait-ai .picked .ops { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; }\n .mc-portrait-ai .picked .ops button {\n width: 26px; height: 26px;\n background: rgba(255,255,255,.92); border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--accent-black); cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .mc-portrait-ai .picked .ops button:hover { border-color: var(--heat); color: var(--heat); }\n .mc-portrait-ai .picked .ops button svg { width: 12px; height: 12px; }\n .mc-portrait-ai .picked .badge {\n position: absolute; top: 8px; left: 8px;\n background: var(--heat); color: var(--accent-white);\n padding: 2px 7px; border-radius: var(--r-sm);\n font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em;\n }\n\n /* 立绘模块 · 本地上传(多张) */\n .mc-portrait-local .drop {\n border: 1.5px dashed var(--black-alpha-24);\n border-radius: var(--r-md);\n padding: 20px 14px;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;\n cursor: pointer;\n background: var(--background-lighter);\n transition: border-color var(--t-base), background var(--t-base);\n text-align: center;\n }\n .mc-portrait-local .drop:hover, .mc-portrait-local .drop.dragover { border-color: var(--heat); background: var(--heat-12); }\n .mc-portrait-local .drop .ic {\n width: 32px; height: 32px;\n background: var(--heat); color: var(--accent-white);\n border-radius: 50%; display: grid; place-items: center;\n }\n .mc-portrait-local .drop .ic svg { width: 14px; height: 14px; }\n .mc-portrait-local .drop .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }\n .mc-portrait-local .drop .d { font-size: 11px; color: var(--black-alpha-48); }\n .mc-portrait-local .list-h { display: flex; align-items: center; gap: 4px; margin-top: 6px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .mc-portrait-local .list-h .ct { color: var(--accent-black); font-weight: 600; }\n .mc-portrait-local .list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }\n .mc-portrait-local .list:empty { display: none; }\n .mc-portrait-local .thumb {\n position: relative; aspect-ratio: 3/4;\n border-radius: var(--r-sm); overflow: hidden;\n background: var(--background-lighter); border: 1px solid var(--border-faint);\n }\n .mc-portrait-local .thumb img { width: 100%; height: 100%; object-fit: cover; }\n .mc-portrait-local .thumb .x {\n position: absolute; top: 4px; right: 4px;\n width: 20px; height: 20px;\n background: rgba(0,0,0,.7); color: var(--accent-white);\n border: 0; border-radius: 50%;\n display: grid; place-items: center; cursor: pointer;\n }\n .mc-portrait-local .thumb .x svg { width: 10px; height: 10px; }\n\n /* 三视图模块 · 始终展示 16:9 占位 · 空态时生成按钮居中覆盖 · 有版本时按钮消失,显示重跑 + 历史 */\n .mc-triview .result-wrap { display: flex; flex-direction: column; gap: 8px; }\n .mc-triview .result {\n position: relative; aspect-ratio: 16/9;\n background: var(--background-lighter); border: 1.5px solid var(--border-faint);\n border-radius: var(--r-md); overflow: hidden;\n transition: border-color var(--t-base);\n }\n .mc-triview.has-result .result { border-color: var(--heat); }\n .mc-triview .result .ph-frame {\n position: absolute; inset: 0; display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32); letter-spacing: .02em;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n pointer-events: none;\n }\n .mc-triview .result.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }\n\n /* 居中覆盖生成按钮(无版本 / 非生成中时显示) */\n .mc-triview .overlay-gen-btn {\n position: absolute; top: 50%; left: 50%;\n transform: translate(-50%, -50%);\n z-index: 2;\n display: inline-flex; align-items: center; gap: 6px;\n height: 36px; padding: 0 18px;\n background: var(--heat); color: var(--accent-white);\n border: 0; border-radius: var(--r-pill);\n font-family: inherit; font-size: 13px; font-weight: 500;\n cursor: pointer;\n box-shadow: 0 4px 12px rgba(250, 93, 25, .28);\n transition: filter var(--t-base), opacity var(--t-base), transform var(--t-base), box-shadow var(--t-base);\n }\n .mc-triview .overlay-gen-btn:hover:not(:disabled) {\n filter: brightness(1.06);\n transform: translate(-50%, -50%) scale(1.03);\n box-shadow: 0 6px 16px rgba(250, 93, 25, .36);\n }\n .mc-triview .overlay-gen-btn:disabled {\n background: var(--black-alpha-24); color: var(--surface);\n cursor: not-allowed; box-shadow: none;\n }\n .mc-triview .overlay-gen-btn svg { width: 13px; height: 13px; }\n .mc-triview .overlay-hint {\n position: absolute; left: 0; right: 0; bottom: 10px;\n text-align: center;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n pointer-events: none;\n z-index: 2;\n }\n\n .mc-triview .result-ops { display: flex; gap: 6px; align-items: center; }\n .mc-triview .result-ops .cost { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .mc-triview .result-ops button {\n display: inline-flex; align-items: center; gap: 5px;\n height: 28px; padding: 0 10px;\n background: var(--surface); border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-family: inherit; font-size: 11.5px;\n color: var(--accent-black); cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .mc-triview .result-ops button:hover { border-color: var(--heat); color: var(--heat); }\n .mc-triview .result-ops button svg { width: 11px; height: 11px; }\n\n /* 历史版本 strip · 同商品详情页 popover 视觉 */\n .mc-triview .history { display: flex; flex-direction: column; gap: 6px; }\n .mc-triview .history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n .mc-triview .history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }\n .mc-triview .history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }\n .mc-triview .history .h-thumb {\n flex: 0 0 auto;\n width: 72px; aspect-ratio: 16/9;\n background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);\n position: relative; cursor: pointer;\n display: grid; place-items: center; overflow: hidden;\n transition: border-color var(--t-base);\n }\n .mc-triview .history .h-thumb:hover { border-color: var(--heat-40); }\n .mc-triview .history .h-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }\n .mc-triview .history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .mc-triview .history .h-thumb.active .v { color: var(--heat); font-weight: 600; }\n .mc-triview .history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }\n .mc-triview .history .h-thumb.active .badge { display: block; }\n\n /* 底部提交栏 */\n .mc-up-foot { padding: 12px 20px 16px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 10px; flex-shrink: 0; flex-wrap: wrap; }\n .mc-up-foot .stat { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .mc-up-foot .stat b { color: var(--accent-black); font-weight: 600; }\n .mc-up-foot .stat.ok { color: var(--heat); }\n .mc-up-foot .commit-btn {\n display: inline-flex; align-items: center; gap: 5px;\n height: 32px; padding: 0 14px;\n background: var(--heat); color: var(--accent-white);\n border: 0; border-radius: var(--r-sm);\n font-family: inherit; font-size: 12.5px; cursor: pointer;\n transition: filter var(--t-base), opacity var(--t-base);\n }\n .mc-up-foot .commit-btn:hover { filter: brightness(1.05); }\n .mc-up-foot .commit-btn:disabled { opacity: .4; cursor: not-allowed; filter: none; }\n .mc-up-foot .commit-btn svg { width: 12px; height: 12px; }\n\n /* 左侧结果卡 · 可点击选为立绘(仅 AI tab) */\n .mc-cell { cursor: pointer; }\n .mc-cell.selected { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-12); }\n .mc-cell .pick-badge { position: absolute; top: 6px; left: 6px; background: var(--heat); color: var(--accent-white); padding: 2px 7px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; display: none; }\n .mc-cell.selected .pick-badge { display: block; }\n .ml-toolbar {\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n display: flex; align-items: center; gap: 18px;\n flex-shrink: 0;\n flex-wrap: wrap;\n }\n .ml-toolbar .btn-up {\n height: 32px;\n padding: 0 14px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--surface);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n color: var(--accent-black);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .ml-toolbar .btn-up:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }\n .ml-toolbar .btn-up svg { width: 14px; height: 14px; }\n .ml-toolbar .chip-group {\n display: inline-flex; align-items: center; gap: 6px;\n }\n .ml-toolbar .chip-group .lbl {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n margin-right: 4px;\n }\n .ml-toolbar .chip {\n height: 26px;\n padding: 0 12px;\n border-radius: 999px;\n background: transparent;\n border: 1px solid var(--black-alpha-12);\n color: var(--black-alpha-72);\n font-size: 12px;\n cursor: pointer;\n font-family: inherit;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .ml-toolbar .chip:hover { color: var(--accent-black); }\n .ml-toolbar .chip.active {\n background: var(--heat-12); color: var(--heat);\n border-color: var(--heat-40); font-weight: 600;\n }\n .ml-scroll {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 20px 28px 28px;\n }\n .ml-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));\n gap: 12px;\n }\n .ml-grid .model-card { padding: 10px; }\n\n /* 「添加模特」入口卡:虚线轮廓 + 居中 + 号,与现有卡片同尺寸 */\n .ml-grid .ml-upload-card {\n border: 1.5px dashed var(--black-alpha-24);\n background: var(--surface);\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .ml-grid .ml-upload-card:hover {\n border-color: var(--heat);\n background: var(--heat-12);\n }\n .ml-grid .ml-upload-card:focus-visible {\n outline: 2px solid var(--heat);\n outline-offset: 2px;\n }\n .ml-grid .ml-upload-card .up-thumb {\n aspect-ratio: 3/4;\n border-radius: var(--r-sm);\n background: transparent;\n display: grid; place-items: center;\n }\n .ml-grid .ml-upload-card .up-plus {\n width: 44px; height: 44px;\n border-radius: 50%;\n background: var(--surface);\n border: 1px solid var(--black-alpha-12);\n color: var(--black-alpha-56);\n display: grid; place-items: center;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base), transform var(--t-base);\n }\n .ml-grid .ml-upload-card:hover .up-plus {\n background: var(--heat); border-color: var(--heat); color: var(--accent-white);\n transform: scale(1.06);\n }\n .ml-grid .ml-upload-card .up-plus svg { width: 22px; height: 22px; }\n .ml-grid .ml-upload-card .m-name { color: var(--accent-black); }\n .ml-grid .ml-upload-card:hover .m-name { color: var(--heat); }\n .ml-grid .ml-upload-card .m-tag { color: var(--black-alpha-48); }\n\n /* ─── 添加模特 · 选择 modal (AI 生成 / 本地上传) ─── */\n .ml-up-choice-bg {\n position: fixed; inset: 0; z-index: 1200;\n background: rgba(21, 20, 15, .42);\n display: none; place-items: center; padding: 16px;\n }\n .ml-up-choice-bg.show { display: grid; }\n .ml-up-choice {\n width: min(560px, 92vw);\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 16px 48px rgba(21, 20, 15, .18);\n overflow: hidden;\n position: relative;\n }\n .ml-up-choice .uc-h {\n display: flex; align-items: center; gap: 12px;\n padding: 18px 22px 14px;\n border-bottom: 1px solid var(--border-faint);\n }\n .ml-up-choice .uc-h .ic-m {\n width: 36px; height: 36px;\n border-radius: var(--r-md);\n background: var(--heat-12); color: var(--heat);\n display: grid; place-items: center;\n flex-shrink: 0;\n }\n .ml-up-choice .uc-h .ic-m svg { width: 18px; height: 18px; }\n .ml-up-choice .uc-h .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }\n .ml-up-choice .uc-h .ti strong { font-size: 15px; color: var(--accent-black); font-weight: 600; }\n .ml-up-choice .uc-h .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .ml-up-choice .uc-h .uc-x {\n margin-left: auto;\n width: 28px; height: 28px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n color: var(--black-alpha-56); cursor: pointer;\n display: grid; place-items: center;\n }\n .ml-up-choice .uc-h .uc-x:hover { background: var(--background-lighter); color: var(--accent-black); }\n .ml-up-choice .uc-h .uc-x svg { width: 14px; height: 14px; }\n\n .ml-up-choice .uc-body {\n display: grid; grid-template-columns: 1fr 1fr; gap: 12px;\n padding: 20px 22px 22px;\n }\n .ml-up-choice .uc-option {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 18px 16px;\n text-align: left;\n cursor: pointer;\n font-family: inherit;\n display: flex; flex-direction: column; gap: 10px;\n transition: border-color var(--t-base), background var(--t-base), transform var(--t-base);\n }\n .ml-up-choice .uc-option:hover {\n border-color: var(--heat);\n background: var(--heat-12);\n }\n .ml-up-choice .uc-option .opt-ic {\n width: 40px; height: 40px; border-radius: var(--r-md);\n background: var(--background-lighter);\n color: var(--heat); border: 1px solid var(--heat-20);\n display: grid; place-items: center;\n transition: background var(--t-base), color var(--t-base);\n }\n .ml-up-choice .uc-option:hover .opt-ic { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }\n .ml-up-choice .uc-option .opt-ic svg { width: 18px; height: 18px; }\n .ml-up-choice .uc-option .opt-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }\n .ml-up-choice .uc-option .opt-d {\n font-family: var(--font-mono); font-size: 11.5px;\n color: var(--black-alpha-56); letter-spacing: .02em; line-height: 1.55;\n }\n .ml-up-choice .uc-option .opt-tag {\n margin-top: auto;\n align-self: flex-start;\n font-family: var(--font-mono); font-size: 10.5px;\n padding: 2px 8px; border-radius: var(--r-sm);\n background: var(--background-lighter);\n color: var(--black-alpha-72);\n letter-spacing: .04em;\n }\n .ml-up-choice .uc-option:hover .opt-tag { background: var(--surface); color: var(--heat); }\n\n /* 模特详情 居中弹窗 — 参考布局 v2 (与 pipeline.html / library.html 一致) */\n .md-modal-bg { position: fixed; inset: 0; background: rgba(21,20,15,.42); backdrop-filter: blur(8px); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }\n .md-modal-bg.show { display: flex; }\n .md-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); }\n .md-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }\n .md-modal-h h3 { font-size: 15px; font-weight: 600; }\n .md-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .md-modal-h .x { margin-left: auto; 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); }\n .md-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }\n .md-modal-h .x svg { width: 14px; height: 14px; }\n .md-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }\n .md-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }\n /* 左栏 · 大立绘 + 缩略图 */\n .md-lead { display: flex; flex-direction: column; gap: 10px; }\n .md-lead-wrap { position: relative; }\n .md-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); background: var(--background-lighter); border: 1px solid var(--border-faint); position: relative; overflow: hidden; }\n .md-lead-img .ph-frame { position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); background: rgba(255,255,255,.85); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); padding: 2px 10px; border-radius: var(--r-sm); }\n .md-lead-img .ph-name { position: absolute; left: 0; top: 0; padding: 12px 14px; font-size: 14px; font-weight: 600; color: var(--black-alpha-72); background: linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,0)); width: 100%; }\n /* 查看大图 icon · 悬浮容器才显示 · 32×32 icon-only */\n .md-zoom-btn { position: absolute; right: 8px; bottom: 8px; width: 32px; height: 32px; padding: 0; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-sm); display: grid; place-items: center; cursor: pointer; opacity: 0; transition: opacity var(--t-base), background var(--t-base); z-index: 3; }\n .md-zoom-btn:hover { background: rgba(21,20,15,.92); }\n .md-zoom-btn svg { width: 14px; height: 14px; }\n .md-lead-wrap:hover .md-zoom-btn,\n .md-views .md-view:hover .md-zoom-btn { opacity: 1; }\n .md-views .md-view { position: relative; }\n .md-thumbs { display: flex; gap: 8px; }\n .md-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); background: var(--background-lighter); cursor: pointer; overflow: hidden; position: relative; }\n .md-thumbs .thumb:hover { border-color: var(--heat-40); }\n .md-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }\n /* 右栏 sections */\n .md-right .md-section + .md-section { margin-top: 18px; }\n .md-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }\n .md-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }\n .md-section-h .ic svg { width: 14px; height: 14px; }\n .md-section-h .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); }\n .md-section-h .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; }\n .md-section-h .icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }\n .md-section-h .icon-btn svg { width: 12px; height: 12px; }\n /* 三视图 — 始终单张 16:9 大图 */\n .md-views { display: flex; flex-direction: column; gap: 8px; }\n .md-views .md-view { aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; display: grid; place-items: end center; overflow: hidden; }\n .md-views .md-view .lbl { position: absolute; bottom: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); background: rgba(255,255,255,.85); padding: 2px 8px; border-radius: var(--r-sm); }\n /* 三视图 · 用户上传:历史版本横向 strip · 与 mc-triview history 视觉对齐 */\n .md-view-versions { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }\n .md-view-versions .v-thumb { flex: 0 0 auto; width: 72px; aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); position: relative; cursor: pointer; display: grid; place-items: center; overflow: hidden; transition: border-color var(--t-base); }\n .md-view-versions .v-thumb:hover { border-color: var(--heat-40); }\n .md-view-versions .v-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }\n .md-view-versions .v-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .md-view-versions .v-thumb.active .v { color: var(--heat); font-weight: 600; }\n /* 立绘大图 · 支持 <img> · cover */\n .md-lead-img img.md-lead-pic { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }\n /* 缩略图 · 支持 <img> · 多张时允许换行(避免横向爆款) */\n .md-thumbs { flex-wrap: wrap; }\n .md-thumbs .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }\n .md-thumbs .thumb .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); }\n /* 简介 */\n .md-intro { font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); margin: 0 0 12px; }\n .md-tags { display: flex; flex-wrap: wrap; gap: 8px; }\n .md-tags .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); }\n .md-tags .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; }\n .md-tags .tag-add:hover { border-color: var(--heat); color: var(--heat); }\n .md-tags .tag-add svg { width: 12px; height: 12px; }\n /* 属性表 */\n .md-props { margin-top: 18px; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 24px; border-top: 1px solid var(--border-faint); padding-top: 16px; }\n .md-props .prop { display: flex; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; min-height: 38px; }\n .md-props .prop:nth-last-child(-n+3) { border-bottom: 0; }\n .md-props .prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }\n .md-props .prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }\n /* footer */\n .md-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }\n .md-modal-f .foot-stats { display: flex; gap: 6px; margin-right: auto; }\n .md-modal-f .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; }\n .md-modal-f .stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }\n .md-modal-f .stat-btn svg { width: 13px; height: 13px; }\n .md-modal-f .stat-btn b { color: var(--accent-black); font-weight: 600; }\n\n /* ─── 离开工作台 · 二次确认弹窗 ─── */\n .mc-leave-bg {\n position: fixed; inset: 0;\n background: rgba(21,20,15,.42);\n backdrop-filter: blur(8px);\n z-index: 1200;\n display: none;\n align-items: center; justify-content: center;\n padding: 40px;\n }\n .mc-leave-bg.show { display: flex; }\n .mc-leave {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n width: 420px; max-width: 100%;\n box-shadow: 0 16px 48px rgba(0,0,0,.18);\n overflow: hidden;\n position: relative;\n }\n .mc-leave .lv-h {\n display: flex; align-items: center; gap: 10px;\n padding: 14px 20px 10px;\n }\n .mc-leave .lv-h .ic {\n width: 28px; height: 28px;\n display: grid; place-items: center;\n border-radius: var(--r-sm);\n background: var(--crimson-bg);\n color: var(--accent-crimson);\n flex-shrink: 0;\n }\n .mc-leave .lv-h .ic svg { width: 16px; height: 16px; }\n .mc-leave .lv-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }\n .mc-leave .lv-h .mono {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n }\n .mc-leave .lv-b {\n padding: 4px 20px 18px;\n font-size: 13px; line-height: 1.65;\n color: var(--black-alpha-72);\n }\n .mc-leave .lv-b b { color: var(--accent-black); font-weight: 600; }\n .mc-leave .lv-f {\n display: flex; align-items: center; gap: 8px;\n padding: 12px 20px;\n border-top: 1px solid var(--border-faint);\n background: var(--background-lighter);\n }\n .mc-leave .lv-f .spacer { flex: 1; }\n .mc-leave .lv-f .btn { height: 34px; padding: 0 14px; font-size: 13px; }\n .mc-leave .btn-danger {\n background: var(--accent-crimson);\n color: var(--accent-white);\n border-color: var(--accent-crimson);\n font-weight: 600;\n }\n .mc-leave .btn-danger:hover {\n background: var(--accent-crimson);\n border-color: var(--accent-crimson);\n filter: brightness(.95);\n }\n\n /* 编辑商品 drawer (复用 .drawer 基础样式, 提高 z-index 覆盖商品库) */\n .pc-drawer { width: 720px; max-width: 100vw; z-index: 1101; }\n .pc-drawer .drawer-b { padding: 24px 28px; }\n #pc-drawer-bg.drawer-bg { z-index: 1100; }\n .pc-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }\n .pc-field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); }\n .pc-field-label .req { color: var(--accent-crimson); margin-left: 2px; }\n .pc-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 18px; }\n .pc-field-row > div { display: flex; flex-direction: column; gap: 6px; }\n .pc-bullets {\n list-style: none; padding: 0; margin: 0;\n display: flex; flex-direction: column; gap: 6px;\n }\n .pc-bullets li {\n display: flex; align-items: center; gap: 8px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n padding: 0 10px;\n height: 36px;\n }\n .pc-bullets li.add { border-style: dashed; border-color: var(--heat-40); }\n .pc-bullets li .num {\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); width: 18px; text-align: center;\n flex-shrink: 0;\n }\n .pc-bullets li.add .num { color: var(--heat); }\n .pc-bullets li input {\n flex: 1; border: 0; background: transparent; outline: none;\n font-size: 13px; color: var(--accent-black);\n font-family: inherit;\n }\n .pc-bullets li input::placeholder { color: var(--black-alpha-48); }\n .pc-bullets li .rm {\n width: 22px; height: 22px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n color: var(--black-alpha-48); cursor: pointer;\n display: grid; place-items: center;\n }\n .pc-bullets li .rm:hover { color: var(--accent-crimson); background: var(--black-alpha-4); }\n .pc-bullets li .rm svg { width: 11px; height: 11px; }\n /* 商品图片 grid (对齐 product-detail .ov-images-grid) */\n .pc-imgs {\n display: grid;\n grid-template-columns: repeat(6, 1fr);\n gap: 8px;\n }\n .pc-imgs .thumb {\n aspect-ratio: 1 / 1;\n border-radius: var(--r-sm);\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n position: relative;\n overflow: hidden;\n }\n .pc-imgs .thumb .ph-frame {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-32);\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .pc-imgs .thumb .rm {\n position: absolute; top: 4px; right: 4px;\n width: 18px; height: 18px;\n background: rgba(0,0,0,.5);\n color: #fff; border: 0;\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n cursor: pointer;\n opacity: 0;\n transition: opacity var(--t-base);\n }\n .pc-imgs .thumb:hover .rm { opacity: 1; }\n .pc-imgs .thumb .rm svg { width: 10px; height: 10px; }\n .pc-imgs .img-upload {\n aspect-ratio: 1 / 1;\n border-radius: var(--r-sm);\n background: var(--heat-12);\n border: 1.5px dashed var(--heat-40);\n display: grid; place-items: center;\n color: var(--heat);\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pc-imgs .img-upload:hover { background: var(--heat-20); border-color: var(--heat); }\n .pc-imgs .img-upload svg { width: 18px; height: 18px; }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n <div class=\"mp-layout\">\n\n <!-- ===== 最左栏 · 商品空间 (单选 · 当前商品决定结果区批次) ===== -->\n <aside class=\"mp-prod-space\" id=\"prod-space\">\n <!-- 顶部 · 返回 + 折叠 (跟图片创作风格一致) -->\n <div class=\"mp-side-top\">\n <button class=\"back-pill\" type=\"button\" onclick=\"history.length > 1 ? history.back() : location.href='asset-factory.html'\" title=\"返回\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 18l-6-6 6-6\"/></svg>\n <span>返回</span>\n </button>\n <button class=\"fold\" type=\"button\" title=\"折叠侧栏\" style=\"margin-left:auto\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M9 3v18\"/></svg>\n </button>\n </div>\n <div class=\"mp-ps-h\">\n <div class=\"mp-ps-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" id=\"ps-search-input\" placeholder=\"搜索商品 / 分类\">\n </div>\n </div>\n <!-- 商品列表 标题行 · 右上显眼新建按钮 -->\n <div class=\"mp-list-h\">\n <span class=\"mono\">// 商品空间</span>\n <button class=\"new-prod\" type=\"button\" id=\"ps-new-btn\" title=\"新建商品\">\n <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>\n <span>新建商品</span>\n </button>\n </div>\n <div class=\"mp-ps-list\" id=\"ps-list\"></div>\n <button class=\"mp-ps-all\" type=\"button\" id=\"ps-all-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M3 9h18M9 4v16\"/></svg>\n <span>全部商品</span>\n <span class=\"ct\" id=\"ps-all-ct\">0 个</span>\n <svg style=\"margin-left:0\" 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>\n </button>\n </aside>\n\n <!-- ===== 主区 · 头部 + 参数/结果 双栏 ===== -->\n <section class=\"mp-main\">\n\n <!-- 主区顶部 · toolbar (商品标题 + 搜索 + 筛选 · 跟图片创作一致) -->\n <div class=\"mp-main-h\">\n <button class=\"side-restore-btn\" type=\"button\" id=\"mp-side-restore\" hidden title=\"展开商品空间\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M9 3v18\"/></svg>\n 商品\n </button>\n <div class=\"cur-title\">\n <span class=\"crumb\">// 商品空间</span>\n <span class=\"nm placeholder\" id=\"cur-prod-nm\">未选择 · 请在左侧商品空间选一个</span>\n </div>\n <span class=\"spacer\"></span>\n <div class=\"tb-search-wrap\" id=\"mp-search-wrap\">\n <button class=\"search-btn\" type=\"button\" title=\"搜索批次/模特\" id=\"mp-search-toggle\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </button>\n <input type=\"text\" class=\"tb-search-input\" id=\"mp-search-input\" placeholder=\"搜索批次/模特/比例…\" autocomplete=\"off\">\n </div>\n <div class=\"tb-menu-wrap\" data-filter=\"time\">\n <button class=\"tb-chip\" type=\"button\" id=\"mp-chip-time\">\n <span class=\"lbl\">时间</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"tb-menu\" id=\"mp-menu-time\" role=\"listbox\" aria-labelledby=\"mp-chip-time\">\n <button class=\"tb-menu-item active\" type=\"button\" data-val=\"all\">全部时间</button>\n <button class=\"tb-menu-item\" type=\"button\" data-val=\"today\">今天</button>\n <button class=\"tb-menu-item\" type=\"button\" data-val=\"1h\">1 小时内</button>\n <button class=\"tb-menu-item\" type=\"button\" data-val=\"10min\">10 分钟内</button>\n </div>\n </div>\n <div class=\"tb-menu-wrap\" data-filter=\"model\">\n <button class=\"tb-chip\" type=\"button\" id=\"mp-chip-model\">\n <span class=\"lbl\">模特</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"tb-menu\" id=\"mp-menu-model\" role=\"listbox\" aria-labelledby=\"mp-chip-model\">\n <button class=\"tb-menu-item active\" type=\"button\" data-val=\"all\">全部模特</button>\n <div class=\"tb-menu-empty\">暂无批次,生成后可按模特筛选</div>\n </div>\n </div>\n </div>\n\n <div class=\"mp-main-body\">\n\n <!-- 左 · 参数 -->\n <div class=\"mp-form\">\n\n <!-- ① 选择模特 -->\n <div class=\"mp-step\">\n <div class=\"mp-step-h\">\n <span class=\"num\">1</span>\n <span class=\"title\">选择模特</span>\n <span class=\"right\" id=\"open-model-lib\">全部模特 →</span>\n </div>\n <div class=\"model-grid\" id=\"model-grid-mini\">\n <div class=\"model-card\" data-id=\"m1\" data-name=\"Ava\">\n <div class=\"m-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder m-thumb\"><span class=\"ph-frame\">Ava</span></div>\n <div class=\"m-name\">Ava</div>\n <div class=\"m-tag\">亚洲·25岁·清新</div>\n </div>\n <div class=\"model-card\" data-id=\"m2\" data-name=\"Luna\">\n <div class=\"m-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder m-thumb\"><span class=\"ph-frame\">Luna</span></div>\n <div class=\"m-name\">Luna</div>\n <div class=\"m-tag\">亚洲·22岁·学生</div>\n </div>\n <div class=\"model-card\" data-id=\"m3\" data-name=\"Mia\">\n <div class=\"m-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder m-thumb\"><span class=\"ph-frame\">Mia</span></div>\n <div class=\"m-name\">Mia</div>\n <div class=\"m-tag\">混血·28岁·OL</div>\n </div>\n <div class=\"model-card\" data-id=\"m4\" data-name=\"Zoe\">\n <div class=\"m-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder m-thumb\"><span class=\"ph-frame\">Zoe</span></div>\n <div class=\"m-name\">Zoe</div>\n <div class=\"m-tag\">亚洲·30岁·健身</div>\n </div>\n </div>\n </div>\n\n <!-- ② 生成设置 -->\n <div class=\"mp-step\">\n <div class=\"mp-step-h\">\n <span class=\"num\">2</span>\n <span class=\"title\">生成设置</span>\n </div>\n <div class=\"mp-sub\">\n <div class=\"mp-sub-h\">// 生成数量 (每模特)</div>\n <div class=\"pill-row\" data-key=\"count\">\n <button type=\"button\" class=\"opt active\" data-val=\"4\">4 张</button>\n <button type=\"button\" class=\"opt\" data-val=\"8\">8 张</button>\n <button type=\"button\" class=\"opt\" data-val=\"12\">12 张</button>\n </div>\n </div>\n <div class=\"mp-sub\">\n <div class=\"mp-sub-h\">// 图片比例</div>\n <div class=\"pill-row\" data-key=\"ratio\">\n <button type=\"button\" class=\"opt active\" data-val=\"1:1\">1:1</button>\n <button type=\"button\" class=\"opt\" data-val=\"3:4\">3:4</button>\n <button type=\"button\" class=\"opt\" data-val=\"9:16\">9:16</button>\n </div>\n </div>\n </div>\n\n <!-- 底部 立即生成 -->\n <div class=\"mp-cta\">\n <button class=\"btn btn-primary btn-gen\" id=\"mp-go-btn\" type=\"button\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 3l14 9-14 9V3z\"/></svg>\n 立即生成 (预估 <span id=\"cost-total\">¥1.20</span>)\n </button>\n <div class=\"mp-cta-hint\">// 采用即扣费并入对应商品 AI 素材 · 未采用不扣</div>\n </div>\n\n </div>\n\n <!-- ===== 右栏 · 预览 ===== -->\n <div class=\"mp-preview\">\n <!-- 空态(新任务态 & 还没立即生成时显示) -->\n <div class=\"mp-pv-empty\" id=\"pv-empty\">\n <div class=\"mono\">// EMPTY STATE</div>\n <div class=\"title\">还没有生成结果</div>\n <div class=\"hint\">先选商品、选模特,点击 <b>立即生成</b> 后,效果图会出现在这里</div>\n </div>\n\n <!-- pv-summary 已并入主区头部 toolbar,仅保留隐藏 DOM 让旧 JS 引用不报错 -->\n <div class=\"mp-pv-h\" id=\"pv-summary\" hidden style=\"display:none\">\n <div class=\"pv-meta\"><b id=\"pv-count\">4</b> 张 · <b id=\"pv-ratio\">1:1</b></div>\n <div class=\"pv-line\"><span class=\"v\" id=\"pv-prod\">未选择</span></div>\n <div class=\"pv-line\"><span class=\"v\" id=\"pv-model\">未选择</span><span class=\"swap\" id=\"pv-swap\">更换</span></div>\n </div>\n\n <div class=\"mp-pv-grid\" id=\"pv-grid\">\n <!-- 默认占位批次; 生成后填充为真实批次 -->\n <div class=\"mp-result-batch placeholder-batch\">\n <div class=\"mp-result-grid\">\n <div class=\"mp-result placeholder-only\"><div class=\"mp-r-thumb\"><span class=\"ph-frame\">待生成 · 1:1</span></div></div>\n <div class=\"mp-result placeholder-only\"><div class=\"mp-r-thumb\"><span class=\"ph-frame\">待生成 · 1:1</span></div></div>\n <div class=\"mp-result placeholder-only\"><div class=\"mp-r-thumb\"><span class=\"ph-frame\">待生成 · 1:1</span></div></div>\n <div class=\"mp-result placeholder-only\"><div class=\"mp-r-thumb\"><span class=\"ph-frame\">待生成 · 1:1</span></div></div>\n </div>\n </div>\n </div>\n\n <div class=\"mp-pv-foot\" id=\"pv-foot\">\n // 采用即扣费并入对应商品的 <a href=\"products.html\">AI 素材库 →</a>;未采用的图不扣费、不保存\n <br>// 切换左侧商品空间 · 查看其他商品的批次记录\n </div>\n </div>\n\n </div><!-- /.mp-main-body -->\n </section><!-- /.mp-main -->\n\n </div>\n</div>\n\n<!-- ===== 商品库 全屏(无遮罩自适应,多选) ===== -->\n<div class=\"pl-modal-bg\" id=\"pl-modal-bg\">\n <div class=\"pl-modal\">\n <div class=\"pl-modal-h\">\n <h2>商品库</h2>\n <span class=\"ct\" id=\"pl-total-ct\">// 共 7 个商品</span>\n <div class=\"actions\">\n <button class=\"x\" type=\"button\" id=\"pl-close-btn\" aria-label=\"关闭\" style=\"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)\">\n <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>\n </button>\n </div>\n </div>\n <div class=\"pl-modal-body\">\n <aside class=\"pl-side\">\n <div class=\"pl-side-h\">分类</div>\n <div class=\"pl-side-item active\" data-cat=\"\">全部 <span class=\"ct\" id=\"pl-ct-all\">7</span></div>\n <div class=\"pl-side-item\" data-cat=\"美妆个护\">美妆个护 <span class=\"ct\">2</span></div>\n <div class=\"pl-side-item\" data-cat=\"数码 3C\">数码 3C <span class=\"ct\">1</span></div>\n <div class=\"pl-side-item\" data-cat=\"食品饮料\">食品饮料 <span class=\"ct\">2</span></div>\n <div class=\"pl-side-item\" data-cat=\"家居家电\">家居家电 <span class=\"ct\">1</span></div>\n <div class=\"pl-side-item\" data-cat=\"运动户外\">运动户外 <span class=\"ct\">1</span></div>\n </aside>\n <div class=\"pl-main\">\n <div class=\"pl-toolbar\">\n <div class=\"search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" id=\"pl-search-input\" placeholder=\"搜索商品名\">\n </div>\n <button class=\"btn-new\" type=\"button\" id=\"pl-new-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n 新建商品\n </button>\n </div>\n <div class=\"pl-scroll\">\n <div class=\"pl-grid\" id=\"pl-grid\">\n <!-- JS 渲染 -->\n </div>\n </div>\n </div>\n </div>\n <div class=\"pl-modal-f\">\n <div class=\"summary\">// 已选 <b id=\"pl-sel-ct\">0</b> 个商品</div>\n <button class=\"btn\" type=\"button\" id=\"pl-cancel-btn\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"pl-confirm-btn\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M5 12l5 5L20 7\"/></svg>\n 确认选择\n </button>\n </div>\n </div>\n</div>\n\n<!-- ===== 编辑商品 drawer (在商品库内点编辑触发,prefilled) ===== -->\n<div class=\"drawer-bg\" id=\"pc-drawer-bg\"></div>\n<aside class=\"drawer pc-drawer\" id=\"pc-drawer\" role=\"dialog\" aria-label=\"编辑商品\" aria-hidden=\"true\">\n <div class=\"drawer-h\">\n <h3 id=\"pc-drawer-title\">编辑商品</h3>\n <button class=\"x\" type=\"button\" id=\"pc-drawer-close\" aria-label=\"关闭\">\n <svg width=\"14\" height=\"14\" 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>\n </button>\n </div>\n <div class=\"drawer-b\">\n <div class=\"pc-field\">\n <label class=\"pc-field-label\">商品名称<span class=\"req\">*</span></label>\n <input class=\"input\" id=\"pcf-name\" placeholder=\"请输入商品名称(必填)\" maxlength=\"100\">\n </div>\n <div class=\"pc-field-row\">\n <div>\n <label class=\"pc-field-label\">品类<span class=\"req\">*</span></label>\n <select class=\"select\" id=\"pcf-cat\">\n <option>美妆个护</option>\n <option>服饰内衣</option>\n <option>食品饮料</option>\n <option>家居家电</option>\n <option>数码 3C</option>\n <option>个护清洁</option>\n <option>运动户外</option>\n <option>母婴亲子</option>\n </select>\n </div>\n <div>\n <label class=\"pc-field-label\">目标人群<span style=\"color:var(--black-alpha-48);margin-left:2px\">(选填)</span></label>\n <input class=\"input\" id=\"pcf-target\" placeholder=\"例: 22-32 岁女性、敏感肌、办公室通勤\">\n </div>\n </div>\n <div class=\"pc-field\">\n <label class=\"pc-field-label\">商品图片<span style=\"color:var(--black-alpha-48);margin-left:2px\">(<span id=\"pcf-imgs-ct\">6</span>)</span></label>\n <div class=\"pc-imgs\" id=\"pcf-imgs\"></div>\n </div>\n <div class=\"pc-field\">\n <label class=\"pc-field-label\">核心卖点<span class=\"req\">*</span></label>\n <ul class=\"pc-bullets\" id=\"pcf-bullets\">\n <li class=\"add\"><span class=\"num\">+</span><input id=\"pcf-add-input\" placeholder=\"添加新卖点 · 回车确认\"></li>\n </ul>\n </div>\n </div>\n <div class=\"drawer-f\">\n <button class=\"btn\" type=\"button\" id=\"pc-cancel-btn\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"pc-save-btn\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n 保存修改\n </button>\n </div>\n</aside>\n\n<!-- ===== 模特库 全屏(无遮罩自适应) ===== -->\n<div class=\"ml-modal-bg\" id=\"ml-modal-bg\">\n <div class=\"ml-modal\">\n <div class=\"ml-modal-h\">\n <h2>模特库</h2>\n <button class=\"x\" type=\"button\" id=\"ml-close-btn\" aria-label=\"关闭\">\n <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>\n </button>\n </div>\n <div class=\"ml-modal-body\">\n <aside class=\"ml-side\">\n <div class=\"ml-side-h\">来源</div>\n <div class=\"ml-side-item active\" data-source=\"all\">全部 <span class=\"ct\">12</span></div>\n <div class=\"ml-side-item\" data-source=\"preset\">平台预设 <span class=\"ct\">10</span></div>\n <div class=\"ml-side-item\" data-source=\"own\">我的上传 <span class=\"ct\">2</span></div>\n </aside>\n <div class=\"ml-main\">\n <div class=\"ml-toolbar\">\n <div class=\"chip-group\" data-key=\"gender\">\n <span class=\"lbl\">性别</span>\n <button class=\"chip active\" type=\"button\" data-val=\"\">全部</button>\n <button class=\"chip\" type=\"button\" data-val=\"女\">女</button>\n <button class=\"chip\" type=\"button\" data-val=\"男\">男</button>\n </div>\n <div class=\"chip-group\" data-key=\"age\">\n <span class=\"lbl\">年龄</span>\n <button class=\"chip active\" type=\"button\" data-val=\"\">全部</button>\n <button class=\"chip\" type=\"button\" data-val=\"青年\">青年</button>\n <button class=\"chip\" type=\"button\" data-val=\"中年\">中年</button>\n </div>\n </div>\n <div class=\"ml-scroll\">\n <div class=\"ml-grid\" id=\"ml-grid\">\n <!-- 12 个模特卡片 (placeholder) -->\n </div>\n </div>\n\n <!-- 添加模特 · 工作台画布(默认隐藏,点「添加模特」卡片后展开) -->\n <div class=\"ml-canvas\" id=\"ml-canvas\" aria-hidden=\"true\">\n <div class=\"ml-canvas-h\">\n <button class=\"back-btn\" type=\"button\" id=\"ml-canvas-back\" aria-label=\"返回模特库\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg>\n 返回\n </button>\n <h3>添加模特</h3>\n <span class=\"mono\">// 添加模特 · 工作台</span>\n <span style=\"flex:1;\"></span>\n </div>\n <div class=\"ml-canvas-body\">\n\n <!-- 左 · AI 生成(主视觉,照搬图片创作页面的内容区) -->\n <section class=\"mc-ai\">\n <div class=\"mc-stream\" id=\"mc-stream\">\n <div class=\"mc-stream-inner\" id=\"mc-stream-inner\">\n <div class=\"mc-empty\">\n <div class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z\"/><path d=\"M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z\"/></svg>\n </div>\n <span class=\"badge\">// AI · STUDIO</span>\n <h2>用 AI 生成一位新模特</h2>\n <p>描述外形 + 风格 + 服装,AI 会同时生成立绘 + 正/侧/背三视图,加入模特库。</p>\n <div class=\"examples\">\n <button class=\"ex\" type=\"button\" data-ex=\"清新校园风女生,黑色长直发,白色 T 恤 + 牛仔短裙,室内自然光\">清新校园风女生</button>\n <button class=\"ex\" type=\"button\" data-ex=\"都市 OL 通勤,黑色西装套装,30 岁知性气质\">都市 OL 通勤</button>\n <button class=\"ex\" type=\"button\" data-ex=\"健身房教练男性,运动背心 + 短裤,健身房布景\">健身教练 · 男</button>\n <button class=\"ex\" type=\"button\" data-ex=\"日系简约女生,棕色短发,米色针织衫,温柔气质\">日系简约</button>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"mc-input-wrap\">\n <div class=\"mc-input\">\n <!-- 上行 · 参考图 + 加号 (同一 flex 行,均 64×64) -->\n <div class=\"mc-input-top\">\n <div class=\"mc-input-refs\" id=\"mc-input-refs\"></div>\n <button class=\"add-btn\" type=\"button\" id=\"mc-add-btn\" title=\"上传参考图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </button>\n <input type=\"file\" id=\"mc-ai-ref-input\" accept=\"image/*\" multiple hidden>\n </div>\n\n <!-- 中行 · textarea 满宽 -->\n <textarea id=\"mc-input-text\" rows=\"1\" placeholder=\"描述模特外形、年龄、风格、服饰…例如:清新校园风女生,黑色长直发\"></textarea>\n\n <!-- 下行 · 参数 + 右 meta + 发送按钮 -->\n <div class=\"mc-input-bottom\">\n <div class=\"param\"><span class=\"lbl-mono\">比例</span><span>3:4</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></div>\n <div class=\"param\"><span class=\"lbl-mono\">风格</span><span>默认</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></div>\n <div class=\"param\"><span class=\"lbl-mono\">张数</span><span>4</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></div>\n <span class=\"right-meta\">预估 <span class=\"val\">¥0.80</span> · 余额 <span class=\"val\">¥327.40</span></span>\n <button class=\"send-btn\" type=\"button\" id=\"mc-send-btn\" disabled title=\"生成\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </button>\n </div>\n </div>\n </div>\n </section>\n\n <!-- 右 · 副视觉 · tab 切换(AI 生成 / 本地上传)+ 3 模块(姓名/立绘/三视图) -->\n <aside class=\"mc-up\">\n <div class=\"mc-up-tabs\">\n <button class=\"mc-up-tab active\" type=\"button\" data-tab=\"ai\">AI 生成</button>\n <button class=\"mc-up-tab\" type=\"button\" data-tab=\"local\">本地上传</button>\n </div>\n <div class=\"mc-up-body\">\n\n <!-- ① 模特姓名 -->\n <div class=\"mc-up-section\">\n <div class=\"mc-up-sec-h\">// 模特姓名</div>\n <input class=\"mc-up-name\" type=\"text\" id=\"mc-up-name\" placeholder=\"给模特起个名字…\" maxlength=\"20\">\n </div>\n\n <!-- ② 模特立绘 -->\n <div class=\"mc-up-section\">\n <div class=\"mc-up-sec-h\">// 模特立绘</div>\n <!-- AI 模式:从左侧 AI 生成结果选中 -->\n <div class=\"mc-portrait-ai\" data-show=\"ai\">\n <div class=\"empty\" id=\"mc-portrait-ai-empty\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"8\" r=\"4\"/><path d=\"M3 21c0-3.5 3-6 6-6s6 2.5 6 6\"/></svg></div>\n <div class=\"desc\">在左侧 AI 生成后<br>点击想要的立绘添加到这里</div>\n <div class=\"mono\">// 待选中</div>\n </div>\n <div class=\"picked\" id=\"mc-portrait-ai-picked\" hidden>\n <span class=\"badge\">已选用</span>\n <div class=\"ph-frame\" id=\"mc-portrait-ai-label\">模特立绘</div>\n <div class=\"ops\">\n <button type=\"button\" id=\"mc-portrait-ai-clear\" title=\"移除\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n </div>\n </div>\n <!-- 本地模式:上传多张 -->\n <div class=\"mc-portrait-local\" data-show=\"local\" hidden>\n <div class=\"drop\" id=\"mc-portrait-local-drop\" tabindex=\"0\" role=\"button\" aria-label=\"点击或拖入立绘\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/></svg></div>\n <div class=\"t\">点击或拖入立绘</div>\n <div class=\"d\">支持多张 JPG / PNG / WEBP · ≤ 10MB / 张</div>\n </div>\n <div class=\"list-h\">\n <span>// 已上传</span>\n <span class=\"ct\" id=\"mc-portrait-local-count\">0</span>\n <span>张</span>\n </div>\n <div class=\"list\" id=\"mc-portrait-local-list\"></div>\n <input type=\"file\" id=\"mc-portrait-local-input\" accept=\"image/*\" multiple hidden>\n </div>\n </div>\n\n <!-- ③ 模特三视图 · 16:9 占位始终在,空态时居中覆盖生成按钮 -->\n <div class=\"mc-up-section mc-triview\" id=\"mc-triview-sec\">\n <div class=\"mc-up-sec-h\">// 模特三视图</div>\n <div class=\"result-wrap\">\n <div class=\"result\" id=\"mc-triview-result\">\n <div class=\"ph-frame\" id=\"mc-triview-frame\">三视图(正/侧/背)</div>\n <!-- 空态居中按钮 + 提示 -->\n <button class=\"overlay-gen-btn\" type=\"button\" id=\"mc-triview-gen-btn\" disabled>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z\"/></svg>\n 生成三视图\n </button>\n <div class=\"overlay-hint\" id=\"mc-triview-hint\">// 先选中左侧 AI 立绘</div>\n </div>\n <!-- 有版本时的操作行 + 历史 -->\n <div class=\"result-ops\" id=\"mc-triview-ops\" hidden>\n <button type=\"button\" id=\"mc-triview-rerun\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg> 重跑</button>\n <span class=\"cost\">~¥0.30 / 次</span>\n </div>\n <div class=\"history\" id=\"mc-triview-history\" hidden>\n <div class=\"h-lbl\">// 历史版本 · <span class=\"ct\" id=\"mc-triview-history-count\">0</span> 版</div>\n <div class=\"h-row\" id=\"mc-triview-history-row\"></div>\n </div>\n </div>\n </div>\n\n </div>\n <div class=\"mc-up-foot\">\n <span class=\"stat\" id=\"mc-up-stat\">// 待完成</span>\n <button type=\"button\" class=\"commit-btn\" id=\"mc-up-commit\" disabled>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n 加入模特库\n </button>\n </div>\n </aside>\n\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<!-- ===== 添加模特 · 选择来源 ===== -->\n<div class=\"ml-up-choice-bg\" id=\"ml-up-choice-bg\">\n <div class=\"ml-up-choice\" role=\"dialog\" aria-label=\"添加模特\">\n <div class=\"uc-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"8\" r=\"4\"/><path d=\"M3 21c0-3.5 3-6 6-6s6 2.5 6 6\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n </div>\n <div class=\"ti\">\n <strong>添加模特</strong>\n <span class=\"mono\">// 选择来源 · AI 生成或本地上传</span>\n </div>\n <button class=\"uc-x\" type=\"button\" id=\"ml-up-x\" aria-label=\"关闭\">\n <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>\n </button>\n </div>\n <div class=\"uc-body\">\n <button type=\"button\" class=\"uc-option\" id=\"ml-up-ai\">\n <span class=\"opt-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z\"/><path d=\"M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z\"/></svg>\n </span>\n <div class=\"opt-t\">AI 生成</div>\n <div class=\"opt-d\">描述外形 + 风格,AI 自动生成新模特形象与三视图</div>\n <span class=\"opt-tag\">[ AI · STUDIO ]</span>\n </button>\n <button type=\"button\" class=\"uc-option\" id=\"ml-up-local\">\n <span class=\"opt-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/></svg>\n </span>\n <div class=\"opt-t\">本地上传</div>\n <div class=\"opt-d\">上传商家真人 / 既有素材,后续可生成三视图统一镜头</div>\n <span class=\"opt-tag\">[ UPLOAD ]</span>\n </button>\n </div>\n </div>\n</div>\n<input type=\"file\" id=\"ml-up-file\" accept=\"image/*\" multiple hidden>\n\n<!-- ===== 模特详情 居中弹窗 (参考布局 v2) ===== -->\n<div class=\"md-modal-bg\" id=\"md-modal-bg\">\n <div class=\"md-modal\">\n <div class=\"md-modal-h\">\n <h3 id=\"md-title\">模特详情</h3>\n <span class=\"ad-tag\" id=\"md-kind\">/ 人物 · 模特</span>\n <button class=\"x\" type=\"button\" id=\"md-close-btn\" aria-label=\"关闭\">\n <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>\n </button>\n </div>\n <div class=\"md-modal-body\">\n <div class=\"md-detail-grid\">\n <!-- 左栏 · 大立绘 + 缩略图 -->\n <div class=\"md-lead\">\n <div class=\"md-lead-wrap\">\n <div class=\"md-lead-img\">\n <div class=\"ph-name\" id=\"md-portrait-name\">—</div>\n <div class=\"ph-frame\">立绘 · 3:4</div>\n </div>\n <button class=\"md-zoom-btn\" type=\"button\" id=\"md-lead-zoom\" aria-label=\"查看大图\" title=\"查看大图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg>\n </button>\n </div>\n <div class=\"md-thumbs\" id=\"md-thumbs\"></div>\n </div>\n <!-- 右栏 · 三视图 + 简介 + 属性 -->\n <div class=\"md-right\">\n <div class=\"md-section\">\n <div class=\"md-section-h\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/></svg></span>\n <span class=\"t\">三视图</span>\n <span class=\"ratio-chip\">16:9</span>\n <button class=\"icon-btn\" type=\"button\" title=\"下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n </div>\n <div class=\"md-views\">\n <div class=\"md-view\"><div class=\"lbl\">正 / 侧 / 背 · 三视图</div></div>\n </div>\n </div>\n <div class=\"md-section\">\n <div class=\"md-section-h\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6h16M4 12h16M4 18h10\"/></svg></span>\n <span class=\"t\">简介</span>\n </div>\n <p class=\"md-intro\" id=\"md-intro\">—</p>\n <div class=\"md-tags\" id=\"md-tags\"></div>\n </div>\n <div class=\"md-props\" id=\"md-props\"></div>\n </div>\n </div>\n </div>\n <div class=\"md-modal-f\">\n <div class=\"foot-stats\">\n <button class=\"stat-btn\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>\n 下载\n </button>\n </div>\n <button class=\"btn btn-primary\" type=\"button\" id=\"md-select\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M5 12l5 5L20 7\"/></svg>\n 选用此模特\n </button>\n </div>\n </div>\n</div>\n\n<!-- ===== 缺三视图 · 保存前提醒弹窗 ===== -->\n<div class=\"mc-leave-bg\" id=\"mc-notri-bg\" aria-hidden=\"true\">\n <div class=\"mc-leave\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"mc-notri-title\">\n <div class=\"lv-h\">\n <span class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z\"/></svg>\n </span>\n <h3 id=\"mc-notri-title\">缺三视图 · 仍要保存吗?</h3>\n <span class=\"mono\">// MISSING TRI-VIEW</span>\n </div>\n <div class=\"lv-b\">\n 该模特尚未生成 <b>正 / 侧 / 背</b> 三视图。直接进入后续图片/视频生成时,模型缺少多角度参考,<b>角色一致性、姿态稳定性可能下降</b>。\n <br><br>\n 建议先点「<b>去生成三视图</b>」补齐(约 12s · ¥0.30);若现在不生成,后续也可以在<b>资产详情页</b>里随时补回。\n </div>\n <div class=\"lv-f\">\n <span class=\"spacer\"></span>\n <button class=\"btn\" type=\"button\" id=\"mc-notri-save\">仍要保存</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"mc-notri-gen\">去生成三视图</button>\n </div>\n </div>\n</div>\n\n<!-- ===== 离开工作台 · 二次确认弹窗 ===== -->\n<div class=\"mc-leave-bg\" id=\"mc-leave-bg\" aria-hidden=\"true\">\n <div class=\"mc-leave\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"mc-leave-title\">\n <div class=\"lv-h\">\n <span class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z\"/></svg>\n </span>\n <h3 id=\"mc-leave-title\">退出工作台?</h3>\n <span class=\"mono\">// UNSAVED</span>\n </div>\n <div class=\"lv-b\" id=\"mc-leave-body\">\n 工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「加入模特库」来保留进度。\n </div>\n <div class=\"lv-f\">\n <span class=\"spacer\"></span>\n <button class=\"btn\" type=\"button\" id=\"mc-leave-cancel\">继续编辑</button>\n <button class=\"btn btn-danger\" type=\"button\" id=\"mc-leave-confirm\">不保存,退出</button>\n </div>\n </div>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script src=\"/exact/assets/new-product-drawer.js?v=202605211643\"></script>\n<script>\nShell.render({\n active: 'asset-factory',\n crumbs: [\n { label: '工作台', href: 'index.html' },\n { label: '图片生成', href: 'asset-factory.html' },\n { label: '模特上身图' }\n ]\n});\n\n// ─── 商品库数据 (mock,与 products.html 7 个商品对齐) ───\nconst PRODUCTS = [\n { id: 'p1', name: '透真玻尿酸补水面膜', cat: '美妆个护', meta: '熬夜党 · 124 素材' },\n { id: 'p2', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', meta: '通勤 · 96 素材' },\n { id: 'p3', name: '滋啦速食牛肉面 6 桶装', cat: '食品饮料', meta: '加班 · 96 素材' },\n { id: 'p4', name: '透真清透物理防晒霜', cat: '美妆个护', meta: 'SPF50 · 76 素材' },\n { id: 'p5', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', meta: '提神 · 68 素材' },\n { id: 'p6', name: '小熊 4L 可视空气炸锅', cat: '家居家电', meta: '小户型 · 54 素材' },\n { id: 'p7', name: '露露同款裸感瑜伽裤', cat: '运动户外', meta: '健身房 · 42 素材' },\n];\n\n// ─── State (单选 · 默认全空) ───\nconst state = {\n selectedProd: null, // string | null\n selectedModel: null, // string | null\n count: 4,\n ratio: '1:1',\n};\nconst UNIT_PRICE = 0.30;\n\n// ─── 商品空间 (左侧栏) 渲染 ───\nlet _psQuery = '';\nfunction renderProdSpace() {\n const listEl = document.getElementById('ps-list');\n const ctEl = document.getElementById('ps-count');\n const allCtEl = document.getElementById('ps-all-ct');\n if (!listEl) return;\n if (ctEl) ctEl.textContent = PRODUCTS.length;\n if (allCtEl) allCtEl.textContent = PRODUCTS.length + ' 个';\n const q = _psQuery.trim();\n const filtered = q\n ? PRODUCTS.filter(p => p.name.includes(q) || p.cat.includes(q))\n : PRODUCTS;\n if (!filtered.length) {\n listEl.innerHTML = `<div class=\"mp-ps-empty\">// NO MATCH<br>试试其他关键词</div>`;\n return;\n }\n listEl.innerHTML = filtered.map(p => `\n <div class=\"mp-prod-item${state.selectedProd === p.id ? ' active' : ''}\" data-id=\"${p.id}\">\n <div class=\"placeholder thumb\"></div>\n <div class=\"body\">\n <div class=\"nm\">${p.name}</div>\n <div class=\"sub\">// ${p.cat}</div>\n </div>\n </div>\n `).join('');\n listEl.querySelectorAll('.mp-prod-item').forEach(el => {\n el.addEventListener('click', () => selectProduct(el.dataset.id));\n });\n}\n\n// 选中商品 (sidebar 单选 · 同步更新表单/预览/Cost)\nfunction selectProduct(id) {\n state.selectedProd = id;\n // 商品空间 active 态\n document.querySelectorAll('.mp-prod-item').forEach(el => {\n el.classList.toggle('active', el.dataset.id === id);\n });\n // 当前商品 header strip\n updateCurProdHeader();\n // 预览区: 按商品过滤批次重渲染\n if (typeof renderBatchesForCurrentProd === 'function') renderBatchesForCurrentProd();\n // 同步 pv-summary 商品名\n const p = PRODUCTS.find(x => x.id === id);\n document.getElementById('pv-prod').textContent = p ? p.name : '未选择';\n updateCost();\n}\n\n// 当前商品 header strip\nfunction updateCurProdHeader() {\n const nmEl = document.getElementById('cur-prod-nm');\n const statsEl = document.getElementById('cur-prod-stats');\n const batchesEl = document.getElementById('cur-prod-batches');\n if (!nmEl) return;\n const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;\n if (!p) {\n nmEl.textContent = '未选择 · 请在左侧商品空间选一个';\n nmEl.classList.add('placeholder');\n if (statsEl) statsEl.hidden = true;\n } else {\n nmEl.textContent = p.name;\n nmEl.classList.remove('placeholder');\n const ct = (window._countBatchesForProd ? window._countBatchesForProd(p.id) : 0);\n if (batchesEl) batchesEl.textContent = ct;\n if (statsEl) statsEl.hidden = false;\n }\n}\n\n// 保留旧函数名 alias (兼容旧 call site)\nfunction renderSelectedProds() {\n renderProdSpace();\n updateCurProdHeader();\n const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;\n document.getElementById('pv-prod').textContent = p ? p.name : '未选择';\n updateCost();\n}\n\n// ─── 商品库全屏弹窗 (单选) ───\nlet _plDraft = null; // string | null\nlet _plCatFilter = '';\nlet _plQuery = '';\n\nfunction renderProdLib() {\n const grid = document.getElementById('pl-grid');\n let list = PRODUCTS;\n if (_plCatFilter) list = list.filter(p => p.cat === _plCatFilter);\n if (_plQuery) list = list.filter(p => p.name.includes(_plQuery));\n grid.innerHTML = list.map(p => `\n <div class=\"pl-card${_plDraft === p.id ? ' selected' : ''}\" data-id=\"${p.id}\">\n <div class=\"pl-card-actions\">\n <button class=\"pl-act\" type=\"button\" data-edit=\"${p.id}\" title=\"编辑商品\" aria-label=\"编辑\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4 12.5-12.5z\"/></svg>\n </button>\n <button class=\"pl-act danger\" type=\"button\" data-del=\"${p.id}\" title=\"删除商品\" aria-label=\"删除\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </button>\n </div>\n <div class=\"pl-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder pl-thumb\"><span class=\"ph-frame\">${p.name}</span></div>\n <div class=\"pl-name\">${p.name}</div>\n <div class=\"pl-meta\">${p.cat} · ${p.meta}</div>\n </div>\n `).join('');\n grid.querySelectorAll('.pl-card').forEach(card => {\n card.addEventListener('click', e => {\n // 点击编辑/删除按钮不切换选中\n if (e.target.closest('[data-edit]') || e.target.closest('[data-del]')) return;\n const id = card.dataset.id;\n // 单选: 选中当前,取消其他\n _plDraft = (_plDraft === id) ? null : id;\n grid.querySelectorAll('.pl-card').forEach(c => c.classList.toggle('selected', c.dataset.id === _plDraft));\n document.getElementById('pl-sel-ct').textContent = _plDraft ? 1 : 0;\n });\n });\n grid.querySelectorAll('[data-edit]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n openEditProductDrawer(btn.dataset.edit);\n });\n });\n grid.querySelectorAll('[data-del]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const id = btn.dataset.del;\n const p = PRODUCTS.find(x => x.id === id);\n if (!p) return;\n if (!confirm('确认删除「' + p.name + '」?\\n该操作不可撤销,商品下生成的素材记录也会一并清理。')) return;\n // 从 mock 数据移除\n const idx = PRODUCTS.findIndex(x => x.id === id);\n if (idx >= 0) PRODUCTS.splice(idx, 1);\n if (_plDraft === id) _plDraft = null;\n if (state.selectedProd === id) state.selectedProd = null;\n renderProdLib();\n renderSelectedProds();\n Shell.toast('已删除', p.name);\n });\n });\n document.getElementById('pl-sel-ct').textContent = _plDraft ? 1 : 0;\n}\n\n// ─── 编辑商品 drawer (在商品库内 prefill 数据) ───\n// mock 商品扩展属性 (target + bullets),缺失则给默认值\nconst PRODUCT_EXTRA = {\n p1: { target: '熬夜党 · 25-35 岁女性 · 敏感肌', bullets: ['72h 长效补水', '官方授权正品', '通勤补妆神器'], imgs: 6 },\n p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'], imgs: 6 },\n p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'], imgs: 6 },\n p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'], imgs: 6 },\n p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'], imgs: 6 },\n p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'], imgs: 6 },\n p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'], imgs: 6 },\n};\n\n// 渲染商品图片 grid · n 张占位 + 上传按钮\nfunction renderProdImgs(n) {\n const grid = document.getElementById('pcf-imgs');\n const ct = document.getElementById('pcf-imgs-ct');\n if (!grid) return;\n if (ct) ct.textContent = n;\n let html = '';\n for (let i = 0; i < n; i++) {\n html += `<div class=\"thumb\"><span class=\"ph-frame\">1:1</span><button class=\"rm\" type=\"button\" title=\"删除\" data-idx=\"${i}\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button></div>`;\n }\n html += `<div class=\"img-upload\" id=\"pcf-img-add\" title=\"上传图片\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg></div>`;\n grid.innerHTML = html;\n grid.querySelectorAll('.thumb .rm').forEach(btn => {\n btn.addEventListener('click', () => {\n btn.closest('.thumb').remove();\n if (ct) ct.textContent = grid.querySelectorAll('.thumb').length;\n });\n });\n const addBtn = document.getElementById('pcf-img-add');\n if (addBtn) addBtn.addEventListener('click', () => {\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('上传图片', '// 演示版暂不支持真实上传');\n });\n}\n\nlet _editingProdId = null;\n\nfunction openEditProductDrawer(id) {\n const p = PRODUCTS.find(x => x.id === id);\n if (!p) return;\n _editingProdId = id;\n // prefill\n document.getElementById('pc-drawer-title').textContent = '编辑商品 · ' + p.name;\n document.getElementById('pcf-name').value = p.name;\n document.getElementById('pcf-cat').value = p.cat;\n const extra = PRODUCT_EXTRA[id] || { target: '', bullets: [], imgs: 0 };\n document.getElementById('pcf-target').value = extra.target || '';\n // 渲染商品图片 (n 张占位)\n renderProdImgs(typeof extra.imgs === 'number' ? extra.imgs : 6);\n // 渲染 bullets\n const ul = document.getElementById('pcf-bullets');\n // 移除除 .add 之外的所有 li\n ul.querySelectorAll('li:not(.add)').forEach(li => li.remove());\n const addLi = ul.querySelector('.add');\n (extra.bullets || []).forEach((b, i) => {\n const li = document.createElement('li');\n li.innerHTML = `\n <span class=\"num\">${i + 1}</span>\n <input value=\"${b.replace(/\"/g, '&quot;')}\">\n <button class=\"rm\" type=\"button\" aria-label=\"删除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button>\n `;\n ul.insertBefore(li, addLi);\n li.querySelector('.rm').addEventListener('click', () => {\n li.remove();\n renumberBullets();\n });\n });\n document.getElementById('pcf-add-input').value = '';\n document.getElementById('pc-drawer-bg').classList.add('show');\n document.getElementById('pc-drawer').classList.add('show');\n document.getElementById('pc-drawer').setAttribute('aria-hidden', 'false');\n}\n\nfunction renumberBullets() {\n const ul = document.getElementById('pcf-bullets');\n [...ul.querySelectorAll('li:not(.add) .num')].forEach((s, i) => { s.textContent = i + 1; });\n}\n\nfunction closeEditProductDrawer() {\n document.getElementById('pc-drawer-bg').classList.remove('show');\n document.getElementById('pc-drawer').classList.remove('show');\n document.getElementById('pc-drawer').setAttribute('aria-hidden', 'true');\n _editingProdId = null;\n}\n\n// 新增 bullet · 回车\ndocument.getElementById('pcf-add-input').addEventListener('keydown', e => {\n if (e.key !== 'Enter') return;\n const v = e.target.value.trim();\n if (!v) return;\n const ul = document.getElementById('pcf-bullets');\n const addLi = ul.querySelector('.add');\n const li = document.createElement('li');\n li.innerHTML = `\n <span class=\"num\">0</span>\n <input value=\"${v.replace(/\"/g, '&quot;')}\">\n <button class=\"rm\" type=\"button\" aria-label=\"删除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button>\n `;\n ul.insertBefore(li, addLi);\n li.querySelector('.rm').addEventListener('click', () => { li.remove(); renumberBullets(); });\n e.target.value = '';\n renumberBullets();\n});\n\ndocument.getElementById('pc-drawer-close').addEventListener('click', closeEditProductDrawer);\ndocument.getElementById('pc-cancel-btn').addEventListener('click', closeEditProductDrawer);\ndocument.getElementById('pc-drawer-bg').addEventListener('click', closeEditProductDrawer);\ndocument.getElementById('pc-save-btn').addEventListener('click', () => {\n if (!_editingProdId) return;\n const newName = document.getElementById('pcf-name').value.trim();\n const newCat = document.getElementById('pcf-cat').value;\n const newTarget = document.getElementById('pcf-target').value.trim();\n if (!newName) { Shell.toast('请填写商品名称'); return; }\n // 写回 PRODUCTS\n const p = PRODUCTS.find(x => x.id === _editingProdId);\n if (p) { p.name = newName; p.cat = newCat; }\n // 写回 PRODUCT_EXTRA (含 imgs 数量)\n const bullets = [...document.querySelectorAll('#pcf-bullets li:not(.add) input')].map(i => i.value.trim()).filter(Boolean);\n const imgs = document.querySelectorAll('#pcf-imgs .thumb').length;\n PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets, imgs };\n Shell.toast('已保存', newName);\n closeEditProductDrawer();\n renderProdLib();\n renderSelectedProds();\n});\n\n// 全部商品 入口 (左侧栏底部 · 打开商品库 modal)\nfunction openProdLibModal() {\n _plDraft = state.selectedProd;\n _plCatFilter = '';\n _plQuery = '';\n document.getElementById('pl-search-input').value = '';\n document.querySelectorAll('.pl-side-item').forEach(x => x.classList.toggle('active', x.dataset.cat === ''));\n renderProdLib();\n document.getElementById('pl-modal-bg').classList.add('show');\n}\ndocument.getElementById('ps-all-btn').addEventListener('click', openProdLibModal);\n\n// 商品空间 · 搜索框 · 新建按钮\ndocument.getElementById('ps-search-input').addEventListener('input', e => {\n _psQuery = e.target.value;\n renderProdSpace();\n});\ndocument.getElementById('ps-new-btn').addEventListener('click', () => {\n if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }\n window.NewProductDrawer.open({\n onSave: function (p) {\n const product = {\n id: p.id,\n name: p.name,\n cat: p.cat,\n meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',\n };\n PRODUCTS.unshift(product);\n renderProdSpace();\n selectProduct(product.id);\n Shell.toast('已加入商品库', '+ ' + product.name);\n }\n });\n});\ndocument.getElementById('pl-close-btn').addEventListener('click', () => {\n document.getElementById('pl-modal-bg').classList.remove('show');\n});\ndocument.getElementById('pl-cancel-btn').addEventListener('click', () => {\n document.getElementById('pl-modal-bg').classList.remove('show');\n});\ndocument.getElementById('pl-confirm-btn').addEventListener('click', () => {\n if (!_plDraft) { Shell.toast('请先选择商品', '只能选 1 个'); return; }\n document.getElementById('pl-modal-bg').classList.remove('show');\n selectProduct(_plDraft);\n});\ndocument.getElementById('pl-new-btn').addEventListener('click', () => {\n if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }\n // 商品库保持 open(drawer z-index 1101 > pl-modal-bg 998 会覆盖之上)\n window.NewProductDrawer.open({\n onSave: function (p) {\n // 把新商品注入本页 PRODUCTS,刷新商品库 + 已选列表\n const product = {\n id: p.id,\n name: p.name,\n cat: p.cat,\n meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',\n };\n PRODUCTS.unshift(product);\n // 单选: 新建商品直接选中(覆盖原选)\n _plDraft = product.id;\n // 强制 reset filter/query,保证新商品在首位可见\n _plCatFilter = '';\n _plQuery = '';\n const searchInput = document.getElementById('pl-search-input');\n if (searchInput) searchInput.value = '';\n renderProdLib();\n renderProdSpace();\n selectProduct(product.id);\n Shell.toast('已加入商品库', '+ ' + product.name);\n }\n });\n});\ndocument.querySelectorAll('.pl-side-item').forEach(item => {\n item.addEventListener('click', () => {\n document.querySelectorAll('.pl-side-item').forEach(x => x.classList.remove('active'));\n item.classList.add('active');\n _plCatFilter = item.dataset.cat;\n renderProdLib();\n });\n});\ndocument.getElementById('pl-search-input').addEventListener('input', e => {\n _plQuery = e.target.value.trim();\n renderProdLib();\n});\n\n// ─── 模特选择 (单选) ───\nfunction updateModelSummary() {\n const id = state.selectedModel;\n const card = id ? document.querySelector('.model-card[data-id=\"' + id + '\"]') : null;\n const name = card ? card.dataset.name : '';\n document.getElementById('pv-model').textContent = name\n ? name + ' (亚洲·25岁·清新)'\n : '未选择';\n updateCost();\n}\nfunction updateCost() {\n const hasProd = !!state.selectedProd;\n const hasModel = !!state.selectedModel;\n const total = (hasProd && hasModel ? 1 : 0) * state.count * UNIT_PRICE;\n document.getElementById('cost-total').textContent = '¥' + total.toFixed(2);\n const btn = document.getElementById('mp-go-btn');\n if (!hasProd || !hasModel) btn.classList.add('disabled');\n else btn.classList.remove('disabled');\n}\nfunction selectModel(id) {\n state.selectedModel = (state.selectedModel === id) ? null : id;\n renderModelMini();\n // 同步所有出现的 model-card (lib grid 里的)\n document.querySelectorAll('.ml-grid .model-card').forEach(c =>\n c.classList.toggle('selected', c.dataset.id === state.selectedModel)\n );\n updateModelSummary();\n}\n\n/* mini grid · 动态渲染 · 选中的模特(如不在默认 4 张里)会顶替到首位 */\nconst MINI_DEFAULT_IDS = ['m1','m2','m3','m4'];\nfunction renderModelMini() {\n const grid = document.getElementById('model-grid-mini');\n if (!grid) return;\n const selId = state.selectedModel;\n let ids;\n if (!selId || MINI_DEFAULT_IDS.includes(selId)) {\n ids = MINI_DEFAULT_IDS.slice();\n } else {\n ids = [selId, ...MINI_DEFAULT_IDS.slice(0, 3)];\n }\n grid.innerHTML = ids.map(id => {\n const m = MODELS.find(x => x.id === id);\n if (!m) return '';\n const isSelected = m.id === selId;\n return `\n <div class=\"model-card${isSelected ? ' selected' : ''}\" data-id=\"${m.id}\" data-name=\"${m.name}\">\n <div class=\"placeholder m-thumb\"><span class=\"ph-frame\">${m.name}</span></div>\n <div class=\"m-name\">${m.name}</div>\n <div class=\"m-tag\">${m.gender}·${m.age}·${m.style}</div>\n </div>\n `;\n }).join('');\n grid.querySelectorAll('.model-card').forEach(card => {\n card.addEventListener('click', e => {\n if (e.target.closest('.m-thumb')) {\n openModelDetail(card.dataset.id);\n return;\n }\n selectModel(card.dataset.id);\n });\n });\n}\n// 注:首次 renderModelMini() 调用挪到 MODELS 声明之后,避免 TDZ\n// ─── 立即生成设置 ───\ndocument.querySelectorAll('.pill-row').forEach(row => {\n row.addEventListener('click', e => {\n const btn = e.target.closest('.opt');\n if (!btn) return;\n row.querySelectorAll('.opt').forEach(o => o.classList.remove('active'));\n btn.classList.add('active');\n const key = row.dataset.key;\n state[key] = isNaN(+btn.dataset.val) ? btn.dataset.val : +btn.dataset.val;\n if (key === 'count') document.getElementById('pv-count').textContent = btn.dataset.val;\n if (key === 'ratio') document.getElementById('pv-ratio').textContent = btn.dataset.val;\n updateCost();\n });\n});\n\n// ─── 预览区空态 / 内容 切换 ───\nfunction showPreviewEmpty() {\n const empty = document.getElementById('pv-empty');\n const sum = document.getElementById('pv-summary');\n const grid = document.getElementById('pv-grid');\n const foot = document.getElementById('pv-foot');\n if (empty) empty.hidden = false;\n if (sum) sum.hidden = true;\n if (grid) grid.hidden = true;\n if (foot) foot.hidden = true;\n}\nfunction showPreviewContent() {\n const empty = document.getElementById('pv-empty');\n const sum = document.getElementById('pv-summary');\n const grid = document.getElementById('pv-grid');\n const foot = document.getElementById('pv-foot');\n if (empty) empty.hidden = true;\n if (sum) sum.hidden = false;\n if (grid) grid.hidden = false;\n if (foot) foot.hidden = false;\n // pv-batch 由 renderResultCards 单独控制\n}\n\n// ─── 立即生成 + 生成结果交互 (hover 重跑/采用 + 批量) ───\nconst RERUN_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"23 4 23 10 17 10\"/><polyline points=\"1 20 1 14 7 14\"/><path d=\"M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15\"/></svg>';\nconst ADOPT_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>';\n\nlet _batchSeq = 0;\nconst CELL_RERUN_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>';\nconst CELL_DL_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>';\nconst CELL_MORE_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/></svg>';\nconst CELL_ADOPT_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg>';\nconst CELL_DEL_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>';\nconst CELL_EDIT_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z\"/></svg>';\n\nfunction buildResultCard(label) {\n const div = document.createElement('div');\n div.className = 'mp-result gen';\n div.innerHTML = `\n <div class=\"mp-r-thumb\"><span class=\"ph-frame\">${label || state.ratio}</span></div>\n <span class=\"adopt-badge\">已采用</span>\n <div class=\"cell-feedback\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n <span>已采用</span>\n </div>\n <div class=\"cell-ops\">\n <button class=\"r-rerun\" type=\"button\" title=\"再次生成\" aria-label=\"再次生成\">${CELL_RERUN_SVG}</button>\n <button class=\"r-dl\" type=\"button\" title=\"下载\" aria-label=\"下载\">${CELL_DL_SVG}</button>\n <div class=\"cell-more-wrap\">\n <button class=\"r-more\" type=\"button\" title=\"更多\" aria-label=\"更多\">${CELL_MORE_SVG}</button>\n <div class=\"cell-more-menu\">\n <button class=\"r-adopt\" type=\"button\">${CELL_ADOPT_SVG}<span>加入资产库</span></button>\n <button class=\"r-del danger\" type=\"button\">${CELL_DEL_SVG}<span>删除</span></button>\n </div>\n </div>\n </div>\n `;\n // 模拟 ~1.2s 后切到 ok 状态\n setTimeout(() => {\n if (div.classList.contains('gen')) div.classList.remove('gen');\n }, 1200);\n div.querySelector('.r-rerun').addEventListener('click', e => { e.stopPropagation(); rerunOne(div); });\n div.querySelector('.r-dl').addEventListener('click', e => {\n e.stopPropagation();\n Shell.toast('下载', '已开始下载 · MOCK');\n });\n // 更多 menu 开/合\n const moreBtn = div.querySelector('.r-more');\n const moreWrap = div.querySelector('.cell-more-wrap');\n moreBtn.addEventListener('click', e => {\n e.stopPropagation();\n const willOpen = !moreWrap.classList.contains('open');\n document.querySelectorAll('.mp-result .cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (willOpen) moreWrap.classList.add('open');\n });\n div.querySelector('.r-adopt').addEventListener('click', e => {\n e.stopPropagation();\n moreWrap.classList.remove('open');\n adoptOne(div);\n });\n div.querySelector('.r-del').addEventListener('click', e => {\n e.stopPropagation();\n moreWrap.classList.remove('open');\n const batch = div.closest('.mp-result-batch');\n div.remove();\n if (batch) {\n // 如果该批次空了, 整批移除\n if (!batch.querySelectorAll('.mp-result:not(.placeholder-only)').length) batch.remove();\n else updateBatchSummary();\n }\n Shell.toast('已删除');\n });\n return div;\n}\nfunction appendBatch(n, kind) {\n const grid = document.getElementById('pv-grid');\n _batchSeq += 1;\n const batch = document.createElement('div');\n batch.className = 'mp-result-batch';\n batch.dataset.kind = kind;\n const ts = new Date();\n const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`;\n const labCls = kind === 'gen' ? 'gen' : 'rerun';\n const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`);\n const _curModel = state.selectedModel ? MODELS.find(x => x.id === state.selectedModel) : null;\n batch.dataset.ts = String(ts.getTime());\n batch.dataset.modelId = _curModel ? _curModel.id : '';\n batch.dataset.modelName = _curModel ? _curModel.name : '';\n batch.dataset.ratio = state.ratio || '';\n batch.dataset.search = [\n labTxt, _curModel ? _curModel.name : '',\n _curModel ? _curModel.style : '',\n state.ratio || '', n + '张'\n ].join(' ').toLowerCase();\n batch.innerHTML = `\n <div class=\"mp-batch-head\">\n <span class=\"lab ${labCls}\">${labTxt}</span>\n <span class=\"sep\">·</span>\n <span>${n} 张 · ${state.ratio}</span>\n ${_curModel ? `<span class=\"sep\">·</span><span>${_curModel.name}</span>` : ''}\n <span class=\"sep\">·</span>\n <span>${tsStr}</span>\n </div>\n <div class=\"mp-result-grid\"></div>\n <div class=\"mp-pv-batch batch-foot\">\n <button class=\"pill-btn edit-batch\" type=\"button\" title=\"重新编辑\">\n ${CELL_EDIT_SVG}\n <span>重新编辑</span>\n </button>\n <button class=\"pill-btn rerun-batch\" type=\"button\" title=\"再次生成这一批\">\n ${CELL_RERUN_SVG}\n <span>再次生成</span>\n </button>\n <div class=\"batch-more-wrap\">\n <button class=\"pill-btn icon batch-more\" type=\"button\" title=\"更多\" aria-label=\"更多\">${CELL_MORE_SVG}</button>\n <div class=\"batch-more-menu\" role=\"menu\">\n <button class=\"batch-save-all\" type=\"button\">${CELL_ADOPT_SVG}<span>全部加入资产库</span></button>\n <button class=\"batch-del danger\" type=\"button\">${CELL_DEL_SVG}<span>删除该批结果</span></button>\n </div>\n </div>\n </div>\n `;\n const gridInner = batch.querySelector('.mp-result-grid');\n for (let i = 0; i < n; i++) gridInner.appendChild(buildResultCard());\n batch.querySelector('.edit-batch').addEventListener('click', () => {\n // 跳回左侧表单(模特/数量/比例) · 与图片创作「重新编辑」语义对齐\n const form = document.querySelector('.mp-form');\n if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });\n Shell.toast('重新编辑', '请在左侧调整模特 / 比例 / 数量后再生成');\n });\n batch.querySelector('.rerun-batch').addEventListener('click', () => {\n appendBatch(n, 'rerun-all');\n Shell.toast('再次生成', n + ' 张图重新生成中 · 新批次已追加');\n });\n // 提供 _adoptAll 内联函数 (给 更多 menu 的「全部加入资产库」复用)\n const _adoptAll = () => {\n const cards = batch.querySelectorAll('.mp-result:not(.adopted)');\n if (!cards.length) { Shell.toast('该批次已全部采用'); return; }\n cards.forEach(c => { c.classList.remove('gen'); c.classList.add('adopted'); });\n updateBatchSummary();\n Shell.toast('已全部加入资产库', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));\n };\n // 批次「更多」按钮 → 开/合 menu\n const _bMoreBtn = batch.querySelector('.batch-more');\n const _bMoreWrap = batch.querySelector('.batch-more-wrap');\n if (_bMoreBtn && _bMoreWrap) {\n _bMoreBtn.addEventListener('click', e => {\n e.stopPropagation();\n const willOpen = !_bMoreWrap.classList.contains('open');\n document.querySelectorAll('.mp-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (willOpen) _bMoreWrap.classList.add('open');\n });\n }\n batch.querySelector('.batch-save-all').addEventListener('click', e => {\n e.stopPropagation();\n if (_bMoreWrap) _bMoreWrap.classList.remove('open');\n _adoptAll();\n });\n batch.querySelector('.batch-del').addEventListener('click', e => {\n e.stopPropagation();\n if (_bMoreWrap) _bMoreWrap.classList.remove('open');\n batch.remove();\n updateBatchSummary();\n Shell.toast('已删除该批结果');\n });\n grid.appendChild(batch);\n batch.scrollIntoView({ behavior: 'smooth', block: 'end' });\n updateBatchSummary();\n if (typeof _refreshModelMenu === 'function') _refreshModelMenu();\n if (typeof applyPvFilters === 'function') applyPvFilters();\n}\nfunction renderResultCards(n) {\n const grid = document.getElementById('pv-grid');\n // 首次生成:清掉默认 placeholder-batch 占位,但保留已有真实批次\n // 再次「立即生成」(用户换了设置):追加新批次到底部,不再覆盖\n grid.querySelectorAll('.placeholder-batch').forEach(el => el.remove());\n appendBatch(n, 'gen');\n}\n\nfunction rerunOne(card) {\n if (!card) return;\n appendBatch(1, 'rerun-one');\n}\nfunction adoptOne(card) {\n if (!card || card.classList.contains('adopted')) return;\n card.classList.remove('gen');\n card.classList.add('adopted');\n // spec §4.18 · 就地中央反馈 (替代全局 toast,用户不必转头看屏幕角落)\n card.classList.add('show-feedback');\n setTimeout(() => card.classList.remove('show-feedback'), 1500);\n updateBatchSummary();\n}\n// 点击页面其它位置 → 关闭单图/批次 more menu\ndocument.addEventListener('click', e => {\n if (!e.target.closest('.mp-result .cell-more-wrap')) {\n document.querySelectorAll('.mp-result .cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n }\n if (!e.target.closest('.mp-pv-batch .batch-more-wrap')) {\n document.querySelectorAll('.mp-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));\n }\n});\nfunction updateBatchSummary() {\n // 逐批次更新「全部采用 · 已采用/总数」\n document.querySelectorAll('#pv-grid .mp-result-batch').forEach(batch => {\n const cards = batch.querySelectorAll('.mp-result:not(.placeholder-only)');\n const adopted = batch.querySelectorAll('.mp-result.adopted').length;\n const adoptedEl = batch.querySelector('.adopt-batch .adopted');\n const totalEl = batch.querySelector('.adopt-batch .total');\n if (adoptedEl) adoptedEl.textContent = adopted;\n if (totalEl) totalEl.textContent = cards.length;\n });\n}\n\ndocument.getElementById('mp-go-btn').addEventListener('click', () => {\n if (!state.selectedModel || !state.selectedProd) return;\n const grid = document.getElementById('pv-grid');\n const hasReal = grid && grid.querySelector('.mp-result-batch:not(.placeholder-batch)');\n const prod = PRODUCTS.find(p => p.id === state.selectedProd);\n if (hasReal) {\n Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');\n } else {\n Shell.toast('已提交任务', (prod ? prod.name + ' · ' : '') + state.count + ' 张图生成中');\n }\n showPreviewContent();\n renderResultCards(state.count);\n});\n\n// 批量按钮已下沉到每个批次内部 (.rerun-batch / .adopt-batch)\n// 不再有全局 #pv-rerun-all / #pv-adopt-all\n\n// ============================================================\n// 工具台头部 · 搜索 / 时间 / 模特 筛选\n// ============================================================\nconst _pvFilter = { time: 'all', model: 'all', search: '' };\n\nfunction _pvTimeMatch(ts, key) {\n if (key === 'all') return true;\n const now = Date.now();\n const diff = now - Number(ts);\n if (key === '10min') return diff <= 10 * 60 * 1000;\n if (key === '1h') return diff <= 60 * 60 * 1000;\n if (key === 'today') {\n const a = new Date(now); const b = new Date(Number(ts));\n return a.toDateString() === b.toDateString();\n }\n return true;\n}\n\nfunction applyPvFilters() {\n const grid = document.getElementById('pv-grid');\n if (!grid) return;\n const q = (_pvFilter.search || '').trim().toLowerCase();\n let visible = 0;\n grid.querySelectorAll('.mp-result-batch:not(.placeholder-batch)').forEach(batch => {\n let ok = true;\n if (!_pvTimeMatch(batch.dataset.ts, _pvFilter.time)) ok = false;\n if (ok && _pvFilter.model !== 'all' && batch.dataset.modelId !== _pvFilter.model) ok = false;\n if (ok && q && !(batch.dataset.search || '').includes(q)) ok = false;\n batch.dataset.hidden = ok ? '0' : '1';\n if (ok) visible += 1;\n });\n // 占位批次:只有当无真实批次 & 无 active filter 时显示\n const hasReal = !!grid.querySelector('.mp-result-batch:not(.placeholder-batch)');\n const filterActive = _pvFilter.time !== 'all' || _pvFilter.model !== 'all' || q.length > 0;\n grid.querySelectorAll('.placeholder-batch').forEach(ph => {\n ph.dataset.hidden = (hasReal || filterActive) ? '1' : '0';\n });\n}\n\nfunction _refreshModelMenu() {\n const menu = document.getElementById('mp-menu-model');\n if (!menu) return;\n const grid = document.getElementById('pv-grid');\n const used = new Map();\n if (grid) {\n grid.querySelectorAll('.mp-result-batch:not(.placeholder-batch)').forEach(b => {\n const id = b.dataset.modelId; const nm = b.dataset.modelName;\n if (id) used.set(id, nm || id);\n });\n }\n const items = ['<button class=\"tb-menu-item' + (_pvFilter.model === 'all' ? ' active' : '') + '\" type=\"button\" data-val=\"all\">全部模特</button>'];\n if (used.size === 0) {\n items.push('<div class=\"tb-menu-empty\">暂无批次,生成后可按模特筛选</div>');\n } else {\n used.forEach((nm, id) => {\n items.push(`<button class=\"tb-menu-item${_pvFilter.model === id ? ' active' : ''}\" type=\"button\" data-val=\"${id}\">${nm}</button>`);\n });\n }\n menu.innerHTML = items.join('');\n}\n\nfunction _setChipLabel(chipId, baseLabel, val, valText) {\n const lbl = document.querySelector('#' + chipId + ' .lbl');\n const chip = document.getElementById(chipId);\n if (!lbl || !chip) return;\n if (val === 'all' || !val) {\n lbl.textContent = baseLabel;\n chip.classList.remove('active');\n } else {\n lbl.textContent = baseLabel;\n chip.classList.add('active');\n // 触发 ::after ':' 伪元素显示已选项\n chip.title = baseLabel + ':' + valText;\n }\n}\n\nfunction _closeAllMenus(except) {\n document.querySelectorAll('.mp-main-h .tb-menu-wrap.open').forEach(w => {\n if (w !== except) w.classList.remove('open');\n });\n}\n\n// chip 点击 → 开/合菜单\ndocument.querySelectorAll('.mp-main-h .tb-menu-wrap').forEach(wrap => {\n const chip = wrap.querySelector('.tb-chip');\n chip.addEventListener('click', e => {\n e.stopPropagation();\n const willOpen = !wrap.classList.contains('open');\n _closeAllMenus(wrap);\n wrap.classList.toggle('open', willOpen);\n if (willOpen && wrap.dataset.filter === 'model') _refreshModelMenu();\n });\n});\n// 菜单项 → 选中并应用\ndocument.querySelectorAll('#mp-menu-time, #mp-menu-model').forEach(menu => {\n menu.addEventListener('click', e => {\n const btn = e.target.closest('.tb-menu-item');\n if (!btn) return;\n const val = btn.dataset.val;\n const txt = btn.textContent.trim();\n const wrap = menu.closest('.tb-menu-wrap');\n const key = wrap.dataset.filter;\n _pvFilter[key] = val;\n menu.querySelectorAll('.tb-menu-item').forEach(it => it.classList.toggle('active', it === btn));\n wrap.classList.remove('open');\n const baseLabel = key === 'time' ? '时间' : '模特';\n _setChipLabel(key === 'time' ? 'mp-chip-time' : 'mp-chip-model', baseLabel, val, txt);\n applyPvFilters();\n });\n});\n// 点击页面其它位置关闭菜单\ndocument.addEventListener('click', e => {\n if (!e.target.closest('.mp-main-h .tb-menu-wrap')) _closeAllMenus(null);\n});\n\n// 搜索 toggle + 输入\n(function setupSearch() {\n const wrap = document.getElementById('mp-search-wrap');\n const toggle = document.getElementById('mp-search-toggle');\n const input = document.getElementById('mp-search-input');\n if (!wrap || !toggle || !input) return;\n toggle.addEventListener('click', e => {\n e.stopPropagation();\n const willExpand = !wrap.classList.contains('expanded');\n wrap.classList.toggle('expanded', willExpand);\n if (willExpand) setTimeout(() => input.focus(), 50);\n else {\n input.value = '';\n _pvFilter.search = '';\n applyPvFilters();\n }\n });\n input.addEventListener('input', () => {\n _pvFilter.search = input.value;\n applyPvFilters();\n });\n input.addEventListener('keydown', e => {\n if (e.key === 'Escape') {\n input.value = '';\n _pvFilter.search = '';\n wrap.classList.remove('expanded');\n applyPvFilters();\n }\n });\n})();\n\n// ─── 模特库 全屏 ───\nconst MODELS = [\n { id: 'm1', name: 'Ava', gender: '女', age: '青年', style: '清新自然', source: 'preset', used: 12, region: '东亚', skin: '白皙', height: '中等', build: '标准', hairLen: '长发', hairColor: '黑', vibe: '温柔', feature: '邻家女孩气质,微笑亲和' },\n { id: 'm2', name: 'Luna', gender: '女', age: '青年', style: '学生少女', source: 'preset', used: 8, region: '东亚', skin: '白皙', height: '偏小', build: '纤细', hairLen: '中发', hairColor: '深棕', vibe: '甜美', feature: '校园风,书卷气重' },\n { id: 'm3', name: 'Mia', gender: '女', age: '青年', style: 'OL 通勤', source: 'preset', used: 5, region: '东亚', skin: '小麦', height: '中等', build: '标准', hairLen: '短发', hairColor: '黑', vibe: '干练', feature: '都市职场气场,锐利眼神' },\n { id: 'm4', name: 'Zoe', gender: '女', age: '青年', style: '健身运动', source: 'preset', used: 9, region: '东亚', skin: '健康', height: '偏高', build: '运动', hairLen: '中发', hairColor: '栗色', vibe: '活力', feature: '马尾辫,健身房常客' },\n { id: 'm5', name: 'Iris', gender: '女', age: '中年', style: '都市精致', source: 'preset', used: 3, region: '东亚', skin: '白皙', height: '中等', build: '标准', hairLen: '中发', hairColor: '酒红', vibe: '优雅', feature: '熟女气场,精致妆容' },\n { id: 'm6', name: 'Lily', gender: '女', age: '青年', style: '甜美韩系', source: 'preset', used: 7, region: '东亚', skin: '白皙', height: '偏小', build: '纤细', hairLen: '长发', hairColor: '浅棕', vibe: '甜美', feature: '韩系混血感,微卷长发' },\n { id: 'm7', name: 'Sora', gender: '女', age: '青年', style: '日系简约', source: 'preset', used: 6, region: '东亚', skin: '白皙', height: '中等', build: '纤细', hairLen: '短发', hairColor: '黑', vibe: '清冷', feature: '日系氛围感,齐刘海' },\n { id: 'm8', name: 'Eden', gender: '男', age: '青年', style: '商务通勤', source: 'preset', used: 4, region: '东亚', skin: '健康', height: '偏高', build: '标准', hairLen: '短发', hairColor: '黑', vibe: '稳重', feature: '商务精英范,西装常驻' },\n { id: 'm9', name: 'Kai', gender: '男', age: '青年', style: '街头潮流', source: 'preset', used: 5, region: '东亚', skin: '小麦', height: '中等', build: '运动', hairLen: '中发', hairColor: '亚麻', vibe: '潮酷', feature: '街头潮人,鼻钉耳骨钉' },\n { id: 'm10', name: 'Leo', gender: '男', age: '中年', style: '熟男品质', source: 'preset', used: 2, region: '东亚', skin: '健康', height: '偏高', build: '标准', hairLen: '短发', hairColor: '微银', vibe: '沉稳', feature: '熟男魅力,胡须利落' },\n { id: 'm11', name: 'YouA', gender: '女', age: '青年', style: '我的模特', source: 'own', used: 0, region: '—', skin: '—', height: '—', build: '—', hairLen: '—', hairColor: '—', vibe: '—', feature: '用户上传素材,未生成特征' },\n { id: 'm12', name: 'YouB', gender: '女', age: '青年', style: '我的模特', source: 'own', used: 0, region: '—', skin: '—', height: '—', build: '—', hairLen: '—', hairColor: '—', vibe: '—', feature: '用户上传素材,未生成特征' },\n];\n\nfunction renderModelLib(filter) {\n const grid = document.getElementById('ml-grid');\n let list = MODELS;\n if (filter.source && filter.source !== 'all') list = list.filter(m => m.source === filter.source);\n if (filter.gender) list = list.filter(m => m.gender === filter.gender);\n if (filter.age) list = list.filter(m => m.age === filter.age);\n\n // 「添加模特」入口卡 · 平台预设是只读素材库,不展示入口\n const uploadCard = (filter.source === 'preset') ? '' : `\n <div class=\"model-card ml-upload-card\" id=\"ml-upload-card\" role=\"button\" tabindex=\"0\" aria-label=\"上传或生成新模特\">\n <div class=\"m-thumb up-thumb\">\n <div class=\"up-plus\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </div>\n </div>\n <div class=\"m-name\">添加模特</div>\n <div class=\"m-tag\">// AI 生成 / 本地上传</div>\n </div>\n `;\n\n grid.innerHTML = uploadCard + list.map(m => `\n <div class=\"model-card${state.selectedModel === m.id ? ' selected' : ''}\" data-id=\"${m.id}\" data-name=\"${m.name}\">\n <div class=\"placeholder m-thumb\"><span class=\"ph-frame\">${m.name} · ${m.style}</span></div>\n <div class=\"m-name\">${m.name}</div>\n <div class=\"m-tag\">${m.gender}·${m.age}·${m.style}</div>\n </div>\n `).join('');\n\n // 绑定 click (单选) · 排除上传卡片\n grid.querySelectorAll('.model-card:not(.ml-upload-card)').forEach(card => {\n card.addEventListener('click', e => {\n if (e.target.closest('.m-thumb')) {\n openModelDetail(card.dataset.id);\n return;\n }\n selectModel(card.dataset.id);\n });\n });\n\n // 上传卡点击 → 打开选择 modal\n const upCard = grid.querySelector('#ml-upload-card');\n if (upCard) {\n upCard.addEventListener('click', () => openUploadChoice());\n upCard.addEventListener('keydown', e => {\n if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openUploadChoice(); }\n });\n }\n}\n\n/* ─── 添加模特 · 工作台画布(取代原 modal) ─── */\nconst _uploadCanvas = document.getElementById('ml-canvas');\nfunction openUploadChoice() {\n _uploadCanvas.classList.add('show');\n _uploadCanvas.setAttribute('aria-hidden', 'false');\n}\nfunction _closeUploadCanvasNow() {\n _uploadCanvas.classList.remove('show');\n _uploadCanvas.setAttribute('aria-hidden', 'true');\n}\n\n/* 工作台是否「脏」:右侧栏任一字段已有内容 · 直接读 _mcAi / _mcLocal */\nfunction _isWorkbenchDirty() {\n if (!_uploadCanvas.classList.contains('show')) return false;\n if (typeof _mcAi !== 'undefined') {\n if ((_mcAi.name || '').trim()) return true;\n if (_mcAi.portrait) return true;\n if (_mcAi.triVersions && _mcAi.triVersions.length > 0) return true;\n }\n if (typeof _mcLocal !== 'undefined') {\n if ((_mcLocal.name || '').trim()) return true;\n if (_mcLocal.portraits && _mcLocal.portraits.length > 0) return true;\n if (_mcLocal.triVersions && _mcLocal.triVersions.length > 0) return true;\n }\n return false;\n}\n\n/* 工作台离开 · 重置右侧栏状态(供退出 / 跳转后清场) */\nfunction _resetWorkbenchState() {\n if (typeof _mcAi !== 'undefined') {\n if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');\n _mcAi.name = ''; _mcAi.portrait = null;\n _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n }\n if (typeof _mcLocal !== 'undefined') {\n _mcLocal.name = ''; _mcLocal.portraits = [];\n _mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;\n }\n const nameInput = document.getElementById('mc-up-name');\n if (nameInput) nameInput.value = '';\n if (typeof _renderRight === 'function') { try { _renderRight(); } catch(e) {} }\n}\n\n/* 二次确认弹窗 · 通用 · onConfirm 在用户点击「不保存,退出」后执行 */\nconst _leaveBg = document.getElementById('mc-leave-bg');\nconst _leaveBody = document.getElementById('mc-leave-body');\nconst _leaveCancel = document.getElementById('mc-leave-cancel');\nconst _leaveConfirm = document.getElementById('mc-leave-confirm');\nlet _leavePending = null;\nfunction _openLeaveConfirm(mode, onConfirm) {\n if (mode === 'nav') {\n _leaveBody.innerHTML = '工作台已有内容,跳转到其他页面后<b>不会保存</b>。可继续编辑并点「加入模特库」来保留进度。';\n } else {\n _leaveBody.innerHTML = '工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「加入模特库」来保留进度。';\n }\n _leavePending = onConfirm || null;\n _leaveBg.classList.add('show');\n _leaveBg.setAttribute('aria-hidden', 'false');\n}\nfunction _closeLeaveConfirm() {\n _leaveBg.classList.remove('show');\n _leaveBg.setAttribute('aria-hidden', 'true');\n _leavePending = null;\n}\n_leaveCancel.addEventListener('click', _closeLeaveConfirm);\n_leaveBg.addEventListener('click', e => { if (e.target === _leaveBg) _closeLeaveConfirm(); });\n_leaveConfirm.addEventListener('click', () => {\n const fn = _leavePending;\n _closeLeaveConfirm();\n if (typeof fn === 'function') fn();\n});\n\n/* 关闭工作台 · 脏态先二次确认 */\nfunction closeUploadChoice() {\n if (_isWorkbenchDirty()) {\n _openLeaveConfirm('exit', () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n });\n return;\n }\n _closeUploadCanvasNow();\n}\ndocument.getElementById('ml-canvas-x')?.addEventListener('click', closeUploadChoice);\ndocument.getElementById('ml-canvas-back').addEventListener('click', closeUploadChoice);\ndocument.addEventListener('keydown', e => {\n if (e.key === 'Escape' && _uploadCanvas.classList.contains('show') && !_leaveBg.classList.contains('show')) {\n closeUploadChoice();\n }\n});\n\n/* 全局拦截外链跳转 + 模特库内的离场操作 · 仅工作台展开且脏态时 */\ndocument.addEventListener('click', e => {\n if (!_uploadCanvas.classList.contains('show')) return;\n if (!_isWorkbenchDirty()) return;\n\n // 1) 外页跳转(侧边栏 nav · 面包屑)\n const a = e.target.closest('a[href]');\n if (a) {\n const href = a.getAttribute('href');\n if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;\n e.preventDefault(); e.stopPropagation();\n _openLeaveConfirm('nav', () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n location.href = href;\n });\n return;\n }\n\n // 2) 余额胶囊(inline onclick=location.href='account.html')\n if (e.target.closest('.balance-chip')) {\n e.preventDefault(); e.stopPropagation();\n _openLeaveConfirm('nav', () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n location.href = 'account.html';\n });\n return;\n }\n\n // 3) 模特库 X · 关闭整个模特库 modal(等同退出工作台 + 返回主页)\n if (e.target.closest('#ml-close-btn')) {\n e.preventDefault(); e.stopPropagation();\n _openLeaveConfirm('exit', () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n document.getElementById('ml-modal-bg').classList.remove('show');\n });\n return;\n }\n\n // 4) 模特库左侧「来源」筛选项 · 切换筛选 = 离开工作台回到列表\n const sideItem = e.target.closest('.ml-side .ml-side-item');\n if (sideItem) {\n e.preventDefault(); e.stopPropagation();\n const src = sideItem.dataset.source;\n _openLeaveConfirm('nav', () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n // 应用筛选 + 同步 active\n _libFilter.source = src;\n document.querySelectorAll('.ml-side .ml-side-item').forEach(x =>\n x.classList.toggle('active', x.dataset.source === src));\n renderModelLib(_libFilter);\n });\n return;\n }\n}, true); // capture · 早于 inline onclick\n\n/* 浏览器后退 / 刷新 / 关闭 · 原生 beforeunload 兜底 */\nwindow.addEventListener('beforeunload', e => {\n if (_isWorkbenchDirty()) { e.preventDefault(); e.returnValue = ''; return ''; }\n});\n\n/* ─── 工作台画布 · 左 AI 生成 区 ─── */\nconst _mcInputText = document.getElementById('mc-input-text');\nconst _mcSendBtn = document.getElementById('mc-send-btn');\n_mcInputText?.addEventListener('input', () => {\n _mcSendBtn.disabled = _mcInputText.value.trim().length === 0;\n // textarea 自适应高度 · 与 .io-input 一致 max-height 220\n _mcInputText.style.height = 'auto';\n _mcInputText.style.height = Math.min(_mcInputText.scrollHeight, 220) + 'px';\n});\nconst _MC_SVG = {\n rerun: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>',\n dl: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>',\n more: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/></svg>',\n adopt: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg>',\n del: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>',\n edit: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z\"/></svg>',\n saveAll: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z\"/><polyline points=\"17 21 17 13 7 13 7 21\"/><polyline points=\"7 3 7 8 15 8\"/></svg>',\n};\n\nfunction _mcAppendMsg(prompt, refs) {\n const inner = document.getElementById('mc-stream-inner');\n if (!inner) return;\n // 首次发送 · 移除空态\n const empty = inner.querySelector('.mc-empty');\n if (empty) empty.remove();\n const safe = String(prompt).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'})[c]);\n const tags = ['3:4', '默认', '4 张'];\n const tagsHtml = tags.map(t => `<span class=\"meta-chip\">${t}</span>`).join('<span class=\"sep\">·</span>');\n const cells = Array.from({ length: 4 }, (_, i) => `\n <div class=\"mc-cell gen\" data-idx=\"${i}\">\n <div class=\"ph-frame\">生成中 · v${i + 1}</div>\n <div class=\"cell-ops\" hidden>\n <button type=\"button\" data-act=\"cell-rerun\" title=\"再次生成\">${_MC_SVG.rerun}</button>\n <button type=\"button\" data-act=\"cell-dl\" title=\"下载\">${_MC_SVG.dl}</button>\n <div class=\"cell-more-wrap\">\n <button type=\"button\" data-act=\"cell-more\" title=\"更多\">${_MC_SVG.more}</button>\n <div class=\"cell-more-menu\" role=\"menu\">\n <button type=\"button\" data-act=\"cell-adopt\">${_MC_SVG.adopt}<span>加入模特库</span></button>\n <button type=\"button\" class=\"danger\" data-act=\"cell-del\">${_MC_SVG.del}<span>删除</span></button>\n </div>\n </div>\n </div>\n </div>`).join('');\n const msg = document.createElement('div');\n msg.className = 'mc-msg';\n msg.innerHTML = `\n <div class=\"mc-msg-prompt\">\n <div class=\"quote\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21c0-3.5 3-6 6-6s6 2.5 6 6\"/><circle cx=\"9\" cy=\"8\" r=\"4\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n </div>\n <div class=\"pt\">\n <div class=\"pt-text\">${safe}</div>\n <div class=\"pt-tags\">${tagsHtml}</div>\n </div>\n </div>\n <div class=\"mc-msg-grid\">${cells}</div>\n <div class=\"mc-msg-ops\">\n <button type=\"button\" data-act=\"edit\">${_MC_SVG.edit}重新编辑</button>\n <button type=\"button\" data-act=\"rerun\">${_MC_SVG.rerun}再次生成</button>\n <div class=\"msg-more-wrap\">\n <button type=\"button\" class=\"icon\" data-act=\"msg-more\" title=\"更多\">${_MC_SVG.more}</button>\n <div class=\"msg-more-menu\" role=\"menu\">\n <button type=\"button\" class=\"danger\" data-act=\"msg-del\">${_MC_SVG.del}<span>删除该批结果</span></button>\n </div>\n </div>\n </div>\n `;\n inner.appendChild(msg);\n // 模拟生成完成 · 1.6s 后去掉 .gen 动画并显示版本号 + 显示 cell-ops + 绑定选中点击\n setTimeout(() => {\n msg.querySelectorAll('.mc-cell').forEach((c, i) => {\n c.classList.remove('gen');\n const ph = c.querySelector('.ph-frame');\n if (ph) ph.textContent = '模特 · v' + (i + 1);\n const ops = c.querySelector('.cell-ops');\n if (ops) ops.hidden = false;\n });\n _bindMcCellPick();\n }, 1600);\n _mcBindMsgEvents(msg, prompt);\n // 滚到底部\n const stream = document.getElementById('mc-stream');\n if (stream) stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' });\n}\n\nfunction _mcBindMsgEvents(msg, prompt) {\n // 单图 cell-rerun → 重置该 cell 为 gen, 1.2s 后回 ok\n msg.querySelectorAll('[data-act=\"cell-rerun\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const cell = b.closest('.mc-cell');\n const ops = cell.querySelector('.cell-ops');\n const ph = cell.querySelector('.ph-frame');\n const idx = Number(cell.dataset.idx || 0);\n cell.classList.add('gen');\n cell.classList.remove('selected');\n if (ops) ops.hidden = true;\n if (ph) ph.textContent = '生成中 · v' + (idx + 1);\n setTimeout(() => {\n cell.classList.remove('gen');\n if (ph) ph.textContent = '模特 · v' + (idx + 1);\n if (ops) ops.hidden = false;\n }, 1200 + Math.random() * 600);\n Shell.toast('已重跑', '该图重新生成中');\n });\n });\n msg.querySelectorAll('[data-act=\"cell-dl\"]').forEach(b => {\n b.addEventListener('click', e => { e.stopPropagation(); Shell.toast('下载', '已开始下载 · MOCK'); });\n });\n // 单图更多 menu 开/合\n msg.querySelectorAll('[data-act=\"cell-more\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n const willOpen = !wrap.classList.contains('open');\n document.querySelectorAll('.mc-cell .cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (willOpen) wrap.classList.add('open');\n });\n });\n msg.querySelectorAll('[data-act=\"cell-adopt\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n if (wrap) wrap.classList.remove('open');\n Shell.toast('已加入模特库', '已添加到「我的上传」');\n });\n });\n msg.querySelectorAll('[data-act=\"cell-del\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n if (wrap) wrap.classList.remove('open');\n const cell = b.closest('.mc-cell');\n cell.remove();\n if (!msg.querySelectorAll('.mc-cell').length) msg.remove();\n Shell.toast('已删除');\n });\n });\n // 批次操作\n msg.querySelectorAll('[data-act=\"rerun\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n _mcAppendMsg(prompt, []);\n });\n });\n msg.querySelectorAll('[data-act=\"edit\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const inp = document.getElementById('mc-input-text');\n if (inp) {\n inp.value = prompt;\n inp.focus();\n inp.dispatchEvent(new Event('input'));\n }\n });\n });\n // 批次更多 menu\n msg.querySelectorAll('[data-act=\"msg-more\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.msg-more-wrap');\n const willOpen = !wrap.classList.contains('open');\n document.querySelectorAll('.mc-msg-ops .msg-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (willOpen) wrap.classList.add('open');\n });\n });\n msg.querySelectorAll('[data-act=\"msg-del\"]').forEach(b => {\n b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.msg-more-wrap');\n if (wrap) wrap.classList.remove('open');\n msg.remove();\n Shell.toast('已删除该批结果');\n });\n });\n}\n\n// 全局点击 → 关闭 mc 单图/批次 more menu\ndocument.addEventListener('click', e => {\n if (!e.target.closest('.mc-cell .cell-more-wrap')) {\n document.querySelectorAll('.mc-cell .cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n }\n if (!e.target.closest('.mc-msg-ops .msg-more-wrap')) {\n document.querySelectorAll('.mc-msg-ops .msg-more-wrap.open').forEach(w => w.classList.remove('open'));\n }\n});\n\n_mcSendBtn?.addEventListener('click', () => {\n const txt = _mcInputText.value.trim();\n if (!txt) return;\n _mcAppendMsg(txt, _mcRefList.slice());\n _mcInputText.value = '';\n _mcInputText.style.height = 'auto';\n _mcSendBtn.disabled = true;\n // 清空参考图(image-optimize 同款行为)\n _mcRefList = [];\n _renderMcRefs();\n});\n// Cmd/Ctrl + Enter 发送\n_mcInputText?.addEventListener('keydown', (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {\n e.preventDefault();\n _mcSendBtn.click();\n }\n});\n// 示例 chip → 填入 textarea\ndocument.querySelectorAll('.mc-empty .examples .ex').forEach(b => {\n b.addEventListener('click', () => {\n _mcInputText.value = b.dataset.ex || b.textContent.trim();\n _mcInputText.dispatchEvent(new Event('input'));\n _mcInputText.focus();\n });\n});\n// + 按钮上传参考图(仅作展示参考,不入库)\nconst _mcAiRefInput = document.getElementById('mc-ai-ref-input');\nconst _mcRefs = document.getElementById('mc-input-refs');\nlet _mcRefList = [];\ndocument.getElementById('mc-add-btn')?.addEventListener('click', () => _mcAiRefInput.click());\n_mcAiRefInput?.addEventListener('change', e => {\n const files = [...(e.target.files || [])].filter(f => /^image\\//.test(f.type));\n files.forEach(f => _mcRefList.push({ name: f.name, url: URL.createObjectURL(f) }));\n e.target.value = '';\n _renderMcRefs();\n});\nfunction _renderMcRefs() {\n if (!_mcRefs) return;\n _mcRefs.classList.toggle('show', _mcRefList.length > 0);\n _mcRefs.innerHTML = _mcRefList.map((r, i) => `\n <div class=\"mc-input-ref\">\n <img src=\"${r.url}\" alt=\"${r.name}\">\n <button class=\"x\" data-idx=\"${i}\" aria-label=\"移除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg></button>\n </div>`).join('');\n _mcRefs.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {\n _mcRefList.splice(+x.dataset.idx, 1);\n _renderMcRefs();\n }));\n}\n\n/* ─── 工作台画布 · 右栏 · tab 切换 + 3 模块状态机 ─── */\nlet _mcRightTab = 'ai';\nconst _mcAi = { name: '', portrait: null, triVersions: [], triActiveIdx: -1 }; // portrait: { label, cellEl }\nconst _mcLocal = { name: '', portraits: [], triVersions: [], triActiveIdx: -1 }; // portraits: [{file,url,name,size}]\nfunction _mcCurState() { return _mcRightTab === 'ai' ? _mcAi : _mcLocal; }\nfunction _hasTri(s) { return s.triVersions.length > 0 && s.triActiveIdx >= 0; }\n\n// Tab 切换\ndocument.querySelectorAll('.mc-up-tab').forEach(btn => {\n btn.addEventListener('click', () => {\n _mcRightTab = btn.dataset.tab;\n document.querySelectorAll('.mc-up-tab').forEach(b => b.classList.toggle('active', b === btn));\n document.querySelectorAll('.mc-up-body [data-show]').forEach(el => {\n el.hidden = el.dataset.show !== _mcRightTab;\n });\n document.getElementById('mc-up-name').value = _mcCurState().name;\n _renderRight();\n });\n});\n\n// 模特姓名 输入(按当前 tab 写到对应 state)\nconst _mcUpName = document.getElementById('mc-up-name');\n_mcUpName?.addEventListener('input', () => {\n _mcCurState().name = _mcUpName.value;\n _updateCommit();\n});\n\n/* ─── AI tab:立绘选中态 ─── */\nconst _aiEmpty = document.getElementById('mc-portrait-ai-empty');\nconst _aiPicked = document.getElementById('mc-portrait-ai-picked');\nconst _aiLabel = document.getElementById('mc-portrait-ai-label');\n\nfunction _renderAiPortrait() {\n if (_mcAi.portrait) {\n _aiEmpty.hidden = true;\n _aiPicked.hidden = false;\n _aiLabel.textContent = _mcAi.portrait.label || '模特立绘';\n } else {\n _aiEmpty.hidden = false;\n _aiPicked.hidden = true;\n }\n}\nfunction _setAiPortrait(data) {\n _mcAi.portrait = data;\n _renderAiPortrait();\n _updateTriBtn(); _updateCommit();\n}\nfunction _clearAiPortrait() {\n if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');\n _mcAi.portrait = null;\n // 立绘清空 → 三视图历史作废\n _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n _renderAiPortrait();\n _renderTriView();\n _updateTriBtn(); _updateCommit();\n}\ndocument.getElementById('mc-portrait-ai-clear').addEventListener('click', _clearAiPortrait);\n\n/* ─── Local tab:多张立绘 ─── */\nconst _lpDrop = document.getElementById('mc-portrait-local-drop');\nconst _lpInput = document.getElementById('mc-portrait-local-input');\nconst _lpList = document.getElementById('mc-portrait-local-list');\nconst _lpCount = document.getElementById('mc-portrait-local-count');\n\nfunction _renderLocalPortraits() {\n _lpCount.textContent = _mcLocal.portraits.length;\n _lpList.innerHTML = _mcLocal.portraits.map((p, i) => `\n <div class=\"thumb\">\n <img src=\"${p.url}\" alt=\"${p.name}\">\n <button class=\"x\" data-idx=\"${i}\" aria-label=\"移除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg></button>\n </div>\n `).join('');\n _lpList.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {\n _mcLocal.portraits.splice(+x.dataset.idx, 1);\n if (_mcLocal.portraits.length === 0) {\n // 立绘全部清空 → 三视图历史作废\n _mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;\n }\n _renderLocalPortraits();\n _renderTriView();\n _updateTriBtn(); _updateCommit();\n }));\n}\nfunction _lpAdd(files) {\n const imgs = [...(files || [])].filter(f => /^image\\//.test(f.type));\n imgs.forEach(f => _mcLocal.portraits.push({ file: f, url: URL.createObjectURL(f), name: f.name, size: f.size }));\n _renderLocalPortraits();\n _updateTriBtn(); _updateCommit();\n}\n_lpDrop?.addEventListener('click', () => _lpInput.click());\n_lpDrop?.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _lpInput.click(); } });\n_lpInput?.addEventListener('change', e => { _lpAdd(e.target.files); e.target.value = ''; });\n['dragenter', 'dragover'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.add('dragover'); }));\n['dragleave', 'drop'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.remove('dragover'); }));\n_lpDrop?.addEventListener('drop', e => _lpAdd(e.dataTransfer?.files));\n\n/* ─── 三视图模块 · 16:9 占位常驻 · 空态居中覆盖按钮 · 多版本累积可切换 ─── */\nconst _triSec = document.getElementById('mc-triview-sec');\nconst _triResultImg = document.getElementById('mc-triview-result');\nconst _triFrame = document.getElementById('mc-triview-frame');\nconst _triGenBtn = document.getElementById('mc-triview-gen-btn');\nconst _triHint = document.getElementById('mc-triview-hint');\nconst _triOps = document.getElementById('mc-triview-ops');\nconst _triRerunBtn = document.getElementById('mc-triview-rerun');\nconst _triHistory = document.getElementById('mc-triview-history');\nconst _triHistoryRow = document.getElementById('mc-triview-history-row');\nconst _triHistoryCount = document.getElementById('mc-triview-history-count');\nlet _triGenerating = false;\n\nfunction _portraitReady() {\n return _mcRightTab === 'ai' ? !!_mcAi.portrait : _mcLocal.portraits.length > 0;\n}\n\nfunction _renderTriView() {\n const s = _mcCurState();\n const has = _hasTri(s);\n _triSec.classList.toggle('has-result', has);\n\n if (_triGenerating) {\n // 生成中(首次或重跑):主图脉冲 · 隐藏覆盖按钮 · 历史区保留(若已有版本) · 重跑按钮禁用\n _triGenBtn.hidden = true;\n _triHint.hidden = true;\n _triOps.hidden = !has;\n _triHistory.hidden = !has;\n _triFrame.textContent = '三视图生成中…';\n _triResultImg.classList.add('gen');\n _triRerunBtn.disabled = true;\n if (has) _renderTriHistory(s);\n } else if (has) {\n _triGenBtn.hidden = true;\n _triHint.hidden = true;\n _triOps.hidden = false;\n _triHistory.hidden = false;\n const ver = s.triVersions[s.triActiveIdx];\n _triFrame.textContent = `三视图(正/侧/背) · ${ver.label}`;\n _triResultImg.classList.remove('gen');\n _triRerunBtn.disabled = false;\n _renderTriHistory(s);\n } else {\n _triGenBtn.hidden = false;\n _triHint.hidden = false;\n _triOps.hidden = true;\n _triHistory.hidden = true;\n _triFrame.textContent = '三视图(正/侧/背)';\n _triResultImg.classList.remove('gen');\n }\n}\n\nfunction _renderTriHistory(s) {\n _triHistoryCount.textContent = s.triVersions.length;\n _triHistoryRow.innerHTML = s.triVersions.map((ver, i) => `\n <div class=\"h-thumb${i === s.triActiveIdx ? ' active' : ''}\" data-idx=\"${i}\" title=\"${ver.label} · ${ver.ts}${i === s.triActiveIdx ? ' · 当前采用' : ''}\">\n <span class=\"badge\">当前</span>\n <span class=\"v\">${ver.label}</span>\n </div>\n `).join('');\n _triHistoryRow.querySelectorAll('.h-thumb').forEach(el => {\n el.addEventListener('click', () => {\n const idx = Number(el.dataset.idx);\n if (idx === s.triActiveIdx) return;\n s.triActiveIdx = idx;\n _renderTriView(); _updateCommit();\n });\n });\n}\n\nfunction _updateTriBtn() {\n if (_hasTri(_mcCurState())) return; // 已有版本,按钮被 _renderTriView 隐藏\n const ok = _portraitReady() && !_triGenerating;\n _triGenBtn.disabled = !ok;\n _triHint.textContent = _portraitReady()\n ? '// 一键生成正/侧/背 三视图'\n : (_mcRightTab === 'ai' ? '// 先选中左侧 AI 立绘' : '// 先上传至少 1 张立绘');\n}\n\nfunction _startTriGen() {\n if (!_portraitReady() || _triGenerating) return;\n _triGenerating = true;\n const stateAtStart = _mcCurState();\n _renderTriView();\n setTimeout(() => {\n _triGenerating = false;\n const now = new Date();\n const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');\n const label = 'v' + (stateAtStart.triVersions.length + 1);\n stateAtStart.triVersions.push({ ts, label });\n stateAtStart.triActiveIdx = stateAtStart.triVersions.length - 1;\n if (stateAtStart === _mcCurState()) _renderTriView();\n _updateTriBtn(); _updateCommit();\n }, 1600);\n}\n_triGenBtn.addEventListener('click', _startTriGen);\n_triRerunBtn.addEventListener('click', _startTriGen);\n\n/* ─── 整体渲染 + 提交 ─── */\nconst _mcUpCommit = document.getElementById('mc-up-commit');\nconst _mcUpStat = document.getElementById('mc-up-stat');\nfunction _updateCommit() {\n const s = _mcCurState();\n const nameOk = s.name.trim().length > 0;\n const portraitOk = _portraitReady();\n const triOk = _hasTri(s);\n // 三视图改为「可选」: 姓名 + 立绘 即可保存\n const ready = nameOk && portraitOk;\n _mcUpCommit.disabled = !ready;\n if (ready) {\n _mcUpStat.classList.add('ok');\n if (triOk) {\n _mcUpStat.innerHTML = `✓ 已就绪 · <b>${s.name}</b>`;\n } else {\n _mcUpStat.innerHTML = `✓ 可保存 · <b>${s.name}</b> · <span style=\"color:var(--black-alpha-56)\">建议补三视图</span>`;\n }\n } else {\n _mcUpStat.classList.remove('ok');\n const miss = [];\n if (!nameOk) miss.push('姓名');\n if (!portraitOk) miss.push('立绘');\n _mcUpStat.innerHTML = `// 待完成 · <b>${miss.join(' / ')}</b>`;\n }\n}\nfunction _renderRight() {\n _renderAiPortrait();\n _renderLocalPortraits();\n _renderTriView();\n _updateTriBtn();\n _updateCommit();\n}\n_renderRight();\n\nfunction _doMcCommit() {\n const s = _mcCurState();\n const baseName = s.name.trim().slice(0, 12) || 'YouNew';\n const ts = Date.now().toString(36);\n // 持久化:多张立绘(AI tab 1 张 label · Local tab 真实 blob URL N 张)+ 三视图历史版本(可为空)\n const portraits = _mcRightTab === 'ai'\n ? (_mcAi.portrait ? [{ url: '', name: _mcAi.portrait.label || baseName, label: _mcAi.portrait.label || '立绘' }] : [])\n : _mcLocal.portraits.map((p, i) => ({ url: p.url, name: p.name || `${baseName}-${i+1}`, label: `本地 ${i+1}` }));\n const triVersions = s.triVersions.map(v => ({ ts: v.ts, label: v.label }));\n const hasTri = triVersions.length > 0;\n MODELS.unshift({\n id: 'm-up-' + ts,\n name: baseName,\n gender: '女', age: '青年', style: _mcRightTab === 'ai' ? 'AI 生成' : '我的模特',\n source: 'own', used: 0,\n region: '—', skin: '—', height: '—', build: '—',\n hairLen: '—', hairColor: '—', vibe: '—',\n feature: _mcRightTab === 'ai'\n ? (hasTri ? 'AI 生成 · 已含三视图' : 'AI 生成 · 缺三视图')\n : (hasTri ? '用户上传 · 已含三视图' : '用户上传 · 缺三视图'),\n triview: hasTri ? 1 : 0,\n portraits, triVersions,\n });\n // 重置状态\n if (_mcRightTab === 'ai') {\n if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');\n _mcAi.name = ''; _mcAi.portrait = null;\n _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n } else {\n _mcLocal.name = ''; _mcLocal.portraits = [];\n _mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;\n }\n _mcUpName.value = '';\n _renderRight();\n _libFilter.source = 'own';\n document.querySelectorAll('.ml-side .ml-side-item').forEach(x =>\n x.classList.toggle('active', x.dataset.source === 'own'));\n renderModelLib(_libFilter);\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n Shell.toast(\n hasTri ? '已加入模特库' : '已保存 · 缺三视图',\n hasTri\n ? `${baseName} · 来源 ${_mcRightTab === 'ai' ? 'AI 生成' : '我的上传'}`\n : `${baseName} · 后续可在资产详情页补三视图`\n );\n}\n\n// 缺三视图 · 确认弹窗控制\nconst _notriBg = document.getElementById('mc-notri-bg');\nconst _notriSaveBtn = document.getElementById('mc-notri-save');\nconst _notriGenBtn = document.getElementById('mc-notri-gen');\nfunction _openNotriConfirm() { _notriBg.classList.add('show'); _notriBg.setAttribute('aria-hidden', 'false'); }\nfunction _closeNotriConfirm() { _notriBg.classList.remove('show'); _notriBg.setAttribute('aria-hidden', 'true'); }\n_notriBg.addEventListener('click', e => { if (e.target === _notriBg) _closeNotriConfirm(); });\n_notriSaveBtn.addEventListener('click', () => { _closeNotriConfirm(); _doMcCommit(); });\n_notriGenBtn.addEventListener('click', () => {\n _closeNotriConfirm();\n // 滚动到三视图区域并触发生成\n _triSec?.scrollIntoView({ behavior: 'smooth', block: 'center' });\n if (!_triGenBtn.disabled) setTimeout(() => _triGenBtn.click(), 200);\n});\ndocument.addEventListener('keydown', e => {\n if (e.key === 'Escape' && _notriBg.classList.contains('show')) _closeNotriConfirm();\n});\n\n_mcUpCommit?.addEventListener('click', () => {\n if (_mcUpCommit.disabled) return;\n const s = _mcCurState();\n if (!_hasTri(s)) {\n _openNotriConfirm();\n return;\n }\n _doMcCommit();\n});\n\n/* ─── 左侧 .mc-cell 可点击选为立绘(仅 AI tab) ─── */\nfunction _bindMcCellPick() {\n document.querySelectorAll('#mc-stream .mc-cell').forEach(cell => {\n if (cell.dataset.boundPick) return;\n cell.dataset.boundPick = '1';\n // 添加「已选用」徽标\n if (!cell.querySelector('.pick-badge')) {\n const b = document.createElement('span');\n b.className = 'pick-badge';\n b.textContent = '已选用';\n cell.appendChild(b);\n }\n cell.addEventListener('click', () => {\n if (_mcRightTab !== 'ai') {\n Shell.toast('请切到「AI 生成」标签', '只有 AI 模式才能选用左侧立绘');\n return;\n }\n if (cell.classList.contains('gen')) return;\n // 取消选中\n if (cell.classList.contains('selected')) {\n _clearAiPortrait();\n return;\n }\n // 切换 · 清除上一张选中\n document.querySelectorAll('#mc-stream .mc-cell.selected').forEach(c => c.classList.remove('selected'));\n cell.classList.add('selected');\n const label = cell.querySelector('.ph-frame')?.textContent || '模特立绘';\n // 重置三视图(换了立绘要重新生成)\n _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n _renderTriView();\n _setAiPortrait({ label, cellEl: cell });\n });\n });\n}\n\n/* 兼容旧 modal:#ml-up-file 仍存在,但当前未使用(保留以防外部调用) */\nconst _uploadFileInput = document.getElementById('ml-up-file');\n\nlet _libFilter = { source: 'all', gender: '', age: '' };\ndocument.getElementById('open-model-lib').addEventListener('click', () => {\n renderModelLib(_libFilter);\n document.getElementById('ml-modal-bg').classList.add('show');\n});\ndocument.getElementById('ml-close-btn').addEventListener('click', () => {\n document.getElementById('ml-modal-bg').classList.remove('show');\n});\n// 左侧来源 click\ndocument.querySelectorAll('.ml-side .ml-side-item').forEach(item => {\n item.addEventListener('click', () => {\n document.querySelectorAll('.ml-side-item[data-source]').forEach(x => x.classList.remove('active'));\n item.classList.add('active');\n _libFilter.source = item.dataset.source;\n renderModelLib(_libFilter);\n });\n});\n// 顶部 chip 性别/年龄 click\ndocument.querySelectorAll('.ml-toolbar .chip-group').forEach(group => {\n group.addEventListener('click', e => {\n const chip = e.target.closest('.chip');\n if (!chip) return;\n group.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));\n chip.classList.add('active');\n const k = group.dataset.key;\n _libFilter[k] = chip.dataset.val;\n renderModelLib(_libFilter);\n });\n});\n\n// ─── 模特详情 居中弹窗 ───\nlet _detailModelId = null;\nfunction _mdHash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }\nfunction openModelDetail(id) {\n const m = MODELS.find(x => x.id === id);\n if (!m) return;\n _detailModelId = id;\n const sourceLabel = m.source === 'preset' ? '平台预设' : '我的上传';\n const seed = _mdHash(m.name);\n const assetId = 'ASSET-20240520-M' + String(seed % 1000).padStart(3, '0');\n const fileSize = (4 + (seed % 100) / 10).toFixed(1) + 'MB';\n const fav = String(8 + seed % 80);\n const dlN = 200 + seed % 1800;\n const dl = dlN >= 1000 ? (dlN / 1000).toFixed(1) + 'k' : String(dlN);\n const intro = m.feature || (m.name + ' · ' + m.style + '。' + sourceLabel + ' 模特,可作为商品宣传场景的人物资产。');\n const tags = [m.vibe, m.style, m.age, m.hairLen, m.region].filter(Boolean);\n const props = [\n ['性别', m.gender], ['种族', m.region], ['作品ID', assetId],\n ['年龄段', m.age], ['气质', m.vibe], ['创作人', 'Airshelf'],\n ['身高', m.height], ['体格', m.build], ['文件大小', fileSize],\n ['发型', m.hairLen + ' · ' + m.hairColor], ['来源', sourceLabel], ['发布时间', '2024-05-20'],\n ];\n\n document.getElementById('md-title').textContent = m.name;\n document.getElementById('md-kind').textContent = '/ 人物 · ' + sourceLabel;\n document.getElementById('md-portrait-name').textContent = m.name + ' · ' + m.style;\n\n // 当前主立绘 / 三视图 src 缓存,供 zoom 按钮点击使用\n let _mdCurLeadSrc = null;\n let _mdCurLeadName = '';\n let _mdCurTriLabel = '';\n\n // 主立绘 · 移除上次的 <img>(若有),按当前选中重渲\n const leadImgEl = document.querySelector('#md-modal-bg .md-lead-img');\n function _mdSetLead(p) {\n if (!leadImgEl) return;\n const old = leadImgEl.querySelector('img.md-lead-pic');\n if (old) old.remove();\n if (p && p.url) {\n const img = document.createElement('img');\n img.className = 'md-lead-pic';\n img.src = p.url; img.alt = p.name || m.name;\n leadImgEl.insertBefore(img, leadImgEl.firstChild);\n _mdCurLeadSrc = p.url; _mdCurLeadName = p.name || m.name;\n } else {\n _mdCurLeadSrc = null; _mdCurLeadName = p?.label || m.name;\n }\n }\n\n // 缩略图 strip · 用户上传且有 portraits 数组 → 显示多张;平台预设 → 仅 1 张占位\n const userPortraits = (m.source === 'own' && Array.isArray(m.portraits) && m.portraits.length > 0) ? m.portraits : null;\n const thumbList = userPortraits || [{ label: '立绘' }];\n const thumbsEl = document.getElementById('md-thumbs');\n thumbsEl.innerHTML = thumbList.map((p, i) => {\n const inner = p.url\n ? `<img src=\"${p.url}\" alt=\"${(p.name||'').replace(/\"/g,'&quot;')}\">`\n : `<span class=\"ph-frame\">${p.label || ('v'+(i+1))}</span>`;\n return `<div class=\"thumb${i === 0 ? ' active' : ''}\" data-idx=\"${i}\">${inner}</div>`;\n }).join('');\n _mdSetLead(thumbList[0]);\n thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {\n thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n _mdSetLead(thumbList[+t.dataset.idx]);\n }));\n\n // 三视图 · 用户上传且有 triVersions → 保留版本,可切换;否则单张占位\n const userTri = (m.source === 'own' && Array.isArray(m.triVersions) && m.triVersions.length > 0) ? m.triVersions : null;\n const viewsEl = document.querySelector('#md-modal-bg .md-views');\n const _zoomSvg = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg>';\n if (viewsEl) {\n if (userTri) {\n const stripHtml = userTri.map((v, i) => `<div class=\"v-thumb${i === userTri.length - 1 ? ' active' : ''}\" data-idx=\"${i}\"><span class=\"v\">${v.label}</span></div>`).join('');\n const cur = userTri[userTri.length - 1];\n _mdCurTriLabel = cur.label;\n viewsEl.innerHTML = `\n <div class=\"md-view\"><div class=\"lbl\">正 / 侧 / 背 · ${cur.label} · ${cur.ts}</div><button class=\"md-zoom-btn\" type=\"button\" id=\"md-tri-zoom\" aria-label=\"查看大图\" title=\"查看大图\">${_zoomSvg}</button></div>\n <div class=\"md-view-versions\">${stripHtml}</div>`;\n const mainView = viewsEl.querySelector('.md-view .lbl');\n viewsEl.querySelectorAll('.v-thumb').forEach(t => t.addEventListener('click', () => {\n viewsEl.querySelectorAll('.v-thumb').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n const v = userTri[+t.dataset.idx];\n _mdCurTriLabel = v.label;\n if (mainView) mainView.textContent = `正 / 侧 / 背 · ${v.label} · ${v.ts}`;\n }));\n } else {\n _mdCurTriLabel = '三视图';\n viewsEl.innerHTML = `<div class=\"md-view\"><div class=\"lbl\">正 / 侧 / 背 · 三视图</div><button class=\"md-zoom-btn\" type=\"button\" id=\"md-tri-zoom\" aria-label=\"查看大图\" title=\"查看大图\">${_zoomSvg}</button></div>`;\n }\n const triZoom = viewsEl.querySelector('#md-tri-zoom');\n triZoom?.addEventListener('click', e => {\n e.stopPropagation();\n // 三视图当前仅占位渲染,无真实图 src:用占位 + 名字提示\n if (window.Shell?._openLightbox) Shell._openLightbox('', m.name + ' · 三视图 · ' + _mdCurTriLabel);\n });\n }\n\n // 主立绘 zoom 按钮点击 → 打开 lightbox(无 src 时显示占位名)\n document.getElementById('md-lead-zoom')?.replaceWith(document.getElementById('md-lead-zoom').cloneNode(true));\n document.getElementById('md-lead-zoom')?.addEventListener('click', e => {\n e.stopPropagation();\n if (window.Shell?._openLightbox) Shell._openLightbox(_mdCurLeadSrc || '', _mdCurLeadName || m.name);\n });\n\n // 简介 + 标签\n document.getElementById('md-intro').textContent = intro;\n document.getElementById('md-tags').innerHTML = tags.map(t => `<span class=\"tag-chip\">${t}</span>`).join('') +\n `<button class=\"tag-add\" type=\"button\" title=\"添加标签\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M8 3v10M3 8h10\"/></svg></button>`;\n\n // 属性表\n document.getElementById('md-props').innerHTML = props.map(([k, v]) => `<div class=\"prop\"><span class=\"k\">${k}</span><span class=\"v\">${v}</span></div>`).join('');\n\n document.getElementById('md-modal-bg').classList.add('show');\n}\ndocument.getElementById('md-close-btn').addEventListener('click', () => {\n document.getElementById('md-modal-bg').classList.remove('show');\n});\ndocument.getElementById('md-select').addEventListener('click', () => {\n let pickedName = '';\n if (_detailModelId) {\n state.selectedModel = _detailModelId;\n // 重建 mini 网格 · 让选中的模特(无论是否预设)显示在首位\n renderModelMini();\n // 同步模特库 modal 网格里的 .selected 类\n document.querySelectorAll('.ml-grid .model-card').forEach(c =>\n c.classList.toggle('selected', c.dataset.id === state.selectedModel)\n );\n updateModelSummary();\n const m = MODELS.find(x => x.id === _detailModelId);\n if (m) pickedName = m.name;\n }\n // 关闭模特详情 + 模特库两个 modal,回到工作台主视图\n document.getElementById('md-modal-bg').classList.remove('show');\n document.getElementById('ml-modal-bg').classList.remove('show');\n // 工作台主区域 scroll 到「选择模特」step,可视化反馈\n document.getElementById('model-grid-mini')?.scrollIntoView({ behavior: 'smooth', block: 'center' });\n if (pickedName && window.Shell?.toast) {\n Shell.toast('已选用模特「' + pickedName + '」', '可继续选择服装与场景');\n }\n});\n\n// pv-swap 也打开模特库\ndocument.getElementById('pv-swap').addEventListener('click', () => {\n document.getElementById('open-model-lib').click();\n});\n\n// URL ?product=商品名 → 替换默认选中(从 products.html 跳过来时携带)\n(function applyUrlProduct() {\n const q = new URLSearchParams(location.search);\n const productName = q.get('product');\n if (!productName) return;\n let p = PRODUCTS.find(x => x.name === productName);\n if (!p) {\n p = { id: 'np-' + Date.now().toString(36), name: productName, cat: '美妆个护', meta: '新建 · 待补充' };\n PRODUCTS.unshift(p);\n }\n state.selectedProd = p.id;\n})();\n\n/* ============================================================\n 生成批次 (localStorage) · 按当前商品过滤 · 立即生成时追加\n ============================================================ */\n(function () {\n 'use strict';\n const TASK_TYPE = 'model';\n const KEY = 'fs-image-tasks-' + TASK_TYPE;\n\n let tasks = [];\n\n function load() {\n try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; }\n }\n function save(arr) {\n try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch (e) {}\n }\n function buildSnapshot() {\n return {\n selectedProd: state.selectedProd,\n selectedModel: state.selectedModel,\n count: state.count,\n ratio: state.ratio,\n };\n }\n function timeNow() {\n const d = new Date();\n return ('0'+(d.getMonth()+1)).slice(-2) + '.' + ('0'+d.getDate()).slice(-2) + ' ' + ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2);\n }\n\n // 暴露给上层 (给 cur-prod header 用)\n window._countBatchesForProd = function (prodId) {\n return tasks.filter(t => t.snap && t.snap.selectedProd === prodId).length;\n };\n\n // 切换商品 → 清空 pv-grid (历史批次只在当前 session 持有)\n // cur-prod header 的\"本商品 N 批\"由 _countBatchesForProd 提供累计数\n window.renderBatchesForCurrentProd = function () {\n const grid = document.getElementById('pv-grid');\n if (!grid) return;\n grid.innerHTML = '';\n _batchSeq = 0;\n showPreviewEmpty();\n };\n\n // 立即生成 → push 新批次 + persist + 刷新视图\n document.getElementById('mp-go-btn').addEventListener('click', () => {\n if (!state.selectedModel || !state.selectedProd) return;\n const snap = buildSnapshot();\n const _prod = PRODUCTS.find(p => p.id === state.selectedProd);\n const _model = MODELS.find(m => m.id === state.selectedModel);\n const _name = ((_prod && _prod.name) || '商品') + ' × ' + ((_model && _model.name) || '模特');\n const task = {\n id: 'task-' + Date.now(),\n type: TASK_TYPE,\n name: _name,\n snap,\n status: 'ok',\n time: timeNow(),\n createdAt: Date.now(),\n };\n tasks.push(task);\n save(tasks);\n // 不重渲整个 grid (保留 hover 重跑/采用的实时态),只更新 cur-prod 计数\n updateCurProdHeader();\n });\n\n /* ---------- 初始化 ---------- */\n tasks = load();\n})();\n\n/* ---------- 商品空间折叠 / 展开 ---------- */\n(function () {\n const layout = document.querySelector('.mp-layout');\n const foldBtn = document.querySelector('.mp-side-top .fold');\n const restoreBtn = document.getElementById('mp-side-restore');\n function setSideCollapsed(collapsed) {\n layout?.classList.toggle('side-collapsed', collapsed);\n if (restoreBtn) restoreBtn.hidden = !collapsed;\n if (foldBtn) foldBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');\n try { localStorage.setItem('fs-mp-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}\n }\n foldBtn?.addEventListener('click', () => setSideCollapsed(true));\n restoreBtn?.addEventListener('click', () => setSideCollapsed(false));\n try { setSideCollapsed(localStorage.getItem('fs-mp-side-collapsed') === '1'); } catch (e) {}\n})();\n\n// 初始化\nrenderProdSpace();\nrenderSelectedProds();\nrenderModelMini(); // MODELS 已声明,可安全调用\nupdateModelSummary();\nupdateCost();\nshowPreviewEmpty(); // 默认 → 右侧显示空态\n// 默认选中: URL ?product= 优先, 否则选 PRODUCTS 首位 (= 最近编辑)\nconst defaultProdId = state.selectedProd || (PRODUCTS[0] && PRODUCTS[0].id);\nif (defaultProdId) selectProduct(defaultProdId);\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"modelPhotoDemoA": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"model-photo-demo-a.html\">\n<meta charset=\"utf-8\">\n<title>方案 A · 商品空间 · 模特上身图 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .app { height: 100vh; overflow: hidden; }\n main { display: flex; flex-direction: column; min-height: 0; }\n #page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }\n\n /* ===== 两栏:左侧栏 / 右侧主区 ===== */\n .dma {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 260px 1fr;\n }\n\n /* ── 左侧栏 · 仅商品空间(搜索 + 列表 + 全部入口) ── */\n .dma-side {\n border-right: 1px solid var(--border-faint);\n background: var(--surface);\n display: flex; flex-direction: column;\n min-height: 0;\n }\n .dma-side-h {\n padding: 14px 14px 10px;\n flex-shrink: 0;\n }\n .dma-side-h .ti-row {\n display: flex; align-items: center;\n margin-bottom: 10px;\n }\n .dma-side-h .ti {\n font-size: 11px; font-family: var(--font-mono);\n color: var(--black-alpha-48); letter-spacing: .08em;\n text-transform: uppercase;\n }\n .dma-side-h .ti-row .add {\n margin-left: auto;\n width: 22px; height: 22px;\n display: grid; place-items: center;\n background: var(--heat-12); color: var(--heat);\n border: 0; border-radius: var(--r-sm);\n cursor: pointer;\n }\n .dma-side-h .add svg { width: 11px; height: 11px; }\n /* 搜索框 (Q1-A) */\n .dma-search {\n display: flex; align-items: center; gap: 8px;\n height: 32px; padding: 0 10px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n transition: border-color var(--t-base), background var(--t-base);\n }\n .dma-search:focus-within { border-color: var(--heat-40); background: var(--surface); }\n .dma-search svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }\n .dma-search input {\n flex: 1; min-width: 0; height: 100%;\n border: 0; outline: 0; background: transparent;\n font-size: 12.5px; color: var(--accent-black); font-family: inherit;\n }\n\n /* 商品空间列表(flex:1 占据中部所有剩余空间) */\n .dma-prod-list {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 4px 10px 10px;\n display: flex; flex-direction: column; gap: 4px;\n }\n .dma-prod {\n display: flex; align-items: center; gap: 10px;\n padding: 8px;\n border: 1px solid transparent;\n border-radius: var(--r-sm);\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .dma-prod:hover { background: var(--background-lighter); }\n .dma-prod.active { background: var(--heat-12); border-color: var(--heat-20); }\n .dma-prod .thumb {\n flex-shrink: 0;\n width: 40px; height: 40px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden;\n background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.04) 4px 5px);\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 9px;\n color: var(--black-alpha-32);\n }\n .dma-prod.active .thumb { border-color: var(--heat); }\n .dma-prod .body { flex: 1; min-width: 0; }\n .dma-prod .nm {\n font-size: 12.5px;\n color: var(--accent-black); font-weight: 500;\n line-height: 1.3;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .dma-prod.active .nm { color: var(--heat); font-weight: 600; }\n .dma-prod .sub {\n margin-top: 2px;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n\n /* 全部商品入口(Q1-B · 固定贴底) */\n .dma-all {\n flex-shrink: 0;\n margin: 0 10px 12px;\n padding: 10px 12px;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n display: flex; align-items: center; gap: 6px;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .dma-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .dma-all .ct {\n color: var(--black-alpha-48);\n font-family: var(--font-mono); font-size: 10.5px;\n margin-left: auto;\n }\n .dma-all svg { width: 12px; height: 12px; }\n\n /* ── 右侧主区 ── */\n .dma-main {\n display: flex; flex-direction: column;\n min-height: 0;\n overflow: hidden;\n }\n .dma-main-h {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 14px;\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .dma-main-h .crumb {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n }\n .dma-main-h h2 {\n font-size: 20px; font-weight: 600;\n letter-spacing: -.015em;\n color: var(--accent-black);\n }\n .dma-main-h .stats {\n margin-left: auto;\n display: flex; gap: 6px;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .dma-main-h .stats b { color: var(--accent-black); font-weight: 600; }\n .dma-main-h .stats .sep { color: var(--black-alpha-24); }\n\n /* 主区:参数 + 结果横向二栏 */\n .dma-body {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 320px 1fr;\n gap: 0;\n }\n\n /* 左侧参数面板 */\n .dma-form {\n border-right: 1px solid var(--border-faint);\n background: var(--surface);\n display: flex; flex-direction: column;\n min-height: 0;\n }\n .dma-form-scroll {\n flex: 1; min-height: 0; overflow-y: auto;\n padding: 16px 18px;\n }\n .dma-field { margin-bottom: 16px; }\n .dma-field-h {\n font-size: 12px; font-weight: 600;\n color: var(--accent-black);\n margin-bottom: 8px;\n }\n .dma-field-h .opt {\n font-weight: 400; font-size: 11px;\n color: var(--black-alpha-48); margin-left: 4px;\n }\n .dma-models {\n display: grid; grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n }\n .dma-model {\n aspect-ratio: 3 / 4;\n border: 1px solid var(--border-faint);\n background: var(--background-lighter);\n border-radius: var(--r-sm);\n position: relative; cursor: pointer;\n overflow: hidden;\n transition: border-color var(--t-base);\n }\n .dma-model:hover { border-color: var(--black-alpha-32); }\n .dma-model.selected { border-color: var(--heat); border-width: 2px; }\n .dma-model .ph {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-32);\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .dma-model .nm {\n position: absolute; bottom: 0; left: 0; right: 0;\n padding: 4px 6px;\n background: linear-gradient(transparent, rgba(0,0,0,.5));\n font-size: 10px; color: #fff; font-weight: 500;\n }\n .dma-model.selected::after {\n content: ''; position: absolute; top: 4px; right: 4px;\n width: 14px; height: 14px;\n background: var(--heat) url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E\") no-repeat center / 9px;\n border-radius: 50%;\n }\n\n .dma-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }\n .dma-chip {\n display: inline-flex; align-items: center;\n height: 28px; padding: 0 11px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .dma-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .dma-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }\n\n .dma-form-cta {\n flex-shrink: 0;\n padding: 14px 18px;\n border-top: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .dma-cost {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 10px;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .dma-cost .v { color: var(--accent-black); font-weight: 600; }\n .dma-gen {\n width: 100%; height: 42px;\n background: var(--heat); color: #fff;\n border: 1px solid var(--heat); border-radius: var(--r-md);\n font-size: 14px; font-weight: 600; font-family: inherit;\n cursor: pointer;\n display: inline-flex; align-items: center; justify-content: center; gap: 8px;\n box-shadow: var(--shadow-cta);\n }\n .dma-gen svg { width: 15px; height: 15px; }\n\n /* 右侧结果区 */\n .dma-result {\n background: var(--background-base);\n min-height: 0; overflow-y: auto;\n padding: 22px 24px;\n }\n .dma-result-h {\n display: flex; align-items: baseline; gap: 10px;\n margin-bottom: 14px;\n }\n .dma-result-h .ti { font-size: 15px; font-weight: 600; color: var(--accent-black); }\n .dma-result-h .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n\n /* 批次卡 */\n .dma-batch {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 14px 16px;\n margin-bottom: 14px;\n }\n .dma-batch-h {\n display: flex; align-items: center; gap: 10px;\n margin-bottom: 12px;\n padding-bottom: 10px;\n border-bottom: 1px solid var(--border-faint);\n }\n .dma-batch-h .pic {\n width: 28px; height: 28px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--heat);\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n }\n .dma-batch-h .meta { flex: 1; min-width: 0; }\n .dma-batch-h .nm { font-size: 13px; font-weight: 600; color: var(--accent-black); }\n .dma-batch-h .info { margin-top: 2px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .dma-batch-h .info .sep { color: var(--black-alpha-24); }\n .dma-batch-h .ops { display: flex; gap: 4px; }\n .dma-batch-h .ops button {\n width: 28px; height: 28px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-56); cursor: pointer;\n display: grid; place-items: center;\n }\n .dma-batch-h .ops button:hover { border-color: var(--heat-20); color: var(--heat); }\n .dma-batch-h .ops button svg { width: 13px; height: 13px; }\n\n .dma-batch-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }\n @media (max-width: 1400px) { .dma-batch-grid { grid-template-columns: repeat(3, 1fr); } }\n .dma-cell {\n aspect-ratio: 3 / 4;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden; position: relative;\n cursor: pointer;\n }\n .dma-cell .ph {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32);\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .dma-cell .tag {\n position: absolute; top: 6px; left: 6px;\n padding: 2px 6px;\n background: rgba(0,0,0,.65);\n color: #fff;\n border-radius: var(--r-sm);\n font-size: 10px; font-weight: 500;\n backdrop-filter: blur(4px);\n }\n\n /* 空态 */\n .dma-empty {\n height: 100%;\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n gap: 12px;\n color: var(--black-alpha-48);\n text-align: center;\n }\n .dma-empty .ic {\n width: 56px; height: 56px;\n background: var(--surface); border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--heat);\n }\n .dma-empty .ic svg { width: 22px; height: 22px; }\n .dma-empty .mono { font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em; }\n\n /* 顶部\"提示条\" · 这是 demo */\n .dma-banner {\n margin: 12px 28px 0;\n padding: 8px 12px;\n background: var(--heat-12);\n border: 1px dashed var(--heat-20);\n border-radius: var(--r-sm);\n font-size: 12px;\n color: var(--accent-black);\n font-family: var(--font-mono); letter-spacing: .02em;\n }\n .dma-banner b { color: var(--heat); }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n <div class=\"dma-banner\">// DEMO · 方案 A · <b>商品 = 项目空间</b>(Q1: A+B)。左栏仅商品空间:🔍 搜索 + 最近 6 条 + <b>全部商品</b>兜底入口;历史任务已挪进主区。主区:模特卡 + 张数 + 比例 + 立即生成,生成结果自动绑定到当前商品。</div>\n\n <div class=\"dma\">\n\n <!-- ===== 左侧栏 · 仅商品空间 (搜索 + 列表 + 全部入口) ===== -->\n <aside class=\"dma-side\">\n <div class=\"dma-side-h\">\n <div class=\"ti-row\">\n <span class=\"ti\">商品空间</span>\n <button class=\"add\" type=\"button\" title=\"新建商品\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </button>\n </div>\n <!-- Q1-A · 搜索框 -->\n <div class=\"dma-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" placeholder=\"搜索商品 / 分类\">\n </div>\n </div>\n\n <!-- 商品列表 · 最近 6 条 -->\n <div class=\"dma-prod-list\">\n <div class=\"dma-prod active\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">透真补水面膜</div>\n <div class=\"sub\">// 美妆个护 · 6 批</div>\n </div>\n </div>\n <div class=\"dma-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">透真清透防晒霜</div>\n <div class=\"sub\">// 美妆个护 · 3 批</div>\n </div>\n </div>\n <div class=\"dma-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">南卡 Lite Pro 蓝牙耳机</div>\n <div class=\"sub\">// 数码 3C · 2 批</div>\n </div>\n </div>\n <div class=\"dma-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">滋啦速食牛肉面</div>\n <div class=\"sub\">// 食品饮料 · 1 批</div>\n </div>\n </div>\n <div class=\"dma-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">三顿半同款冻干咖啡</div>\n <div class=\"sub\">// 食品饮料 · 1 批</div>\n </div>\n </div>\n <div class=\"dma-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">小熊 4L 可视空气炸锅</div>\n <div class=\"sub\">// 家居家电 · 0 批</div>\n </div>\n </div>\n </div>\n\n <!-- Q1-B · 全部商品入口(贴底) -->\n <button class=\"dma-all\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M3 9h18M9 4v16\"/></svg>\n 全部商品\n <span class=\"ct\">24 个</span>\n <svg style=\"margin-left:4px\" 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>\n </button>\n </aside>\n\n <!-- ===== 右侧主区 ===== -->\n <section class=\"dma-main\">\n\n <!-- 顶部 · 商品名 + 统计 -->\n <div class=\"dma-main-h\">\n <div>\n <div class=\"crumb\">// 商品空间</div>\n <h2>透真补水面膜</h2>\n </div>\n <div class=\"stats\">\n <span>本商品 <b>6</b> 批</span>\n <span class=\"sep\">·</span>\n <span>累计 <b>22</b> 张图</span>\n <span class=\"sep\">·</span>\n <span>最近 <b>3 分钟前</b></span>\n </div>\n </div>\n\n <!-- 中间:参数面板 + 结果 -->\n <div class=\"dma-body\">\n\n <!-- 参数面板(单一职责:挑模特 + 张数) -->\n <div class=\"dma-form\">\n <div class=\"dma-form-scroll\">\n\n <div class=\"dma-field\">\n <div class=\"dma-field-h\">选择模特<span class=\"opt\">(已锁定商品 · 透真补水面膜)</span></div>\n <div class=\"dma-models\">\n <div class=\"dma-model selected\">\n <div class=\"ph\">Ava · 3:4</div>\n <div class=\"nm\">Ava</div>\n </div>\n <div class=\"dma-model\">\n <div class=\"ph\">Zoe · 3:4</div>\n <div class=\"nm\">Zoe</div>\n </div>\n <div class=\"dma-model\">\n <div class=\"ph\">Ben · 3:4</div>\n <div class=\"nm\">Ben</div>\n </div>\n <div class=\"dma-model\">\n <div class=\"ph\">Lin · 3:4</div>\n <div class=\"nm\">Lin</div>\n </div>\n <div class=\"dma-model\">\n <div class=\"ph\">Mia · 3:4</div>\n <div class=\"nm\">Mia</div>\n </div>\n <div class=\"dma-model\" style=\"border-style:dashed;display:flex;align-items:center;justify-content:center;color:var(--black-alpha-48);\">\n <span style=\"font-size:18px\">+</span>\n </div>\n </div>\n </div>\n\n <div class=\"dma-field\">\n <div class=\"dma-field-h\">生成张数</div>\n <div class=\"dma-chip-row\">\n <button class=\"dma-chip\" type=\"button\">1 张</button>\n <button class=\"dma-chip\" type=\"button\">2 张</button>\n <button class=\"dma-chip active\" type=\"button\">4 张</button>\n <button class=\"dma-chip\" type=\"button\">8 张</button>\n </div>\n </div>\n\n <div class=\"dma-field\">\n <div class=\"dma-field-h\">画面比例</div>\n <div class=\"dma-chip-row\">\n <button class=\"dma-chip\" type=\"button\">1:1</button>\n <button class=\"dma-chip active\" type=\"button\">3:4</button>\n <button class=\"dma-chip\" type=\"button\">9:16</button>\n <button class=\"dma-chip\" type=\"button\">16:9</button>\n </div>\n </div>\n\n <div class=\"dma-field\">\n <div class=\"dma-field-h\">补充提示词<span class=\"opt\">(选填)</span></div>\n <textarea style=\"width:100%;min-height:60px;padding:8px 10px;background:var(--background-lighter);border:1px solid var(--black-alpha-12);border-radius:var(--r-sm);font-family:inherit;font-size:12.5px;outline:none;resize:vertical\" placeholder=\"例:户外阳光、敷面膜的特写、白底产品摄影\"></textarea>\n </div>\n\n </div>\n\n <div class=\"dma-form-cta\">\n <div class=\"dma-cost\">\n <span>预估扣费 <span class=\"v\">≈ ¥1.20</span></span>\n <span>余额 ¥327.40</span>\n </div>\n <button class=\"dma-gen\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z\"/></svg>\n 立即生成 · 透真补水面膜 × Ava\n </button>\n </div>\n </div>\n\n <!-- 右侧结果 · 当前商品全部批次 -->\n <div class=\"dma-result\">\n <div class=\"dma-result-h\">\n <span class=\"ti\">最近批次 · Ava × 4 张</span>\n <span class=\"sub\">// 3 分钟前 · 已完成</span>\n </div>\n\n <!-- 批次 1 -->\n <div class=\"dma-batch\">\n <div class=\"dma-batch-h\">\n <div class=\"pic\">4×</div>\n <div class=\"meta\">\n <div class=\"nm\">Ava × 4 张</div>\n <div class=\"info\">透真补水面膜 <span class=\"sep\">·</span> 3:4 <span class=\"sep\">·</span> 3 分钟前 <span class=\"sep\">·</span> ¥1.20</div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"全部重跑\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg></button>\n <button type=\"button\" title=\"全部下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n <button type=\"button\" title=\"加入资产库\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg></button>\n </div>\n </div>\n <div class=\"dma-batch-grid\">\n <div class=\"dma-cell\"><div class=\"ph\">Ava · #1</div><span class=\"tag\">3:4</span></div>\n <div class=\"dma-cell\"><div class=\"ph\">Ava · #2</div><span class=\"tag\">3:4</span></div>\n <div class=\"dma-cell\"><div class=\"ph\">Ava · #3</div><span class=\"tag\">3:4</span></div>\n <div class=\"dma-cell\"><div class=\"ph\">Ava · #4</div><span class=\"tag\">3:4</span></div>\n </div>\n </div>\n\n <!-- 批次 2 -->\n <div class=\"dma-batch\">\n <div class=\"dma-batch-h\">\n <div class=\"pic\">4×</div>\n <div class=\"meta\">\n <div class=\"nm\">Zoe × 4 张</div>\n <div class=\"info\">透真补水面膜 <span class=\"sep\">·</span> 3:4 <span class=\"sep\">·</span> 12 分钟前 <span class=\"sep\">·</span> ¥1.20</div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"全部重跑\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg></button>\n <button type=\"button\" title=\"全部下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n <button type=\"button\" title=\"加入资产库\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg></button>\n </div>\n </div>\n <div class=\"dma-batch-grid\">\n <div class=\"dma-cell\"><div class=\"ph\">Zoe · #1</div><span class=\"tag\">3:4</span></div>\n <div class=\"dma-cell\"><div class=\"ph\">Zoe · #2</div><span class=\"tag\">3:4</span></div>\n <div class=\"dma-cell\"><div class=\"ph\">Zoe · #3</div><span class=\"tag\">3:4</span></div>\n <div class=\"dma-cell\"><div class=\"ph\">Zoe · #4</div><span class=\"tag\">3:4</span></div>\n </div>\n </div>\n\n <!-- 批次 3 -->\n <div class=\"dma-batch\">\n <div class=\"dma-batch-h\">\n <div class=\"pic\">2×</div>\n <div class=\"meta\">\n <div class=\"nm\">Ben × 2 张</div>\n <div class=\"info\">透真补水面膜 <span class=\"sep\">·</span> 3:4 <span class=\"sep\">·</span> 刚刚 <span class=\"sep\">·</span> 生成中</div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"取消\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button>\n </div>\n </div>\n <div class=\"dma-batch-grid\">\n <div class=\"dma-cell\"><div class=\"ph\">生成中…</div></div>\n <div class=\"dma-cell\"><div class=\"ph\">生成中…</div></div>\n </div>\n </div>\n\n </div>\n </div>\n\n </section>\n\n </div>\n\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({\n active: 'asset-factory',\n crumbs: [\n { label: '工作台', href: 'index.html' },\n { label: '图片生成', href: 'asset-factory.html' },\n { label: '模特上身图 · 方案 A · Demo' }\n ]\n});\n\n// 简单交互:点击商品 / 任务切换激活态\ndocument.querySelectorAll('.dma-prod').forEach(el => {\n el.addEventListener('click', () => {\n document.querySelectorAll('.dma-prod').forEach(x => x.classList.remove('active'));\n el.classList.add('active');\n });\n});\ndocument.querySelectorAll('.dma-task').forEach(el => {\n el.addEventListener('click', () => {\n document.querySelectorAll('.dma-task').forEach(x => x.classList.remove('active'));\n el.classList.add('active');\n });\n});\ndocument.querySelectorAll('.dma-model').forEach(el => {\n el.addEventListener('click', () => {\n document.querySelectorAll('.dma-model').forEach(x => x.classList.remove('selected'));\n el.classList.add('selected');\n });\n});\ndocument.querySelectorAll('.dma-chip').forEach(el => {\n el.addEventListener('click', () => {\n // 同组互斥\n el.parentElement.querySelectorAll('.dma-chip').forEach(x => x.classList.remove('active'));\n el.classList.add('active');\n });\n});\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"modelPhotoDemoB": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"model-photo-demo-b.html\">\n<meta charset=\"utf-8\">\n<title>方案 A · v2 · 模特上身图 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .app { height: 100vh; overflow: hidden; }\n main { display: flex; flex-direction: column; min-height: 0; }\n #page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }\n\n /* ===== 两栏:左栏(纯商品空间) + 主区 ===== */\n .dmb {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 260px 1fr;\n }\n\n /* ── 左栏 · 商品空间 ── */\n .dmb-side {\n border-right: 1px solid var(--border-faint);\n background: var(--surface);\n display: flex; flex-direction: column;\n min-height: 0;\n }\n .dmb-side-h {\n padding: 14px 16px 10px;\n flex-shrink: 0;\n }\n .dmb-side-h .ti {\n font-size: 11px; font-family: var(--font-mono);\n color: var(--black-alpha-48); letter-spacing: .08em;\n text-transform: uppercase;\n margin-bottom: 10px;\n }\n /* 搜索框 */\n .dmb-search {\n display: flex; align-items: center; gap: 8px;\n height: 32px; padding: 0 10px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n transition: border-color var(--t-base);\n }\n .dmb-search:focus-within { border-color: var(--heat-40); background: var(--surface); }\n .dmb-search svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }\n .dmb-search input {\n flex: 1; min-width: 0; height: 100%;\n border: 0; outline: 0; background: transparent;\n font-size: 12.5px; color: var(--accent-black); font-family: inherit;\n }\n\n /* 商品列表 */\n .dmb-list {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 8px 10px 8px;\n display: flex; flex-direction: column; gap: 6px;\n }\n .dmb-prod {\n display: flex; align-items: center; gap: 10px;\n padding: 8px;\n border: 1px solid transparent;\n border-radius: var(--r-sm);\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .dmb-prod:hover { background: var(--background-lighter); }\n .dmb-prod.active { background: var(--heat-12); border-color: var(--heat-20); }\n .dmb-prod .thumb {\n flex-shrink: 0;\n width: 44px; height: 44px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden; position: relative;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 9px;\n color: var(--black-alpha-32);\n background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.04) 4px 5px);\n }\n .dmb-prod.active .thumb { border-color: var(--heat); }\n .dmb-prod .body { flex: 1; min-width: 0; }\n .dmb-prod .nm {\n font-size: 12.5px;\n color: var(--accent-black); font-weight: 500;\n line-height: 1.3;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .dmb-prod.active .nm { color: var(--heat); font-weight: 600; }\n .dmb-prod .sub {\n margin-top: 2px;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n\n /* 底部 · 全部商品入口 */\n .dmb-all {\n flex-shrink: 0;\n margin: 0 10px 12px;\n padding: 10px 12px;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12px;\n cursor: pointer;\n display: flex; align-items: center; gap: 6px;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .dmb-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .dmb-all .ct { color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; margin-left: auto; }\n .dmb-all svg { width: 12px; height: 12px; }\n\n /* ── 主区 ── */\n .dmb-main {\n display: flex; flex-direction: column;\n min-height: 0; overflow: hidden;\n position: relative;\n }\n .dmb-main-h {\n flex-shrink: 0;\n padding: 16px 28px 12px;\n border-bottom: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .dmb-main-h .crumb {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n margin-bottom: 4px;\n }\n .dmb-main-h h2 {\n font-size: 22px; font-weight: 600;\n letter-spacing: -.015em;\n color: var(--accent-black);\n }\n .dmb-main-h .row {\n display: flex; align-items: center; gap: 16px;\n margin-top: 6px;\n }\n .dmb-main-h .stats {\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n display: flex; gap: 4px;\n }\n .dmb-main-h .stats b { color: var(--accent-black); font-weight: 600; }\n .dmb-main-h .stats .sep { color: var(--black-alpha-24); }\n .dmb-main-h .spacer { flex: 1; }\n\n /* 主区 toolbar (筛选条 · 仿 image-optimize 顶部) */\n .dmb-main-tb { display: flex; gap: 8px; align-items: center; }\n .dmb-main-tb .icbtn {\n width: 30px; height: 30px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n cursor: pointer; display: grid; place-items: center;\n }\n .dmb-main-tb .icbtn:hover { border-color: var(--heat-20); color: var(--heat); }\n .dmb-main-tb .icbtn svg { width: 13px; height: 13px; }\n .dmb-main-tb .chip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 30px; padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer;\n }\n .dmb-main-tb .chip:hover { border-color: var(--heat-20); color: var(--heat); }\n .dmb-main-tb .chip svg { width: 10px; height: 10px; opacity: .6; }\n\n /* 主区滚动体 · 任务流 */\n .dmb-stream {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 22px 28px 200px; /* 底部留出参数面板高度 */\n background: var(--background-base);\n }\n .dmb-day-h {\n display: flex; align-items: baseline; gap: 8px;\n margin: 6px 0 10px;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .06em;\n text-transform: uppercase;\n }\n .dmb-day-h::before {\n content: ''; width: 14px; height: 1px;\n background: var(--black-alpha-24);\n display: inline-block; margin-right: 2px;\n }\n .dmb-day-h .ct {\n color: var(--black-alpha-72); font-weight: 500;\n margin-left: auto;\n text-transform: none; letter-spacing: 0;\n }\n\n /* 批次卡 */\n .dmb-batch {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 14px 16px;\n margin-bottom: 14px;\n }\n .dmb-batch-h {\n display: flex; align-items: center; gap: 10px;\n margin-bottom: 12px;\n }\n .dmb-batch-h .pic {\n flex-shrink: 0;\n width: 30px; height: 30px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--heat);\n font-family: var(--font-mono); font-size: 11px; font-weight: 600;\n }\n .dmb-batch-h .meta { flex: 1; min-width: 0; }\n .dmb-batch-h .nm { font-size: 13.5px; font-weight: 600; color: var(--accent-black); }\n .dmb-batch-h .info {\n margin-top: 2px;\n display: flex; flex-wrap: wrap; gap: 4px;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .dmb-batch-h .info .sep { color: var(--black-alpha-24); }\n .dmb-batch-h .ops { display: flex; gap: 4px; }\n .dmb-batch-h .ops button {\n width: 28px; height: 28px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-56); cursor: pointer;\n display: grid; place-items: center;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .dmb-batch-h .ops button:hover { border-color: var(--heat-20); color: var(--heat); }\n .dmb-batch-h .ops button svg { width: 13px; height: 13px; }\n\n /* 状态 pill 紧贴标题右侧 */\n .dmb-batch-h .stat-pill {\n margin-left: 8px;\n padding: 2px 7px;\n font-size: 10px;\n }\n .dmb-batch-h .stat-pill .dot { width: 4px; height: 4px; }\n\n .dmb-batch-grid {\n display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;\n }\n @media (max-width: 1400px) { .dmb-batch-grid { grid-template-columns: repeat(3, 1fr); } }\n .dmb-cell {\n aspect-ratio: 3 / 4;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden; position: relative;\n cursor: pointer;\n }\n .dmb-cell .ph {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-32);\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .dmb-cell.gen .ph { animation: dmb-pulse 1.4s ease-in-out infinite; }\n @keyframes dmb-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }\n .dmb-cell.err { border-color: var(--accent-crimson, #c43d3d); }\n .dmb-cell.err .ph { color: var(--accent-crimson, #c43d3d); background: rgba(196,61,61,.05); }\n .dmb-cell .tag {\n position: absolute; top: 6px; left: 6px;\n padding: 2px 6px;\n background: rgba(0,0,0,.65); color: #fff;\n border-radius: var(--r-sm);\n font-size: 10px; font-weight: 500; backdrop-filter: blur(4px);\n }\n\n /* ── 底部 fixed 参数面板 ── */\n .dmb-param-wrap {\n position: absolute; left: 0; right: 0; bottom: 0;\n padding: 14px 28px 22px;\n background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px);\n z-index: 5;\n }\n .dmb-param {\n max-width: 1180px; margin: 0 auto;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: 14px;\n padding: 10px 14px;\n display: flex; align-items: center; gap: 10px;\n box-shadow: 0 6px 24px rgba(0,0,0,.06);\n }\n .dmb-param .label-mono {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n flex-shrink: 0;\n }\n .dmb-param .pchip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 30px; padding: 0 12px;\n background: var(--background-lighter);\n border: 1px solid transparent;\n border-radius: var(--r-pill);\n font-size: 12px; color: var(--black-alpha-72);\n cursor: pointer; font-family: inherit;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .dmb-param .pchip:hover { background: var(--surface); border-color: var(--border-faint); }\n .dmb-param .pchip.active { background: var(--heat-12); color: var(--heat); }\n .dmb-param .pchip svg { width: 10px; height: 10px; opacity: .6; }\n .dmb-param .pchip .lbl-mono {\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .dmb-param .pchip.active .lbl-mono { color: var(--heat); }\n .dmb-param .spacer { flex: 1; }\n .dmb-param .meta-right {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n margin-right: 6px;\n text-align: right;\n }\n .dmb-param .meta-right .v { color: var(--accent-black); font-weight: 600; }\n .dmb-param .gen-btn {\n height: 38px; padding: 0 20px;\n background: var(--heat); color: #fff;\n border: 0; border-radius: var(--r-md);\n font-size: 13.5px; font-weight: 600; font-family: inherit;\n cursor: pointer;\n display: inline-flex; align-items: center; gap: 8px;\n box-shadow: var(--shadow-cta);\n }\n .dmb-param .gen-btn svg { width: 14px; height: 14px; }\n\n /* 顶部 demo 提示条 */\n .dmb-banner {\n margin: 12px 28px 0;\n padding: 8px 12px;\n background: var(--heat-12);\n border: 1px dashed var(--heat-20);\n border-radius: var(--r-sm);\n font-size: 12px; color: var(--accent-black);\n font-family: var(--font-mono); letter-spacing: .02em;\n }\n .dmb-banner b { color: var(--heat); }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n <div class=\"dmb-banner\">// DEMO v2 · 方案 A · <b>商品空间(A+B) + 任务流主区</b>。左栏只保留商品空间(搜索+最近6条+全部入口),任务列表搬到主区,筛选放主区顶部 toolbar,参数面板底部 fixed 化(类 image-optimize)。</div>\n\n <div class=\"dmb\">\n\n <!-- ===== 左栏 · 商品空间 ===== -->\n <aside class=\"dmb-side\">\n <div class=\"dmb-side-h\">\n <div class=\"ti\">商品空间</div>\n <div class=\"dmb-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" placeholder=\"搜索商品 / 分类\">\n </div>\n </div>\n\n <div class=\"dmb-list\">\n <div class=\"dmb-prod active\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">透真补水面膜</div>\n <div class=\"sub\">美妆个护 · 6 批</div>\n </div>\n </div>\n <div class=\"dmb-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">透真清透防晒霜</div>\n <div class=\"sub\">美妆个护 · 3 批</div>\n </div>\n </div>\n <div class=\"dmb-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">南卡 Lite Pro 蓝牙耳机</div>\n <div class=\"sub\">数码 3C · 2 批</div>\n </div>\n </div>\n <div class=\"dmb-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">滋啦速食牛肉面</div>\n <div class=\"sub\">食品饮料 · 1 批</div>\n </div>\n </div>\n <div class=\"dmb-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">三顿半同款冻干咖啡</div>\n <div class=\"sub\">食品饮料 · 1 批</div>\n </div>\n </div>\n <div class=\"dmb-prod\">\n <div class=\"thumb\">主图</div>\n <div class=\"body\">\n <div class=\"nm\">小熊 4L 可视空气炸锅</div>\n <div class=\"sub\">家居家电 · 0 批</div>\n </div>\n </div>\n </div>\n\n <button class=\"dmb-all\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M3 9h18M9 4v16\"/></svg>\n 全部商品\n <span class=\"ct\">24 个</span>\n <svg style=\"margin-left:4px\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </button>\n </aside>\n\n <!-- ===== 主区 ===== -->\n <section class=\"dmb-main\">\n\n <!-- 顶部 标题 + stats + toolbar -->\n <div class=\"dmb-main-h\">\n <div class=\"crumb\">// 商品空间 · 模特上身图</div>\n <h2>透真补水面膜</h2>\n <div class=\"row\">\n <div class=\"stats\">\n <span>美妆个护</span><span class=\"sep\">·</span>\n <span>本商品 <b>6</b> 批</span><span class=\"sep\">·</span>\n <span>累计 <b>22</b> 张图</span><span class=\"sep\">·</span>\n <span>最近 <b>3 分钟前</b></span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"dmb-main-tb\">\n <button class=\"icbtn\" type=\"button\" title=\"搜索批次\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </button>\n <button class=\"chip\" type=\"button\">时间 <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <button class=\"chip\" type=\"button\">状态 <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <button class=\"chip\" type=\"button\">模特 <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n </div>\n </div>\n </div>\n\n <!-- 任务流 -->\n <div class=\"dmb-stream\">\n\n <div class=\"dmb-day-h\">\n <span>今天</span>\n <span class=\"ct\">3 批 · 10 张</span>\n </div>\n\n <!-- 批次 1 -->\n <div class=\"dmb-batch\">\n <div class=\"dmb-batch-h\">\n <div class=\"pic\">4×</div>\n <div class=\"meta\">\n <div class=\"nm\">Ava × 4 张 <span class=\"pill ok stat-pill\"><span class=\"dot\"></span>已完成</span></div>\n <div class=\"info\">\n <span>3:4</span><span class=\"sep\">·</span>\n <span>3 分钟前</span><span class=\"sep\">·</span>\n <span>¥1.20</span>\n </div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"全部重跑\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg></button>\n <button type=\"button\" title=\"全部下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n <button type=\"button\" title=\"加入资产库\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg></button>\n </div>\n </div>\n <div class=\"dmb-batch-grid\">\n <div class=\"dmb-cell\"><div class=\"ph\">Ava · #1</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Ava · #2</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Ava · #3</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Ava · #4</div><span class=\"tag\">3:4</span></div>\n </div>\n </div>\n\n <!-- 批次 2 -->\n <div class=\"dmb-batch\">\n <div class=\"dmb-batch-h\">\n <div class=\"pic\">4×</div>\n <div class=\"meta\">\n <div class=\"nm\">Zoe × 4 张 <span class=\"pill ok stat-pill\"><span class=\"dot\"></span>已完成</span></div>\n <div class=\"info\">\n <span>3:4</span><span class=\"sep\">·</span>\n <span>12 分钟前</span><span class=\"sep\">·</span>\n <span>¥1.20</span>\n </div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"全部重跑\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg></button>\n <button type=\"button\" title=\"全部下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n <button type=\"button\" title=\"加入资产库\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg></button>\n </div>\n </div>\n <div class=\"dmb-batch-grid\">\n <div class=\"dmb-cell\"><div class=\"ph\">Zoe · #1</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Zoe · #2</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Zoe · #3</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Zoe · #4</div><span class=\"tag\">3:4</span></div>\n </div>\n </div>\n\n <!-- 批次 3 · 生成中 -->\n <div class=\"dmb-batch\">\n <div class=\"dmb-batch-h\">\n <div class=\"pic\">2×</div>\n <div class=\"meta\">\n <div class=\"nm\">Ben × 2 张 <span class=\"pill info stat-pill\"><span class=\"dot\"></span>生成中</span></div>\n <div class=\"info\">\n <span>3:4</span><span class=\"sep\">·</span>\n <span>刚刚</span><span class=\"sep\">·</span>\n <span>¥0.60</span>\n </div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"取消\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button>\n </div>\n </div>\n <div class=\"dmb-batch-grid\">\n <div class=\"dmb-cell gen\"><div class=\"ph\">生成中…</div></div>\n <div class=\"dmb-cell gen\"><div class=\"ph\">生成中…</div></div>\n </div>\n </div>\n\n <!-- 昨天 -->\n <div class=\"dmb-day-h\">\n <span>昨天</span>\n <span class=\"ct\">2 批 · 8 张</span>\n </div>\n\n <div class=\"dmb-batch\">\n <div class=\"dmb-batch-h\">\n <div class=\"pic\">4×</div>\n <div class=\"meta\">\n <div class=\"nm\">Lin × 4 张 <span class=\"pill ok stat-pill\"><span class=\"dot\"></span>已完成</span></div>\n <div class=\"info\">\n <span>3:4</span><span class=\"sep\">·</span>\n <span>昨天 18:24</span><span class=\"sep\">·</span>\n <span>¥1.20</span>\n </div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"全部重跑\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg></button>\n <button type=\"button\" title=\"全部下载\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg></button>\n <button type=\"button\" title=\"加入资产库\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg></button>\n </div>\n </div>\n <div class=\"dmb-batch-grid\">\n <div class=\"dmb-cell\"><div class=\"ph\">Lin · #1</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Lin · #2</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Lin · #3</div><span class=\"tag\">3:4</span></div>\n <div class=\"dmb-cell\"><div class=\"ph\">Lin · #4</div><span class=\"tag\">3:4</span></div>\n </div>\n </div>\n\n <!-- 更早 -->\n <div class=\"dmb-day-h\">\n <span>更早</span>\n <span class=\"ct\">1 批 · 2 张 · 含 1 失败</span>\n </div>\n\n <div class=\"dmb-batch\">\n <div class=\"dmb-batch-h\">\n <div class=\"pic\">2×</div>\n <div class=\"meta\">\n <div class=\"nm\">Ava × 2 张 <span class=\"pill err stat-pill\"><span class=\"dot\"></span>失败</span></div>\n <div class=\"info\">\n <span>3:4</span><span class=\"sep\">·</span>\n <span>2 天前</span>\n </div>\n </div>\n <div class=\"ops\">\n <button type=\"button\" title=\"全部重跑\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg></button>\n <button type=\"button\" title=\"删除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/></svg></button>\n </div>\n </div>\n <div class=\"dmb-batch-grid\">\n <div class=\"dmb-cell err\"><div class=\"ph\">失败 · 点重跑</div></div>\n <div class=\"dmb-cell err\"><div class=\"ph\">失败 · 点重跑</div></div>\n </div>\n </div>\n\n </div>\n\n <!-- 底部 fixed 参数面板 -->\n <div class=\"dmb-param-wrap\">\n <div class=\"dmb-param\">\n <button class=\"pchip active\" type=\"button\">\n <span class=\"lbl-mono\">模特</span>\n <span>Ava</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <button class=\"pchip\" type=\"button\">\n <span class=\"lbl-mono\">张数</span>\n <span>4</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <button class=\"pchip\" type=\"button\">\n <span class=\"lbl-mono\">比例</span>\n <span>3:4</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <button class=\"pchip\" type=\"button\">\n <span class=\"lbl-mono\">补充提示词</span>\n <span style=\"color:var(--black-alpha-48)\">+ 添加</span>\n </button>\n <span class=\"spacer\"></span>\n <span class=\"meta-right\">预估 <span class=\"v\">¥1.20</span> · 余额 <span class=\"v\">¥327.40</span></span>\n <button class=\"gen-btn\" type=\"button\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" 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>\n 生成 · 透真补水面膜 × Ava\n </button>\n </div>\n </div>\n\n </section>\n\n </div>\n\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({\n active: 'asset-factory',\n crumbs: [\n { label: '工作台', href: 'index.html' },\n { label: '图片生成', href: 'asset-factory.html' },\n { label: '模特上身图 · 方案 A · v2' }\n ]\n});\n\n// 商品 / chip 切换\ndocument.querySelectorAll('.dmb-prod').forEach(el => {\n el.addEventListener('click', () => {\n document.querySelectorAll('.dmb-prod').forEach(x => x.classList.remove('active'));\n el.classList.add('active');\n });\n});\ndocument.querySelectorAll('.dmb-param .pchip').forEach(el => {\n el.addEventListener('click', () => {\n // demo:只切自己 active(不互斥,每个 chip 都可独立切下拉)\n el.classList.toggle('active');\n });\n});\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"pipeline": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"pipeline.html\">\n<meta charset=\"utf-8\">\n<title id=\"page-title\">流水线 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── Project header ─── */\n .proj-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 22px; align-items: flex-start; }\n .proj-head h1 { font-size: 20px; font-weight: 700; letter-spacing: -.012em; }\n\n /* ─── 顶部胶囊式 Stage 状态 · 注入到 .topbar 中部 ─── */\n .topbar { position: relative; } /* 锚定 pill */\n .pipeline-topbar-left {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n max-width: min(36vw, 520px);\n }\n .pipeline-back {\n height: 34px;\n padding: 0 13px 0 11px;\n border-radius: var(--r-pill);\n flex: 0 0 auto;\n }\n .pipeline-back svg { width: 14px; height: 14px; }\n .pipeline-topbar-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13.5px;\n font-weight: 500;\n color: var(--accent-black);\n }\n .pipeline-topbar-title .mono {\n margin-left: 8px;\n font-size: 10.5px;\n font-weight: 400;\n letter-spacing: .04em;\n color: var(--black-alpha-48);\n }\n @media (max-width: 1500px) {\n .pipeline-topbar-title { display: none; }\n }\n .stage-pill {\n position: absolute; left: 50%; top: 50%;\n transform: translate(-50%, -50%);\n display: inline-flex; align-items: center; gap: 0;\n padding: 6px 16px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n z-index: 3;\n }\n .stage-pill .sp-dot {\n position: relative;\n display: inline-flex; align-items: center; gap: 6px;\n padding: 2px 8px;\n text-decoration: none;\n cursor: pointer;\n border-radius: var(--r-sm);\n transition: background var(--t-base);\n }\n .stage-pill .sp-dot:hover { background: var(--background-lighter); }\n /* 圆点本体 · 默认(待开始):浅灰实心 · 对齐 .prog span 默认 */\n .stage-pill .sp-dot .d {\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--black-alpha-8);\n border: 1.5px solid transparent;\n transition: background var(--t-base), border-color var(--t-base), box-shadow var(--t-base);\n }\n /* 文字标签 · 始终显示 */\n .stage-pill .sp-dot .l {\n font-size: 12px; color: var(--black-alpha-56);\n font-weight: 500; letter-spacing: .01em;\n white-space: nowrap;\n transition: color var(--t-base);\n }\n .stage-pill .sp-dot:hover .l { color: var(--accent-black); }\n /* done · 森林绿 · 对齐 .prog span.done */\n .stage-pill .sp-dot.done .d {\n background: var(--accent-forest);\n border-color: var(--accent-forest);\n }\n .stage-pill .sp-dot.done .l { color: var(--accent-black); }\n /* active · 主橙 + 光晕 + 脉动 · 对齐 .prog span.cur(单橙锚点) */\n .stage-pill .sp-dot.active .d {\n background: var(--heat);\n border-color: var(--heat);\n box-shadow: 0 0 0 3px var(--heat-12);\n animation: prog-pulse 1.4s ease-in-out infinite;\n }\n .stage-pill .sp-dot.active .l { color: var(--heat); font-weight: 600; }\n /* fail · crimson · 对齐 .prog span.fail */\n .stage-pill .sp-dot.fail .d {\n background: var(--accent-crimson);\n border-color: var(--accent-crimson);\n }\n .stage-pill .sp-dot.fail .l { color: var(--accent-crimson); }\n /* 连接线 · 对齐 .prog 色卡 */\n .stage-pill .sp-line {\n width: 14px; height: 1.5px;\n background: var(--black-alpha-8);\n transition: background var(--t-base);\n }\n .stage-pill .sp-line.done { background: var(--accent-forest); }\n /* anchor 节点本身只承载初始模板,挂入 topbar 后变 .stage-pill */\n .stage-pill-anchor[hidden] { display: none; }\n\n /* ─── Stage panes ─── */\n .stage { display: none; }\n .stage.active { display: block; }\n\n /* Common pane */\n .pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); }\n .pane-h { display: flex; align-items: center; gap: 8px; padding: 14px 18px; border-bottom: 1px solid var(--border-faint); }\n .pane-h strong { font-size: 14px; font-weight: 600; }\n\n /* Stage foot */\n .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); }\n .stage-foot .info { font-size: 12.5px; color: var(--black-alpha-56); }\n .stage-foot .info .mono { font-family: var(--font-mono); color: var(--black-alpha-48); font-size: 11.5px; letter-spacing: .02em; }\n .stage-foot .hstack { gap: 10px; align-items: center; }\n .stage-foot .btn {\n height: 40px;\n min-height: 40px;\n padding: 0 18px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 7px;\n font-size: 13.5px;\n line-height: 1;\n white-space: nowrap;\n }\n .stage-foot .btn-primary { padding: 0 20px; font-weight: 600; }\n .stage-foot .btn svg { width: 14px; height: 14px; flex: 0 0 14px; }\n\n /* ─── 全高度布局 · 除 Stage 2 外,操作模块 hug content、内容区域 fill content ─── */\n /* JS 在 activateStage(1/3/4/5) 时给 .content 加 .content--fh,Stage 2 留常规文档流 */\n .content.content--fh {\n display: flex; flex-direction: column;\n overflow: hidden; /* 内部 .stage 接管滚动 */\n padding: 24px 28px 20px; /* 默认上下 padding · 让 stage-foot 真正贴近视口底部 */\n }\n /* flat 模式 · 像 model-photo 那样的双区扁平布局 · 取消卡片,全宽贴边 */\n .content.content--fh-flat {\n padding: 0;\n }\n /* flat 模式下 · 取消 .pane 卡片描边/圆角(Stage 1 两侧面板) */\n .content--fh-flat .stage.active > .stage-script .pane {\n background: var(--surface);\n border: 0; border-radius: 0;\n }\n /* 左/右 pane 白底贴边 · 仅内部元素对齐到 topbar 28px 版心 */\n /* Stage 1 · 镜头脚本 + 脚本助手 */\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.shot-list > .pane-h,\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.shot-list > .shots-body {\n padding-left: 28px;\n }\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.chat-pane > .pane-h,\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.chat-pane > .chat-body,\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.chat-pane > .chat-input {\n padding-left: 28px;\n padding-right: 28px;\n }\n /* Stage 1 · flat 模式去除分割线:两侧 pane-h 底线 · gutter 竖线 · chat-input 顶线 */\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.shot-list > .pane-h,\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.chat-pane > .pane-h {\n border-bottom: 0;\n }\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .pane.chat-pane > .chat-input {\n border-top: 0;\n }\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .stage-script-gutter::after {\n background: transparent;\n }\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .stage-script-gutter:hover::after,\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script > .stage-script-gutter.dragging::after {\n background: var(--heat);\n }\n /* Stage 2/3 的内部对齐规则放到对应 flat 块里(避免被后面规则吃掉) */\n /* Stage 5 · editor-preview 左 · editor-props 右 · timeline 两侧 */\n .content--fh-flat .stage[data-stage-pane=\"5\"].active > .editor > .editor-preview {\n padding-left: 28px;\n }\n .content--fh-flat .stage[data-stage-pane=\"5\"].active > .editor > .editor-props {\n padding-right: 28px;\n }\n .content--fh-flat .stage[data-stage-pane=\"5\"].active > .editor > .timeline {\n padding-left: 28px;\n padding-right: 28px;\n }\n /* 镜头脚本 | 拖拽分隔条 | 脚本助手 三列布局 · 分隔条可拖动 */\n .content--fh-flat .stage[data-stage-pane=\"1\"].active > .stage-script {\n gap: 0;\n grid-template-columns: minmax(0, 1fr) 6px var(--chat-w, 520px);\n }\n /* 拖拽 gutter · 默认 1px 灰线,hover/拖中加重 · 鼠标 col-resize */\n .stage-script-gutter {\n position: relative;\n background: transparent;\n cursor: col-resize;\n transition: background var(--t-base);\n }\n .stage-script-gutter::after {\n content: '';\n position: absolute; top: 0; bottom: 0; left: 50%;\n width: 1px; transform: translateX(-50%);\n background: var(--border-faint);\n transition: background var(--t-base), width var(--t-base);\n }\n .stage-script-gutter:hover::after,\n .stage-script-gutter.dragging::after {\n background: var(--heat);\n width: 2px;\n }\n .stage-script-gutter.dragging { background: var(--heat-12); }\n /* flat 模式 · stage-foot 改为全宽 flat 底栏:白底 + border-top,不再像卡片下沿 */\n .content--fh-flat .stage.active > .stage-foot {\n margin-top: 0;\n padding: 14px 28px;\n background: var(--surface);\n border-top: 1px solid var(--border-faint);\n }\n\n /* ─── Stage 3 flat · 故事板双区:左 canvas | 右 side,用 border-right 分隔 ─── */\n .content--fh-flat .stage[data-stage-pane=\"3\"].active > .stage-storyboard {\n gap: 0;\n }\n .content--fh-flat .stage[data-stage-pane=\"3\"].active > .stage-storyboard > .sb-canvas {\n border: 0; border-radius: 0;\n background: var(--surface);\n padding: 18px 14px 18px 28px;\n align-items: center;\n }\n .content--fh-flat .stage[data-stage-pane=\"3\"].active > .stage-storyboard > .sb-canvas > .sb-main-img {\n width: 100%;\n }\n /* sb-side · 撑满网格行高 · 内 .pane fill content + 内部可滚 */\n .content--fh-flat .stage[data-stage-pane=\"3\"].active > .stage-storyboard > .sb-side {\n display: flex; flex-direction: column; min-height: 0;\n }\n .content--fh-flat .stage[data-stage-pane=\"3\"].active > .stage-storyboard > .sb-side > .pane {\n flex: 1 1 0; min-height: 0; overflow-y: auto;\n border: 0; border-radius: 0;\n background: var(--surface);\n padding: 18px 28px;\n }\n /* 解除 sb-scenes-col 560 上限,跟随父级填满 */\n .content--fh-flat .stage[data-stage-pane=\"3\"].active .sb-scenes-col { max-height: none; }\n\n /* ─── Stage 2 flat · 左 200 资产侧栏 | 右 内容,用 border-right 分隔 ─── */\n .content--fh-flat .stage[data-stage-pane=\"2\"].active > .stage-assets {\n gap: 0;\n height: 100%;\n }\n .content--fh-flat .stage[data-stage-pane=\"2\"].active > .stage-assets > .asset-side {\n position: static; align-self: stretch;\n padding: 18px 16px;\n background: var(--background-base);\n overflow-y: auto;\n }\n .content--fh-flat .stage[data-stage-pane=\"2\"].active > .stage-assets > .asset-main {\n padding: 18px 28px;\n overflow-y: auto;\n background: var(--background-base);\n }\n\n /* ─── Stage 4 flat · 顶部 queue-bar 改 toolbar · 视频卡片网格区 ─── */\n .content--fh-flat .stage[data-stage-pane=\"4\"].active > .queue-bar {\n border: 0; border-radius: 0;\n border-bottom: 1px solid var(--border-faint);\n margin: 0;\n padding: 14px 28px;\n }\n .content--fh-flat .stage[data-stage-pane=\"4\"].active > .video-grid {\n padding: 18px 28px;\n background: var(--background-base);\n }\n\n /* ─── Stage 5 flat · 编辑器外壳去卡片 ─── */\n .content--fh-flat .stage[data-stage-pane=\"5\"].active > .editor {\n border: 0; border-radius: 0;\n }\n\n .content--fh .stage.active {\n display: flex; flex-direction: column;\n flex: 1 1 auto; min-height: 0;\n }\n /* 内容主体(stage-foot 之上的最后一个块)· fill content */\n .content--fh .stage[data-stage-pane=\"1\"].active > .stage-script,\n .content--fh .stage[data-stage-pane=\"2\"].active > .stage-assets,\n .content--fh .stage[data-stage-pane=\"3\"].active > .stage-storyboard,\n .content--fh .stage[data-stage-pane=\"4\"].active > .video-grid,\n .content--fh .stage[data-stage-pane=\"5\"].active > .editor {\n flex: 1 1 0; min-height: 0;\n }\n /* Stage 1 / 2 主体不整体滚,把滚动交给左右栏各自的 body */\n .content--fh .stage[data-stage-pane=\"1\"].active > .stage-script,\n .content--fh .stage[data-stage-pane=\"2\"].active > .stage-assets { overflow: hidden; }\n /* 其他 stage 主体仍可整体滚动 */\n .content--fh .stage[data-stage-pane=\"3\"].active > .stage-storyboard,\n .content--fh .stage[data-stage-pane=\"4\"].active > .video-grid,\n .content--fh .stage[data-stage-pane=\"5\"].active > .editor { overflow-y: auto; }\n /* stage-foot · hug content:高度只由按钮决定,不拉伸、不压缩 */\n .content--fh .stage.active > .stage-foot { flex: 0 0 auto; }\n /* Stage 1 脚本网格:取消固定 min-height,允许 flex 接管 */\n .content--fh .stage[data-stage-pane=\"1\"].active > .stage-script { min-height: 0; }\n /* Stage 1 内部 panes:左右栏各自独立滚动 */\n .content--fh .stage[data-stage-pane=\"1\"].active > .stage-script > .shot-list,\n .content--fh .stage[data-stage-pane=\"1\"].active > .stage-script > .chat-pane {\n min-height: 0; min-width: 0;\n }\n .content--fh .stage[data-stage-pane=\"1\"].active .shots-body,\n .content--fh .stage[data-stage-pane=\"1\"].active .chat-body {\n max-height: none;\n flex: 1 1 0; min-height: 0; overflow-y: auto;\n }\n /* Stage 5 编辑器:解除固定 580px 高,让 flex 接管 */\n .content--fh .stage[data-stage-pane=\"5\"].active > .editor { height: auto; }\n\n /* === STAGE 1 · 脚本(镜头脚本 : 脚本助手 = 7 : 3,助手在 3:2 基础上再缩 1/4) === */\n .stage-script { display: grid; grid-template-columns: 7fr 3fr; gap: 16px; min-height: 560px; }\n\n .chat-pane { display: flex; flex-direction: column; }\n .chat-body { padding: 16px 18px; flex: 1; overflow-y: auto; max-height: 460px; display: flex; flex-direction: column; gap: 14px; }\n .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); }\n .msg.ai .bubble { background: var(--surface); }\n .msg.user { display: flex; flex-direction: column; align-items: flex-end; }\n .msg.user .bubble { background: var(--heat-12); color: var(--accent-black); border-color: var(--heat-20); }\n .msg .time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 4px; letter-spacing: .02em; }\n .msg .actions { display: flex; gap: 6px; margin-top: 6px; }\n .ai-avatar { width: 26px; height: 26px; flex-shrink: 0; background: var(--heat); color: var(--accent-white); display: grid; place-items: center; font-size: 11px; font-weight: 700; border: 1px solid var(--heat); border-radius: 50%; }\n .del { text-decoration: line-through; color: var(--black-alpha-48); }\n .ins { background: var(--forest-bg); color: var(--accent-forest); padding: 0 3px; }\n .chat-input { padding: 14px 18px 18px; border-top: 1px solid var(--border-faint); }\n .chat-input-card {\n background: var(--background-base);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 12px 14px 10px;\n transition: border-color var(--t-base), box-shadow var(--t-base);\n }\n .chat-input-card:focus-within { border-color: var(--accent-black); box-shadow: 0 0 0 3px rgba(0,0,0,.04); }\n .chat-input-area {\n width: 100%; border: none; outline: none; background: transparent;\n font-family: var(--font-sans); font-size: 13px; color: var(--accent-black);\n line-height: 1.55; resize: none; padding: 0; min-height: 42px;\n }\n .chat-input-area::placeholder { color: var(--black-alpha-40); }\n .chat-input-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; }\n .chat-input-foot .hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-40); letter-spacing: .02em; }\n .chat-input-foot .spacer { flex: 1; }\n .chat-icon-btn {\n width: 28px; height: 28px; display: grid; place-items: center;\n background: transparent; border: 1px solid var(--border-faint);\n border-radius: 50%; color: var(--black-alpha-56); cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .chat-icon-btn:hover { border-color: var(--accent-black); color: var(--accent-black); }\n .chat-send-btn {\n width: 32px; height: 32px; display: grid; place-items: center;\n background: var(--accent-black); border: 1px solid var(--accent-black);\n border-radius: 50%; color: var(--accent-white); cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);\n }\n .chat-send-btn:hover { background: var(--heat); border-color: var(--heat); }\n .chat-send-btn:active { transform: scale(.95); }\n .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; }\n .chat-attach-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }\n .chat-attach-chip {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 3px 6px 3px 8px; background: var(--surface);\n border: 1px solid var(--border-faint); border-radius: var(--r-sm);\n font-family: var(--font-mono); font-size: 11px; color: var(--accent-black);\n }\n .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%; }\n .chat-attach-chip .x:hover { background: var(--black-alpha-08); color: var(--accent-black); }\n\n .shot-list { display: flex; flex-direction: column; }\n .shots-body { padding: 12px 16px; flex: 1; overflow-y: auto; max-height: 540px; display: flex; flex-direction: column; gap: 0; }\n .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); }\n .shot-card.highlight { border-color: var(--heat); background: var(--heat-12); }\n .shot-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }\n .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); }\n .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); }\n .shot-row { display: grid; grid-template-columns: 36px 1fr; gap: 8px; padding: 4px 0; }\n .shot-k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); padding-top: 2px; letter-spacing: .04em; }\n .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); }\n .shot-v[contenteditable=\"true\"]:hover { background: var(--heat-12); cursor: text; }\n .shot-v[contenteditable=\"true\"]:focus { background: var(--surface); box-shadow: inset 0 0 0 1px var(--heat); }\n .shot-v[data-empty=\"true\"]::before { content: attr(data-placeholder); color: var(--black-alpha-32); font-style: italic; }\n .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; }\n .icon-mini-btn:hover { color: var(--heat); border-color: var(--heat); }\n\n /* 镜头卡片间 hover 加分镜插槽 */\n /* 镜头卡片间 hover 加分镜插槽 · 卡片移开一行 + 渐显按钮 */\n .shot-insert-gap {\n height: 10px;\n position: relative;\n display: flex; align-items: center; justify-content: center;\n padding: 0;\n transition: height .24s cubic-bezier(.18,.72,.28,1), padding .24s cubic-bezier(.18,.72,.28,1);\n }\n .shot-insert-gap:hover {\n height: 72px;\n padding: 14px 0;\n }\n .shot-insert-gap .add-shot-btn {\n opacity: 0;\n transform: translateY(4px) scale(.96);\n height: 28px; padding: 0 14px;\n background: var(--surface);\n color: var(--heat);\n border: 1px dashed var(--heat-40);\n border-radius: var(--r-md);\n font-size: 12.5px; font-family: inherit; font-weight: 500;\n cursor: pointer;\n transition: opacity .2s ease .04s, transform .24s cubic-bezier(.18,.72,.28,1) .04s, background var(--t-base), border-color var(--t-base), color var(--t-base);\n display: inline-flex; align-items: center; gap: 6px;\n pointer-events: none;\n white-space: nowrap;\n }\n .shot-insert-gap .add-shot-btn svg { width: 12px; height: 12px; }\n .shot-insert-gap:hover .add-shot-btn {\n opacity: 1; transform: translateY(0) scale(1);\n pointer-events: auto;\n }\n .shot-insert-gap .add-shot-btn:hover {\n background: var(--heat-12);\n border-style: solid;\n border-color: var(--heat);\n color: var(--heat);\n }\n\n /* 镜头脚本顶栏 · 自动从脚本抓取的人物/场景标签 · 可编辑/删除/添加 */\n .shot-list > .pane-h { flex-wrap: wrap; row-gap: 8px; }\n .shot-headline { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }\n .script-brief-summary { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }\n .script-brief-pill { gap: 4px; padding: 3px 8px; font-size: 11px; }\n .script-brief-pill .k { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .script-brief-pill .v { color: var(--accent-black); max-width: 116px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .script-tags { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 14px; margin-left: 6px; }\n .script-tags .tag-group { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n .script-tags .tg-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; flex-shrink: 0; }\n .script-tags .script-tag { display: inline-flex; align-items: center; gap: 2px; padding: 2px 2px 2px 10px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: 999px; font-size: 11.5px; color: var(--accent-black); transition: border-color var(--t-base), background var(--t-base); }\n .script-tags .script-tag:hover { border-color: var(--heat-40); background: var(--heat-12); }\n .script-tags .script-tag .t { outline: none; padding: 0 4px; border-radius: 3px; min-width: 8px; cursor: text; }\n .script-tags .script-tag .t:focus { background: var(--surface); box-shadow: inset 0 0 0 1px var(--heat); }\n .script-tags .script-tag .x { width: 16px; height: 16px; display: grid; place-items: center; background: transparent; border: 0; color: var(--black-alpha-40); border-radius: 50%; cursor: pointer; font-size: 13px; line-height: 1; transition: background var(--t-base), color var(--t-base); }\n .script-tags .script-tag .x:hover { background: var(--black-alpha-08); color: var(--accent-crimson); }\n .script-tags .tag-add { width: 20px; height: 20px; display: grid; place-items: center; background: transparent; border: 1px dashed var(--black-alpha-24); border-radius: 50%; color: var(--black-alpha-48); cursor: pointer; font-size: 13px; line-height: 1; padding: 0; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }\n .script-tags .tag-add:hover { border-style: solid; border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n\n /* 镜头脚本空缺省态 */\n .shots-empty { padding: 36px 24px; margin: auto; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 12px; color: var(--black-alpha-48); }\n .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); }\n .shots-empty .empty-title { font-size: 14px; font-weight: 500; color: var(--accent-black); }\n .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; }\n\n /* 对话空态三胶囊 */\n .chat-empty { padding: 28px 18px 14px; margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; }\n .chat-empty .ce-title { font-size: 13.5px; color: var(--accent-black); font-weight: 500; }\n .chat-empty .ce-hint { font-size: 11.5px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }\n .chat-modes { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }\n .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); }\n .chat-mode:hover { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }\n .chat-mode.primary { background: var(--heat-12); border-color: var(--heat); color: var(--heat); }\n .chat-mode svg { width: 13px; height: 13px; }\n .script-brief-card { margin-top: 8px; padding: 12px; background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: flex; flex-direction: column; gap: 10px; }\n .script-brief-row { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 10px; row-gap: 4px; align-items: center; }\n .script-brief-row .k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .script-brief-row .why { grid-column: 2 / 3; font-size: 11.5px; color: var(--black-alpha-48); line-height: 1.5; }\n .script-brief-select { position: relative; display: inline-flex; width: 100%; min-width: 0; }\n .script-brief-value {\n width: 100%;\n min-width: 0;\n height: 36px;\n padding: 0 12px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n background: var(--surface);\n color: var(--accent-black);\n font-size: 13px;\n font-weight: 500;\n font-family: inherit;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .script-brief-value .v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }\n .script-brief-value::after {\n content: '';\n width: 5px;\n height: 5px;\n border-right: 1px solid currentColor;\n border-bottom: 1px solid currentColor;\n transform: rotate(45deg) translateY(-1px);\n transition: transform var(--t-base);\n color: var(--black-alpha-48);\n flex-shrink: 0;\n }\n .script-brief-value:hover {\n background: var(--heat-12);\n border-color: var(--heat-20);\n color: var(--heat);\n }\n .script-brief-select.open .script-brief-value {\n background: var(--heat-12);\n border-color: var(--heat);\n color: var(--heat);\n }\n .script-brief-select.open .script-brief-value::after {\n color: var(--heat);\n transform: rotate(225deg) translate(-1px, -1px);\n }\n .script-brief-select.open .chip-menu { display: block; }\n .script-brief-select .chip-menu { min-width: 168px; right: 0; left: auto; z-index: 80; }\n .script-brief-select .chip-menu .mi { width: 100%; border: 0; background: transparent; font-family: inherit; text-align: left; }\n .script-brief-actions { display: grid; grid-template-columns: 56px minmax(0, 1fr); column-gap: 10px; align-items: center; padding-top: 2px; }\n .script-brief-actions .action-row { grid-column: 2 / 3; display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }\n\n /* AI 思考态 typing indicator */\n .ai-thinking .dots { display: inline-flex; gap: 3px; }\n .ai-thinking .dots span { width: 6px; height: 6px; background: var(--black-alpha-32); border-radius: 50%; animation: thinking 1.2s ease-in-out infinite; }\n .ai-thinking .dots span:nth-child(2) { animation-delay: .2s; }\n .ai-thinking .dots span:nth-child(3) { animation-delay: .4s; }\n @keyframes thinking { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }\n\n /* 视口锁定 · 只让主内容区滚动 (sidebar + topbar 固定,不随页面滚) */\n html, body { height: 100%; overflow: hidden; max-width: 100vw; }\n .app { height: 100vh; max-height: 100vh; overflow: hidden; }\n .app > .sidebar { height: 100vh; overflow-y: auto; }\n .app > main { height: 100vh; max-height: 100vh; overflow: hidden; display: flex; flex-direction: column; min-width: 0; }\n .app > main > .topbar { flex-shrink: 0; }\n .app > main > .content { flex: 1 1 0; min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden; }\n\n /* === STAGE 2 · 基础资产 === */\n .stage-assets { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; }\n .stage-assets > div { min-width: 0; }\n .asset-side { position: sticky; top: 16px; align-self: start; }\n .asset-sec { min-width: 0; }\n .asset-strip-wrap { min-width: 0; }\n .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); }\n .asset-side .ttab:hover { background: var(--background-lighter); }\n .asset-side .ttab.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }\n .asset-side .ttab .num { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); margin-left: auto; }\n .asset-side .ttab.active .num { color: var(--heat); }\n .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); }\n .asset-side .info strong { color: var(--black-alpha-56); display: block; }\n .asset-side .info .mono { font-family: var(--font-mono); }\n\n .asset-sec { scroll-margin-top: 16px; }\n .asset-sec + .asset-sec { margin-top: 32px; }\n .asset-sec .sec-h { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }\n .asset-sec .sec-h h3 { font-size: 15px; font-weight: 600; }\n /* .pill-tip 主样式定义在下方 (heat 主色) */\n\n /* 预设库横滑行(卡片尺寸与主区 .asset-card-2 一致) */\n .asset-strip-wrap { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-faint); }\n .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; }\n .asset-strip { display: flex; gap: 14px; overflow-x: auto; overflow-y: hidden; padding: 2px 2px 14px; scrollbar-width: thin; }\n .asset-strip::-webkit-scrollbar { height: 8px; }\n .asset-strip::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }\n .asset-strip .asset-card-2 { flex: 0 0 240px; min-width: 240px; max-width: 240px; }\n\n /* 「去 XX 库」CTA 胶囊 · 主操作色,更显眼 */\n .asset-sec .sec-h .pill-tip,\n .asset-strip-wrap .strip-h .pill-tip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 28px; padding: 0 14px;\n background: var(--heat-12);\n border: 1px solid var(--heat-20);\n border-radius: 999px;\n font-size: 12px; color: var(--heat); font-weight: 500;\n cursor: pointer; font-family: inherit;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .asset-sec .sec-h .pill-tip:hover,\n .asset-strip-wrap .strip-h .pill-tip:hover {\n background: var(--heat); color: var(--accent-white); border-color: var(--heat);\n box-shadow: var(--shadow-cta);\n }\n .asset-sec .sec-h .pill-tip svg,\n .asset-strip-wrap .strip-h .pill-tip svg { width: 12px; height: 12px; }\n\n .asset-grid-2 { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }\n /* 商品行:左侧商品卡 + 右侧三视图预览(三视图是单张 16:9 图,不是 3 张) */\n .prod-row { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }\n /* 三视图卡固定 360 高;商品卡同高,宽度按 3:5 比例反推(≈216px),内部元素 flex 自适应 */\n .prod-row > .asset-card-2 {\n flex: 0 0 auto;\n width: auto; max-width: none; min-width: 0;\n height: 360px;\n aspect-ratio: 3 / 5;\n }\n .prod-preview { flex: 0 0 360px; height: 360px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px; display: none; flex-direction: column; gap: 10px; }\n .prod-preview.show { display: flex; }\n .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; }\n .prod-preview-img { aspect-ratio: 16/9; }\n .prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }\n /* 三视图历史版本缩略图 strip */\n .prod-preview-history { display: none; flex-direction: column; gap: 6px; }\n .prod-preview-history.show { display: flex; }\n .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; }\n .prod-preview-history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }\n .prod-preview-history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; scrollbar-width: thin; }\n .prod-preview-history .h-row::-webkit-scrollbar { height: 4px; }\n .prod-preview-history .h-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }\n .prod-preview-history .h-thumb {\n flex: 0 0 auto;\n width: 72px; aspect-ratio: 16/9;\n background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);\n position: relative; cursor: pointer; transition: border-color var(--t-base), transform var(--t-base);\n display: grid; place-items: center; overflow: hidden;\n }\n .prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }\n /* 已采用版本:主橙描边 + 「已采用」徽标 */\n .prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }\n /* 仅预览(未采用):黑色描边,无徽标 */\n .prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }\n .prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }\n .prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }\n .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; }\n .prod-preview-history .h-thumb.adopted .badge { display: block; }\n\n /* 「已采用」状态 · 浅橙 + 主橙文字,与已采用徽标视觉呼应 */\n #prod-preview-adopt:disabled,\n #prod-preview-adopt:disabled:hover {\n color: var(--heat);\n border-color: var(--heat-40);\n background: var(--heat-12);\n cursor: not-allowed;\n opacity: 1;\n }\n\n /* 主图可点击放大 */\n .prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }\n .prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }\n .prod-preview-img.is-zoomable::after {\n content: '';\n position: absolute; top: 8px; right: 8px;\n width: 22px; height: 22px;\n 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;\n border-radius: var(--r-sm);\n opacity: 0; transition: opacity var(--t-base);\n pointer-events: none;\n }\n .prod-preview-img.is-zoomable:hover::after { opacity: 1; }\n\n /* 三视图放大查看 lightbox */\n #tri-lightbox-bg { z-index: 80; }\n #tri-lightbox-bg .tri-lightbox {\n position: relative;\n width: min(1100px, 92vw);\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 18px 20px 20px;\n display: flex; flex-direction: column; gap: 12px;\n box-shadow: 0 24px 64px rgba(0,0,0,.24);\n }\n .tri-lightbox-head {\n display: flex; align-items: center; gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px; letter-spacing: .04em; text-transform: uppercase;\n color: var(--black-alpha-56);\n padding-right: 32px;\n }\n .tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }\n .tri-lightbox-head .lb-tag {\n margin-left: 6px;\n padding: 2px 6px;\n background: var(--heat-12); color: var(--heat);\n border-radius: 3px;\n font-size: 10px;\n }\n .tri-lightbox-close {\n position: absolute;\n top: 12px; right: 12px;\n width: 28px; height: 28px;\n display: grid; place-items: center;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-56);\n cursor: pointer;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n z-index: 2;\n }\n .tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }\n .tri-lightbox-close svg { width: 14px; height: 14px; }\n .tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }\n .tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }\n .tri-lightbox-foot .spc { flex: 1; }\n .tri-lightbox-foot kbd {\n display: inline-block;\n padding: 1px 5px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-bottom-width: 2px;\n border-radius: 3px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--black-alpha-72);\n }\n .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; }\n .asset-card-2:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }\n .asset-card-2 .thumb-2 { aspect-ratio: 1; }\n .asset-card-2 .body-2 { padding: 12px 14px; }\n .asset-card-2 .body-2 .btn:disabled,\n .asset-card-2 .body-2 .btn.disabled {\n background: transparent;\n border-color: transparent;\n color: var(--black-alpha-32);\n box-shadow: none;\n cursor: not-allowed;\n opacity: .72;\n transform: none;\n }\n .asset-card-2 .body-2 .btn:disabled:hover,\n .asset-card-2 .body-2 .btn.disabled:hover {\n background: transparent;\n border-color: transparent;\n color: var(--black-alpha-32);\n box-shadow: none;\n transform: none;\n }\n .asset-card-2 .body-2 .btn-apply { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }\n .asset-card-2 .body-2 .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }\n\n /* stage2 商品卡 · 与商品库 .product-card 视觉一致 */\n .asset-card-2.prod-lib-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }\n /* 商品图 · 占据卡片剩余高度(fill);宽度 stretch 到卡片宽 */\n .asset-card-2.prod-lib-card .prod-thumb { flex: 1 1 0; min-height: 0; position: relative; aspect-ratio: auto; }\n .asset-card-2.prod-lib-card .prod-body { padding: 14px 14px 12px; flex: 0 0 auto; }\n .asset-card-2.prod-lib-card .prod-name {\n font-size: 14px; font-weight: 600;\n color: var(--accent-black);\n line-height: 1.3;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .asset-card-2.prod-lib-card .prod-cat {\n display: inline-flex; align-items: center;\n margin-top: 8px;\n padding: 2px 8px;\n background: var(--background-lighter);\n color: var(--black-alpha-72);\n border-radius: var(--r-sm);\n font-size: 11.5px;\n }\n .asset-card-2.prod-lib-card .prod-date {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n margin-top: 10px;\n letter-spacing: .02em;\n }\n .asset-card-2.prod-lib-card .prod-footer {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n column-gap: 8px;\n padding: 10px 12px;\n border-top: 1px solid var(--border-faint);\n font-size: 11.5px;\n color: var(--black-alpha-56);\n background: var(--background-base);\n }\n .asset-card-2.prod-lib-card .prod-footer .stat {\n display: inline-flex; align-items: center; justify-content: center; gap: 5px;\n padding: 3px 8px;\n border-radius: var(--r-sm);\n font-family: var(--font-mono);\n letter-spacing: .02em;\n white-space: nowrap;\n justify-self: center;\n }\n .asset-card-2.prod-lib-card .prod-footer .stat svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }\n .asset-card-2.prod-lib-card .prod-footer .stat b { color: var(--accent-black); font-weight: 600; }\n .asset-card-2.prod-lib-card .prod-footer .sep { color: var(--black-alpha-24); font-family: var(--font-mono); flex-shrink: 0; }\n .asset-card-2.prod-lib-card .prod-action {\n padding: 10px 12px;\n border-top: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .asset-card-2.prod-lib-card .prod-action[hidden] { display: none; }\n .asset-card-2.prod-lib-card .prod-action .btn-aigen {\n width: 100%;\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n height: 34px; padding: 0 14px;\n background: var(--heat);\n color: var(--accent-white);\n border: 1px solid var(--heat);\n border-radius: var(--r-sm);\n font-size: 13px; font-weight: 500;\n cursor: pointer;\n font-family: inherit;\n box-shadow:\n inset 0 -2px 4px rgba(250, 93, 25, 0.20),\n 0 1px 1px rgba(250, 93, 25, 0.12),\n 0 2px 4px rgba(250, 93, 25, 0.10);\n transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);\n }\n .asset-card-2.prod-lib-card .prod-action .btn-aigen:hover {\n background: #FB6E2E;\n box-shadow:\n inset 0 -2px 4px rgba(250, 93, 25, 0.24),\n 0 2px 4px rgba(250, 93, 25, 0.20),\n 0 4px 12px rgba(250, 93, 25, 0.18);\n transform: translateY(-1px);\n }\n .asset-card-2.prod-lib-card .prod-action .btn-aigen:active { transform: translateY(0); }\n .asset-card-2.prod-lib-card .prod-action .btn-aigen:disabled {\n opacity: .65; cursor: not-allowed; transform: none;\n box-shadow: inset 0 -2px 4px rgba(250, 93, 25, 0.20);\n }\n .asset-card-2.prod-lib-card .prod-action .btn-aigen .ai-spark {\n width: 14px; height: 14px;\n flex-shrink: 0;\n }\n\n /* 通用资产详情 modal · 参考布局 v2 */\n .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; }\n .asset-modal-bg.show { display: flex; }\n .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); }\n .asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }\n .asset-modal-h h2 { font-size: 15px; font-weight: 600; }\n .asset-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .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; }\n .asset-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }\n .asset-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }\n .asset-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }\n /* 左栏:大立绘 + 缩略 */\n .asset-detail-lead { display: flex; flex-direction: column; gap: 10px; }\n .asset-detail-lead .ad-lead-wrap { position: relative; }\n .asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); }\n /* 查看大图 icon · 悬浮容器才显示 · 32×32 icon-only */\n .ad-zoom-btn {\n position: absolute; right: 8px; bottom: 8px;\n width: 32px; height: 32px; padding: 0;\n background: rgba(21,20,15,.7); color: #fff;\n border: 0; border-radius: var(--r-sm);\n display: grid; place-items: center;\n cursor: pointer; backdrop-filter: blur(4px);\n opacity: 0;\n transition: opacity var(--t-base), background var(--t-base);\n z-index: 3;\n }\n .ad-zoom-btn:hover { background: rgba(21,20,15,.92); }\n .ad-zoom-btn svg { width: 14px; height: 14px; }\n .asset-detail-lead .ad-lead-wrap:hover .ad-zoom-btn,\n .asset-detail-tri-row .placeholder:hover .ad-zoom-btn { opacity: 1; }\n .asset-detail-tri-row .placeholder { position: relative; }\n .asset-detail-lead .ad-thumbs {\n display: flex; gap: 8px;\n }\n .asset-detail-lead .ad-thumbs .thumb {\n flex: 0 0 64px;\n aspect-ratio: 3/4;\n border-radius: var(--r-sm);\n border: 1px solid var(--border-faint);\n cursor: pointer; overflow: hidden;\n transition: border-color var(--t-base);\n }\n .asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }\n .asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }\n\n /* 右栏 section 通用 */\n .asset-detail-right .ad-section + .ad-section { margin-top: 18px; }\n .asset-detail-section-h {\n display: flex; align-items: center; gap: 8px;\n font-size: 13px; font-weight: 600; color: var(--accent-black);\n margin-bottom: 10px;\n }\n .asset-detail-section-h .ic {\n width: 14px; height: 14px;\n color: var(--heat); flex-shrink: 0;\n display: grid; place-items: center;\n }\n .asset-detail-section-h .ic svg { width: 14px; height: 14px; }\n .asset-detail-section-h .ad-ratio-chip {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 10.5px;\n padding: 2px 8px; border-radius: var(--r-sm);\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n color: var(--black-alpha-56); letter-spacing: .02em;\n }\n .asset-detail-section-h .ad-icon-btn {\n width: 28px; height: 28px;\n display: grid; place-items: center;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-56); cursor: pointer;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .asset-detail-section-h .ad-icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }\n .asset-detail-section-h .ad-icon-btn svg { width: 12px; height: 12px; }\n\n /* 三视图 — 始终单张 16:9 大图 (不分 3 张) */\n .asset-detail-tri-row { margin-top: 0; }\n .asset-detail-tri-row .placeholder { aspect-ratio: 16 / 9; border-radius: var(--r-md); }\n /* 三视图 · 用户上传 历史版本 strip */\n .md-view-versions { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }\n .md-view-versions .v-thumb { flex: 0 0 auto; width: 72px; aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); position: relative; cursor: pointer; display: grid; place-items: center; overflow: hidden; transition: border-color var(--t-base); }\n .md-view-versions .v-thumb:hover { border-color: var(--heat-40); }\n .md-view-versions .v-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }\n .md-view-versions .v-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .md-view-versions .v-thumb.active .v { color: var(--heat); font-weight: 600; }\n .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; }\n .asset-detail-tri-row .placeholder.missing:hover { border-color: var(--heat); color: var(--heat); }\n\n /* 简介文字 + 标签 */\n .ad-intro {\n font-size: 13px; line-height: 1.65;\n color: var(--black-alpha-72);\n margin: 0 0 12px;\n }\n .ad-tags {\n display: flex; flex-wrap: wrap; gap: 8px;\n }\n .ad-tags .ad-tag-chip {\n height: 26px; padding: 0 12px;\n display: inline-flex; align-items: center;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12px; color: var(--accent-black);\n }\n .ad-tags .ad-tag-add {\n width: 26px; height: 26px;\n display: grid; place-items: center;\n background: var(--background-lighter);\n border: 1px dashed var(--black-alpha-24);\n border-radius: var(--r-sm);\n color: var(--black-alpha-56); cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .ad-tags .ad-tag-add:hover { border-color: var(--heat); color: var(--heat); }\n .ad-tags .ad-tag-add svg { width: 12px; height: 12px; }\n\n /* 属性表 · 3 列 × N 行 */\n .ad-props {\n margin-top: 18px;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n column-gap: 24px;\n row-gap: 0;\n border-top: 1px solid var(--border-faint);\n padding-top: 16px;\n }\n .ad-props .ad-prop {\n display: flex; align-items: baseline;\n padding: 10px 0;\n border-bottom: 1px solid var(--border-faint);\n font-size: 12.5px;\n min-height: 38px;\n }\n .ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }\n .ad-props .ad-prop .k {\n flex: 0 0 64px;\n color: var(--black-alpha-56);\n font-family: var(--font-mono); font-size: 11px;\n }\n .ad-props .ad-prop .v {\n color: var(--accent-black);\n font-weight: 500;\n word-break: break-all;\n }\n\n .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; }\n .asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }\n .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; }\n\n /* footer · 左侧统计 + 右侧按钮 */\n .asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }\n .asset-modal-f .ad-foot-stats { display: flex; gap: 6px; margin-right: auto; }\n .asset-modal-f .ad-stat-btn {\n height: 32px; padding: 0 12px;\n display: inline-flex; align-items: center; gap: 6px;\n background: transparent;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12.5px; font-family: inherit;\n cursor: pointer;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .asset-modal-f .ad-stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }\n .asset-modal-f .ad-stat-btn svg { width: 13px; height: 13px; }\n .asset-modal-f .ad-stat-btn b { color: var(--accent-black); font-weight: 600; }\n\n /* 演员库 / 场景库 全屏弹窗(沿用 model-photo .ml-modal 结构) */\n .ml-modal-bg { position: fixed; inset: 0; background: var(--surface); z-index: 1000; display: none; }\n .ml-modal-bg.show { display: flex; }\n .ml-modal { margin: 0; flex: 1; background: var(--surface); border-radius: 0; overflow: hidden; display: flex; flex-direction: column; }\n .ml-modal-h { display: flex; align-items: center; padding: 14px 28px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; }\n .ml-modal-h h2 { font-size: 16px; font-weight: 600; }\n .ml-modal-h .ct { margin-left: 10px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .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); }\n .ml-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }\n .ml-modal-h .x svg { width: 16px; height: 16px; }\n .ml-modal-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 200px 1fr; }\n .ml-side { border-right: 1px solid var(--border-faint); padding: 18px 0; overflow-y: auto; }\n .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; }\n .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); }\n .ml-side .ml-side-item:hover { background: var(--black-alpha-4); }\n .ml-side .ml-side-item.active { background: var(--heat-12); color: var(--accent-black); border-left-color: var(--heat); font-weight: 600; }\n .ml-side .ml-side-item .ct { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }\n .ml-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; min-width: 0; position: relative; }\n .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; }\n .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); }\n .ml-toolbar .btn-up:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }\n .ml-toolbar .btn-up svg { width: 14px; height: 14px; }\n .ml-toolbar .chip-group { display: inline-flex; align-items: center; gap: 6px; }\n .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; }\n .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); }\n .ml-toolbar .chip:hover { color: var(--accent-black); }\n .ml-toolbar .chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); font-weight: 600; }\n .ml-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }\n /* 卡片 · 视觉对齐 model-photo .model-card (padding 8 / gap 6 / 无 foot 行) */\n .ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; }\n .ml-card {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 8px;\n cursor: pointer;\n display: flex; flex-direction: column; gap: 6px;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .ml-card:hover { background: var(--surface); }\n .ml-card .placeholder { aspect-ratio: 3/4; border-radius: var(--r-sm); }\n .ml-card .ml-card-nm { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }\n .ml-card .ml-card-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n\n /* 「添加演员/场景」入口卡 · 与 model-photo 模特库视觉一致 */\n .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); }\n .ml-card.ml-upload-card:hover { border-color: var(--heat); background: var(--heat-12); box-shadow: none; }\n .ml-card.ml-upload-card:focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }\n .ml-card.ml-upload-card .up-thumb { aspect-ratio: 3/4; border-radius: var(--r-sm); background: transparent; display: grid; place-items: center; }\n .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); }\n .ml-card.ml-upload-card:hover .up-plus { background: var(--heat); border-color: var(--heat); color: var(--accent-white); transform: scale(1.06); }\n .ml-card.ml-upload-card .up-plus svg { width: 22px; height: 22px; }\n .ml-card.ml-upload-card .ml-card-nm { color: var(--accent-black); }\n .ml-card.ml-upload-card:hover .ml-card-nm { color: var(--heat); }\n\n /* ════════ 添加演员 / 场景 · 工作台画布 ════════ */\n .ml-canvas {\n position: absolute; inset: 0; z-index: 10;\n background: var(--background-base);\n display: flex; flex-direction: column;\n opacity: 0; visibility: hidden;\n transform: scale(.94); transform-origin: 32px 80px;\n transition: opacity .28s ease, transform .32s cubic-bezier(.18,.72,.28,1), visibility .32s;\n pointer-events: none;\n }\n .ml-canvas.show { opacity: 1; visibility: visible; transform: scale(1); pointer-events: auto; }\n .ml-canvas-h { display: flex; align-items: center; gap: 12px; padding: 14px 28px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; background: var(--surface); }\n .ml-canvas-h .back-btn { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 10px; background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-72); font-family: inherit; font-size: 12px; cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }\n .ml-canvas-h .back-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }\n .ml-canvas-h .back-btn svg { width: 12px; height: 12px; }\n .ml-canvas-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }\n .ml-canvas-h .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n\n .ml-canvas-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 2fr 1fr; overflow: hidden; }\n .mc-ai { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--background-base); }\n .mc-up { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--surface); border-left: 1px solid var(--border-faint); }\n\n .mc-stream { flex: 1; min-height: 0; overflow-y: auto; padding: 28px 28px 220px; background: var(--background-base); }\n .mc-stream-inner { width: 100%; margin: 0 auto; display: flex; flex-direction: column; gap: 28px; }\n .mc-empty { flex: 1; min-height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 40px; color: var(--black-alpha-56); text-align: center; }\n .mc-empty .badge { font-family: var(--font-mono); font-size: 11px; letter-spacing: .08em; color: var(--black-alpha-48); text-transform: uppercase; }\n .mc-empty h2 { font-size: 22px; font-weight: 600; color: var(--accent-black); letter-spacing: -.015em; margin: 0; }\n .mc-empty p { font-size: 13px; max-width: 460px; line-height: 1.6; margin: 0; }\n .mc-empty .ic { width: 64px; height: 64px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--heat); }\n .mc-empty .ic svg { width: 28px; height: 28px; }\n .mc-empty .examples { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; max-width: 720px; }\n .mc-empty .examples .ex { padding: 6px 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-pill); font-size: 12px; color: var(--black-alpha-72); cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }\n .mc-empty .examples .ex:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }\n\n .mc-msg { display: flex; flex-direction: column; gap: 14px; }\n .mc-msg-prompt { display: flex; align-items: flex-start; gap: 12px; }\n .mc-msg-prompt .quote { flex-shrink: 0; width: 28px; height: 28px; border-radius: var(--r-sm); background: var(--surface); border: 1px solid var(--border-faint); color: var(--heat); display: grid; place-items: center; }\n .mc-msg-prompt .quote svg { width: 13px; height: 13px; }\n .mc-msg-prompt .pt { flex: 1; min-width: 0; padding-top: 4px; }\n .mc-msg-prompt .pt-text { font-size: 14px; color: var(--accent-black); line-height: 1.55; word-break: break-word; }\n .mc-msg-prompt .pt-tags { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; align-items: center; }\n .mc-msg-prompt .pt-tags .meta-chip { padding: 2px 8px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }\n .mc-msg-prompt .pt-tags .sep { color: var(--black-alpha-24); }\n .mc-msg-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }\n @media (max-width: 1280px) { .mc-msg-grid { grid-template-columns: repeat(3, 1fr); } }\n .mc-cell { position: relative; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; aspect-ratio: 3/4; cursor: pointer; }\n .mc-cell.selected { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-12); }\n .mc-cell .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32); letter-spacing: .02em; background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); }\n .mc-cell.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }\n @keyframes mc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }\n .mc-cell:hover { border-color: var(--black-alpha-32); }\n .mc-cell .pick-badge { position: absolute; top: 6px; left: 6px; background: var(--heat); color: var(--accent-white); padding: 2px 7px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; display: none; }\n .mc-cell.selected .pick-badge { display: block; }\n .mc-cell .cell-ops { position: absolute; top: 6px; right: 6px; display: flex; gap: 4px; opacity: 0; transition: opacity var(--t-base); z-index: 2; }\n .mc-cell:hover .cell-ops { opacity: 1; }\n .mc-cell .cell-ops button { width: 26px; height: 26px; background: rgba(255,255,255,.92); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--accent-black); cursor: pointer; display: grid; place-items: center; backdrop-filter: blur(4px); transition: border-color var(--t-base), color var(--t-base); }\n .mc-cell .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }\n .mc-cell .cell-ops button svg { width: 12px; height: 12px; }\n\n .mc-msg-ops { display: flex; gap: 8px; }\n .mc-msg-ops button { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 12.5px; color: var(--accent-black); font-family: inherit; cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }\n .mc-msg-ops button:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }\n .mc-msg-ops button svg { width: 13px; height: 13px; }\n\n .mc-input-wrap { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 28px 22px; background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px); z-index: 5; }\n .mc-input { max-width: 720px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 18px; padding: 12px 14px 10px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 6px 24px rgba(0,0,0,.06); transition: border-color var(--t-base); }\n .mc-input:focus-within { border-color: var(--heat-40); }\n .mc-input-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n .mc-input-top .add-btn { flex-shrink: 0; width: 64px; height: 64px; background: var(--background-lighter); border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: grid; place-items: center; color: var(--black-alpha-56); cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }\n .mc-input-top .add-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }\n .mc-input-top .add-btn svg { width: 22px; height: 22px; }\n .mc-input-refs { display: contents; }\n .mc-input-ref { position: relative; width: 64px; height: 64px; border-radius: var(--r-md); overflow: hidden; background: var(--background-lighter); border: 1px solid var(--border-faint); flex-shrink: 0; }\n .mc-input-ref img { width: 100%; height: 100%; object-fit: cover; }\n .mc-input-ref .x { position: absolute; top: 3px; right: 3px; width: 18px; height: 18px; background: rgba(0,0,0,.7); color: var(--accent-white); border: 0; border-radius: 50%; display: grid; place-items: center; cursor: pointer; }\n .mc-input-ref .x svg { width: 10px; height: 10px; }\n .mc-input textarea#mc-input-text { width: 100%; border: 0; outline: 0; resize: none; background: transparent; font-family: inherit; font-size: 14px; line-height: 1.5; color: var(--accent-black); min-height: 44px; max-height: 220px; padding: 4px 2px; }\n .mc-input textarea#mc-input-text::placeholder { color: var(--black-alpha-48); }\n .mc-input-bottom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n .mc-input-bottom .param { position: relative; display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 9px; background: var(--background-lighter); border: 1px solid transparent; border-radius: var(--r-pill); font-size: 11.5px; color: var(--black-alpha-72); cursor: pointer; transition: border-color var(--t-base), color var(--t-base), background var(--t-base); }\n .mc-input-bottom .param:hover { background: var(--surface); border-color: var(--border-faint); }\n .mc-input-bottom .param .lbl-mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-right: 1px; }\n .mc-input-bottom .param svg { width: 10px; height: 10px; opacity: .6; }\n .mc-input-bottom .right-meta { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .mc-input-bottom .right-meta .val { color: var(--accent-black); }\n .mc-input .send-btn { flex-shrink: 0; width: 32px; height: 32px; background: var(--heat); color: var(--accent-white); border: 0; border-radius: var(--r-md); cursor: pointer; display: grid; place-items: center; transition: opacity var(--t-base), filter var(--t-base); margin-left: 8px; }\n .mc-input .send-btn:hover { filter: brightness(1.05); }\n .mc-input .send-btn:disabled { opacity: .4; cursor: not-allowed; }\n .mc-input .send-btn svg { width: 15px; height: 15px; }\n\n .mc-up-tabs { display: flex; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; background: var(--surface); }\n .mc-up-tab { flex: 1; height: 44px; background: transparent; border: 0; border-bottom: 2px solid transparent; font-family: inherit; font-size: 13px; font-weight: 500; color: var(--black-alpha-56); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }\n .mc-up-tab:hover { color: var(--accent-black); background: var(--background-lighter); }\n .mc-up-tab.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; background: var(--surface); }\n .mc-up-body { flex: 1; min-height: 0; padding: 18px 20px 14px; display: flex; flex-direction: column; gap: 18px; overflow-y: auto; }\n .mc-up-section { display: flex; flex-direction: column; gap: 8px; }\n .mc-up-sec-h { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n .mc-up-name { width: 100%; height: 36px; padding: 0 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-family: inherit; font-size: 13.5px; color: var(--accent-black); outline: none; transition: border-color var(--t-base), background var(--t-base); }\n .mc-up-name:focus { border-color: var(--heat-40); background: var(--surface); }\n .mc-up-name::placeholder { color: var(--black-alpha-40); }\n\n .mc-portrait-ai .empty { aspect-ratio: 3/4; max-height: 220px; border: 1.5px dashed var(--black-alpha-24); border-radius: var(--r-md); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; background: var(--background-lighter); text-align: center; padding: 14px; }\n .mc-portrait-ai .empty[hidden] { display: none; }\n .mc-portrait-ai .picked[hidden] { display: none; }\n .mc-portrait-ai .empty .ic { width: 38px; height: 38px; border-radius: 50%; background: var(--surface); border: 1px solid var(--border-faint); display: grid; place-items: center; color: var(--black-alpha-48); }\n .mc-portrait-ai .empty .ic svg { width: 16px; height: 16px; }\n .mc-portrait-ai .empty .desc { font-size: 12.5px; color: var(--black-alpha-72); line-height: 1.55; }\n .mc-portrait-ai .empty .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .mc-portrait-ai .picked { position: relative; aspect-ratio: 3/4; max-height: 280px; background: var(--background-lighter); border: 1.5px solid var(--heat); border-radius: var(--r-md); overflow: hidden; }\n .mc-portrait-ai .picked .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32); letter-spacing: .02em; background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); }\n .mc-portrait-ai .picked .ops { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; }\n .mc-portrait-ai .picked .ops button { width: 26px; height: 26px; background: rgba(255,255,255,.92); border: 1px solid var(--border-faint); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--accent-black); cursor: pointer; transition: border-color var(--t-base), color var(--t-base); }\n .mc-portrait-ai .picked .ops button:hover { border-color: var(--heat); color: var(--heat); }\n .mc-portrait-ai .picked .ops button svg { width: 12px; height: 12px; }\n .mc-portrait-ai .picked .badge { position: absolute; top: 8px; left: 8px; background: var(--heat); color: var(--accent-white); padding: 2px 7px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; }\n\n .mc-portrait-local .drop { border: 1.5px dashed var(--black-alpha-24); border-radius: var(--r-md); padding: 20px 14px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; background: var(--background-lighter); transition: border-color var(--t-base), background var(--t-base); text-align: center; }\n .mc-portrait-local .drop:hover, .mc-portrait-local .drop.dragover { border-color: var(--heat); background: var(--heat-12); }\n .mc-portrait-local .drop .ic { width: 32px; height: 32px; background: var(--heat); color: var(--accent-white); border-radius: 50%; display: grid; place-items: center; }\n .mc-portrait-local .drop .ic svg { width: 14px; height: 14px; }\n .mc-portrait-local .drop .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }\n .mc-portrait-local .drop .d { font-size: 11px; color: var(--black-alpha-48); }\n .mc-portrait-local .list-h { display: flex; align-items: center; gap: 4px; margin-top: 6px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .mc-portrait-local .list-h .ct { color: var(--accent-black); font-weight: 600; }\n .mc-portrait-local .list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }\n .mc-portrait-local .list:empty { display: none; }\n .mc-portrait-local .thumb { position: relative; aspect-ratio: 3/4; border-radius: var(--r-sm); overflow: hidden; background: var(--background-lighter); border: 1px solid var(--border-faint); }\n .mc-portrait-local .thumb img { width: 100%; height: 100%; object-fit: cover; }\n .mc-portrait-local .thumb .x { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; background: rgba(0,0,0,.7); color: var(--accent-white); border: 0; border-radius: 50%; display: grid; place-items: center; cursor: pointer; }\n .mc-portrait-local .thumb .x svg { width: 10px; height: 10px; }\n\n .mc-up[data-kind=\"scene\"] .mc-triview { display: none; }\n .mc-triview .result-wrap { display: flex; flex-direction: column; gap: 8px; }\n .mc-triview .result { position: relative; aspect-ratio: 16/9; background: var(--background-lighter); border: 1.5px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; transition: border-color var(--t-base); }\n .mc-triview.has-result .result { border-color: var(--heat); }\n .mc-triview .result .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32); letter-spacing: .02em; background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); pointer-events: none; }\n .mc-triview .result.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }\n .mc-triview .overlay-gen-btn { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2; display: inline-flex; align-items: center; gap: 6px; height: 36px; padding: 0 18px; background: var(--heat); color: var(--accent-white); border: 0; border-radius: var(--r-pill); font-family: inherit; font-size: 13px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(250, 93, 25, .28); transition: filter var(--t-base), opacity var(--t-base), transform var(--t-base), box-shadow var(--t-base); }\n .mc-triview .overlay-gen-btn:hover:not(:disabled) { filter: brightness(1.06); transform: translate(-50%, -50%) scale(1.03); box-shadow: 0 6px 16px rgba(250, 93, 25, .36); }\n .mc-triview .overlay-gen-btn:disabled { background: var(--black-alpha-24); color: var(--surface); cursor: not-allowed; box-shadow: none; }\n .mc-triview .overlay-gen-btn svg { width: 13px; height: 13px; }\n .mc-triview .overlay-hint { position: absolute; left: 0; right: 0; bottom: 10px; text-align: center; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; pointer-events: none; z-index: 2; }\n .mc-triview .result-ops { display: flex; gap: 6px; align-items: center; }\n .mc-triview .result-ops .cost { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .mc-triview .result-ops button { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-family: inherit; font-size: 11.5px; color: var(--accent-black); cursor: pointer; transition: border-color var(--t-base), color var(--t-base); }\n .mc-triview .result-ops button:hover { border-color: var(--heat); color: var(--heat); }\n .mc-triview .result-ops button svg { width: 11px; height: 11px; }\n .mc-triview .history { display: flex; flex-direction: column; gap: 6px; }\n .mc-triview .history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n .mc-triview .history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }\n .mc-triview .history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }\n .mc-triview .history .h-thumb { flex: 0 0 auto; width: 72px; aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); position: relative; cursor: pointer; display: grid; place-items: center; overflow: hidden; transition: border-color var(--t-base); }\n .mc-triview .history .h-thumb:hover { border-color: var(--heat-40); }\n .mc-triview .history .h-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }\n .mc-triview .history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .mc-triview .history .h-thumb.active .v { color: var(--heat); font-weight: 600; }\n .mc-triview .history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }\n .mc-triview .history .h-thumb.active .badge { display: block; }\n\n .mc-up-foot { padding: 12px 20px 16px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 10px; flex-shrink: 0; flex-wrap: wrap; }\n .mc-up-foot .stat { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .mc-up-foot .stat b { color: var(--accent-black); font-weight: 600; }\n .mc-up-foot .stat.ok { color: var(--heat); }\n .mc-up-foot .commit-btn { display: inline-flex; align-items: center; gap: 5px; height: 32px; padding: 0 14px; background: var(--heat); color: var(--accent-white); border: 0; border-radius: var(--r-sm); font-family: inherit; font-size: 12.5px; cursor: pointer; transition: filter var(--t-base), opacity var(--t-base); }\n .mc-up-foot .commit-btn:hover { filter: brightness(1.05); }\n .mc-up-foot .commit-btn:disabled { opacity: .4; cursor: not-allowed; filter: none; }\n .mc-up-foot .commit-btn svg { width: 12px; height: 12px; }\n\n /* 离开工作台 · 二次确认 */\n .mc-leave-bg { position: fixed; inset: 0; background: rgba(21,20,15,.42); backdrop-filter: blur(8px); z-index: 1300; display: none; align-items: center; justify-content: center; padding: 40px; }\n .mc-leave-bg.show { display: flex; }\n .mc-leave { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: 420px; max-width: 100%; box-shadow: 0 16px 48px rgba(0,0,0,.18); overflow: hidden; }\n .mc-leave .lv-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px 10px; }\n .mc-leave .lv-h .ic { width: 28px; height: 28px; display: grid; place-items: center; border-radius: var(--r-sm); background: var(--crimson-bg); color: var(--accent-crimson); flex-shrink: 0; }\n .mc-leave .lv-h .ic svg { width: 16px; height: 16px; }\n .mc-leave .lv-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }\n .mc-leave .lv-h .mono { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .mc-leave .lv-b { padding: 4px 20px 18px; font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); }\n .mc-leave .lv-b b { color: var(--accent-black); font-weight: 600; }\n .mc-leave .lv-f { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--border-faint); background: var(--background-lighter); }\n .mc-leave .lv-f .spacer { flex: 1; }\n .mc-leave .lv-f .btn { height: 34px; padding: 0 14px; font-size: 13px; }\n .mc-leave .btn-danger { background: var(--accent-crimson); color: var(--accent-white); border-color: var(--accent-crimson); font-weight: 600; }\n .mc-leave .btn-danger:hover { background: var(--accent-crimson); border-color: var(--accent-crimson); filter: brightness(.95); }\n\n /* ─── 添加来源 · 选择 modal (AI 生成 / 本地上传) ─── */\n .ml-up-choice-bg { position: fixed; inset: 0; z-index: 1200; background: rgba(21, 20, 15, .42); display: none; place-items: center; padding: 16px; }\n .ml-up-choice-bg.show { display: grid; }\n .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; }\n .ml-up-choice .uc-h { display: flex; align-items: center; gap: 12px; padding: 18px 22px 14px; border-bottom: 1px solid var(--border-faint); }\n .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; }\n .ml-up-choice .uc-h .ic-m svg { width: 18px; height: 18px; }\n .ml-up-choice .uc-h .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }\n .ml-up-choice .uc-h .ti strong { font-size: 15px; color: var(--accent-black); font-weight: 600; }\n .ml-up-choice .uc-h .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .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; }\n .ml-up-choice .uc-h .uc-x:hover { background: var(--background-lighter); color: var(--accent-black); }\n .ml-up-choice .uc-h .uc-x svg { width: 14px; height: 14px; }\n .ml-up-choice .uc-body { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 20px 22px 22px; }\n .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); }\n .ml-up-choice .uc-option:hover { border-color: var(--heat); background: var(--heat-12); }\n .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); }\n .ml-up-choice .uc-option:hover .opt-ic { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }\n .ml-up-choice .uc-option .opt-ic svg { width: 18px; height: 18px; }\n .ml-up-choice .uc-option .opt-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }\n .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; }\n .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; }\n .ml-up-choice .uc-option:hover .opt-tag { background: var(--surface); color: var(--heat); }\n\n /* 新增人物 modal · 立绘 + 三视图 上传区 */\n .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; }\n .upload-zone:hover { border-color: var(--heat); background: var(--heat-12); color: var(--heat); }\n .upload-zone.lead { aspect-ratio: 3/4; }\n .upload-zone svg { width: 20px; height: 20px; }\n .upload-zone-tri { aspect-ratio: 1; padding: 8px; font-size: 10.5px; }\n .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); }\n .prompt-box[contenteditable=\"true\"] { cursor: text; outline: none; }\n .prompt-box[contenteditable=\"true\"]:hover { border-color: var(--heat-20); }\n .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); }\n .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%; }\n\n /* === STAGE 3 · 故事板(略缩图竖向侧栏 + 主图区)=== */\n .stage-storyboard { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 16px; align-items: stretch; }\n .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; }\n .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; }\n .sb-scenes-col::-webkit-scrollbar { width: 6px; }\n .sb-scenes-col::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }\n .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); }\n .sb-scene-thumb:hover { background: var(--background-lighter); }\n .sb-scene-thumb.selected { border-color: var(--heat); background: var(--heat-12); }\n .sb-scene-thumb .placeholder { aspect-ratio: 1; }\n .sb-scene-thumb .nm { font-size: 11.5px; font-weight: 500; color: var(--accent-black); }\n .sb-scene-thumb .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }\n .sb-main-img { aspect-ratio: 16/9; min-height: 0; }\n\n .sb-rerun-note {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n padding: 10px 12px;\n margin-bottom: 14px;\n background: rgba(180,83,9,.08);\n border: 1px solid rgba(180,83,9,.20);\n border-radius: var(--r-md);\n color: #7C3A05;\n line-height: 1.55;\n }\n .sb-rerun-note .warn-ic {\n width: 22px;\n height: 22px;\n border-radius: var(--r-sm);\n background: rgba(180,83,9,.12);\n color: #B45309;\n display: grid;\n place-items: center;\n flex: 0 0 22px;\n }\n .sb-rerun-note .warn-ic svg {\n width: 14px;\n height: 14px;\n }\n .sb-rerun-note .note-copy {\n min-width: 0;\n font-size: 11.5px;\n }\n .sb-rerun-note strong { color: #B45309; }\n .sb-rerun-note a { color: #B45309; text-decoration: underline; text-underline-offset: 2px; }\n\n .sb-stage-actions { display: flex; gap: 8px; margin-top: 14px; margin-bottom: 12px; }\n\n /* 故事板历史版本 */\n .sb-history { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border-faint); }\n .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; }\n .sb-history-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }\n .sb-history-row::-webkit-scrollbar { height: 6px; }\n .sb-history-row::-webkit-scrollbar-thumb { background: var(--border-faint); }\n .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); }\n .sb-history-thumb:hover { border-color: var(--heat); }\n .sb-history-thumb.current { border-color: var(--heat); background: var(--heat-12); }\n .sb-history-thumb .placeholder { aspect-ratio: 1; }\n .sb-history-thumb .ts { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); text-align: center; }\n .sb-history-thumb.current .ts { color: var(--heat); font-weight: 600; }\n\n .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); }\n .pill-cta.heat { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }\n .pill-cta.heat:hover { box-shadow: var(--shadow-cta-hover); }\n .pill-cta.ghost { background: var(--surface); color: var(--accent-black); border: 1px solid var(--border-faint); }\n .pill-cta.ghost:hover { background: var(--background-lighter); border-color: var(--heat-20); color: var(--heat); }\n .pill-cta svg { width: 13px; height: 13px; }\n\n /* === STAGE 3 / 4 跳过条 === */\n .skip-row { display: flex; justify-content: flex-end; margin-bottom: 12px; }\n\n .sb-side .pane { padding: 18px; }\n .prompt-edit { background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 12px 14px; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.7; color: var(--accent-black); white-space: pre-wrap; min-height: 200px; outline: none; letter-spacing: .01em; cursor: text; transition: border-color var(--t-base), background var(--t-base), box-shadow var(--t-base); }\n .prompt-edit:hover { border-color: var(--heat-20); }\n .prompt-edit:focus { border-color: var(--heat); background: var(--surface); box-shadow: 0 0 0 3px var(--heat-12); }\n .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; }\n .asset-tag .dotc { width: 14px; height: 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 50%; }\n\n /* === STAGE 4 · 视频片段 === */\n .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; }\n .queue-bar .bar-wrap { flex: 1; height: 6px; background: var(--background-lighter); overflow: hidden; }\n .queue-bar .bar-wrap > span { display: block; height: 100%; background: var(--heat); }\n\n .video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(196px, 216px)); gap: 14px; align-content: start; align-items: start; justify-content: start; }\n .video-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base), background var(--t-base); overflow: hidden; display: flex; flex-direction: column; min-height: 0; }\n .video-card:hover { border-color: var(--heat-40); background: var(--background-lighter); }\n .video-thumb { width: 100%; aspect-ratio: 9/16; position: relative; border-radius: var(--r-md) var(--r-md) 0 0; overflow: hidden; }\n .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; }\n .video-thumb:hover .play { opacity: 1; }\n .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; }\n .video-card .body { padding: 12px 12px 14px; flex: 1 1 auto; min-height: 118px; display: flex; flex-direction: column; }\n .video-card-head { display: flex; align-items: flex-start; gap: 8px; }\n .video-card-title { min-width: 0; flex: 1 1 auto; font-size: 13px; line-height: 1.4; font-weight: 600; color: var(--accent-black); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .video-card-head .pill { flex: 0 0 auto; }\n .video-meta { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 5px; }\n .video-actions { margin-top: auto; padding-top: 12px; display: flex; align-items: center; gap: 10px; }\n\n /* 视频详情 modal 大视频 + 历史版本 */\n .vd-main-wrap { display: flex; gap: 18px; align-items: flex-start; }\n .vd-main { flex: 0 0 280px; aspect-ratio: 9/16; max-height: 460px; }\n .vd-main .placeholder { aspect-ratio: 9/16; height: 100%; }\n .vd-info { flex: 1; min-width: 0; }\n .vd-prompt-field { margin-top: 16px; }\n .vd-prompt-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px; }\n .vd-prompt-head .label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--black-alpha-56); letter-spacing: .04em; }\n .vd-prompt-head .hint { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .vd-prompt-edit { min-height: 120px; }\n .vd-prompt-edit:empty::before { content: attr(data-placeholder); color: var(--black-alpha-40); }\n .vd-history { margin-top: 16px; }\n .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; }\n .vd-history-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }\n .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; }\n .vd-history-thumb:hover { border-color: var(--heat); }\n .vd-history-thumb.current { border-color: var(--heat); background: var(--heat-12); }\n .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; }\n .vd-history-thumb .placeholder { aspect-ratio: 9/16; }\n .vd-history-thumb .ts { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); text-align: center; }\n .vd-modal-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }\n .vd-modal-actions .pill-cta { min-width: 96px; justify-content: center; }\n\n /* === STAGE 5 · 编辑器 === */\n .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); }\n .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; }\n .editor-preview .canvas { flex: 1 1 0; min-height: 0; aspect-ratio: 9/16; margin: 0 auto; background:\n repeating-linear-gradient(135deg, rgba(0,0,0,0.03) 0 1px, transparent 1px 12px),\n var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 12px; }\n .editor-preview .controls { display: flex; align-items: center; gap: 8px; justify-content: center; }\n .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); }\n .ctl-btn:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }\n\n .editor-props { padding: 16px; border-bottom: 1px solid var(--border-faint); overflow-y: auto; }\n .props-tabs { display: flex; gap: 0; margin-bottom: 14px; border-bottom: 1px solid var(--border-faint); }\n .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; }\n .props-tabs > div.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; }\n .style-swatch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n .swatch-card { padding: 10px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; }\n .swatch-card:hover { background: var(--background-lighter); }\n .swatch-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .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; }\n .swatch-card .demo.b { background: var(--accent-black); color: var(--accent-white); font-family: serif; }\n .swatch-card .demo.c { color: var(--heat); -webkit-text-stroke: 0.5px var(--accent-black); }\n .swatch-card .demo.d { background: var(--accent-honey); color: var(--accent-black); font-weight: 700; }\n .swatch-card .nm { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }\n .props-row { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; }\n .props-row:last-child { border-bottom: 0; }\n .props-row .k { color: var(--black-alpha-48); flex: 1; font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em; }\n .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); }\n\n /* ── 时间轴 · 剪映风格(Restraint 浅色规范) ── */\n .timeline { position: relative; grid-column: 1 / -1; padding: 14px 16px; background: var(--background-base); }\n\n /* 工具栏 */\n .tl-toolbar { display: flex; align-items: center; gap: 4px; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--border-faint); }\n .tl-toolbar .tl-action { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 10px; background: transparent; border: 1px solid transparent; border-radius: var(--r-sm); color: var(--black-alpha-72); font-size: 12px; font-family: inherit; cursor: pointer; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }\n .tl-toolbar .tl-action:hover { background: var(--surface); border-color: var(--border-faint); color: var(--accent-black); }\n .tl-toolbar .tl-action.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }\n .tl-toolbar .tl-action svg { width: 13px; height: 13px; }\n .tl-toolbar .tl-sep { width: 1px; height: 16px; background: var(--border-faint); margin: 0 4px; }\n .tl-toolbar .tl-zoom { display: inline-flex; align-items: center; gap: 8px; }\n .tl-toolbar .tl-zoom .lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }\n .tl-toolbar .tl-zoom input[type=\"range\"] { width: 120px; accent-color: var(--heat); }\n\n /* 时间刻度 · 主/次刻度 */\n .tl-ruler { display: grid; grid-template-columns: 80px 1fr; align-items: end; padding: 0; margin-bottom: 4px; }\n .tl-ruler .l { font-family: var(--font-mono); color: var(--black-alpha-48); padding: 0 4px 4px; font-size: 10.5px; letter-spacing: .04em; align-self: end; }\n .tl-ruler .rule-track { position: relative; height: 22px; border-bottom: 1px solid var(--border-faint); }\n .tl-ruler .rule-track .tick { position: absolute; bottom: 0; width: 1px; background: var(--black-alpha-24); }\n .tl-ruler .rule-track .tick.major { height: 8px; background: var(--black-alpha-48); }\n .tl-ruler .rule-track .tick.minor { height: 4px; }\n .tl-ruler .rule-track .t { position: absolute; bottom: 10px; transform: translateX(-50%); font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; white-space: nowrap; }\n\n /* 轨道行 */\n .tl-track { display: grid; grid-template-columns: 80px 1fr; align-items: center; padding: 3px 0; }\n .tl-track .label { display: flex; align-items: center; gap: 6px; padding-left: 4px; font-size: 11.5px; color: var(--black-alpha-72); font-weight: 500; }\n .tl-track .label .ico { width: 18px; height: 18px; display: grid; place-items: center; border-radius: var(--r-sm); flex-shrink: 0; }\n .tl-track .label .ico svg { width: 12px; height: 12px; }\n .tl-track .label.video .ico { background: var(--heat-12); color: var(--heat); }\n .tl-track .label.subtitle .ico { background: var(--forest-bg); color: var(--accent-forest); }\n .tl-track .label.bgm .ico { background: rgba(144, 97, 255, .10); color: var(--accent-amethyst); }\n\n /* 轨道 lane · 绝对定位容器 + 1s 网格线 */\n .tl-track .lane {\n position: relative;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n }\n .tl-track.video-track .lane { height: 46px; }\n .tl-track.subtitle-track .lane { height: 28px; }\n .tl-track.bgm-track .lane { height: 34px; }\n .tl-track .lane::before {\n content: \"\"; position: absolute; inset: 0;\n background-image: repeating-linear-gradient(to right,\n var(--border-faint) 0, var(--border-faint) 1px,\n transparent 1px, transparent calc(100% / 15));\n pointer-events: none; opacity: .55;\n border-radius: inherit;\n }\n .tl-track .lane.is-snapping .clip,\n .tl-track .lane.is-reordering .clip:not(.dragging) { transition: left .16s ease, width .16s ease; }\n .tl-align-guide {\n position: absolute;\n width: 1.5px;\n background: var(--accent-forest);\n opacity: 0;\n pointer-events: none;\n z-index: 9;\n }\n .tl-align-guide.show { opacity: 1; }\n .tl-insert-ghost {\n position: absolute; top: 3px; bottom: 3px;\n border: 1px dashed var(--heat);\n background: var(--heat-12);\n border-radius: 4px;\n pointer-events: none;\n box-sizing: border-box;\n z-index: 2;\n }\n\n /* 片段公共 · 绝对定位 · left/width 由 data 驱动 */\n .clip {\n position: absolute; top: 3px; bottom: 3px;\n display: flex; align-items: center; gap: 6px;\n padding: 0 8px; font-size: 11px;\n border: 1px solid transparent;\n border-radius: 4px;\n cursor: grab; overflow: hidden; white-space: nowrap; user-select: none;\n box-sizing: border-box;\n }\n .clip:hover { filter: brightness(1.04); }\n .clip .num { font-family: var(--font-mono); font-weight: 700; opacity: .85; flex-shrink: 0; }\n .clip .lbl { overflow: hidden; text-overflow: ellipsis; }\n\n /* 视频片段 · 内嵌胶卷帧条(预览缩略) */\n .clip.video {\n background: var(--heat-12); border-color: var(--heat-40); color: var(--heat);\n }\n .clip.video .frames {\n position: absolute; top: 0; bottom: 0; left: 0;\n width: var(--src-width, 100%);\n transform: translateX(var(--src-offset, 0%));\n display: flex; gap: 0;\n pointer-events: none; z-index: 0;\n border-radius: inherit;\n overflow: hidden;\n }\n .clip.video .frames .fr {\n flex: 1; min-width: 0;\n background:\n repeating-linear-gradient(45deg,\n transparent 0, transparent 4px,\n rgba(38,38,38,.06) 4px, rgba(38,38,38,.06) 5px),\n rgba(250,93,25,.10);\n }\n .clip.video .frames .fr + .fr { border-left: 1px solid rgba(255,255,255,.55); }\n .clip.video:hover .frames .fr { background-color: rgba(250,93,25,.18); }\n .clip.video .num, .clip.video .lbl { position: relative; z-index: 1; }\n .clip.video.selected {\n background: var(--heat); color: var(--accent-white); border-color: var(--heat);\n box-shadow: var(--shadow-cta);\n z-index: 3;\n }\n .clip.video.selected .frames .fr {\n background:\n repeating-linear-gradient(45deg,\n transparent 0, transparent 4px,\n rgba(255,255,255,.22) 4px, rgba(255,255,255,.22) 5px),\n rgba(255,255,255,.06);\n }\n .clip.video.selected .frames .fr + .fr { border-left-color: rgba(255,255,255,.28); }\n\n /* 字幕片段 · 薄条 + 引号符号 */\n .clip.subtitle {\n background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest);\n font-size: 11px;\n }\n .clip.subtitle .lbl::before {\n content: \"“\"; font-family: serif; font-size: 14px; opacity: .55; margin-right: 2px;\n }\n .clip.subtitle:hover { background: rgba(31, 138, 81, .14); }\n .clip.subtitle.selected {\n background: var(--accent-forest); color: var(--accent-white); border-color: var(--accent-forest);\n box-shadow: 0 2px 6px rgba(31, 138, 81, .35);\n z-index: 3;\n }\n .clip.subtitle.selected .lbl::before { opacity: .8; }\n\n /* BGM 片段 · 内嵌波形 */\n .clip.bgm {\n background: rgba(144,97,255,.10); border-color: rgba(144,97,255,.30);\n color: var(--accent-amethyst);\n }\n .clip.bgm .wave {\n position: absolute; inset: 6px 10px; pointer-events: none;\n opacity: .5; display: block; z-index: 0;\n }\n .clip.bgm .wave svg { width: 100%; height: 100%; display: block; }\n .clip.bgm:hover { background: rgba(144,97,255,.16); }\n .clip.bgm.selected {\n background: var(--accent-amethyst); color: var(--accent-white); border-color: var(--accent-amethyst);\n box-shadow: 0 2px 6px rgba(144, 97, 255, .35);\n z-index: 3;\n }\n .clip.bgm.selected .wave { opacity: .75; }\n .clip.bgm.selected .wave svg rect { fill: rgba(255,255,255,.9); }\n .clip.bgm .lbl, .clip.bgm .num { position: relative; z-index: 1; }\n\n /* 通用 trim 把手 · 三轨皆可剪 */\n .clip.selected .trim-l,\n .clip.selected .trim-r {\n position: absolute; top: 3px; bottom: 3px; width: 4px;\n background: rgba(255,255,255,.92); border-radius: 2px;\n z-index: 5;\n cursor: ew-resize; pointer-events: auto;\n }\n .clip.selected .trim-l { left: 2px; }\n .clip.selected .trim-r { right: 2px; }\n .clip.selected .trim-l::before,\n .clip.selected .trim-r::before {\n content: \"\"; position: absolute; top: 50%; left: 50%;\n transform: translate(-50%, -50%);\n width: 2px; height: 10px;\n background: rgba(38,38,38,.4); border-radius: 1px;\n }\n\n /* 拖拽时片段移动光标 */\n .clip { cursor: grab; }\n .clip.selected { cursor: grab; }\n .clip:active,\n .clip.selected:active { cursor: grabbing; }\n .clip.dragging { cursor: grabbing; opacity: .9; z-index: 7 !important; }\n\n /* Playhead · 顶到时间尺、贯穿三条轨 · 可拖拽 */\n .playhead {\n position: absolute; top: -90px; bottom: -44px;\n width: 18px; transform: translateX(-50%);\n background: transparent;\n z-index: 10;\n pointer-events: auto;\n cursor: ew-resize;\n touch-action: none;\n }\n .playhead::after {\n content: ''; position: absolute; top: 0; bottom: 0; left: 50%;\n transform: translateX(-50%);\n width: 1.5px; background: var(--heat);\n pointer-events: none;\n }\n .playhead::before {\n content: ''; position: absolute; top: -4px; left: 50%;\n transform: translateX(-50%) rotate(45deg);\n width: 10px; height: 10px; background: var(--heat);\n box-shadow: 0 0 0 1.5px var(--surface);\n border-radius: 1px;\n pointer-events: none;\n }\n .playhead .ph-grab {\n position: absolute; top: -10px; left: 50%; transform: translateX(-50%);\n width: 24px; height: 24px;\n cursor: ew-resize; pointer-events: auto;\n border-radius: 50%;\n }\n .playhead.is-dragging::after { background: var(--heat); }\n .timeline.is-dragging-playhead { cursor: ew-resize; user-select: none; }\n\n /* Ruler 可点击 seek */\n .tl-ruler .rule-track { cursor: pointer; }\n .tl-ruler .rule-track:hover .tick.major { background: var(--heat-40); }\n\n /* Lane 也可点击 seek (空白处) */\n .tl-track .lane { cursor: pointer; }\n\n /* 播放/暂停按钮 active 态 · ctl-btn.is-playing 显示暂停 icon */\n .ctl-btn { transition: color var(--t-base); }\n .ctl-btn.is-playing { color: var(--heat); }\n\n /* 工具栏按钮 disabled */\n .tl-action:disabled { opacity: .4; cursor: not-allowed; }\n .tl-action:disabled:hover { background: transparent; border-color: transparent; color: var(--black-alpha-72); }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<!-- Project header 移除 · 改为 topbar 中部 stage-pill(Shell.render 后注入)-->\n<!-- Stage stepper 移除 · 由 topbar 中部 5 个圆点替代,JS 仍读 .sp-dot[data-stage] 维持单一源 -->\n<div class=\"stage-pill-anchor\" id=\"stage-pill-anchor\" hidden>\n <a class=\"sp-dot\" data-stage=\"1\" href=\"#stage-1\"><span class=\"d\"></span><span class=\"l\">脚本</span></a>\n <span class=\"sp-line\"></span>\n <a class=\"sp-dot\" data-stage=\"2\" href=\"#stage-2\"><span class=\"d\"></span><span class=\"l\">基础资产</span></a>\n <span class=\"sp-line\"></span>\n <a class=\"sp-dot\" data-stage=\"3\" href=\"#stage-3\"><span class=\"d\"></span><span class=\"l\">故事板</span></a>\n <span class=\"sp-line\"></span>\n <a class=\"sp-dot\" data-stage=\"4\" href=\"#stage-4\"><span class=\"d\"></span><span class=\"l\">视频</span></a>\n <span class=\"sp-line\"></span>\n <a class=\"sp-dot\" data-stage=\"5\" href=\"#stage-5\"><span class=\"d\"></span><span class=\"l\">拼接导出</span></a>\n</div>\n\n<!-- ============= STAGE 1 · 脚本 ============= -->\n<section class=\"stage active\" data-stage-pane=\"1\">\n <div class=\"stage-script\">\n <div class=\"pane shot-list\">\n <div class=\"pane-h\">\n <div class=\"shot-headline\">\n <strong>镜头脚本</strong>\n <span class=\"muted-2 mono\" id=\"shots-meta\" style=\"font-size:11px;\">· 空 · 待生成</span>\n </div>\n <div class=\"script-brief-summary\" aria-label=\"当前创作方向\">\n <span class=\"pill neutral script-brief-pill\"><span class=\"k\">来源</span><span class=\"v\" id=\"brief-source\">未选择</span></span>\n <span class=\"pill neutral script-brief-pill\"><span class=\"k\">风格</span><span class=\"v\" id=\"brief-style\">待确认</span></span>\n <span class=\"pill neutral script-brief-pill\"><span class=\"k\">人物</span><span class=\"v\" id=\"brief-persona\">待确认</span></span>\n </div>\n <div class=\"script-tags\" id=\"script-tags\">\n <div class=\"tag-group\" data-kind=\"char\">\n <span class=\"tg-lbl\">// 人物</span>\n <button class=\"tag-add\" type=\"button\" aria-label=\"添加人物\">+</button>\n </div>\n <div class=\"tag-group\" data-kind=\"scene\">\n <span class=\"tg-lbl\">// 场景</span>\n <button class=\"tag-add\" type=\"button\" aria-label=\"添加场景\">+</button>\n </div>\n </div>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" id=\"chat-regen-btn\">↻ 整体重写</button>\n </div>\n <div class=\"shots-body\" id=\"shots-body\">\n <!-- JS 注入空态/镜头卡片 -->\n </div>\n </div>\n\n <div class=\"stage-script-gutter\" id=\"stage-script-gutter\" role=\"separator\" aria-orientation=\"vertical\" aria-label=\"拖动调整脚本助手宽度\"></div>\n\n <div class=\"pane chat-pane\">\n <div class=\"pane-h\">\n <div class=\"ai-avatar\">AI</div>\n <strong>脚本助手</strong>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">· GPT-4o</span>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" id=\"chat-clear-btn\">清空对话</button>\n </div>\n <div class=\"chat-body\" id=\"chat-body\">\n <!-- JS 注入空态/对话内容 -->\n </div>\n <div class=\"chat-input\">\n <div class=\"chat-input-card\">\n <div class=\"chat-attach-row\" id=\"chat-attach-row\" hidden></div>\n <textarea class=\"chat-input-area\" id=\"chat-textarea\" placeholder=\"直接说怎么改,如:更像小红书种草 / 换成熬夜党\" rows=\"2\"></textarea>\n <div class=\"chat-input-foot\">\n <button class=\"chat-icon-btn\" id=\"chat-upload-btn\" title=\"上传脚本附件\" aria-label=\"上传脚本附件\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </button>\n <span class=\"spacer\"></span>\n <button class=\"chat-send-btn\" id=\"chat-send-btn\" title=\"发送\" aria-label=\"发送\">\n <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>\n </button>\n </div>\n </div>\n <input type=\"file\" id=\"chat-upload-input\" hidden accept=\".txt,.md,.docx,.doc,.pdf,.srt,.json\">\n </div>\n </div>\n </div>\n\n <div class=\"stage-foot\">\n <div class=\"info\"><span class=\"mono\">[ LLM 用量 ~2.4k tokens · ¥0.04 · 失败不扣 · 通过后扣 ]</span></div>\n <div class=\"hstack\">\n <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>\n <button class=\"btn btn-primary btn-lg\" onclick=\"location.hash='#stage-2'\">确认脚本,进入下一步 <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button>\n </div>\n </div>\n</section>\n\n<!-- ============= STAGE 2 · 基础资产 ============= -->\n<section class=\"stage\" data-stage-pane=\"2\">\n <div class=\"stage-assets\">\n <div class=\"asset-side\">\n <div class=\"ttab active\" data-jump=\"asset-sec-products\"><span>商品</span><span class=\"num\">3 张</span></div>\n <div class=\"ttab\" data-jump=\"asset-sec-characters\"><span>人物</span><span class=\"num\">2/2</span></div>\n <div class=\"ttab\" data-jump=\"asset-sec-scenes\"><span>场景</span><span class=\"num\">3/3</span></div>\n <div class=\"info\">\n 基础资产是后续故事板的素材。所有卡片同时展示,点左侧分类直接定位。\n <br><br>\n <strong class=\"mono\">// 人物 +¥0.20/张</strong>\n <strong class=\"mono\">// 场景 +¥0.15/张</strong>\n <span style=\"color:var(--black-alpha-48);\">商品图无成本(直接复用商品库)</span>\n </div>\n </div>\n\n <div class=\"asset-main\">\n <!-- ===== 商品(项目内只有 1 个商品,从 URL ?product= 取)===== -->\n <section class=\"asset-sec\" id=\"asset-sec-products\">\n <div class=\"sec-h\">\n <h3>商品 · <span id=\"asset-prod-name\">透真补水面膜</span></h3>\n <span class=\"spacer\"></span>\n </div>\n <div class=\"prod-row\">\n <div class=\"asset-card-2 prod-lib-card\" data-asset-kind=\"product\" data-asset-id=\"prod-main\" id=\"asset-prod-card\">\n <div class=\"placeholder prod-thumb\">\n <span class=\"tri-missing-badge\" id=\"asset-prod-tri-badge\" tabindex=\"0\" role=\"button\" aria-label=\"缺三视图,查看说明\">\n <span class=\"ico\" aria-hidden=\"true\"></span>\n <span class=\"lbl-mono\">缺三视图</span>\n <span class=\"tri-missing-pop\" role=\"tooltip\">\n <span class=\"pop-h\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01\"/><path d=\"M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/></svg>\n MISSING TRI-VIEW\n </span>\n <span class=\"pop-body\">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>\n <span class=\"pop-tip\">建议:点右下 <b>AI 生成三视图</b> 先补齐三视图,再发起后续生成。</span>\n </span>\n </span>\n <span class=\"ph-frame\" id=\"asset-prod-thumb-label\">透真补水面膜 · 主图</span>\n </div>\n <div class=\"prod-body\">\n <div class=\"prod-name\" id=\"asset-prod-card-name\">透真补水面膜</div>\n <div class=\"prod-cat\">美妆个护</div>\n <div class=\"prod-date\">2026-05-15 创建</div>\n </div>\n <div class=\"prod-action\" id=\"asset-prod-action\">\n <button class=\"btn-aigen\" type=\"button\" data-stop id=\"asset-prod-aigen-btn\">\n <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\">\n <path d=\"M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3z\"/>\n <path d=\"M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7L19 14z\"/>\n </svg>\n AI 生成三视图\n </button>\n </div>\n </div>\n <div class=\"prod-preview\" id=\"asset-prod-preview\">\n <div class=\"prod-preview-h\">// 三视图预览 · <span id=\"prod-preview-status\">生成中</span></div>\n <div class=\"placeholder prod-preview-img\" id=\"prod-preview-img\"></div>\n <div class=\"prod-preview-foot\" id=\"prod-preview-foot\"></div>\n <div class=\"prod-preview-history\" id=\"prod-preview-history\">\n <div class=\"h-lbl\">// 历史版本 · <span class=\"ct\" id=\"prod-preview-history-count\">0</span> 版</div>\n <div class=\"h-row\" id=\"prod-preview-history-row\"></div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- ===== 人物 ===== -->\n <section class=\"asset-sec\" id=\"asset-sec-characters\">\n <div class=\"sec-h\">\n <h3>人物 · 2 个</h3>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-sm\" id=\"asset-add-character\" hidden>+ 新增人物</button>\n </div>\n\n <div class=\"asset-grid-2\">\n <div class=\"asset-card-2\" data-asset-kind=\"character\" data-asset-id=\"ch-linxi\">\n <div class=\"placeholder thumb-2\"><span class=\"ph-frame\">林夕 · 都市白领</span></div>\n <div class=\"body-2\">\n <div class=\"hstack\"><strong style=\"font-size:13.5px;\">主角 · 林夕</strong><span class=\"spacer\"></span></div>\n <div class=\"prompt-box\" contenteditable=\"true\" spellcheck=\"false\" data-stop>25-30 岁都市白领,长发,穿宽松米色家居服,温柔但带点疲倦感。</div>\n <div class=\"hstack\" style=\"margin-top:10px;\">\n <button class=\"btn btn-ghost btn-sm\" data-stop data-rerun>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" data-stop data-replace>替换</button>\n </div>\n </div>\n </div>\n <div class=\"asset-card-2\" data-asset-kind=\"character\" data-asset-id=\"ch-anan\">\n <div class=\"placeholder thumb-2\">\n <div style=\"display:flex; flex-direction:column; gap:8px; align-items:center;\">\n <div class=\"spinner\"></div>\n <span class=\"ph-frame\">生成中 · 约 8s</span>\n </div>\n </div>\n <div class=\"body-2\">\n <div class=\"hstack\"><strong style=\"font-size:13.5px;\">朋友/同事 · 阿楠</strong><span class=\"spacer\"></span></div>\n <div class=\"prompt-box\" contenteditable=\"true\" spellcheck=\"false\" data-stop>25-30 岁同龄女性,短发,穿白色衬衫,妆容精致皮肤好,作为对比。</div>\n <div class=\"hstack\" style=\"margin-top:10px;\">\n <button class=\"btn btn-ghost btn-sm\" data-stop data-rerun disabled>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" data-stop data-replace disabled>替换</button>\n </div>\n </div>\n </div>\n </div>\n\n </section>\n\n <!-- ===== 场景 ===== -->\n <section class=\"asset-sec\" id=\"asset-sec-scenes\">\n <div class=\"sec-h\">\n <h3>场景 · 3 个</h3>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-sm\" id=\"asset-add-scene\" hidden>+ 新增场景</button>\n </div>\n\n <div class=\"asset-grid-2\">\n <div class=\"asset-card-2\" data-asset-kind=\"scene\" data-asset-id=\"sc-desk\">\n <div class=\"placeholder thumb-2\"><span class=\"ph-frame\">深夜办公桌</span></div>\n <div class=\"body-2\">\n <div class=\"hstack\"><strong style=\"font-size:13.5px;\">深夜办公桌</strong><span class=\"spacer\"></span></div>\n <div class=\"prompt-box\" contenteditable=\"true\" spellcheck=\"false\" data-stop>深夜居家办公环境,木质书桌,台灯暖光,电脑屏幕亮着。</div>\n <div class=\"hstack\" style=\"margin-top:10px;\">\n <button class=\"btn btn-ghost btn-sm\" data-stop data-rerun>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" data-stop data-replace>替换</button>\n </div>\n </div>\n </div>\n <div class=\"asset-card-2\" data-asset-kind=\"scene\" data-asset-id=\"sc-bed\">\n <div class=\"placeholder thumb-2\"><span class=\"ph-frame\">床头特写</span></div>\n <div class=\"body-2\">\n <div class=\"hstack\"><strong style=\"font-size:13.5px;\">卧室床头</strong><span class=\"spacer\"></span></div>\n <div class=\"prompt-box\" contenteditable=\"true\" spellcheck=\"false\" data-stop>米白色床品,木质床头柜,闹钟显示晚间时间。</div>\n <div class=\"hstack\" style=\"margin-top:10px;\">\n <button class=\"btn btn-ghost btn-sm\" data-stop data-rerun>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" data-stop data-replace>替换</button>\n </div>\n </div>\n </div>\n <div class=\"asset-card-2\" data-asset-kind=\"scene\" data-asset-id=\"sc-subway\">\n <div class=\"placeholder thumb-2\">\n <div style=\"display:flex; flex-direction:column; gap:6px; align-items:center;\">\n <div class=\"fail-icon\">!</div>\n <span class=\"ph-frame\">生成失败</span>\n </div>\n </div>\n <div class=\"body-2\">\n <div class=\"hstack\"><strong style=\"font-size:13.5px;\">通勤地铁</strong><span class=\"spacer\"></span><span class=\"pill err\"><span class=\"dot\"></span>失败</span></div>\n <div class=\"prompt-box\" contenteditable=\"true\" spellcheck=\"false\" data-stop>早高峰地铁车厢,光线偏冷,年轻通勤族,氛围紧张。</div>\n <div class=\"hstack\" style=\"margin-top:10px;\">\n <button class=\"btn btn-ghost btn-sm\" data-stop data-rerun>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" data-stop data-replace>替换</button>\n </div>\n </div>\n </div>\n </div>\n\n </section>\n </div>\n </div>\n\n <div class=\"stage-foot\">\n <div class=\"info\"><span class=\"mono\">[ 已确认 ¥0.85 · 待生成 ¥0.20 · 失败 ¥0(不扣) ]</span></div>\n <div class=\"hstack\">\n <button class=\"btn\" onclick=\"location.hash='#stage-1'\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg> 返回脚本</button>\n <button class=\"btn btn-primary btn-lg\" onclick=\"location.hash='#stage-3'\">确认资产,进入故事板 <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button>\n </div>\n </div>\n</section>\n\n<!-- ============= STAGE 3 · 故事板(按场分) ============= -->\n<section class=\"stage\" data-stage-pane=\"3\">\n <div class=\"stage-storyboard\">\n <div class=\"sb-canvas\">\n <div class=\"sb-scenes-col\" id=\"sb-scenes-row\">\n <!-- JS 注入 略缩图 (竖向) -->\n </div>\n <div class=\"placeholder sb-main-img\" id=\"sb-main-img\"><span class=\"ph-frame\">未选择</span></div>\n </div>\n\n <div class=\"sb-side\">\n <div class=\"pane\" style=\"padding:18px;\">\n <div class=\"hstack\" style=\"margin-bottom:10px;\">\n <strong style=\"font-size:14px;\">故事板 · <span id=\"sb-side-scene\">场 1</span></strong>\n <span class=\"spacer\"></span>\n <span class=\"pill ok\"><span class=\"dot\"></span>已生成</span>\n </div>\n <div class=\"muted-2\" style=\"font-size:12px; line-height:1.55; margin-bottom:10px;\">\n 整张故事板由 image-2 一次性输出,包含画面 + 镜头说明。\n </div>\n <div class=\"sb-rerun-note\">\n <span class=\"warn-ic\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\"/><path d=\"M12 9v4M12 17h.01\"/></svg>\n </span>\n <div class=\"note-copy\">\n <strong>仅支持整张重跑</strong> · 不能局部改某一镜。如需调单镜,先在 <a href=\"#stage-1\">Stage 1 脚本</a> 改镜头描述,再回此处整张重跑。\n </div>\n </div>\n\n <div class=\"muted mono\" style=\"font-size:11px; font-weight:500; margin-bottom:6px; letter-spacing:.04em;\">// 本场提示词</div>\n <div class=\"prompt-edit\" contenteditable=\"true\" id=\"sb-prompt-edit\"></div>\n\n <div class=\"sb-stage-actions\">\n <button class=\"pill-cta heat\" id=\"sb-rerun-btn\">\n <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>\n 整张重跑\n </button>\n <span class=\"spacer\"></span>\n <span class=\"muted-2 mono\" style=\"font-size:11px; align-self: center;\">~¥0.45/场</span>\n </div>\n\n <div class=\"sb-history\">\n <div class=\"sb-history-h\">// 历史版本(<span id=\"sb-history-ct\">0</span>)</div>\n <div class=\"sb-history-row\" id=\"sb-history-row\">\n <div style=\"font-size: 11.5px; color: var(--black-alpha-48); padding: 12px 4px;\">// 暂无历史版本</div>\n </div>\n </div>\n\n <div class=\"divider\" style=\"margin-top: 16px;\"></div>\n\n <div class=\"muted mono\" style=\"font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;\">// 绑定的资产</div>\n <div style=\"display:flex; gap:6px; flex-wrap:wrap;\" id=\"sb-bound-assets\">\n <span class=\"asset-tag\"><span class=\"dotc\"></span>林夕(人物)</span>\n <span class=\"asset-tag\"><span class=\"dotc\"></span>深夜办公桌(场景)</span>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"stage-foot\">\n <div class=\"info\"><span class=\"mono\">[ image-2 单场 ¥0.45 · 累计 ¥1.35 · 整张重跑,失败不扣 ]</span></div>\n <div class=\"hstack\">\n <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>\n <button class=\"btn btn-primary btn-lg\" onclick=\"location.hash='#stage-4'\">确认故事板,开始生成视频 <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button>\n </div>\n </div>\n</section>\n\n<!-- ============= STAGE 4 · 视频(按场分,15s/场) ============= -->\n<section class=\"stage\" data-stage-pane=\"4\">\n <div class=\"queue-bar\">\n <div>\n <div style=\"font-size:14px; font-weight:600;\">视频生成 · 3 / 3 完成</div>\n <div class=\"muted-2 mono\" style=\"font-size:11px; margin-top:3px; letter-spacing:.02em;\">// 每场 Seedance 约 <span id=\"seedance-avg\">15</span> 秒 · 已完成所有场次</div>\n </div>\n <div class=\"bar-wrap\"><span style=\"width:100%\"></span></div>\n <span class=\"muted mono\" style=\"font-size:12px;\">100%</span>\n <button class=\"btn btn-sm\" onclick=\"Quota.preflight({stage:'Stage 4 视频片段 · 全部重跑', est: 1.35, force: true, demo:'block'})\">↻ 全部重跑</button>\n <input type=\"file\" id=\"stage4-upload-input\" accept=\"video/*\" multiple hidden>\n <button class=\"btn btn-sm\" type=\"button\" onclick=\"document.getElementById('stage4-upload-input').click()\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"margin-right:4px;\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"M17 8l-5-5-5 5\"/><path d=\"M12 3v12\"/></svg>\n 上传视频\n </button>\n </div>\n\n <div class=\"video-grid\" id=\"video-grid\">\n <div class=\"video-card\" data-video-id=\"v1\" data-duration=\"15\">\n <div class=\"placeholder video-thumb\">\n <span class=\"ph-frame\">场 1 · 0-15s</span>\n <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>\n </div>\n <div class=\"body\">\n <div class=\"video-card-head\"><strong class=\"video-card-title\">场 1 · 深夜办公桌</strong><span class=\"pill ok\"><span class=\"dot\"></span>完成</span></div>\n <div class=\"video-meta\">15s · 1080×1920 · ¥0.45</div>\n <div class=\"video-actions\">\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" data-vstop>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" data-vstop>下载</button>\n </div>\n </div>\n </div>\n <div class=\"video-card\" data-video-id=\"v2\" data-duration=\"12\">\n <div class=\"placeholder video-thumb\">\n <span class=\"ph-frame\">场 2 · 15-27s</span>\n <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>\n </div>\n <div class=\"body\">\n <div class=\"video-card-head\"><strong class=\"video-card-title\">场 2 · 面膜包装/特写</strong><span class=\"pill ok\"><span class=\"dot\"></span>完成</span></div>\n <div class=\"video-meta\">12s · 1080×1920 · ¥0.45</div>\n <div class=\"video-actions\">\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" data-vstop>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" data-vstop>下载</button>\n </div>\n </div>\n </div>\n <div class=\"video-card\" data-video-id=\"v3\" data-duration=\"13\">\n <div class=\"placeholder video-thumb\">\n <span class=\"ph-frame\">场 3 · 27-40s</span>\n <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>\n </div>\n <div class=\"body\">\n <div class=\"video-card-head\"><strong class=\"video-card-title\">场 3 · 化妆台/产品定格</strong><span class=\"pill ok\"><span class=\"dot\"></span>完成</span></div>\n <div class=\"video-meta\">13s · 1080×1920 · ¥0.45</div>\n <div class=\"video-actions\">\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" data-vstop>重跑</button>\n <span class=\"spacer\"></span>\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" data-vstop>下载</button>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"stage-foot\">\n <div class=\"info\"><span class=\"mono\">[ 已完成 3 场 · 累计 ¥1.35 · 总时长 <span id=\"seedance-total\">40</span>s · 失败不扣 · 通过后扣 ]</span></div>\n <div class=\"hstack\">\n <button class=\"btn\" onclick=\"location.hash='#stage-3'\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg> 返回故事板</button>\n <button class=\"btn btn-primary btn-lg\" onclick=\"location.hash='#stage-5'\">确认视频,进入拼接 <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg></button>\n </div>\n </div>\n</section>\n\n<!-- ===== Stage 4 · 视频详情 modal ===== -->\n<div class=\"asset-modal-bg\" id=\"video-detail-modal\">\n <div class=\"asset-modal\" style=\"width: min(880px, 100%);\">\n <div class=\"asset-modal-h\">\n <h2 id=\"vd-title\">视频详情</h2>\n <span style=\"font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48);\" id=\"vd-sub\">// 场 1 · 15s</span>\n <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>\n </div>\n <div class=\"asset-modal-body\">\n <div class=\"vd-main-wrap\">\n <div class=\"vd-main\">\n <div class=\"placeholder\" id=\"vd-main-img\"><span class=\"ph-frame\">大视频预览</span></div>\n </div>\n <div class=\"vd-info\">\n <div class=\"asset-detail-section-h\">// 基础信息</div>\n <div class=\"asset-detail-info\" id=\"vd-info\"></div>\n\n <div class=\"vd-history\" style=\"margin-top:18px;\">\n <div class=\"vd-history-h\">// 历史版本 · <span id=\"vd-history-ct\">3</span> 版</div>\n <div class=\"vd-history-row\" id=\"vd-history-row\"></div>\n </div>\n\n <div class=\"vd-prompt-field\">\n <div class=\"vd-prompt-head\">\n <span class=\"label\">// 视频提示词</span>\n </div>\n <div class=\"prompt-edit vd-prompt-edit\" contenteditable=\"true\" role=\"textbox\" aria-label=\"视频提示词\" spellcheck=\"false\" id=\"vd-prompt-edit\" data-placeholder=\"// 输入本场 Seedance 提示词\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"asset-modal-f vd-modal-f\">\n <button class=\"btn btn-ghost\" type=\"button\" data-modal-close>关闭</button>\n <div class=\"vd-modal-actions\">\n <button class=\"pill-cta ghost\" type=\"button\" id=\"vd-regen-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg>\n 重跑本场\n </button>\n <button class=\"pill-cta heat\" type=\"button\" id=\"vd-adopt-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n 采用此版\n </button>\n </div>\n </div>\n </div>\n</div>\n\n<!-- ============= STAGE 5 · 拼接编辑器 ============= -->\n<section class=\"stage\" data-stage-pane=\"5\">\n <div class=\"editor\">\n <div class=\"editor-preview\">\n <div class=\"canvas\" id=\"ed-canvas\"><span id=\"ed-canvas-label\">9:16 预览 · 1080×1920</span></div>\n <div class=\"controls\">\n <button class=\"ctl-btn\" id=\"ed-prev-btn\" title=\"上一帧 (←)\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><path d=\"M3 3v10l4-5zM9 3v10l4-5z\" fill=\"currentColor\"/></svg></button>\n <button class=\"ctl-btn\" id=\"ed-play-btn\" title=\"播放 / 暂停 (空格)\"><svg id=\"ed-play-icon\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M5 4l7 4-7 4z\" fill=\"currentColor\"/></svg></button>\n <button class=\"ctl-btn\" id=\"ed-next-btn\" title=\"下一帧 (→)\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><path d=\"M13 3v10l-4-5zM7 3v10l-4-5z\" fill=\"currentColor\"/></svg></button>\n <span class=\"muted mono\" style=\"font-size:12px; margin-left:8px;\"><span id=\"ed-cur-time\">00:00.00</span> / <span id=\"ed-total-time\">00:15.00</span></span>\n </div>\n </div>\n\n <div class=\"editor-props\">\n <div class=\"props-tabs\">\n <div class=\"active\">字幕</div>\n <div>转场</div>\n <div>BGM</div>\n </div>\n <div class=\"muted mono\" style=\"font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;\">// 字幕样式</div>\n <div class=\"style-swatch\">\n <div class=\"swatch-card selected\"><div class=\"demo\">真实分享</div><div class=\"nm\">朴素白底</div></div>\n <div class=\"swatch-card\"><div class=\"demo b\">真实分享</div><div class=\"nm\">影视黑底</div></div>\n <div class=\"swatch-card\"><div class=\"demo c\">真实分享</div><div class=\"nm\">手写描边</div></div>\n <div class=\"swatch-card\"><div class=\"demo d\">真实分享</div><div class=\"nm\">综艺暖黄</div></div>\n </div>\n\n <div class=\"divider\"></div>\n\n <div class=\"muted mono\" style=\"font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;\">// 当前选中(<span id=\"ed-inspect-name\">未选</span>)</div>\n <div class=\"props-row\"><span class=\"k\">起始</span><input class=\"input-mini\" id=\"ed-inspect-start\" value=\"—\"></div>\n <div class=\"props-row\"><span class=\"k\">时长</span><input class=\"input-mini\" id=\"ed-inspect-dur\" value=\"—\"></div>\n <div class=\"props-row\"><span class=\"k\">音量</span><input class=\"input-mini\" value=\"100\"></div>\n <div class=\"props-row\"><span class=\"k\">速度</span><input class=\"input-mini\" value=\"1.0x\"></div>\n <div class=\"props-row\"><span class=\"k\">入场</span><span class=\"mono\" style=\"font-size:11.5px;\">交叉淡化</span></div>\n\n <div class=\"divider\"></div>\n\n <div class=\"muted mono\" style=\"font-size:11px; font-weight:500; margin-bottom:8px; letter-spacing:.04em;\">// BGM</div>\n <div class=\"props-row\" style=\"border-bottom:0;\">\n <span style=\"font-size:12px; flex:1;\">温柔治愈钢琴 · 0:42</span>\n <button class=\"btn btn-ghost btn-sm\">替换</button>\n </div>\n </div>\n\n <div class=\"timeline\" id=\"ed-timeline\">\n <div class=\"tl-toolbar\">\n <button class=\"tl-action\" id=\"ed-undo-btn\" title=\"撤销\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 7v6h6\"/><path d=\"M21 17a9 9 0 0 0-15-6.7L3 13\"/></svg>\n </button>\n <button class=\"tl-action\" id=\"ed-redo-btn\" title=\"重做\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 7v6h-6\"/><path d=\"M3 17a9 9 0 0 1 15-6.7L21 13\"/></svg>\n </button>\n <span class=\"tl-sep\"></span>\n <button class=\"tl-action\" id=\"ed-split-btn\" title=\"在播放头处分割选中片段\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"6\" cy=\"6\" r=\"3\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M20 4L8.12 15.88\"/><path d=\"M14.47 14.48L20 20\"/><path d=\"M8.12 8.12L12 12\"/></svg>\n 分割\n </button>\n <button class=\"tl-action\" id=\"ed-copy-btn\" title=\"复制选中片段\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"/><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"/></svg>\n 复制\n </button>\n <button class=\"tl-action danger\" id=\"ed-del-btn\" title=\"删除选中片段 (Delete)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 0 1-2 1.8H8.5a2 2 0 0 1-2-1.8L5 6\"/></svg>\n 删除\n </button>\n <span class=\"spacer\"></span>\n <div class=\"tl-zoom\">\n <span class=\"lbl\">// zoom</span>\n <input type=\"range\" min=\"50\" max=\"200\" value=\"100\" id=\"ed-zoom-input\">\n </div>\n </div>\n\n <div class=\"tl-ruler\">\n <div class=\"l\">// time</div>\n <div class=\"rule-track\" id=\"ed-ruler\">\n <span class=\"tick major\" style=\"left:0%\"><span class=\"t\">0s</span></span>\n <span class=\"tick minor\" style=\"left:6.67%\"></span>\n <span class=\"tick major\" style=\"left:13.33%\"><span class=\"t\">2s</span></span>\n <span class=\"tick minor\" style=\"left:20%\"></span>\n <span class=\"tick major\" style=\"left:26.67%\"><span class=\"t\">4s</span></span>\n <span class=\"tick minor\" style=\"left:33.33%\"></span>\n <span class=\"tick major\" style=\"left:40%\"><span class=\"t\">6s</span></span>\n <span class=\"tick minor\" style=\"left:46.67%\"></span>\n <span class=\"tick major\" style=\"left:53.33%\"><span class=\"t\">8s</span></span>\n <span class=\"tick minor\" style=\"left:60%\"></span>\n <span class=\"tick major\" style=\"left:66.67%\"><span class=\"t\">10s</span></span>\n <span class=\"tick minor\" style=\"left:73.33%\"></span>\n <span class=\"tick major\" style=\"left:80%\"><span class=\"t\">12s</span></span>\n <span class=\"tick minor\" style=\"left:86.67%\"></span>\n <span class=\"tick major\" style=\"left:93.33%\"><span class=\"t\">14s</span></span>\n <span class=\"tick major\" style=\"left:100%\"><span class=\"t\">15s</span></span>\n </div>\n </div>\n\n <div class=\"tl-track video-track\">\n <div class=\"label video\">\n <span class=\"ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"2.18\"/><path d=\"M7 2v20M17 2v20M2 12h20M2 7h5M2 17h5M17 17h5M17 7h5\"/></svg></span>\n 视频\n </div>\n <div class=\"lane\" id=\"ed-lane-video\" data-track=\"video\">\n <div class=\"clip video\" data-track=\"video\" data-label=\"深夜办公桌\" data-dur=\"2\" data-max=\"2\"><span class=\"frames\"><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span></span><span class=\"num\">1</span><span class=\"lbl\">深夜办公桌</span></div>\n <div class=\"clip video\" data-track=\"video\" data-label=\"面膜包装\" data-dur=\"3\" data-max=\"3\"><span class=\"frames\"><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span></span><span class=\"num\">2</span><span class=\"lbl\">面膜包装</span></div>\n <div class=\"clip video\" data-track=\"video\" data-label=\"精华液微距\" data-dur=\"3\" data-max=\"3\"><span class=\"frames\"><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span></span><span class=\"num\">3</span><span class=\"lbl\">精华液微距</span></div>\n <div class=\"clip video\" data-track=\"video\" data-label=\"敷面膜平躺\" data-dur=\"3\" data-max=\"3\"><span class=\"frames\"><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span></span><span class=\"num\">4</span><span class=\"lbl\">敷面膜平躺</span></div>\n <div class=\"clip video\" data-track=\"video\" data-label=\"化妆台\" data-dur=\"2\" data-max=\"2\"><span class=\"frames\"><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span></span><span class=\"num\">5</span><span class=\"lbl\">化妆台</span></div>\n <div class=\"clip video\" data-track=\"video\" data-label=\"产品定格\" data-dur=\"2\" data-max=\"2\"><span class=\"frames\"><span class=\"fr\"></span><span class=\"fr\"></span><span class=\"fr\"></span></span><span class=\"num\">6</span><span class=\"lbl\">产品定格</span></div>\n </div>\n </div>\n\n <div class=\"tl-track subtitle-track\">\n <div class=\"label subtitle\">\n <span class=\"ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 7V4h16v3\"/><path d=\"M9 20h6\"/><path d=\"M12 4v16\"/></svg></span>\n 字幕\n </div>\n <div class=\"lane\" id=\"ed-lane-subtitle\" data-track=\"subtitle\">\n <div class=\"clip subtitle\" data-track=\"subtitle\" data-label=\"加班三天 脸已经不能看了…\" data-dur=\"2\" data-max=\"2\"><span class=\"lbl\">加班三天 脸已经不能看了…</span></div>\n <div class=\"clip subtitle\" data-track=\"subtitle\" data-label=\"还好我有这个 透真玻尿酸面膜\" data-dur=\"3\" data-max=\"3\"><span class=\"lbl\">还好我有这个 透真玻尿酸面膜</span></div>\n <div class=\"clip subtitle\" data-track=\"subtitle\" data-label=\"30g 精华 一片顶三片\" data-dur=\"3\" data-max=\"3\"><span class=\"lbl\">30g 精华 一片顶三片</span></div>\n <div class=\"clip subtitle\" data-track=\"subtitle\" data-label=\"敷完起来脸是软的\" data-dur=\"3\" data-max=\"3\"><span class=\"lbl\">敷完起来脸是软的</span></div>\n <div class=\"clip subtitle\" data-track=\"subtitle\" data-label=\"化妆都能看出来\" data-dur=\"2\" data-max=\"2\"><span class=\"lbl\">化妆都能看出来</span></div>\n <div class=\"clip subtitle\" data-track=\"subtitle\" data-label=\"5 片 ¥39.9 囤起来\" data-dur=\"2\" data-max=\"2\"><span class=\"lbl\">5 片 ¥39.9 囤起来</span></div>\n <div class=\"playhead\" id=\"ed-playhead\" style=\"left:0%;\"><span class=\"ph-grab\"></span></div>\n </div>\n </div>\n\n <div class=\"tl-track bgm-track\">\n <div class=\"label bgm\">\n <span class=\"ico\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 18V5l12-2v13\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><circle cx=\"18\" cy=\"16\" r=\"3\"/></svg></span>\n BGM\n </div>\n <div class=\"lane\">\n <div class=\"clip bgm\" data-track=\"bgm\" data-label=\"温柔治愈钢琴\" data-dur=\"15\" data-max=\"15\">\n <span class=\"wave\"><svg viewBox=\"0 0 600 20\" preserveAspectRatio=\"none\" fill=\"currentColor\"><rect x=\"0\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"4\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"8\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"12\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"16\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"20\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"24\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"28\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"32\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"36\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"40\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"44\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"48\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"52\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"56\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"60\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"64\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"68\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"72\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"76\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"80\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"84\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"88\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"92\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"96\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"100\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"104\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"108\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"112\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"116\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"120\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"124\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"128\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"132\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"136\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"140\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"144\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"148\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"152\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"156\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"160\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"164\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"168\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"172\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"176\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"180\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"184\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"188\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"192\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"196\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"200\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"204\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"208\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"212\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"216\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"220\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"224\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"228\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"232\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"236\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"240\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"244\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"248\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"252\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"256\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"260\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"264\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"268\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"272\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"276\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"280\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"284\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"288\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"292\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"296\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"300\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"304\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"308\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"312\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"316\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"320\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"324\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"328\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"332\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"336\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"340\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"344\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"348\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"352\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"356\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"360\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"364\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"368\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"372\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"376\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"380\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"384\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"388\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"392\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"396\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"400\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"404\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"408\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"412\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"416\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"420\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"424\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"428\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"432\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"436\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"440\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"444\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"448\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"452\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"456\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"460\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"464\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"468\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"472\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"476\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"480\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"484\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"488\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"492\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"496\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"500\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"504\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"508\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"512\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"516\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"520\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"524\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"528\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"532\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"536\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"540\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"544\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"548\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"552\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"556\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"560\" y=\"2\" width=\"2\" height=\"16\"/><rect x=\"564\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"568\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"572\" y=\"7\" width=\"2\" height=\"6\"/><rect x=\"576\" y=\"6\" width=\"2\" height=\"8\"/><rect x=\"580\" y=\"4\" width=\"2\" height=\"12\"/><rect x=\"584\" y=\"8\" width=\"2\" height=\"4\"/><rect x=\"588\" y=\"5\" width=\"2\" height=\"10\"/><rect x=\"592\" y=\"3\" width=\"2\" height=\"14\"/><rect x=\"596\" y=\"6\" width=\"2\" height=\"8\"/></svg></span>\n <span class=\"lbl\">温柔治愈钢琴 · 0:42(循环 1 次,淡入淡出)</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"stage-foot\">\n <div class=\"info\"><span class=\"mono\">[ 合成预估 ~30s · 拼接 / 导出全程 0 token · 已结算 ¥1.39 ]</span></div>\n <div class=\"hstack\">\n <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>\n <button class=\"btn\" onclick=\"Shell.toast('已保存草稿', '/projects/p3/draft')\">保存草稿</button>\n <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>\n </div>\n </div>\n</section>\n\n<!-- ===== Stage 2 通用 · 资产详情 modal (参考布局 v2) ===== -->\n<div class=\"asset-modal-bg\" id=\"asset-detail-modal\">\n <div class=\"asset-modal\">\n <div class=\"asset-modal-h\">\n <h2 id=\"asset-detail-title\">资产详情</h2>\n <span class=\"ad-tag\" id=\"asset-detail-kind\">/ kind</span>\n <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>\n </div>\n <div class=\"asset-modal-body\">\n <div class=\"asset-detail-grid\">\n <!-- 左栏 · 大立绘 + 缩略图 -->\n <div class=\"asset-detail-lead\">\n <div class=\"ad-lead-wrap\">\n <div class=\"placeholder ad-lead-img\" id=\"asset-detail-lead-img\"><span class=\"ph-frame\">立绘</span></div>\n <button class=\"ad-zoom-btn\" type=\"button\" id=\"asset-detail-zoom-btn\" aria-label=\"查看大图\" title=\"查看大图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg>\n </button>\n </div>\n <div class=\"ad-thumbs\" id=\"asset-detail-thumbs\"></div>\n </div>\n <!-- 右栏 · 三视图 + 简介 + 属性 -->\n <div class=\"asset-detail-right\">\n <!-- 三视图 -->\n <div class=\"ad-section\" id=\"asset-detail-tri-section\">\n <div class=\"asset-detail-section-h\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/><rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/></svg></span>\n <span class=\"t\">三视图</span>\n <span class=\"ad-ratio-chip\" id=\"asset-detail-ratio\">16:9</span>\n <button class=\"ad-icon-btn\" type=\"button\" title=\"下载\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>\n </button>\n </div>\n <div class=\"asset-detail-tri-row\" id=\"asset-detail-tri\">\n <div class=\"placeholder\"><span class=\"ph-frame\">正 / 侧 / 背 · 三视图</span></div>\n </div>\n <div class=\"asset-detail-tip\" id=\"asset-detail-tip\" style=\"display:none;\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/></svg>\n <span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>\n <button class=\"ai-gen-btn\" type=\"button\">AI 生成三视图</button>\n </div>\n </div>\n <!-- 简介 -->\n <div class=\"ad-section\">\n <div class=\"asset-detail-section-h\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6h16M4 12h16M4 18h10\"/></svg></span>\n <span class=\"t\">简介</span>\n </div>\n <p class=\"ad-intro\" id=\"asset-detail-intro\"></p>\n <div class=\"ad-tags\" id=\"asset-detail-tags\"></div>\n </div>\n <!-- 属性表 -->\n <div class=\"ad-props\" id=\"asset-detail-props\"></div>\n </div>\n </div>\n </div>\n <div class=\"asset-modal-f\">\n <div class=\"ad-foot-stats\">\n <button class=\"ad-stat-btn\" type=\"button\" id=\"asset-detail-download\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>\n 下载\n </button>\n </div>\n <button class=\"btn btn-primary\" type=\"button\" id=\"asset-detail-apply-btn\">使用该资产</button>\n </div>\n </div>\n</div>\n\n<!-- ===== Stage 2 · 新增人物 modal ===== -->\n<div class=\"asset-modal-bg\" id=\"new-character-modal\">\n <div class=\"asset-modal\" style=\"width: min(680px, 100%);\">\n <div class=\"asset-modal-h\">\n <h2>新增人物</h2>\n <span style=\"font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48);\">// 立绘必填 + 三视图(可 AI 生成)</span>\n <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>\n </div>\n <div class=\"asset-modal-body\">\n <div class=\"field\" style=\"margin-bottom: 14px;\">\n <label class=\"field-label\" style=\"display:block; font-size: 12.5px; color: var(--black-alpha-56); margin-bottom: 6px;\">人物名称</label>\n <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;\">\n </div>\n <div style=\"display:grid; grid-template-columns: 1fr 1fr; gap: 14px;\">\n <div>\n <div class=\"asset-detail-section-h\">// 立绘<span style=\"color: var(--heat); margin-left:2px;\">*</span></div>\n <div class=\"upload-zone lead\" id=\"nc-upload-lead\">\n <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>\n <span>点击上传立绘</span>\n <span style=\"font-family: var(--font-mono); font-size: 10.5px; opacity: .7;\">PNG / JPG · ≤10MB</span>\n </div>\n </div>\n <div>\n <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>\n <div>\n <div class=\"upload-zone upload-zone-tri\" style=\"aspect-ratio: 16/9;\"><span>正 / 侧 / 背 · 三视图</span></div>\n </div>\n <div class=\"asset-detail-tip\" id=\"nc-tri-tip\" style=\"margin-top: 10px;\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/></svg>\n <span>没有三视图?上传立绘后用 AI 自动生成</span>\n <button class=\"ai-gen-btn\" type=\"button\">AI 生成</button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"asset-modal-f\">\n <button class=\"btn btn-ghost\" type=\"button\" data-modal-close>取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"nc-save-btn\">保存人物</button>\n </div>\n </div>\n</div>\n\n<!-- ===== Stage 2 · 演员库 / 场景库 全屏弹窗(共享 · kind 切换内容)===== -->\n<div class=\"ml-modal-bg\" id=\"ml-modal-bg\">\n <div class=\"ml-modal\">\n <div class=\"ml-modal-h\">\n <h2 id=\"ml-modal-title\">演员库</h2>\n <span class=\"ct\" id=\"ml-modal-ct\">// 共 0 个</span>\n <button class=\"x\" type=\"button\" id=\"ml-close-btn\" aria-label=\"关闭\">\n <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>\n </button>\n </div>\n <div class=\"ml-modal-body\">\n <aside class=\"ml-side\" id=\"ml-side\">\n <!-- JS 注入 来源 -->\n </aside>\n <div class=\"ml-main\">\n <div class=\"ml-toolbar\" id=\"ml-toolbar\">\n <!-- JS 注入 chips + 上传按钮 -->\n </div>\n <div class=\"ml-scroll\">\n <div class=\"ml-grid\" id=\"ml-grid\">\n <!-- JS 注入 卡片 -->\n </div>\n </div>\n\n <!-- 添加演员 / 场景 · 工作台画布 -->\n <div class=\"ml-canvas\" id=\"ml-canvas\" aria-hidden=\"true\">\n <div class=\"ml-canvas-h\">\n <button class=\"back-btn\" type=\"button\" id=\"ml-canvas-back\" aria-label=\"返回\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 12H5M12 19l-7-7 7-7\"/></svg>\n 返回\n </button>\n <h3 id=\"ml-canvas-title\">添加演员</h3>\n <span class=\"mono\" id=\"ml-canvas-mono\">// 添加演员 · 工作台</span>\n <span style=\"flex:1;\"></span>\n </div>\n <div class=\"ml-canvas-body\">\n <section class=\"mc-ai\">\n <div class=\"mc-stream\" id=\"mc-stream\">\n <div class=\"mc-stream-inner\" id=\"mc-stream-inner\">\n <div class=\"mc-empty\">\n <div class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z\"/><path d=\"M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z\"/></svg>\n </div>\n <span class=\"badge\">// AI · STUDIO</span>\n <h2 id=\"mc-empty-title\">用 AI 生成一位新演员</h2>\n <p id=\"mc-empty-desc\">描述外形 + 风格 + 服装,AI 会同时生成立绘 + 正/侧/背三视图,加入演员库。</p>\n <div class=\"examples\" id=\"mc-empty-examples\"></div>\n </div>\n </div>\n </div>\n <div class=\"mc-input-wrap\">\n <div class=\"mc-input\">\n <div class=\"mc-input-top\">\n <div class=\"mc-input-refs\" id=\"mc-input-refs\"></div>\n <button class=\"add-btn\" type=\"button\" id=\"mc-add-btn\" title=\"上传参考图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </button>\n <input type=\"file\" id=\"mc-ai-ref-input\" accept=\"image/*\" multiple hidden>\n </div>\n <textarea id=\"mc-input-text\" rows=\"1\" placeholder=\"描述外形、风格、服饰…\"></textarea>\n <div class=\"mc-input-bottom\">\n <div class=\"param\"><span class=\"lbl-mono\">比例</span><span>3:4</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></div>\n <div class=\"param\"><span class=\"lbl-mono\">风格</span><span>默认</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></div>\n <div class=\"param\"><span class=\"lbl-mono\">张数</span><span>4</span><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></div>\n <span class=\"right-meta\">预估 <span class=\"val\">¥0.80</span> · 余额 <span class=\"val\">¥327.40</span></span>\n <button class=\"send-btn\" type=\"button\" id=\"mc-send-btn\" disabled title=\"生成\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </button>\n </div>\n </div>\n </div>\n </section>\n\n <aside class=\"mc-up\" id=\"mc-up\" data-kind=\"actor\">\n <div class=\"mc-up-tabs\">\n <button class=\"mc-up-tab active\" type=\"button\" data-tab=\"ai\">AI 生成</button>\n <button class=\"mc-up-tab\" type=\"button\" data-tab=\"local\">本地上传</button>\n </div>\n <div class=\"mc-up-body\">\n <div class=\"mc-up-section\">\n <div class=\"mc-up-sec-h\" id=\"mc-up-name-label\">// 演员姓名</div>\n <input class=\"mc-up-name\" type=\"text\" id=\"mc-up-name\" placeholder=\"给演员起个名字…\" maxlength=\"20\">\n </div>\n <div class=\"mc-up-section\">\n <div class=\"mc-up-sec-h\" id=\"mc-up-portrait-label\">// 演员立绘</div>\n <div class=\"mc-portrait-ai\" data-show=\"ai\">\n <div class=\"empty\" id=\"mc-portrait-ai-empty\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"8\" r=\"4\"/><path d=\"M3 21c0-3.5 3-6 6-6s6 2.5 6 6\"/></svg></div>\n <div class=\"desc\" id=\"mc-portrait-ai-empty-desc\">在左侧 AI 生成后<br>点击想要的立绘添加到这里</div>\n <div class=\"mono\">// 待选中</div>\n </div>\n <div class=\"picked\" id=\"mc-portrait-ai-picked\" hidden>\n <span class=\"badge\">已选用</span>\n <div class=\"ph-frame\" id=\"mc-portrait-ai-label\">立绘</div>\n <div class=\"ops\">\n <button type=\"button\" id=\"mc-portrait-ai-clear\" title=\"移除\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n </div>\n </div>\n <div class=\"mc-portrait-local\" data-show=\"local\" hidden>\n <div class=\"drop\" id=\"mc-portrait-local-drop\" tabindex=\"0\" role=\"button\" aria-label=\"点击或拖入立绘\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/></svg></div>\n <div class=\"t\" id=\"mc-portrait-local-t\">点击或拖入立绘</div>\n <div class=\"d\">支持多张 JPG / PNG / WEBP · ≤ 10MB / 张</div>\n </div>\n <div class=\"list-h\">\n <span>// 已上传</span>\n <span class=\"ct\" id=\"mc-portrait-local-count\">0</span>\n <span>张</span>\n </div>\n <div class=\"list\" id=\"mc-portrait-local-list\"></div>\n <input type=\"file\" id=\"mc-portrait-local-input\" accept=\"image/*\" multiple hidden>\n </div>\n </div>\n <div class=\"mc-up-section mc-triview\" id=\"mc-triview-sec\">\n <div class=\"mc-up-sec-h\">// 三视图</div>\n <div class=\"result-wrap\">\n <div class=\"result\" id=\"mc-triview-result\">\n <div class=\"ph-frame\" id=\"mc-triview-frame\">三视图(正/侧/背)</div>\n <button class=\"overlay-gen-btn\" type=\"button\" id=\"mc-triview-gen-btn\" disabled>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z\"/></svg>\n 生成三视图\n </button>\n <div class=\"overlay-hint\" id=\"mc-triview-hint\">// 先选中左侧 AI 立绘</div>\n </div>\n <div class=\"result-ops\" id=\"mc-triview-ops\" hidden>\n <button type=\"button\" id=\"mc-triview-rerun\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg> 重跑</button>\n <span class=\"cost\">~¥0.30 / 次</span>\n </div>\n <div class=\"history\" id=\"mc-triview-history\" hidden>\n <div class=\"h-lbl\">// 历史版本 · <span class=\"ct\" id=\"mc-triview-history-count\">0</span> 版</div>\n <div class=\"h-row\" id=\"mc-triview-history-row\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"mc-up-foot\">\n <span class=\"stat\" id=\"mc-up-stat\">// 待完成</span>\n <button type=\"button\" class=\"commit-btn\" id=\"mc-up-commit\" disabled>\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n <span id=\"mc-up-commit-label\">加入演员库</span>\n </button>\n </div>\n </aside>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n\n<!-- ===== 离开工作台 · 二次确认弹窗 ===== -->\n<div class=\"mc-leave-bg\" id=\"mc-leave-bg\" aria-hidden=\"true\">\n <div class=\"mc-leave\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"mc-leave-title\">\n <div class=\"lv-h\">\n <span class=\"ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z\"/></svg>\n </span>\n <h3 id=\"mc-leave-title\">退出工作台?</h3>\n <span class=\"mono\">// UNSAVED</span>\n </div>\n <div class=\"lv-b\" id=\"mc-leave-body\">\n 工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「加入」来保留进度。\n </div>\n <div class=\"lv-f\">\n <span class=\"spacer\"></span>\n <button class=\"btn\" type=\"button\" id=\"mc-leave-cancel\">继续编辑</button>\n <button class=\"btn btn-danger\" type=\"button\" id=\"mc-leave-confirm\">不保存,退出</button>\n </div>\n </div>\n</div>\n\n<!-- ===== 添加演员 / 场景 · 选择来源 modal ===== -->\n<div class=\"ml-up-choice-bg\" id=\"ml-up-choice-bg\">\n <div class=\"ml-up-choice\" role=\"dialog\" aria-label=\"添加来源\">\n <div class=\"uc-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"9\" cy=\"8\" r=\"4\"/><path d=\"M3 21c0-3.5 3-6 6-6s6 2.5 6 6\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n </div>\n <div class=\"ti\">\n <strong id=\"ml-up-title\">添加</strong>\n <span class=\"mono\">// 选择来源 · AI 生成或本地上传</span>\n </div>\n <button class=\"uc-x\" type=\"button\" id=\"ml-up-x\" aria-label=\"关闭\">\n <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>\n </button>\n </div>\n <div class=\"uc-body\">\n <button type=\"button\" class=\"uc-option\" id=\"ml-up-ai\">\n <span class=\"opt-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z\"/><path d=\"M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z\"/></svg>\n </span>\n <div class=\"opt-t\">AI 生成</div>\n <div class=\"opt-d\" id=\"ml-up-ai-desc\">描述外形 + 风格,AI 自动生成新形象与三视图</div>\n <span class=\"opt-tag\">[ AI · STUDIO ]</span>\n </button>\n <button type=\"button\" class=\"uc-option\" id=\"ml-up-local\">\n <span class=\"opt-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/></svg>\n </span>\n <div class=\"opt-t\">本地上传</div>\n <div class=\"opt-d\" id=\"ml-up-local-desc\">上传真人 / 既有素材,后续可生成三视图统一镜头</div>\n <span class=\"opt-tag\">[ UPLOAD ]</span>\n </button>\n </div>\n </div>\n</div>\n<input type=\"file\" id=\"ml-up-file\" accept=\"image/*\" multiple hidden>\n\n<!-- ===== 额度预检 modal · PRD §10.3 四层预检 ===== -->\n<div class=\"modal-bg\" id=\"quota-bg\" onclick=\"if(event.target===this)Shell.closeModal('quota-bg')\">\n <div class=\"modal\" id=\"quota-modal\" style=\"width: min(440px, 92vw);\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" id=\"quota-ic\">\n <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>\n </div>\n <div class=\"ti\" id=\"quota-title\">额度预检通过<span id=\"quota-sub\">// 4 层检查 · 全部通过</span></div>\n </div>\n <div class=\"modal-b\">\n <div id=\"quota-stage-row\" style=\"font-size:13px; color:var(--black-alpha-72); margin-bottom: 14px;\"></div>\n <div style=\"display:flex; flex-direction:column; gap:8px;\" id=\"quota-checks\">\n <!-- JS 注入 4 行检查 -->\n </div>\n <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;\">\n <strong>任务已拦截</strong> · 余额或额度不足以覆盖本次预估。请联系超管充值,或将团队月限额调高。\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('quota-bg')\" id=\"quota-cancel\">关闭</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"quota-confirm\" style=\"display:none;\">确认扣费 · 开始任务</button>\n <a class=\"btn btn-primary\" id=\"quota-topup\" href=\"account.html\" style=\"display:none;\">前往充值</a>\n </div>\n </div>\n</div>\n\n</div>\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\n/* ─── 商品名贯穿全流程(从 ?product= 读取,无参数时回退到 mock 默认值)─── */\nconst URL_PRODUCT_NAME = (function () {\n try { return decodeURIComponent(new URLSearchParams(location.search).get('product') || ''); }\n catch (e) { return ''; }\n})();\nconst URL_PROJECT_VER = (function () {\n try { return new URLSearchParams(location.search).get('v') || 'v3'; }\n catch (e) { return 'v3'; }\n})();\nconst CURRENT_PRODUCT_NAME = URL_PRODUCT_NAME || '透真补水面膜';\n// 项目名 = 「{product 简称} · 痛点种草 · v3」 — 简称取末 4 字符作为视觉收敛\nfunction shortProductName(name) {\n if (name.length <= 5) return name;\n // 尝试匹配常见品类后缀,否则取末 4 字\n const suffixes = ['面膜', '防晒', '口红', '耳机', '速食面', '咖啡', '瑜伽裤', '保温杯'];\n for (const s of suffixes) { if (name.endsWith(s)) return name.slice(-Math.max(s.length + 2, 4)); }\n return name.slice(-4);\n}\nconst PROJECT_TITLE = shortProductName(CURRENT_PRODUCT_NAME) + ' · 痛点种草 · ' + URL_PROJECT_VER;\n\nShell.render({\n active: 'projects',\n crumbs: []\n});\n\n/* 渲染贯穿商品名 / 项目名 */\ndocument.getElementById('page-title').textContent = PROJECT_TITLE + ' · 流水线 · Airshelf';\n\n(function _injectPipelineTopbarLeft() {\n const topbar = document.querySelector('.topbar');\n const right = topbar?.querySelector('.right');\n if (!topbar || !right) return;\n const esc = s => String(s).replace(/[&<>\"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\n const title = esc(PROJECT_TITLE);\n const left = document.createElement('div');\n left.className = 'pipeline-topbar-left';\n left.innerHTML = `\n <a class=\"btn btn-ghost pipeline-back\" href=\"projects.html\" aria-label=\"返回视频项目\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M19 12H5\"/><path d=\"M12 19l-7-7 7-7\"/></svg>\n 返回视频项目\n </a>\n <div class=\"pipeline-topbar-title\" title=\"${title}\">\n ${title}<span class=\"mono\">// PIPELINE</span>\n </div>\n `;\n topbar.insertBefore(left, right);\n})();\n\nconst ProjectStore = (function () {\n const safeId = (PROJECT_TITLE + '|' + CURRENT_PRODUCT_NAME).replace(/[^\\w\\u4e00-\\u9fa5-]+/g, '_');\n const key = 'airshelf:pipeline:' + safeId;\n const defaults = {\n product: CURRENT_PRODUCT_NAME,\n title: PROJECT_TITLE,\n currentStage: 1,\n completedStage: 0,\n fields: {},\n actions: [],\n jobs: {},\n stage1: null,\n stage2: {},\n stage3: null,\n stage4: null,\n updatedAt: Date.now(),\n };\n let data;\n try {\n data = { ...defaults, ...(JSON.parse(localStorage.getItem(key) || '{}') || {}) };\n } catch (e) {\n data = { ...defaults };\n }\n\n function save() {\n data.updatedAt = Date.now();\n localStorage.setItem(key, JSON.stringify(data));\n try {\n const indexKey = 'airshelf:pipeline-index';\n const list = JSON.parse(localStorage.getItem(indexKey) || '[]');\n const compact = {\n key,\n title: data.title,\n product: data.product,\n currentStage: data.currentStage,\n completedStage: data.completedStage,\n updatedAt: data.updatedAt,\n runningJobs: Object.values(data.jobs || {})\n .filter(j => j.status === 'running')\n .map(j => ({ stage: j.stage, label: j.label, finishAt: j.finishAt })),\n };\n const next = [compact, ...list.filter(item => item.key !== key)].slice(0, 30);\n localStorage.setItem(indexKey, JSON.stringify(next));\n } catch (e) {}\n }\n\n function record(type, detail = {}) {\n data.actions = data.actions || [];\n data.actions.unshift({ type, detail, at: Date.now() });\n data.actions = data.actions.slice(0, 80);\n save();\n }\n\n function setStage(n) {\n data.currentStage = Number(n) || 1;\n data.completedStage = Math.max(Number(data.completedStage) || 0, Math.max(0, data.currentStage - 1));\n save();\n }\n\n function saveFieldsFrom(root = document) {\n root.querySelectorAll('[id][contenteditable=\"true\"], input[id], textarea[id], select[id]').forEach(el => {\n if (el.type === 'file') return;\n data.fields[el.id] = {\n kind: el.matches('[contenteditable=\"true\"]') ? 'text' : 'value',\n value: el.matches('[contenteditable=\"true\"]') ? el.textContent : el.value,\n };\n });\n save();\n }\n\n function restoreFields(root = document) {\n Object.entries(data.fields || {}).forEach(([id, item]) => {\n const el = root.getElementById ? root.getElementById(id) : document.getElementById(id);\n if (!el || item.value == null) return;\n if (item.kind === 'text' && el.matches('[contenteditable=\"true\"]')) el.textContent = item.value;\n else if ('value' in el && el.type !== 'file') el.value = item.value;\n });\n }\n\n function startJob(id, payload) {\n data.jobs[id] = { ...payload, status: 'running', startedAt: Date.now(), updatedAt: Date.now() };\n save();\n }\n\n function finishJob(id, patch = {}) {\n if (!data.jobs[id]) return;\n data.jobs[id] = { ...data.jobs[id], ...patch, status: 'done', finishedAt: Date.now(), updatedAt: Date.now() };\n save();\n }\n\n function getJob(id) { return data.jobs?.[id] || null; }\n function clearJob(id) { if (data.jobs?.[id]) { delete data.jobs[id]; save(); } }\n\n function saveStage(name, value) {\n data[name] = value;\n save();\n }\n\n window.addEventListener('beforeunload', () => saveFieldsFrom());\n document.addEventListener('input', (e) => {\n if (e.target.closest('[contenteditable=\"true\"], input[id], textarea[id], select[id]')) {\n saveFieldsFrom();\n }\n });\n\n return { key, data, save, record, setStage, saveFieldsFrom, restoreFields, startJob, finishJob, getJob, clearJob, saveStage };\n})();\n\n/* ─── 把 stage-pill anchor 注入 .topbar 中部(圆点全状态都靠 .sp-dot 实时同步)─── */\n(function _injectStagePill() {\n const anchor = document.getElementById('stage-pill-anchor');\n const topbar = document.querySelector('.topbar');\n if (!anchor || !topbar) return;\n anchor.classList.add('stage-pill');\n anchor.classList.remove('stage-pill-anchor');\n anchor.removeAttribute('hidden');\n anchor.id = 'stage-pill';\n topbar.appendChild(anchor); // position: absolute · 自动锚定 topbar 中部\n})();\n\n/* ─── Stage 1 拖拽分隔条 · 控制脚本助手宽度 · clamp 在 [380, 680] ─── */\n(function _setupStageScriptGutter() {\n const gutter = document.getElementById('stage-script-gutter');\n const grid = document.querySelector('.stage-script');\n if (!gutter || !grid) return;\n const MIN = 380, MAX = 680;\n let dragging = false;\n gutter.addEventListener('mousedown', (e) => {\n dragging = true;\n gutter.classList.add('dragging');\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n e.preventDefault();\n });\n document.addEventListener('mousemove', (e) => {\n if (!dragging) return;\n const rect = grid.getBoundingClientRect();\n const newRight = rect.right - e.clientX;\n const clamped = Math.max(MIN, Math.min(MAX, newRight));\n grid.style.setProperty('--chat-w', clamped + 'px');\n });\n document.addEventListener('mouseup', () => {\n if (!dragging) return;\n dragging = false;\n gutter.classList.remove('dragging');\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n });\n})();\n\n// hash routing\nfunction activateStage(n) {\n const cur = Number(n);\n document.querySelectorAll('.stage').forEach(s => s.classList.remove('active'));\n document.querySelector(`[data-stage-pane=\"${cur}\"]`)?.classList.add('active');\n ProjectStore.setStage(cur);\n\n // 圆点状态:< cur → done(森林绿) · = cur → active(橙实心+光晕) · > cur → 默认(浅灰)\n const completed = Math.max(Number(ProjectStore.data.completedStage) || 0, cur - 1);\n document.querySelectorAll('#stage-pill .sp-dot').forEach(s => {\n const i = +s.dataset.stage;\n s.classList.remove('active', 'done');\n if (i === cur) s.classList.add('active');\n else if (i <= completed) s.classList.add('done');\n });\n // 连接线 · idx+1 < cur 时染森林绿\n document.querySelectorAll('#stage-pill .sp-line').forEach((ln, idx) => {\n ln.classList.toggle('done', (idx + 1) <= completed);\n });\n\n // 全高度布局:所有 stage 操作模块 hug content、内容区域 fill content\n // 全部走 flat(像 model-photo 那样的全宽扁平布局,去卡片)\n const contentEl = document.getElementById('page-content');\n if (contentEl) {\n contentEl.classList.add('content--fh');\n contentEl.classList.add('content--fh-flat');\n }\n\n window.scrollTo({ top: 0, behavior: 'smooth' });\n requestAnimationFrame(() => ProjectStore.restoreFields());\n}\nfunction readHash() {\n const m = location.hash.match(/stage-(\\d)/);\n if (m) { activateStage(+m[1]); return; }\n // ?stage=N query 参数也接收\n const q = new URLSearchParams(location.search);\n const s = q.get('stage');\n if (s) { activateStage(+s); return; }\n // 兜底:回到上次离开的 stage,等待生成时离开页面也能继续当前项目进度\n activateStage(ProjectStore.data.currentStage || 1);\n}\nwindow.addEventListener('hashchange', readHash);\nreadHash();\n\n/* ============================================================\n STAGE 1 · 脚本助手 + 镜头脚本 状态驱动\n ============================================================ */\nconst Stage1 = (function () {\n let shots = []; // [{ id, painting, dialog, duration }]\n let chatMsgs = []; // [{ role, html, time }]\n let mode = null;\n let scriptTags = { char: [], scene: [] }; // 自动抓取的人物 / 场景 · 可编辑\n const MODE_LABELS = { ai: 'AI 全生', theme: '一句话主题', manual: '自带脚本' };\n const MODE_USER_COPY = {\n ai: '我用 AI 全生',\n theme: '我用一句话主题',\n manual: '我用自带脚本',\n };\n const BRIEF_OPTIONS = {\n style: ['真实测评', '痛点种草', '小红书种草', '开箱测评', '对比展示'],\n persona: ['通勤敏感肌女生', '熬夜党通勤女性', '学生党女生', '精致宝妈', '成分党用户'],\n };\n function makeBrief(nextMode) {\n if (!nextMode) {\n return {\n source: '未选择',\n style: '待确认',\n persona: '待确认',\n styleNote: '选择脚本来源后由助手推荐',\n personaNote: '选择脚本来源后由助手推荐',\n };\n }\n return {\n source: MODE_LABELS[nextMode] || '未选择',\n style: nextMode === 'theme' ? '痛点种草' : '真实测评',\n persona: '通勤敏感肌女生',\n styleNote: nextMode === 'manual' ? '参考文本识别不足 · 根据商品类目推荐' : '根据商品信息推荐',\n personaNote: '根据商品目标人群推荐',\n };\n }\n let scriptBrief = makeBrief(null);\n\n const $cb = () => document.getElementById('chat-body');\n const $sb = () => document.getElementById('shots-body');\n const $sm = () => document.getElementById('shots-meta');\n const safeHtml = s => String(s || '').replace(/[&<>\"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[m]));\n function renderBriefSummary() {\n const source = document.getElementById('brief-source');\n const style = document.getElementById('brief-style');\n const persona = document.getElementById('brief-persona');\n if (source) source.textContent = scriptBrief.source || '未选择';\n if (style) style.textContent = scriptBrief.style || '待确认';\n if (persona) persona.textContent = scriptBrief.persona || '待确认';\n }\n function briefConfirmHtml(intro) {\n const optionMenu = (kind, current) => `\n <span class=\"script-brief-select\">\n <button class=\"script-brief-value\" type=\"button\" data-brief-act=\"${kind}\" aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-label=\"选择${kind === 'style' ? '脚本风格' : '人物设定'}\">\n <span class=\"v\">${safeHtml(current)}</span>\n </button>\n <span class=\"chip-menu align-right\" role=\"listbox\">\n ${BRIEF_OPTIONS[kind].map(opt => `\n <button class=\"mi${opt === current ? ' selected' : ''}\" type=\"button\" data-brief-pick=\"${kind}\" data-value=\"${safeHtml(opt)}\" role=\"option\" aria-selected=\"${opt === current ? 'true' : 'false'}\">\n <svg class=\"mi-check\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m20 6-11 11-5-5\"/></svg>\n ${safeHtml(opt)}\n </button>`).join('')}\n </span>\n </span>`;\n return `${intro}\n <div class=\"script-brief-card\">\n <div class=\"script-brief-row\">\n <span class=\"k\">风格</span>\n ${optionMenu('style', scriptBrief.style)}\n <span class=\"why\">${safeHtml(scriptBrief.styleNote)}</span>\n </div>\n <div class=\"script-brief-row\">\n <span class=\"k\">人物</span>\n ${optionMenu('persona', scriptBrief.persona)}\n <span class=\"why\">${safeHtml(scriptBrief.personaNote)}</span>\n </div>\n <div class=\"script-brief-actions\">\n <div class=\"action-row\">\n <button class=\"btn btn-ghost\" type=\"button\" data-brief-act=\"reroll\">重新推荐</button>\n <button class=\"btn\" type=\"button\" data-brief-act=\"accept\">确定</button>\n </div>\n </div>\n </div>`;\n }\n function migrateBriefMessages() {\n let changed = false;\n chatMsgs = chatMsgs.map(msg => {\n if (!msg || typeof msg.html !== 'string') return msg;\n if (msg.role === 'ai' && /script-brief-card/.test(msg.html)) {\n const cardIndex = msg.html.indexOf('<div class=\"script-brief-card\"');\n const intro = cardIndex > -1 ? msg.html.slice(0, cardIndex).trim() : '';\n changed = true;\n return {\n ...msg,\n html: briefConfirmHtml(intro || '已更新创作方向。确认后我会按这个方向重写镜头脚本。'),\n };\n }\n if (msg.role === 'user' && msg.html === '保持推荐,生成镜头脚本') {\n changed = true;\n return { ...msg, html: '确定' };\n }\n return msg;\n });\n return changed;\n }\n function tuneBriefByText(text) {\n if (/小红书|种草/.test(text)) scriptBrief.style = '小红书种草';\n else if (/测评|评测/.test(text)) scriptBrief.style = '真实测评';\n else if (/痛点/.test(text)) scriptBrief.style = '痛点种草';\n if (/熬夜/.test(text)) scriptBrief.persona = '熬夜党通勤女性';\n else if (/学生/.test(text)) scriptBrief.persona = '学生党女生';\n else if (/宝妈|妈妈/.test(text)) scriptBrief.persona = '精致宝妈';\n }\n function startScriptGeneration() {\n ProjectStore.startJob('stage1-script', {\n stage: 1,\n label: '脚本初稿生成',\n finishAt: Date.now() + 6500,\n });\n if (!chatMsgs.some(x => /正在解析商品卖点/.test(x.html))) {\n pushMsg('ai', '<span class=\"ai-thinking\">正在解析商品卖点与创作方向 <span class=\"dots\"><span></span><span></span><span></span></span></span>');\n }\n ProjectStore.record('stage1.script.generate', { mode, brief: scriptBrief });\n saveState();\n renderChat();\n window.setTimeout(completeAiJob, 6500);\n }\n function refreshLatestBriefMessage(intro) {\n for (let i = chatMsgs.length - 1; i >= 0; i--) {\n if (chatMsgs[i].role === 'ai' && /script-brief-card/.test(chatMsgs[i].html)) {\n chatMsgs[i].html = briefConfirmHtml(intro);\n return;\n }\n }\n pushMsg('ai', briefConfirmHtml(intro));\n }\n function handleBriefPick(kind, value) {\n if (!value) return;\n if (kind === 'style') {\n scriptBrief.style = value;\n scriptBrief.styleNote = '已手动选择';\n } else if (kind === 'persona') {\n scriptBrief.persona = value;\n scriptBrief.personaNote = '已手动选择';\n }\n refreshLatestBriefMessage('已更新创作方向。确认后我会按这个方向重写镜头脚本。');\n ProjectStore.record('stage1.brief.option.selected', { kind, value });\n saveState();\n renderChat();\n }\n function handleBriefAction(action, trigger) {\n if (action === 'accept') {\n pushMsg('user', '确定');\n startScriptGeneration();\n } else if (action === 'reroll') {\n scriptBrief = {\n ...scriptBrief,\n style: scriptBrief.style === '真实测评' ? '痛点种草' : '真实测评',\n persona: scriptBrief.persona === '通勤敏感肌女生' ? '熬夜党通勤女性' : '通勤敏感肌女生',\n styleNote: '已根据商品卖点重新推荐',\n personaNote: '已根据使用场景重新推荐',\n };\n pushMsg('ai', briefConfirmHtml('我重新给你配了一组创作方向,确认后就生成镜头脚本。'));\n saveState();\n renderChat();\n } else if (action === 'style') {\n const box = trigger?.closest('.script-brief-select');\n if (!box) return;\n document.querySelectorAll('.script-brief-select.open').forEach(el => { if (el !== box) el.classList.remove('open'); });\n box.classList.toggle('open');\n trigger.setAttribute('aria-expanded', box.classList.contains('open') ? 'true' : 'false');\n } else if (action === 'persona') {\n const box = trigger?.closest('.script-brief-select');\n if (!box) return;\n document.querySelectorAll('.script-brief-select.open').forEach(el => { if (el !== box) el.classList.remove('open'); });\n box.classList.toggle('open');\n trigger.setAttribute('aria-expanded', box.classList.contains('open') ? 'true' : 'false');\n }\n }\n\n /* 自动从脚本(painting + dialog 文本)抽取人物 / 场景关键词 · 白名单匹配 */\n const CHAR_KEYWORDS = ['女主', '男主', '同事', '闺蜜', '男友', '女友', '妈妈', '爸爸', '老师', '同学', '朋友', '路人', '主播', '老板'];\n const SCENE_KEYWORDS = ['书桌', '卫生间', '床头', '化妆台', '办公桌', '客厅', '厨房', '卧室', '阳台', '电梯', '咖啡店', '公司', '镜前', '桌面', '会议室', '车里', '公园', '商场'];\n function extractScriptTags() {\n const text = shots.map(s => (s.painting || '') + ' ' + (s.dialog || '')).join(' ');\n const dedup = arr => Array.from(new Set(arr));\n const exist = { char: new Set(scriptTags.char), scene: new Set(scriptTags.scene) };\n const newChar = CHAR_KEYWORDS.filter(k => text.includes(k) && !exist.char.has(k));\n const newScene = SCENE_KEYWORDS.filter(k => text.includes(k) && !exist.scene.has(k));\n scriptTags.char = dedup([...scriptTags.char, ...newChar]);\n scriptTags.scene = dedup([...scriptTags.scene, ...newScene]);\n }\n function renderScriptTags() {\n ['char', 'scene'].forEach(kind => {\n const group = document.querySelector(`.script-tags .tag-group[data-kind=\"${kind}\"]`);\n if (!group) return;\n const addBtn = group.querySelector('.tag-add');\n group.querySelectorAll('.script-tag').forEach(el => el.remove());\n scriptTags[kind].forEach((name, i) => {\n const chip = document.createElement('span');\n chip.className = 'script-tag';\n chip.innerHTML = `<span class=\"t\" contenteditable=\"true\" spellcheck=\"false\" data-i=\"${i}\">${name}</span><button class=\"x\" type=\"button\" aria-label=\"删除\">×</button>`;\n group.insertBefore(chip, addBtn);\n });\n group.querySelectorAll('.script-tag').forEach((chip, i) => {\n const t = chip.querySelector('.t');\n const x = chip.querySelector('.x');\n x.addEventListener('click', (e) => { e.stopPropagation(); scriptTags[kind].splice(i, 1); saveState(); renderScriptTags(); });\n t.addEventListener('blur', () => {\n const v = (t.textContent || '').trim();\n if (!v) { scriptTags[kind].splice(i, 1); renderScriptTags(); }\n else { scriptTags[kind][i] = v; }\n saveState();\n });\n t.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); t.blur(); } });\n });\n });\n }\n function bindTagAdders() {\n document.querySelectorAll('.script-tags .tag-add').forEach(btn => {\n btn.addEventListener('click', () => {\n const kind = btn.parentElement.dataset.kind;\n scriptTags[kind].push('');\n saveState();\n renderScriptTags();\n const group = document.querySelector(`.script-tags .tag-group[data-kind=\"${kind}\"]`);\n const chips = group.querySelectorAll('.script-tag .t');\n const last = chips[chips.length - 1];\n if (last) {\n last.focus();\n const sel = window.getSelection();\n const range = document.createRange();\n range.selectNodeContents(last);\n sel.removeAllRanges(); sel.addRange(range);\n }\n });\n });\n }\n\n function now() {\n const d = new Date();\n return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');\n }\n function pushMsg(role, html) { chatMsgs.push({ role, html, time: now() }); }\n function saveState() {\n ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags, scriptBrief });\n renderBriefSummary();\n }\n function loadState() {\n const saved = ProjectStore.data.stage1;\n if (!saved) { renderBriefSummary(); return; }\n if (Array.isArray(saved.shots)) shots = saved.shots;\n if (Array.isArray(saved.chatMsgs)) chatMsgs = saved.chatMsgs;\n if (saved.mode) mode = saved.mode;\n if (saved.scriptBrief && typeof saved.scriptBrief === 'object') scriptBrief = { ...scriptBrief, ...saved.scriptBrief };\n else if (mode) scriptBrief = makeBrief(mode);\n if (saved.scriptTags && Array.isArray(saved.scriptTags.char) && Array.isArray(saved.scriptTags.scene)) {\n scriptTags = saved.scriptTags;\n }\n if (migrateBriefMessages()) {\n ProjectStore.saveStage('stage1', { shots, chatMsgs, mode, scriptTags, scriptBrief });\n }\n renderBriefSummary();\n }\n function getDefaultDraft() {\n return [\n { id: 'sh1', painting: '中景慢推 · 深夜居家书桌全景。屏幕仍亮着 PPT,女主背影瘫在椅子上,屏幕冷光 + 台灯暖光对比。字幕\"凌晨 02:14\"淡入。', dialog: '(无台词 · BGM 渐起)', duration: 5 },\n { id: 'sh2', painting: '近景 · 卫生间镜前。女主低头看脸,T 区起皮、暗沉特写,冷白灯偏惨。', dialog: '\"做完这版稿又是凌晨两点……(叹气)脸已经不能看了。\"', duration: 5 },\n { id: 'sh3', painting: '俯拍特写 · 回到书桌,拉开抽屉。囤好的透真补水面膜露半角,手伸进去抽出一片。', dialog: '\"还好抽屉里囤了透真玻尿酸面膜。\"', duration: 5 },\n { id: 'sh4', painting: '桌面微距特写 · 撕开锡纸包装的瞬间。30g 厚精华液缓缓滴落,面膜布展开,质地拉丝可见。', dialog: '\"30g 一片,精华液比普通面膜厚整整三倍。\"', duration: 6 },\n { id: 'sh5', painting: '床头近景 · 女主敷好面膜闭眼躺下,台灯暖光打在脸侧。膜布贴合脸型,边缘服帖。', dialog: '\"贴上去那一瞬间 —— 凉凉的,像把皮肤泡了一次澡。\"', duration: 6 },\n { id: 'sh6', painting: '中景 · 第二天清晨化妆台。阳光透过窗帘,女主对镜上妆,皮肤透亮、粉底服帖。同事画外音\"你最近用啥了\"。', dialog: '\"第二天脸是软的,粉底都不卡了。同事都跑来问。\"', duration: 8 },\n { id: 'sh7', painting: '平铺俯拍 · 桌面五片装产品 + 单片包装。价格 \"618 · 5 片 ¥39.9\" 弹出,购物车图标右下角浮现。', dialog: '\"618 五片 39.9,自用送人都合适。链接放评论区。\"', duration: 5 },\n ];\n }\n function completeAiJob() {\n const existing = new Set(shots.map(s => s.id));\n getDefaultDraft().forEach(s => {\n if (!existing.has(s.id)) shots.push(s);\n });\n chatMsgs = chatMsgs.filter(x => !(x.role === 'ai' && /ai-thinking/.test(x.html)));\n if (!chatMsgs.some(x => x.html.includes('初稿完成'))) {\n pushMsg('ai', '初稿完成。点击任意卡片文字可直接编辑;鼠标移到卡片之间会出现「+ 添加分镜」。');\n }\n ProjectStore.finishJob('stage1-script');\n ProjectStore.record('stage1.script.ready', { shots: shots.length });\n saveState();\n renderChat();\n renderShots();\n }\n function resumeAiJobIfNeeded() {\n const job = ProjectStore.getJob('stage1-script');\n if (!job || job.status !== 'running') return;\n const remaining = Math.max(0, (job.finishAt || Date.now()) - Date.now());\n if (!chatMsgs.some(x => /ai-thinking/.test(x.html))) {\n pushMsg('ai', '<span class=\"ai-thinking\">脚本生成仍在后台排队 <span class=\"dots\"><span></span><span></span><span></span></span></span>');\n }\n saveState();\n renderChat();\n window.setTimeout(completeAiJob, remaining);\n }\n\n function renderChat() {\n const body = $cb(); if (!body) return;\n if (chatMsgs.length === 0 && !mode) {\n body.innerHTML = `<div class=\"chat-empty\">\n <div class=\"ce-title\">选择一种生成方式开始</div>\n <div class=\"ce-hint\">// 三种,由「最省事」到「最保真原意」</div>\n <div class=\"chat-modes\">\n <button class=\"chat-mode primary\" data-mode=\"ai\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z\"/></svg>AI 全生</button>\n <button class=\"chat-mode\" data-mode=\"theme\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/><path d=\"M15 14a4.65 4.65 0 0 0 1.4-2.5A6 6 0 1 0 6 8c0 1 .23 2.23 1.5 3.5\"/></svg>一句话主题</button>\n <button class=\"chat-mode\" data-mode=\"manual\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6\"/></svg>自带脚本</button>\n </div>\n </div>`;\n body.querySelectorAll('.chat-mode').forEach(btn => {\n btn.addEventListener('click', () => pickMode(btn.dataset.mode));\n });\n return;\n }\n body.innerHTML = chatMsgs.map(msg => {\n if (msg.role === 'ai') {\n 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>`;\n }\n return `<div class=\"msg user\"><div class=\"bubble\">${msg.html}</div><div class=\"time\">${msg.time}</div></div>`;\n }).join('');\n body.querySelectorAll('[data-brief-act]').forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleBriefAction(btn.dataset.briefAct, btn);\n });\n });\n body.querySelectorAll('[data-brief-pick]').forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleBriefPick(btn.dataset.briefPick, btn.dataset.value);\n });\n });\n body.scrollTop = body.scrollHeight;\n }\n\n function renderShots() {\n const body = $sb(); if (!body) return;\n const meta = $sm();\n const phMeta = document.getElementById('page-head-shots-meta'); // 头部已移除,可能为 null · 下方写入全程 ?. 防御\n if (shots.length === 0) {\n body.innerHTML = `<div class=\"shots-empty\">\n <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>\n <div class=\"empty-title\">还没有镜头脚本</div>\n <div class=\"empty-hint\">// 跟右侧脚本助手对话<br>选择一种方式生成你的第一稿</div>\n </div>`;\n if (meta) meta.textContent = '· 空 · 待生成';\n if (phMeta) phMeta.textContent = '待生成镜头脚本';\n renderScriptTags();\n return;\n }\n let cum = 0;\n let html = '';\n shots.forEach((s, i) => {\n const start = cum;\n cum += (s.duration || 5);\n const tlabel = start + '-' + cum + 's';\n html += `<div class=\"shot-card\" data-id=\"${s.id}\">\n <div class=\"shot-head\">\n <div class=\"shot-num\">${i + 1}</div>\n <div class=\"shot-time\">${tlabel}</div>\n <span class=\"spacer\"></span>\n <button class=\"icon-mini-btn\" title=\"重写本场\" data-act=\"regen\" data-id=\"${s.id}\">↻</button>\n <button class=\"icon-mini-btn\" title=\"删除本场\" data-act=\"del\" data-id=\"${s.id}\">×</button>\n </div>\n <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>\n <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>\n </div>`;\n // 每张卡片后都跟一个 gap(包括最后一张),允许在任意位置 hover 加分镜\n html += `<div class=\"shot-insert-gap\" data-after=\"${s.id}\"><button class=\"add-shot-btn\" data-act=\"add-here\" data-after=\"${s.id}\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>添加分镜</button></div>`;\n });\n body.innerHTML = html;\n if (meta) meta.textContent = '· ' + shots.length + ' 镜 · 0-' + cum + 's';\n if (phMeta) phMeta.textContent = shots.length + ' 镜 · 0-' + cum + 's';\n body.querySelectorAll('.shot-v[contenteditable]').forEach(el => {\n el.addEventListener('focus', () => { el.dataset.empty = 'false'; });\n el.addEventListener('blur', () => {\n const card = el.closest('.shot-card');\n const id = card.dataset.id;\n const field = el.dataset.field;\n const v = el.textContent.trim();\n const s = shots.find(x => x.id === id);\n if (s) s[field] = v;\n if (!v) el.dataset.empty = 'true';\n ProjectStore.record('stage1.shot.edited', { id, field });\n saveState();\n });\n });\n body.querySelectorAll('[data-act]').forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const act = btn.dataset.act;\n const id = btn.dataset.id;\n const after = btn.dataset.after;\n if (act === 'del') {\n shots = shots.filter(x => x.id !== id);\n ProjectStore.record('stage1.shot.deleted', { id });\n saveState();\n renderShots();\n } else if (act === 'regen') {\n Shell.toast('已请求重写本场', '↻ shot-' + id);\n ProjectStore.record('stage1.shot.regen', { id });\n } else if (act === 'add-here') {\n const idx = shots.findIndex(x => x.id === after);\n shots.splice(idx + 1, 0, { id: 'sh' + Date.now(), painting: '', dialog: '', duration: 5 });\n ProjectStore.record('stage1.shot.added', { after });\n saveState();\n renderShots();\n }\n });\n });\n // 镜头有变,刷新人物/场景标签(增量抽取 · 已有的保留)\n extractScriptTags();\n renderScriptTags();\n }\n\n function pickMode(m) {\n mode = m;\n scriptBrief = makeBrief(m);\n ProjectStore.record('stage1.mode.selected', { mode: m });\n pushMsg('user', MODE_USER_COPY[m] || '选择脚本来源');\n if (m === 'ai') {\n pushMsg('ai', briefConfirmHtml('我会根据商品信息直接生成第一版。生成前先确认创作方向。'));\n } else if (m === 'theme') {\n pushMsg('ai', '好,请给我一句话主题(5-30 字),例如:<br>· 熬夜党的急救面膜<br>· 加班吃啥不内疚<br>我会先根据这句话补全风格和人物设定,再让你确认。');\n } else if (m === 'manual') {\n pushMsg('ai', '好,把你的脚本(旁白 / 镜头描述均可)粘贴到下面输入框。我会先识别风格和人物设定;识别不到的部分,再用商品信息补齐。');\n }\n saveState();\n renderChat();\n }\n\n function init() {\n loadState();\n renderChat();\n renderShots();\n resumeAiJobIfNeeded();\n ProjectStore.restoreFields();\n document.getElementById('chat-clear-btn')?.addEventListener('click', () => {\n chatMsgs = []; mode = null; shots = []; scriptTags = { char: [], scene: [] };\n scriptBrief = makeBrief(null);\n ProjectStore.clearJob('stage1-script');\n ProjectStore.record('stage1.cleared');\n saveState();\n renderChat(); renderShots();\n });\n bindTagAdders();\n document.getElementById('chat-regen-btn')?.addEventListener('click', () => {\n Shell.toast('已请求整体重写', 'POST /script/regen');\n });\n document.addEventListener('click', (e) => {\n if (e.target.closest('.script-brief-select')) return;\n document.querySelectorAll('.script-brief-select.open').forEach(el => el.classList.remove('open'));\n });\n const sendBtn = document.getElementById('chat-send-btn');\n const ta = document.getElementById('chat-textarea');\n const attachRow = document.getElementById('chat-attach-row');\n let attachments = [];\n const renderAttach = () => {\n if (!attachRow) return;\n if (!attachments.length) { attachRow.hidden = true; attachRow.innerHTML = ''; return; }\n attachRow.hidden = false;\n attachRow.innerHTML = attachments.map((f, i) => `\n <span class=\"chat-attach-chip\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6\"/></svg>\n ${f.name.replace(/</g, '&lt;')}\n <button class=\"x\" data-rm=\"${i}\" aria-label=\"移除\">\n <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg>\n </button>\n </span>`).join('');\n attachRow.querySelectorAll('button[data-rm]').forEach(b => {\n b.addEventListener('click', () => {\n attachments.splice(+b.dataset.rm, 1);\n renderAttach();\n });\n });\n };\n const upBtn = document.getElementById('chat-upload-btn');\n const upInput = document.getElementById('chat-upload-input');\n if (upBtn && upInput) {\n upBtn.addEventListener('click', () => upInput.click());\n upInput.addEventListener('change', (e) => {\n const files = Array.from(e.target.files || []);\n if (!files.length) return;\n attachments.push(...files);\n renderAttach();\n Shell.toast('已附加脚本文件', files.map(f => f.name).join('、'));\n upInput.value = '';\n });\n }\n if (sendBtn && ta) {\n const send = () => {\n const v = ta.value.trim();\n if (!v && !attachments.length) return;\n const fileTags = attachments.length\n ? `<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>`\n : '';\n pushMsg('user', fileTags + (v ? v.replace(/</g, '&lt;') : '<span class=\"muted-2\">(已附加文件)</span>'));\n const fileCt = attachments.length;\n ta.value = '';\n attachments = []; renderAttach();\n ProjectStore.record('stage1.chat.sent', { hasText: !!v, files: fileCt });\n saveState();\n renderChat();\n setTimeout(() => {\n const directionIntent = /小红书|种草|测评|评测|痛点|熬夜|学生|宝妈|妈妈|人物|风格|口吻|换成/.test(v);\n if (mode && (!shots.length || directionIntent)) {\n tuneBriefByText(v);\n const intro = directionIntent\n ? '已更新创作方向。确认后我会按这个方向重写镜头脚本。'\n : '我先根据你给的内容补全创作方向,确认后再生成镜头脚本。';\n pushMsg('ai', briefConfirmHtml(intro));\n } else {\n pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');\n }\n saveState();\n renderChat();\n }, 400);\n };\n sendBtn.addEventListener('click', send);\n ta.addEventListener('keydown', (e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); send(); }\n });\n }\n // 顶部「+ 追加一场」按钮已移除 — 添加分场改为卡片间 hover 时出现「+ 添加分场」\n }\n\n return { init };\n})();\nStage1.init();\n\n/* ============================================================\n STAGE 2 · 基础资产 · 锚点 + 横滑预设 + 详情/库 modal\n ============================================================ */\nconst Stage2 = (function () {\n const MODEL_LIB = [\n { id: 'm1', name: '清新短发女', sub: '通勤白领',\n gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '中等', build: '纤细',\n hairLen: '短发', hairColor: '黑色', vibe: '清新', feature: '邻家气质 · 微笑亲和' },\n { id: 'm2', name: '甜美长发女', sub: '学生党',\n gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '偏小', build: '纤细',\n hairLen: '长发', hairColor: '深棕', vibe: '甜美', feature: '校园风 · 书卷气重' },\n { id: 'm3', name: '商务套装男', sub: '总裁 IP',\n gender: '男', age: '中年', region: '东亚', skin: '健康', height: '偏高', build: '标准',\n hairLen: '短发', hairColor: '黑色', vibe: '稳重', feature: '商务精英范 · 西装常驻' },\n { id: 'm4', name: '宝妈居家女', sub: '家庭决策',\n gender: '女', age: '中年', region: '东亚', skin: '白皙', height: '中等', build: '标准',\n hairLen: '中发', hairColor: '棕黑', vibe: '温柔', feature: '居家氛围 · 决策力强' },\n { id: 'm5', name: '运动健身女', sub: '健身博主',\n gender: '女', age: '青年', region: '东亚', skin: '健康', height: '偏高', build: '运动',\n hairLen: '中发', hairColor: '栗色', vibe: '活力', feature: '马尾 · 健身房常客' },\n { id: 'm6', name: '少年学生男', sub: 'Z 世代',\n gender: '男', age: '青年', region: '东亚', skin: '白皙', height: '中等', build: '纤细',\n hairLen: '短发', hairColor: '黑色', vibe: '阳光', feature: '校服感 · 朝气十足' },\n ];\n const SCENE_LIB = [\n { id: 's1', name: '日系卧室', sub: '居家温柔' },\n { id: 's2', name: '咖啡厅工位', sub: '通勤场景' },\n { id: 's3', name: '梳妆台', sub: '美妆个护' },\n { id: 's4', name: '健身房', sub: '运动场景' },\n { id: 's5', name: '厨房料理台', sub: '家居家电' },\n { id: 's6', name: '日落天台', sub: '氛围户外' },\n ];\n\n // 商品名 / 项目名已提升到 page 顶层 script(see CURRENT_PRODUCT_NAME / PROJECT_TITLE)\n // 此处通过闭包引用即可\n\n const ASSET_DETAILS = {\n 'ch-linxi': { kind: 'character', title: '林夕 · 都市白领', hasTri: true, info: [['类别', '人物 · 主角'], ['年龄', '25-30'], ['服装', '宽松米色家居服'], ['妆面', '日常裸妆 · 略疲倦'], ['用途', '主角出镜 · 痛点共鸣'], ['状态', '已确认']] },\n 'ch-anan': { kind: 'character', title: '阿楠 · 朋友/同事', hasTri: false, info: [['类别', '人物 · 对照角色'], ['年龄', '25-30'], ['服装', '白色衬衫 / 精致'], ['用途', '对照出镜'], ['状态', '生成中']] },\n 'sc-desk': { kind: 'scene', title: '深夜办公桌', info: [['类别', '场景 · 室内'], ['光线', '台灯暖光 · 屏幕冷光'], ['用途', '镜 1 痛点'], ['状态', '已确认']] },\n 'sc-bed': { kind: 'scene', title: '卧室床头', info: [['类别', '场景 · 室内'], ['光线', '夜灯暖光'], ['用途', '镜 4 敷膜'], ['状态', '已确认']] },\n 'sc-subway': { kind: 'scene', title: '通勤地铁', info: [['类别', '场景 · 室内'], ['用途', '镜 5 对照'], ['状态', '失败 · 待重跑']] },\n 'prod-main': { kind: 'product', title: CURRENT_PRODUCT_NAME, hasTri: false, info: [['类别', '商品 · 当前项目'], ['名称', CURRENT_PRODUCT_NAME], ['三视图', '待生成'], ['状态', '缺三视图']] },\n };\n\n // ─── 统一详情面板渲染 ───\n 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); }\n function _fmtAssetId(name, kind) {\n const seed = _hashCode(name);\n const code = String(seed % 1000).padStart(3, '0');\n return 'ASSET-20240520-' + (kind === 'model' ? 'M' : kind === 'scene' ? 'S' : 'P') + code;\n }\n function _fmtFileSize(name) { const seed = _hashCode(name); return (4 + (seed % 100) / 10).toFixed(1) + 'MB'; }\n function _fmtFavCt(name) { return String(8 + _hashCode(name) % 80); }\n function _fmtDlCt(name) { const n = 200 + _hashCode(name) % 1800; return (n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n)); }\n\n function renderAssetDetail(payload) {\n // payload: { title, tagText, leadLabel, kind ('actor'|'scene'|'product'), ratio, intro, tags[], thumbs[], props[], hasTri, missingTriHint, applyLabel, ownPortraits[], ownTriVersions[] }\n document.getElementById('asset-detail-title').textContent = payload.title;\n document.getElementById('asset-detail-kind').textContent = '/ ' + payload.tagText;\n\n // 主立绘 · 用户上传有 portraits[0].url 时显示真实图片;否则占位\n const leadEl = document.getElementById('asset-detail-lead-img');\n // 把当前主图 src/name 挂在 leadEl 的 dataset 上,zoom 按钮读 dataset 始终拿到最新值\n function _adSetLead(p) {\n if (!leadEl) return;\n const old = leadEl.querySelector('img.ad-lead-pic');\n if (old) old.remove();\n const ph = leadEl.querySelector('.ph-frame');\n if (p && p.url) {\n if (ph) ph.style.display = 'none';\n const img = document.createElement('img');\n img.className = 'ad-lead-pic'; img.src = p.url; img.alt = p.name || payload.title;\n img.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;';\n leadEl.appendChild(img);\n leadEl.dataset.curSrc = p.url; leadEl.dataset.curName = p.name || payload.title;\n } else {\n if (ph) { ph.style.display = ''; ph.textContent = (p && p.label) || payload.leadLabel || payload.title; }\n leadEl.dataset.curSrc = ''; leadEl.dataset.curName = (p && p.label) || payload.leadLabel || payload.title;\n }\n }\n leadEl.innerHTML = `<span class=\"ph-frame\">${payload.leadLabel || payload.title}</span>`;\n // 立绘 zoom 按钮 → 打开 lightbox(单次绑定,值从 dataset 读)\n const _leadZoomBtn = document.getElementById('asset-detail-zoom-btn');\n if (_leadZoomBtn && !_leadZoomBtn.dataset.bound) {\n _leadZoomBtn.dataset.bound = '1';\n _leadZoomBtn.addEventListener('click', e => {\n e.stopPropagation();\n if (window.Shell?._openLightbox) Shell._openLightbox(leadEl.dataset.curSrc || '', leadEl.dataset.curName || '');\n });\n }\n\n // 缩略图 strip · 用户上传有 ownPortraits 数组 → 显示多张真实图;平台预设 → 仅 1 张占位\n const ownPortraits = (payload.ownPortraits && payload.ownPortraits.length) ? payload.ownPortraits : null;\n const thumbs = ownPortraits || (payload.thumbs && payload.thumbs.length ? payload.thumbs.map(t => ({ label: t })) : [{ label: payload.kind === 'scene' ? '场景' : '立绘' }]);\n const thumbsEl = document.getElementById('asset-detail-thumbs');\n thumbsEl.innerHTML = thumbs.map((p, i) => {\n const inner = p.url\n ? `<img src=\"${p.url}\" alt=\"${(p.name||'').replace(/\"/g,'&quot;')}\" style=\"width:100%;height:100%;object-fit:cover;display:block;\">`\n : `<span class=\"ph-frame\">${p.label || ('v'+(i+1))}</span>`;\n return `<div class=\"thumb placeholder${i === 0 ? ' active' : ''}\" data-idx=\"${i}\">${inner}</div>`;\n }).join('');\n _adSetLead(thumbs[0]);\n thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {\n thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n _adSetLead(thumbs[+t.dataset.idx]);\n }));\n\n // 三视图区\n const tri = document.getElementById('asset-detail-tri');\n const triSection = document.getElementById('asset-detail-tri-section');\n const tip = document.getElementById('asset-detail-tip');\n const ratioChip = document.getElementById('asset-detail-ratio');\n const ownTri = (payload.ownTriVersions && payload.ownTriVersions.length) ? payload.ownTriVersions : null;\n if (payload.kind === 'scene') {\n triSection.style.display = 'none';\n } else if (payload.kind === 'actor') {\n triSection.style.display = '';\n tri.style.display = '';\n tri.classList.remove('actor');\n ratioChip.textContent = '16:9';\n tip.style.display = 'none';\n const _zoomBtn = `<button class=\"ad-zoom-btn\" type=\"button\" data-zoom-tri aria-label=\"查看大图\" title=\"查看大图\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg></button>`;\n if (ownTri) {\n const cur = ownTri[ownTri.length - 1];\n const stripHtml = ownTri.map((v, i) => `<div class=\"v-thumb${i === ownTri.length - 1 ? ' active' : ''}\" data-idx=\"${i}\" title=\"${v.label} · ${v.ts}\"><span class=\"v\">${v.label}</span></div>`).join('');\n tri.innerHTML = `\n <div class=\"placeholder\"><span class=\"ph-frame\" id=\"ad-tri-main-lbl\">${payload.title} · ${cur.label} · ${cur.ts}</span>${_zoomBtn}</div>\n <div class=\"md-view-versions\" style=\"margin-top:8px;\">${stripHtml}</div>`;\n const mainLbl = tri.querySelector('#ad-tri-main-lbl');\n tri.querySelectorAll('.v-thumb').forEach(t => t.addEventListener('click', () => {\n tri.querySelectorAll('.v-thumb').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n const v = ownTri[+t.dataset.idx];\n if (mainLbl) mainLbl.textContent = `${payload.title} · ${v.label} · ${v.ts}`;\n }));\n } else {\n tri.innerHTML = `<div class=\"placeholder\"><span class=\"ph-frame\">${payload.title} · 三视图 (正/侧/背)</span>${_zoomBtn}</div>`;\n }\n tri.querySelector('[data-zoom-tri]')?.addEventListener('click', e => {\n e.stopPropagation();\n const lbl = tri.querySelector('#ad-tri-main-lbl')?.textContent || (payload.title + ' · 三视图');\n if (window.Shell?._openLightbox) Shell._openLightbox('', lbl);\n });\n } else {\n // product\n triSection.style.display = '';\n tri.style.display = '';\n tri.classList.remove('actor');\n ratioChip.textContent = '16:9';\n if (payload.hasTri !== false) {\n tri.innerHTML = `<div class=\"placeholder\"><span class=\"ph-frame\">${payload.title} · 三视图</span><button class=\"ad-zoom-btn\" type=\"button\" aria-label=\"查看大图\" title=\"查看大图\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5\"/></svg></button></div>`;\n tip.style.display = 'none';\n tri.querySelector('.ad-zoom-btn')?.addEventListener('click', e => {\n e.stopPropagation();\n if (window.Shell?._openLightbox) Shell._openLightbox('', payload.title + ' · 三视图');\n });\n } else {\n tri.innerHTML = `<div class=\"placeholder missing\" data-tri=\"0\">\n <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>\n <span>${payload.missingTriHint || '暂未生成三视图(16:9 单图)'}</span>\n </div>`;\n tip.style.display = 'flex';\n }\n }\n\n // 简介\n document.getElementById('asset-detail-intro').textContent = payload.intro || '暂无简介';\n\n // 标签 chips\n const tags = payload.tags && payload.tags.length ? payload.tags : [];\n document.getElementById('asset-detail-tags').innerHTML = tags.map(t => `<span class=\"ad-tag-chip\">${t}</span>`).join('')\n + `<button class=\"ad-tag-add\" type=\"button\" title=\"添加标签\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M8 3v10M3 8h10\"/></svg></button>`;\n\n // 属性表 (3 列 × N 行 grid)\n const props = payload.props || [];\n document.getElementById('asset-detail-props').innerHTML = props\n .map(([k, v]) => `<div class=\"ad-prop\"><span class=\"k\">${k}</span><span class=\"v\">${v}</span></div>`)\n .join('');\n\n // apply 按钮文案 (默认「使用该资产」)\n const applyBtn = document.getElementById('asset-detail-apply-btn');\n if (applyBtn) applyBtn.textContent = payload.applyLabel || '使用该资产';\n\n document.getElementById('asset-detail-modal').classList.add('show');\n }\n\n function openStripDetail(name, sub, kind) {\n // 优先查用户上传:演员 MODEL_OWN / 场景 SCENE_OWN(带 portraits + triVersions)\n const ownArr = kind === 'model' ? MODEL_OWN : SCENE_OWN;\n const ownItem = ownArr.find(x => x.name === name) || null;\n const isOwn = !!ownItem;\n const sourceLabel = isOwn ? '我的上传' : '平台预设';\n\n if (kind === 'model') {\n const actor = !isOwn ? MODEL_LIB.find(x => x.name === name) : null;\n const a = actor || { gender: '女', age: '青年', region: '东亚', skin: '白皙', height: '中等',\n build: '标准', hairLen: '中发', hairColor: '黑色', vibe: '清新', feature: sub || (isOwn ? '我的上传演员' : '预设演员') };\n const tags = [a.vibe, a.age, a.hairLen, a.region, a.skin].filter(Boolean);\n const props = [\n ['性别', a.gender], ['种族', a.region], ['作品ID', _fmtAssetId(name, 'model')],\n ['年龄段', a.age], ['气质', a.vibe], ['创作人', 'Airshelf'],\n ['身高', a.height], ['体格', a.build], ['文件大小', _fmtFileSize(name)],\n ['发型', a.hairLen + ' · ' + a.hairColor], ['来源', sourceLabel], ['发布时间', '2024-05-20'],\n ];\n renderAssetDetail({\n title: name, tagText: '人物 · ' + (isOwn ? '我的演员' : '预设演员'), leadLabel: name + ' · 立绘',\n kind: 'actor', intro: a.feature || sub || '',\n tags, props, applyLabel: '应用到当前项目',\n ownPortraits: isOwn ? ownItem.portraits : null,\n ownTriVersions: isOwn ? ownItem.triVersions : null,\n });\n } else {\n const props = [\n ['类别', '场景 · ' + (isOwn ? '我的上传' : '预设')], ['标签', sub || '-'], ['作品ID', _fmtAssetId(name, 'scene')],\n ['来源', sourceLabel], ['用途', '本项目场景资产'], ['创作人', 'Airshelf'],\n ['镜头', '通用'], ['光线', '自然光'], ['文件大小', _fmtFileSize(name)],\n ];\n renderAssetDetail({\n title: name, tagText: '场景 · ' + (isOwn ? '我的场景' : '预设'), leadLabel: name + ' · 主图',\n kind: 'scene', intro: sub || '场景资产',\n tags: [sub].filter(Boolean), props, applyLabel: '应用到当前项目',\n ownPortraits: isOwn ? ownItem.portraits : null,\n });\n }\n }\n\n function renderStrip(containerId, items, kind) {\n const el = document.getElementById(containerId);\n if (!el) return;\n 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}\">\n <div class=\"placeholder thumb-2\"><span class=\"ph-frame\">${it.name}</span></div>\n <div class=\"body-2\">\n <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>\n </div>\n </div>`).join('');\n el.querySelectorAll('.asset-card-2').forEach(card => {\n card.addEventListener('click', () => {\n const name = card.dataset.stripName;\n const sub = card.dataset.stripSub;\n openStripDetail(name, sub, kind);\n });\n });\n }\n\n function openDetail(id) {\n const d = ASSET_DETAILS[id];\n if (!d) return;\n const kindMap = { character: 'actor', scene: 'scene', product: 'product' };\n const kindLabelMap = { character: '人物', scene: '场景', product: '商品' };\n const baseProps = d.info.slice();\n baseProps.push(['作品ID', _fmtAssetId(d.title, d.kind === 'character' ? 'model' : d.kind === 'scene' ? 'scene' : 'product')]);\n baseProps.push(['创作人', 'Airshelf']);\n baseProps.push(['文件大小', _fmtFileSize(d.title)]);\n baseProps.push(['发布时间', '2024-05-20']);\n renderAssetDetail({\n title: d.title,\n tagText: kindLabelMap[d.kind] + (d.kind === 'character' ? ' · 主角/对照' : d.kind === 'product' ? ' · 当前项目' : ' · 预设'),\n leadLabel: d.title,\n kind: kindMap[d.kind],\n intro: (d.info.find(r => r[0] === '用途') || [, ''])[1] || '资产用于本项目生成',\n tags: d.info.slice(0, 3).map(r => r[1]).filter(Boolean),\n props: baseProps,\n hasTri: !!d.hasTri,\n missingTriHint: '暂未生成三视图(16:9 单图)',\n applyLabel: '应用到当前项目',\n });\n }\n // 用户上传的演员 / 场景(分别累积,source='own')\n const MODEL_OWN = [];\n const SCENE_OWN = [];\n let _curLibKind = 'model';\n let _curLibSource = 'all';\n function _libItemsForSource(kind, src) {\n const isModel = kind === 'model';\n const presets = isModel ? MODEL_LIB : SCENE_LIB;\n const owns = isModel ? MODEL_OWN : SCENE_OWN;\n if (src === 'preset') return presets;\n if (src === 'own') return owns;\n return [...owns, ...presets];\n }\n function _renderLibGrid() {\n const isModel = _curLibKind === 'model';\n const items = _libItemsForSource(_curLibKind, _curLibSource);\n const grid = document.getElementById('ml-grid');\n\n // 「添加演员 / 添加场景」入口卡 · 平台预设是只读素材,不展示入口\n const uploadCardHTML = (_curLibSource === 'preset') ? '' : `\n <div class=\"ml-card ml-upload-card\" id=\"ml-upload-card\" role=\"button\" tabindex=\"0\" aria-label=\"${isModel ? '添加演员' : '添加场景'}\">\n <div class=\"up-thumb\">\n <div class=\"up-plus\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </div>\n </div>\n <div class=\"ml-card-nm\">${isModel ? '添加演员' : '添加场景'}</div>\n <div class=\"ml-card-sub\">// AI 生成 / 本地上传</div>\n </div>\n `;\n\n grid.innerHTML = uploadCardHTML + items.map(it => `\n <div class=\"ml-card\" data-name=\"${it.name}\" data-sub=\"${it.sub}\">\n <div class=\"placeholder\"><span class=\"ph-frame\">${it.name}</span></div>\n <div class=\"ml-card-nm\">${it.name}</div>\n <div class=\"ml-card-sub\">${it.sub}</div>\n </div>\n `).join('');\n\n const upCard = grid.querySelector('#ml-upload-card');\n if (upCard) {\n upCard.addEventListener('click', () => _openLibUploadChoice());\n upCard.addEventListener('keydown', e => {\n if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _openLibUploadChoice(); }\n });\n }\n\n // 普通卡片 click → 应用 / 详情\n grid.querySelectorAll('.ml-card:not(.ml-upload-card)').forEach(card => {\n card.addEventListener('click', (e) => {\n const name = card.dataset.name;\n const sub = card.dataset.sub;\n if (e.target.closest('[data-apply]')) {\n e.stopPropagation();\n Shell.toast('已应用「' + name + '」', isModel ? '演员库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设') : '场景库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设'));\n document.getElementById('ml-modal-bg').classList.remove('show');\n return;\n }\n openStripDetail(name, sub, _curLibKind);\n });\n });\n }\n\n /* ════════ 添加演员 / 场景 · 工作台(同模特库 ml-canvas)════════ */\n // 词典:模特→演员、立绘→立绘/场景图、三视图(场景隐藏)\n const _MC_LABELS = {\n actor: {\n title: '添加演员', mono: '// 添加演员 · 工作台',\n emptyTitle: '用 AI 生成一位新演员',\n emptyDesc: '描述外形 + 风格 + 服装,AI 会同时生成立绘 + 正/侧/背三视图,加入演员库。',\n nameSec: '// 演员姓名', namePh: '给演员起个名字…',\n portraitSec: '// 演员立绘', portraitPick: '点击或拖入立绘',\n portraitAiDesc: '在左侧 AI 生成后<br>点击想要的立绘添加到这里',\n promptPh: '描述演员外形、年龄、风格、服饰…例如:清新校园风女生',\n commit: '加入演员库', toast: '已加入演员库',\n examples: [\n { ex: '清新校园风女生,黑色长直发,白色 T 恤 + 牛仔短裙,室内自然光', lbl: '清新校园风女生' },\n { ex: '都市 OL 通勤,黑色西装套装,30 岁知性气质', lbl: '都市 OL 通勤' },\n { ex: '健身房教练男性,运动背心 + 短裤,健身房布景', lbl: '健身教练 · 男' },\n { ex: '日系简约女生,棕色短发,米色针织衫,温柔气质', lbl: '日系简约' },\n ],\n },\n scene: {\n title: '添加场景', mono: '// 添加场景 · 工作台',\n emptyTitle: '用 AI 生成一处新场景',\n emptyDesc: '描述类型 + 氛围 + 光线,AI 会生成场景图,加入场景库。',\n nameSec: '// 场景名称', namePh: '给场景起个名字…',\n portraitSec: '// 场景图', portraitPick: '点击或拖入场景图',\n portraitAiDesc: '在左侧 AI 生成后<br>点击想要的场景图添加到这里',\n promptPh: '描述场景类型、氛围、光线…例如:夜景咖啡馆,暖光灯',\n commit: '加入场景库', toast: '已加入场景库',\n examples: [\n { ex: '日系咖啡馆室内,木质桌面,落地窗自然光,午后氛围', lbl: '日系咖啡馆' },\n { ex: '城市天台夜景,霓虹灯背景,冷色调高对比', lbl: '城市天台夜景' },\n { ex: '极简白色摄影棚,无缝背景,顶光均匀', lbl: '极简摄影棚' },\n { ex: '复古港风街头,湿润地面,招牌灯反射', lbl: '港风街头' },\n ],\n },\n };\n\n function _mcKind() { return _curLibKind === 'model' ? 'actor' : 'scene'; }\n function _mcOwnsArr() { return _curLibKind === 'model' ? MODEL_OWN : SCENE_OWN; }\n function _mcPresetsArr() { return _curLibKind === 'model' ? MODEL_LIB : SCENE_LIB; }\n\n // ─── 工作台 DOM 引用 ───\n const _uploadCanvas = document.getElementById('ml-canvas');\n const _mcUp = document.getElementById('mc-up');\n\n function _applyKindToCanvas() {\n const k = _mcKind();\n const L = _MC_LABELS[k];\n _mcUp.dataset.kind = k;\n document.getElementById('ml-canvas-title').textContent = L.title;\n document.getElementById('ml-canvas-mono').textContent = L.mono;\n document.getElementById('mc-empty-title').textContent = L.emptyTitle;\n document.getElementById('mc-empty-desc').textContent = L.emptyDesc;\n document.getElementById('mc-up-name-label').textContent = L.nameSec;\n document.getElementById('mc-up-name').placeholder = L.namePh;\n document.getElementById('mc-up-portrait-label').textContent = L.portraitSec;\n document.getElementById('mc-portrait-ai-empty-desc').innerHTML = L.portraitAiDesc;\n document.getElementById('mc-portrait-local-t').textContent = L.portraitPick;\n // 场景仅允许 1 张本地图;演员允许多张\n const _localInput = document.getElementById('mc-portrait-local-input');\n const _localD = document.querySelector('.mc-portrait-local .drop .d');\n if (k === 'scene') {\n _localInput.removeAttribute('multiple');\n if (_localD) _localD.textContent = '仅支持 1 张 JPG / PNG / WEBP · ≤ 10MB';\n } else {\n _localInput.setAttribute('multiple', '');\n if (_localD) _localD.textContent = '支持多张 JPG / PNG / WEBP · ≤ 10MB / 张';\n }\n document.getElementById('mc-input-text').placeholder = L.promptPh;\n document.getElementById('mc-up-commit-label').textContent = L.commit;\n // 示例 chip 重渲(场景需要不同的示例)\n const ex = document.getElementById('mc-empty-examples');\n if (ex && !ex.dataset.cleanInit) {\n ex.dataset.cleanInit = '1';\n }\n ex.innerHTML = L.examples.map(e => `<button class=\"ex\" type=\"button\" data-ex=\"${e.ex.replace(/\"/g, '&quot;')}\">${e.lbl}</button>`).join('');\n // 重新绑定示例 click\n ex.querySelectorAll('.ex').forEach(b => {\n b.addEventListener('click', () => {\n const t = document.getElementById('mc-input-text');\n t.value = b.dataset.ex;\n t.dispatchEvent(new Event('input'));\n t.focus();\n });\n });\n }\n\n function _openLibUploadChoice() {\n _applyKindToCanvas();\n // 当前 kind 下重渲右侧栏:刷新底部状态文案(场景:名称 / 场景图,无三视图)\n if (typeof _renderRight === 'function') { try { _renderRight(); } catch(e) {} }\n _uploadCanvas.classList.add('show');\n _uploadCanvas.setAttribute('aria-hidden', 'false');\n }\n function _closeUploadCanvasNow() {\n _uploadCanvas.classList.remove('show');\n _uploadCanvas.setAttribute('aria-hidden', 'true');\n }\n\n // ─── 工作台状态(AI / Local 两个 tab 各自独立)───\n let _mcRightTab = 'ai';\n const _mcAi = { name: '', portrait: null, triVersions: [], triActiveIdx: -1 };\n const _mcLocal = { name: '', portraits: [], triVersions: [], triActiveIdx: -1 };\n function _mcCurState() { return _mcRightTab === 'ai' ? _mcAi : _mcLocal; }\n function _hasTri(s) { return s.triVersions.length > 0 && s.triActiveIdx >= 0; }\n function _kindRequiresTri() { return _mcKind() === 'actor'; }\n\n function _isWorkbenchDirty() {\n if (!_uploadCanvas.classList.contains('show')) return false;\n if ((_mcAi.name || '').trim()) return true;\n if (_mcAi.portrait) return true;\n if (_mcAi.triVersions.length > 0) return true;\n if ((_mcLocal.name || '').trim()) return true;\n if (_mcLocal.portraits.length > 0) return true;\n if (_mcLocal.triVersions.length > 0) return true;\n return false;\n }\n function _resetWorkbenchState() {\n if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');\n _mcAi.name = ''; _mcAi.portrait = null; _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n _mcLocal.name = ''; _mcLocal.portraits = []; _mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;\n const nameInput = document.getElementById('mc-up-name');\n if (nameInput) nameInput.value = '';\n _renderRight();\n }\n\n // ─── 二次确认弹窗 ───\n const _leaveBg = document.getElementById('mc-leave-bg');\n const _leaveBody = document.getElementById('mc-leave-body');\n let _leavePending = null;\n function _openLeaveConfirm(mode, onConfirm) {\n const ki = _mcKind() === 'actor' ? '加入演员库' : '加入场景库';\n if (mode === 'nav') {\n _leaveBody.innerHTML = `工作台已有内容,跳转到其他页面后<b>不会保存</b>。可继续编辑并点「${ki}」来保留进度。`;\n } else {\n _leaveBody.innerHTML = `工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「${ki}」来保留进度。`;\n }\n _leavePending = onConfirm || null;\n _leaveBg.classList.add('show');\n _leaveBg.setAttribute('aria-hidden', 'false');\n }\n function _closeLeaveConfirm() {\n _leaveBg.classList.remove('show');\n _leaveBg.setAttribute('aria-hidden', 'true');\n _leavePending = null;\n }\n document.getElementById('mc-leave-cancel').addEventListener('click', _closeLeaveConfirm);\n _leaveBg.addEventListener('click', e => { if (e.target === _leaveBg) _closeLeaveConfirm(); });\n document.getElementById('mc-leave-confirm').addEventListener('click', () => {\n const fn = _leavePending;\n _closeLeaveConfirm();\n if (typeof fn === 'function') fn();\n });\n\n function _closeUploadChoice() {\n if (_isWorkbenchDirty()) {\n _openLeaveConfirm('exit', () => { _resetWorkbenchState(); _closeUploadCanvasNow(); });\n return;\n }\n _closeUploadCanvasNow();\n }\n document.getElementById('ml-canvas-back').addEventListener('click', _closeUploadChoice);\n document.addEventListener('keydown', e => {\n if (e.key === 'Escape' && _uploadCanvas.classList.contains('show') && !_leaveBg.classList.contains('show')) {\n _closeUploadChoice();\n }\n });\n\n // 全局拦截 · 工作台展开时,根据脏态决定是否二次确认\n document.addEventListener('click', e => {\n if (!_uploadCanvas.classList.contains('show')) return;\n const dirty = _isWorkbenchDirty();\n\n // 库 X · 关闭整个库 modal(脏态确认 · 非脏直接关)\n if (e.target.closest('#ml-close-btn')) {\n e.preventDefault(); e.stopPropagation();\n const doClose = () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n document.getElementById('ml-modal-bg').classList.remove('show');\n };\n if (dirty) _openLeaveConfirm('exit', doClose); else doClose();\n return;\n }\n // 库左侧「来源」筛选项 · 切换 = 离开工作台\n const sideItem = e.target.closest('#ml-side .ml-side-item');\n if (sideItem) {\n e.preventDefault(); e.stopPropagation();\n const src = sideItem.dataset.source;\n const doSwitch = () => {\n _resetWorkbenchState();\n _closeUploadCanvasNow();\n _curLibSource = src;\n document.querySelectorAll('#ml-side .ml-side-item').forEach(x =>\n x.classList.toggle('active', x.dataset.source === src));\n _renderLibGrid();\n };\n if (dirty) _openLeaveConfirm('nav', doSwitch); else doSwitch();\n return;\n }\n\n if (!dirty) return; // 后续仅脏态时拦截\n\n // 外页跳转\n const a = e.target.closest('a[href]');\n if (a) {\n const href = a.getAttribute('href');\n if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;\n e.preventDefault(); e.stopPropagation();\n _openLeaveConfirm('nav', () => { _resetWorkbenchState(); _closeUploadCanvasNow(); location.href = href; });\n return;\n }\n // 余额胶囊\n if (e.target.closest('.balance-chip')) {\n e.preventDefault(); e.stopPropagation();\n _openLeaveConfirm('nav', () => { _resetWorkbenchState(); _closeUploadCanvasNow(); location.href = 'account.html'; });\n return;\n }\n }, true);\n\n window.addEventListener('beforeunload', e => {\n if (_isWorkbenchDirty()) { e.preventDefault(); e.returnValue = ''; return ''; }\n });\n\n // ─── 左:AI 生成区 ───\n const _mcInputText = document.getElementById('mc-input-text');\n const _mcSendBtn = document.getElementById('mc-send-btn');\n _mcInputText?.addEventListener('input', () => {\n _mcSendBtn.disabled = _mcInputText.value.trim().length === 0;\n _mcInputText.style.height = 'auto';\n _mcInputText.style.height = Math.min(_mcInputText.scrollHeight, 220) + 'px';\n });\n const _MC_SVG = {\n rerun: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>',\n dl: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>',\n more: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/></svg>',\n adopt: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg>',\n del: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>',\n edit: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z\"/></svg>',\n };\n\n function _mcAppendMsg(prompt, refs) {\n const inner = document.getElementById('mc-stream-inner');\n if (!inner) return;\n const empty = inner.querySelector('.mc-empty');\n if (empty) empty.remove();\n const safe = String(prompt).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'})[c]);\n const tags = ['3:4', '默认', '4 张'];\n const tagsHtml = tags.map(t => `<span class=\"meta-chip\">${t}</span>`).join('<span class=\"sep\">·</span>');\n const subjLabel = _mcKind() === 'actor' ? '演员' : '场景';\n const cells = Array.from({ length: 4 }, (_, i) => `\n <div class=\"mc-cell gen\" data-idx=\"${i}\">\n <div class=\"ph-frame\">生成中 · v${i + 1}</div>\n <div class=\"cell-ops\" hidden>\n <button type=\"button\" data-act=\"cell-rerun\" title=\"再次生成\">${_MC_SVG.rerun}</button>\n <button type=\"button\" data-act=\"cell-dl\" title=\"下载\">${_MC_SVG.dl}</button>\n </div>\n </div>`).join('');\n const msg = document.createElement('div');\n msg.className = 'mc-msg';\n msg.innerHTML = `\n <div class=\"mc-msg-prompt\">\n <div class=\"quote\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21c0-3.5 3-6 6-6s6 2.5 6 6\"/><circle cx=\"9\" cy=\"8\" r=\"4\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n </div>\n <div class=\"pt\"><div class=\"pt-text\">${safe}</div><div class=\"pt-tags\">${tagsHtml}</div></div>\n </div>\n <div class=\"mc-msg-grid\">${cells}</div>\n <div class=\"mc-msg-ops\">\n <button type=\"button\" data-act=\"edit\">${_MC_SVG.edit}重新编辑</button>\n <button type=\"button\" data-act=\"rerun\">${_MC_SVG.rerun}再次生成</button>\n </div>`;\n inner.appendChild(msg);\n setTimeout(() => {\n msg.querySelectorAll('.mc-cell').forEach((c, i) => {\n c.classList.remove('gen');\n const ph = c.querySelector('.ph-frame');\n if (ph) ph.textContent = subjLabel + ' · v' + (i + 1);\n const ops = c.querySelector('.cell-ops');\n if (ops) ops.hidden = false;\n });\n _bindMcCellPick();\n }, 1600);\n msg.querySelectorAll('[data-act=\"rerun\"]').forEach(b =>\n b.addEventListener('click', e => { e.stopPropagation(); _mcAppendMsg(prompt, []); }));\n msg.querySelectorAll('[data-act=\"edit\"]').forEach(b =>\n b.addEventListener('click', e => {\n e.stopPropagation();\n _mcInputText.value = prompt;\n _mcInputText.focus();\n _mcInputText.dispatchEvent(new Event('input'));\n }));\n msg.querySelectorAll('[data-act=\"cell-rerun\"]').forEach(b =>\n b.addEventListener('click', e => {\n e.stopPropagation();\n const cell = b.closest('.mc-cell');\n const ops = cell.querySelector('.cell-ops');\n const ph = cell.querySelector('.ph-frame');\n const idx = Number(cell.dataset.idx || 0);\n cell.classList.add('gen'); cell.classList.remove('selected');\n if (ops) ops.hidden = true;\n if (ph) ph.textContent = '生成中 · v' + (idx + 1);\n setTimeout(() => {\n cell.classList.remove('gen');\n if (ph) ph.textContent = subjLabel + ' · v' + (idx + 1);\n if (ops) ops.hidden = false;\n }, 1200 + Math.random() * 600);\n Shell.toast('已重跑', '该图重新生成中');\n }));\n msg.querySelectorAll('[data-act=\"cell-dl\"]').forEach(b =>\n b.addEventListener('click', e => { e.stopPropagation(); Shell.toast('下载', '已开始下载 · MOCK'); }));\n const stream = document.getElementById('mc-stream');\n if (stream) stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' });\n }\n\n _mcSendBtn?.addEventListener('click', () => {\n const txt = _mcInputText.value.trim();\n if (!txt) return;\n _mcAppendMsg(txt, _mcRefList.slice());\n _mcInputText.value = '';\n _mcInputText.style.height = 'auto';\n _mcSendBtn.disabled = true;\n _mcRefList = [];\n _renderMcRefs();\n });\n _mcInputText?.addEventListener('keydown', e => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); _mcSendBtn.click(); }\n });\n\n // 参考图上传\n const _mcAiRefInput = document.getElementById('mc-ai-ref-input');\n const _mcRefs = document.getElementById('mc-input-refs');\n let _mcRefList = [];\n document.getElementById('mc-add-btn')?.addEventListener('click', () => _mcAiRefInput.click());\n _mcAiRefInput?.addEventListener('change', e => {\n const files = [...(e.target.files || [])].filter(f => /^image\\//.test(f.type));\n files.forEach(f => _mcRefList.push({ name: f.name, url: URL.createObjectURL(f) }));\n e.target.value = '';\n _renderMcRefs();\n });\n function _renderMcRefs() {\n if (!_mcRefs) return;\n _mcRefs.innerHTML = _mcRefList.map((r, i) => `\n <div class=\"mc-input-ref\">\n <img src=\"${r.url}\" alt=\"${r.name}\">\n <button class=\"x\" data-idx=\"${i}\" aria-label=\"移除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg></button>\n </div>`).join('');\n _mcRefs.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {\n _mcRefList.splice(+x.dataset.idx, 1);\n _renderMcRefs();\n }));\n }\n\n // Tab 切换\n document.querySelectorAll('.mc-up-tab').forEach(btn => {\n btn.addEventListener('click', () => {\n _mcRightTab = btn.dataset.tab;\n document.querySelectorAll('.mc-up-tab').forEach(b => b.classList.toggle('active', b === btn));\n document.querySelectorAll('.mc-up-body [data-show]').forEach(el => {\n el.hidden = el.dataset.show !== _mcRightTab;\n });\n document.getElementById('mc-up-name').value = _mcCurState().name;\n _renderRight();\n });\n });\n\n const _mcUpName = document.getElementById('mc-up-name');\n _mcUpName?.addEventListener('input', () => {\n _mcCurState().name = _mcUpName.value;\n _updateCommit();\n });\n\n // AI tab · 立绘选中态\n const _aiEmpty = document.getElementById('mc-portrait-ai-empty');\n const _aiPicked = document.getElementById('mc-portrait-ai-picked');\n const _aiLabel = document.getElementById('mc-portrait-ai-label');\n function _renderAiPortrait() {\n if (_mcAi.portrait) {\n _aiEmpty.hidden = true; _aiPicked.hidden = false;\n _aiLabel.textContent = _mcAi.portrait.label || '立绘';\n } else { _aiEmpty.hidden = false; _aiPicked.hidden = true; }\n }\n function _setAiPortrait(data) {\n _mcAi.portrait = data;\n _renderAiPortrait();\n _updateTriBtn(); _updateCommit();\n }\n function _clearAiPortrait() {\n if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');\n _mcAi.portrait = null;\n _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n _renderAiPortrait(); _renderTriView(); _updateTriBtn(); _updateCommit();\n }\n document.getElementById('mc-portrait-ai-clear').addEventListener('click', _clearAiPortrait);\n\n // Local tab · 多张立绘 / 场景图\n const _lpDrop = document.getElementById('mc-portrait-local-drop');\n const _lpInput = document.getElementById('mc-portrait-local-input');\n const _lpList = document.getElementById('mc-portrait-local-list');\n const _lpCount = document.getElementById('mc-portrait-local-count');\n function _renderLocalPortraits() {\n _lpCount.textContent = _mcLocal.portraits.length;\n _lpList.innerHTML = _mcLocal.portraits.map((p, i) => `\n <div class=\"thumb\">\n <img src=\"${p.url}\" alt=\"${p.name}\">\n <button class=\"x\" data-idx=\"${i}\" aria-label=\"移除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg></button>\n </div>`).join('');\n _lpList.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {\n _mcLocal.portraits.splice(+x.dataset.idx, 1);\n if (_mcLocal.portraits.length === 0) {\n _mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;\n }\n _renderLocalPortraits(); _renderTriView(); _updateTriBtn(); _updateCommit();\n }));\n }\n function _lpAdd(files) {\n const imgs = [...(files || [])].filter(f => /^image\\//.test(f.type));\n if (!imgs.length) return;\n // 场景:仅允许 1 张 · 新选直接替换旧的(后选覆盖前选)\n if (_mcKind() === 'scene') {\n const f = imgs[0];\n _mcLocal.portraits = [{ file: f, url: URL.createObjectURL(f), name: f.name, size: f.size }];\n } else {\n imgs.forEach(f => _mcLocal.portraits.push({ file: f, url: URL.createObjectURL(f), name: f.name, size: f.size }));\n }\n _renderLocalPortraits(); _updateTriBtn(); _updateCommit();\n }\n _lpDrop?.addEventListener('click', () => _lpInput.click());\n _lpDrop?.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _lpInput.click(); } });\n _lpInput?.addEventListener('change', e => { _lpAdd(e.target.files); e.target.value = ''; });\n ['dragenter', 'dragover'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.add('dragover'); }));\n ['dragleave', 'drop'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.remove('dragover'); }));\n _lpDrop?.addEventListener('drop', e => _lpAdd(e.dataTransfer?.files));\n\n // 三视图模块 · 演员使用 · 场景隐藏\n const _triSec = document.getElementById('mc-triview-sec');\n const _triResultImg = document.getElementById('mc-triview-result');\n const _triFrame = document.getElementById('mc-triview-frame');\n const _triGenBtn = document.getElementById('mc-triview-gen-btn');\n const _triHint = document.getElementById('mc-triview-hint');\n const _triOps = document.getElementById('mc-triview-ops');\n const _triRerunBtn = document.getElementById('mc-triview-rerun');\n const _triHistory = document.getElementById('mc-triview-history');\n const _triHistoryRow = document.getElementById('mc-triview-history-row');\n const _triHistoryCount = document.getElementById('mc-triview-history-count');\n let _triGenerating = false;\n\n function _portraitReady() {\n return _mcRightTab === 'ai' ? !!_mcAi.portrait : _mcLocal.portraits.length > 0;\n }\n function _renderTriView() {\n if (!_kindRequiresTri()) return; // 场景模式整个模块被 CSS 隐藏\n const s = _mcCurState();\n const has = _hasTri(s);\n _triSec.classList.toggle('has-result', has);\n if (_triGenerating) {\n _triGenBtn.hidden = true; _triHint.hidden = true;\n _triOps.hidden = !has; _triHistory.hidden = !has;\n _triFrame.textContent = '三视图生成中…';\n _triResultImg.classList.add('gen');\n _triRerunBtn.disabled = true;\n if (has) _renderTriHistory(s);\n } else if (has) {\n _triGenBtn.hidden = true; _triHint.hidden = true;\n _triOps.hidden = false; _triHistory.hidden = false;\n const ver = s.triVersions[s.triActiveIdx];\n _triFrame.textContent = `三视图(正/侧/背) · ${ver.label}`;\n _triResultImg.classList.remove('gen');\n _triRerunBtn.disabled = false;\n _renderTriHistory(s);\n } else {\n _triGenBtn.hidden = false; _triHint.hidden = false;\n _triOps.hidden = true; _triHistory.hidden = true;\n _triFrame.textContent = '三视图(正/侧/背)';\n _triResultImg.classList.remove('gen');\n }\n }\n function _renderTriHistory(s) {\n _triHistoryCount.textContent = s.triVersions.length;\n _triHistoryRow.innerHTML = s.triVersions.map((ver, i) => `\n <div class=\"h-thumb${i === s.triActiveIdx ? ' active' : ''}\" data-idx=\"${i}\" title=\"${ver.label} · ${ver.ts}${i === s.triActiveIdx ? ' · 当前采用' : ''}\">\n <span class=\"badge\">当前</span><span class=\"v\">${ver.label}</span>\n </div>`).join('');\n _triHistoryRow.querySelectorAll('.h-thumb').forEach(el => {\n el.addEventListener('click', () => {\n const idx = Number(el.dataset.idx);\n if (idx === s.triActiveIdx) return;\n s.triActiveIdx = idx;\n _renderTriView(); _updateCommit();\n });\n });\n }\n function _updateTriBtn() {\n if (!_kindRequiresTri()) return;\n if (_hasTri(_mcCurState())) return;\n const ok = _portraitReady() && !_triGenerating;\n _triGenBtn.disabled = !ok;\n _triHint.textContent = _portraitReady()\n ? '// 一键生成正/侧/背 三视图'\n : (_mcRightTab === 'ai' ? '// 先选中左侧 AI 立绘' : '// 先上传至少 1 张立绘');\n }\n function _startTriGen() {\n if (!_kindRequiresTri()) return;\n if (!_portraitReady() || _triGenerating) return;\n _triGenerating = true;\n const stateAtStart = _mcCurState();\n _renderTriView();\n setTimeout(() => {\n _triGenerating = false;\n const now = new Date();\n const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');\n const label = 'v' + (stateAtStart.triVersions.length + 1);\n stateAtStart.triVersions.push({ ts, label });\n stateAtStart.triActiveIdx = stateAtStart.triVersions.length - 1;\n if (stateAtStart === _mcCurState()) _renderTriView();\n _updateTriBtn(); _updateCommit();\n }, 1600);\n }\n _triGenBtn.addEventListener('click', _startTriGen);\n _triRerunBtn.addEventListener('click', _startTriGen);\n\n // 整体渲染 + 提交\n const _mcUpCommit = document.getElementById('mc-up-commit');\n const _mcUpStat = document.getElementById('mc-up-stat');\n function _updateCommit() {\n const s = _mcCurState();\n const nameOk = s.name.trim().length > 0;\n const portraitOk = _portraitReady();\n const needTri = _kindRequiresTri();\n const triOk = needTri ? _hasTri(s) : true;\n const ready = nameOk && portraitOk && triOk;\n _mcUpCommit.disabled = !ready;\n if (ready) {\n _mcUpStat.classList.add('ok');\n _mcUpStat.innerHTML = `✓ 已就绪 · <b>${s.name}</b>`;\n } else {\n _mcUpStat.classList.remove('ok');\n const miss = [];\n if (!nameOk) miss.push(_mcKind() === 'actor' ? '姓名' : '名称');\n if (!portraitOk) miss.push(_mcKind() === 'actor' ? '立绘' : '场景图');\n if (needTri && !triOk) miss.push('三视图');\n _mcUpStat.innerHTML = `// 待完成 · <b>${miss.join(' / ')}</b>`;\n }\n }\n function _renderRight() {\n _renderAiPortrait();\n _renderLocalPortraits();\n _renderTriView();\n _updateTriBtn();\n _updateCommit();\n }\n _renderRight();\n\n _mcUpCommit?.addEventListener('click', () => {\n const s = _mcCurState();\n if (_mcUpCommit.disabled) return;\n const baseName = s.name.trim().slice(0, 12);\n const ts = Date.now().toString(36);\n const subjLabel = _mcKind() === 'actor' ? '演员' : '场景';\n const ownsArr = _mcOwnsArr();\n // 持久化多张立绘 / 场景图 + 三视图历史版本\n const portraits = _mcRightTab === 'ai'\n ? (_mcAi.portrait ? [{ url: '', name: _mcAi.portrait.label || baseName, label: _mcAi.portrait.label || subjLabel }] : [])\n : _mcLocal.portraits.map((p, i) => ({ url: p.url, name: p.name || `${baseName}-${i+1}`, label: `本地 ${i+1}` }));\n const triVersions = _kindRequiresTri() ? s.triVersions.map(v => ({ ts: v.ts, label: v.label })) : [];\n ownsArr.unshift({\n id: 'up-' + ts,\n name: baseName,\n sub: (_mcRightTab === 'ai' ? 'AI 生成' : '我的上传') + ' · ' + (_kindRequiresTri() ? '已含三视图' : subjLabel),\n source: 'own',\n portraits, triVersions,\n kind: _mcKind(),\n });\n const L = _MC_LABELS[_mcKind()];\n Shell.toast(L.toast, `${baseName} · 来源 ${_mcRightTab === 'ai' ? 'AI 生成' : '我的上传'}`);\n _resetWorkbenchState();\n _curLibSource = 'own';\n const side = document.getElementById('ml-side');\n side.querySelectorAll('.ml-side-item').forEach(x =>\n x.classList.toggle('active', x.dataset.source === 'own'));\n const ownCt = side.querySelector('.ml-side-item[data-source=\"own\"] .ct');\n if (ownCt) ownCt.textContent = ownsArr.length;\n const allCt = side.querySelector('.ml-side-item[data-source=\"all\"] .ct');\n if (allCt) allCt.textContent = ownsArr.length + _mcPresetsArr().length;\n _renderLibGrid();\n _closeUploadCanvasNow();\n });\n\n // 左侧 cell 可点击选为立绘(仅 AI tab)\n function _bindMcCellPick() {\n document.querySelectorAll('#mc-stream .mc-cell').forEach(cell => {\n if (cell.dataset.boundPick) return;\n cell.dataset.boundPick = '1';\n if (!cell.querySelector('.pick-badge')) {\n const b = document.createElement('span');\n b.className = 'pick-badge';\n b.textContent = '已选用';\n cell.appendChild(b);\n }\n cell.addEventListener('click', () => {\n if (_mcRightTab !== 'ai') {\n Shell.toast('请切到「AI 生成」标签', '只有 AI 模式才能选用左侧立绘');\n return;\n }\n if (cell.classList.contains('gen')) return;\n if (cell.classList.contains('selected')) { _clearAiPortrait(); return; }\n document.querySelectorAll('#mc-stream .mc-cell.selected').forEach(c => c.classList.remove('selected'));\n cell.classList.add('selected');\n const label = cell.querySelector('.ph-frame')?.textContent || '立绘';\n _mcAi.triVersions = []; _mcAi.triActiveIdx = -1;\n _renderTriView();\n _setAiPortrait({ label, cellEl: cell });\n });\n });\n }\n\n function openLib(kind) {\n _curLibKind = kind;\n _curLibSource = 'all';\n const isModel = kind === 'model';\n const title = isModel ? '演员库' : '场景库';\n const presets = isModel ? MODEL_LIB : SCENE_LIB;\n const owns = isModel ? MODEL_OWN : SCENE_OWN;\n\n document.getElementById('ml-modal-title').textContent = title;\n document.getElementById('ml-modal-ct').textContent = '// 共 ' + (presets.length + owns.length) + ' 个';\n\n // 侧栏 · 来源\n const side = document.getElementById('ml-side');\n side.innerHTML = `\n <div class=\"ml-side-h\">来源</div>\n <div class=\"ml-side-item active\" data-source=\"all\">全部 <span class=\"ct\">${presets.length + owns.length}</span></div>\n <div class=\"ml-side-item\" data-source=\"preset\">平台预设 <span class=\"ct\">${presets.length}</span></div>\n <div class=\"ml-side-item\" data-source=\"own\">我的上传 <span class=\"ct\">${owns.length}</span></div>\n `;\n side.querySelectorAll('.ml-side-item').forEach(it => {\n it.addEventListener('click', () => {\n side.querySelectorAll('.ml-side-item').forEach(x => x.classList.remove('active'));\n it.classList.add('active');\n _curLibSource = it.dataset.source;\n _renderLibGrid();\n });\n });\n\n // toolbar · chip groups (去掉了 btn-up 上传按钮,改用网格内入口卡)\n const toolbar = document.getElementById('ml-toolbar');\n if (isModel) {\n toolbar.innerHTML = `\n <div class=\"chip-group\">\n <span class=\"lbl\">性别</span>\n <button class=\"chip active\" type=\"button\">全部</button>\n <button class=\"chip\" type=\"button\">女</button>\n <button class=\"chip\" type=\"button\">男</button>\n </div>\n <div class=\"chip-group\">\n <span class=\"lbl\">年龄</span>\n <button class=\"chip active\" type=\"button\">全部</button>\n <button class=\"chip\" type=\"button\">青年</button>\n <button class=\"chip\" type=\"button\">中年</button>\n </div>\n `;\n } else {\n toolbar.innerHTML = `\n <div class=\"chip-group\">\n <span class=\"lbl\">类型</span>\n <button class=\"chip active\" type=\"button\">全部</button>\n <button class=\"chip\" type=\"button\">室内</button>\n <button class=\"chip\" type=\"button\">室外</button>\n </div>\n <div class=\"chip-group\">\n <span class=\"lbl\">氛围</span>\n <button class=\"chip active\" type=\"button\">全部</button>\n <button class=\"chip\" type=\"button\">日</button>\n <button class=\"chip\" type=\"button\">夜</button>\n </div>\n `;\n }\n toolbar.querySelectorAll('.chip-group').forEach(group => {\n group.querySelectorAll('.chip').forEach(c => {\n c.addEventListener('click', () => {\n group.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));\n c.classList.add('active');\n });\n });\n });\n\n // 卡片网格(含 + 入口 + apply 绑定都在 _renderLibGrid 内完成)\n _renderLibGrid();\n\n document.getElementById('ml-modal-bg').classList.add('show');\n }\n function closeLib() {\n document.getElementById('ml-modal-bg').classList.remove('show');\n }\n\n function init() {\n // 侧栏 ttab → 锚点\n document.querySelectorAll('.asset-side .ttab[data-jump]').forEach(tab => {\n tab.addEventListener('click', () => {\n document.querySelectorAll('.asset-side .ttab').forEach(t => t.classList.remove('active'));\n tab.classList.add('active');\n const target = document.getElementById(tab.dataset.jump);\n if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });\n });\n });\n // 卡片 click → 详情(点击空白区域;按钮和可编辑提示词带 data-stop 不触发)\n document.querySelectorAll('.asset-card-2[data-asset-id]').forEach(card => {\n card.addEventListener('click', (e) => {\n if (e.target.closest('[data-stop]')) return;\n openDetail(card.dataset.assetId);\n });\n });\n // 重跑按钮\n document.querySelectorAll('[data-rerun]').forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const card = btn.closest('.asset-card-2');\n const name = card ? card.querySelector('strong').textContent : '资产';\n Shell.toast('重跑「' + name + '」', 'POST /assets/regen · 使用当前提示词');\n });\n });\n // 替换按钮\n document.querySelectorAll('[data-replace]').forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const card = btn.closest('.asset-card-2');\n const name = card ? card.querySelector('strong').textContent : '资产';\n const kind = card ? card.dataset.assetKind : '';\n if (kind === 'character') openLib('model');\n else if (kind === 'scene') openLib('scene');\n else Shell.toast('替换「' + name + '」', '请从素材库挑选或上传');\n });\n });\n // 提示词区域 blur → 保存\n document.querySelectorAll('.asset-card-2 .prompt-box[contenteditable=\"true\"]').forEach(box => {\n box.addEventListener('blur', () => {\n const card = box.closest('.asset-card-2');\n const name = card ? card.querySelector('strong').textContent : '资产';\n Shell.toast('提示词已更新', name + ' · 下次重跑生效');\n });\n });\n // 演员库 / 场景库\n document.getElementById('open-model-lib')?.addEventListener('click', () => openLib('model'));\n document.getElementById('open-scene-lib')?.addEventListener('click', () => openLib('scene'));\n // 新增\n document.getElementById('asset-add-character')?.addEventListener('click', () => {\n document.getElementById('new-character-modal').classList.add('show');\n });\n document.getElementById('asset-add-scene')?.addEventListener('click', () => {\n Shell.toast('+ 新增场景', '请上传场景图或填写提示词');\n });\n // modal 通用关闭\n document.querySelectorAll('.asset-modal-bg').forEach(bg => {\n bg.addEventListener('click', (e) => { if (e.target === bg) bg.classList.remove('show'); });\n bg.querySelectorAll('.x, [data-modal-close]').forEach(el => {\n el.addEventListener('click', () => bg.classList.remove('show'));\n });\n });\n // 详情 modal · 应用 → 关详情 + 关演员/场景库 → 回到项目页\n document.getElementById('asset-detail-apply-btn')?.addEventListener('click', () => {\n const name = document.getElementById('asset-detail-title').textContent;\n document.getElementById('asset-detail-modal').classList.remove('show');\n document.getElementById('ml-modal-bg')?.classList.remove('show');\n Shell.toast('已应用「' + name + '」', '已加入当前项目');\n });\n // 详情 modal · AI 生成三视图\n document.querySelectorAll('.ai-gen-btn').forEach(btn => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n Shell.toast('AI 生成三视图中', '约 12s · POST /assets/tri-view');\n });\n });\n // 新增人物 · 保存\n document.getElementById('nc-save-btn')?.addEventListener('click', () => {\n Shell.toast('已新增人物', '保存到资产库 · 待 AI 补三视图');\n document.getElementById('new-character-modal').classList.remove('show');\n });\n // 把所有 modal 提升到 body 直接子级,避免被 .content 滚动容器裁切\n document.querySelectorAll('.asset-modal-bg, .ml-modal-bg').forEach(el => {\n if (el.parentElement !== document.body) document.body.appendChild(el);\n });\n // 演员库 / 场景库 全屏弹窗关闭按钮(仍保留,经\"替换\"气泡进入)\n document.getElementById('ml-close-btn')?.addEventListener('click', closeLib);\n // 注入当前项目的商品名(从 URL ?product= 或默认)\n const nameEls = ['asset-prod-name', 'asset-prod-card-name'];\n nameEls.forEach(eid => {\n const el = document.getElementById(eid);\n if (el) el.textContent = CURRENT_PRODUCT_NAME;\n });\n const thumbLbl = document.getElementById('asset-prod-thumb-label');\n if (thumbLbl) thumbLbl.textContent = CURRENT_PRODUCT_NAME + ' · 主图';\n // 商品卡 · AI 生成三视图 → 右侧 prod-preview · 预览/采用 双状态 + 点击主图放大\n (function setupProdPreview() {\n const aigenBtn = document.getElementById('asset-prod-aigen-btn');\n const pane = document.getElementById('asset-prod-preview');\n const img = document.getElementById('prod-preview-img');\n const statusEl = document.getElementById('prod-preview-status');\n const foot = document.getElementById('prod-preview-foot');\n const triBadge = document.getElementById('asset-prod-tri-badge');\n const prodAction = document.getElementById('asset-prod-action');\n const history = document.getElementById('prod-preview-history');\n const historyRow = document.getElementById('prod-preview-history-row');\n const historyCount = document.getElementById('prod-preview-history-count');\n if (!aigenBtn || !pane || !img || !statusEl || !foot || !history || !historyRow) return;\n\n const versions = []; // [{ ts, label }]\n let previewIdx = -1; // 主图正在「预览」哪一版(浏览态,不动采用状态)\n let adoptedIdx = -1; // 真正被「采用」的那一版,决定商品资产生效版本\n let generating = false;\n const savedTri = ProjectStore.data.stage2?.productTri || null;\n if (savedTri) {\n if (Array.isArray(savedTri.versions)) versions.push(...savedTri.versions);\n if (Number.isInteger(savedTri.previewIdx)) previewIdx = savedTri.previewIdx;\n if (Number.isInteger(savedTri.adoptedIdx)) adoptedIdx = savedTri.adoptedIdx;\n }\n\n function saveTriState() {\n ProjectStore.saveStage('stage2', {\n ...(ProjectStore.data.stage2 || {}),\n productTri: { versions, previewIdx, adoptedIdx },\n });\n }\n\n function prodName() {\n return CURRENT_PRODUCT_NAME || (document.getElementById('asset-prod-card-name')?.textContent ?? '商品');\n }\n\n function renderHistory() {\n if (versions.length === 0) {\n history.classList.remove('show');\n return;\n }\n history.classList.add('show');\n historyCount.textContent = versions.length;\n historyRow.innerHTML = versions.map((ver, i) => {\n const isAdopted = i === adoptedIdx;\n const isPreview = i === previewIdx;\n const cls = [\n isAdopted ? 'adopted' : '',\n isPreview && !isAdopted ? 'previewing' : '',\n ].filter(Boolean).join(' ');\n const titleParts = [ver.label, ver.ts];\n if (isAdopted) titleParts.push('已采用');\n else if (isPreview) titleParts.push('预览中');\n return `\n <div class=\"h-thumb ${cls}\" data-idx=\"${i}\" title=\"${titleParts.join(' · ')}\">\n <span class=\"badge\">已采用</span>\n <span class=\"v\">${ver.label}</span>\n </div>\n `;\n }).join('');\n historyRow.querySelectorAll('.h-thumb').forEach(el => {\n el.addEventListener('click', () => {\n const idx = Number(el.dataset.idx);\n if (idx === previewIdx) return;\n setPreview(idx);\n });\n });\n }\n\n function renderMain() {\n if (previewIdx < 0) return;\n const ver = versions[previewIdx];\n const isAdopted = previewIdx === adoptedIdx;\n img.innerHTML = `<span class=\"ph-frame\">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;\n img.classList.add('is-zoomable');\n img.title = '点击放大查看';\n statusEl.textContent = isAdopted\n ? `${ver.label} · 已采用,不满意可重跑`\n : `${ver.label} · 预览中(未采用)`;\n foot.innerHTML = `\n <button class=\"btn btn-ghost btn-sm\" id=\"prod-preview-rerun\">↻ 重跑</button>\n <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;\">\n <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>\n ${isAdopted ? '已采用' : '采用此版本'}\n </button>\n <span class=\"spacer\"></span>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">~¥0.30 / 次</span>\n `;\n document.getElementById('prod-preview-rerun')?.addEventListener('click', start);\n document.getElementById('prod-preview-adopt')?.addEventListener('click', adoptPreview);\n }\n\n // 仅切预览主图,不动采用/不动商品资产\n function setPreview(idx) {\n previewIdx = idx;\n renderHistory();\n renderMain();\n saveTriState();\n }\n\n // 显式「采用」当前预览版本 · 同步商品资产 + 隐藏缺三视图徽标\n function adoptPreview() {\n if (previewIdx < 0) return;\n if (previewIdx === adoptedIdx) return;\n adoptedIdx = previewIdx;\n applyAdoption(/* fromClick */ true);\n }\n\n function applyAdoption(fromClick) {\n const ver = versions[adoptedIdx];\n if (triBadge) triBadge.hidden = true;\n const detail = ASSET_DETAILS['prod-main'];\n if (detail) {\n detail.hasTri = true;\n detail.currentVersion = ver.label;\n detail.info = [\n ['类别', '商品 · 当前项目'],\n ['名称', prodName()],\n ['三视图', '已采用 · ' + ver.label],\n ['状态', '已三视图'],\n ];\n }\n renderHistory();\n renderMain();\n saveTriState();\n if (fromClick) Shell.toast('已采用 ' + ver.label, prodName() + ' · 商品资产已更新为该版本');\n }\n\n function renderLoading() {\n 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>`;\n img.classList.remove('is-zoomable');\n img.removeAttribute('title');\n statusEl.textContent = '生成中 · 约 12s';\n foot.innerHTML = '<span class=\"muted-2 mono\" style=\"font-size:11px;\">// POST /assets/tri-view</span>';\n aigenBtn.disabled = true;\n }\n\n function finishGeneration() {\n if (!generating && !ProjectStore.getJob('stage2-product-tri')) return;\n generating = false;\n aigenBtn.disabled = false;\n const now = new Date();\n const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');\n const newVer = { ts, label: 'v' + (versions.length + 1) };\n versions.push(newVer);\n const newIdx = versions.length - 1;\n previewIdx = newIdx;\n if (adoptedIdx === -1) {\n adoptedIdx = newIdx;\n applyAdoption(/* fromClick */ false);\n } else {\n renderHistory();\n renderMain();\n Shell.toast('三视图已生成 ' + newVer.label, prodName() + ' · 预览中,满意请点「采用此版本」');\n }\n ProjectStore.finishJob('stage2-product-tri');\n ProjectStore.record('stage2.productTri.ready', { version: newVer.label });\n saveTriState();\n }\n\n function start() {\n if (generating) return;\n generating = true;\n pane.classList.add('show');\n renderLoading();\n ProjectStore.startJob('stage2-product-tri', {\n stage: 2,\n label: '商品三视图生成',\n finishAt: Date.now() + 12000,\n });\n ProjectStore.record('stage2.productTri.started', { product: prodName() });\n saveTriState();\n setTimeout(finishGeneration, 1800);\n }\n\n function resumeGenerationIfNeeded() {\n const job = ProjectStore.getJob('stage2-product-tri');\n if (!job || job.status !== 'running') return;\n generating = true;\n pane.classList.add('show');\n renderLoading();\n const remaining = Math.max(0, (job.finishAt || Date.now()) - Date.now());\n setTimeout(finishGeneration, remaining);\n }\n\n // 主图点击 → 放大查看\n img.addEventListener('click', (e) => {\n if (!img.classList.contains('is-zoomable')) return;\n if (previewIdx < 0) return;\n e.stopPropagation();\n openTriLightbox(versions[previewIdx], previewIdx === adoptedIdx, prodName());\n });\n\n aigenBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n start();\n });\n if (versions.length && previewIdx >= 0) {\n pane.classList.add('show');\n if (adoptedIdx >= 0) applyAdoption(false);\n else { renderHistory(); renderMain(); }\n }\n resumeGenerationIfNeeded();\n })();\n }\n return { init };\n})();\nStage2.init();\n\n/* ============================================================\n STAGE 3 · 故事板 · 按场分 · 切换/重跑/应用/历史\n ============================================================ */\nconst Stage3 = (function () {\n const scenes = [\n { id: 'sc1', name: '场 1 · 深夜办公桌', time: '0-15s', desc: '深夜居家办公环境,女主对镜叹气,皮肤干燥起皮特写。台灯暖光 + 屏幕冷光对比。', prompt: '中景 / 固定机位\\n光线:台灯暖光 + 屏幕冷光\\n演员:林夕(疲倦状态)\\n关键道具:面膜盒(从抽屉露半角)\\n氛围:午夜、安静、些许焦虑', adopted: 0, versions: [{ ts: '14:02', label: 'v1' }] },\n { 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' }] },\n { id: 'sc3', name: '场 3 · 化妆台/产品定格', time: '30-45s', desc: '第二天早上,女主对镜化妆,皮肤透亮。淡入产品定格大图 + 价格标签 ¥39.9。', prompt: '中景 / 定格\\n光线:晨光 + 暖色滤镜\\n演员:林夕(精致妆面)\\n结尾:产品大图 + 价格 + 购物车浮动', adopted: 0, versions: [{ ts: '14:30', label: 'v1' }] },\n ];\n let curId = scenes[0].id;\n const savedStage3 = ProjectStore.data.stage3;\n if (savedStage3) {\n if (savedStage3.curId) curId = savedStage3.curId;\n if (Array.isArray(savedStage3.scenes)) {\n savedStage3.scenes.forEach(ss => {\n const s = scenes.find(x => x.id === ss.id);\n if (!s) return;\n if (Array.isArray(ss.versions)) s.versions = ss.versions;\n if (Number.isInteger(ss.adopted)) s.adopted = ss.adopted;\n if (typeof ss.prompt === 'string') s.prompt = ss.prompt;\n });\n }\n }\n\n function saveState() {\n ProjectStore.saveStage('stage3', {\n curId,\n scenes: scenes.map(s => ({ id: s.id, prompt: s.prompt, adopted: s.adopted, versions: s.versions })),\n });\n }\n\n function renderRow() {\n const row = document.getElementById('sb-scenes-row');\n if (!row) return;\n row.innerHTML = scenes.map(s => `<div class=\"sb-scene-thumb${s.id === curId ? ' selected' : ''}\" data-sid=\"${s.id}\">\n <div class=\"placeholder\"><span class=\"ph-frame\">${s.name.split(' · ')[1] || s.name}</span></div>\n <div class=\"nm\">${s.name.split(' · ')[0]}</div>\n <div class=\"sub\">${s.time}</div>\n </div>`).join('');\n row.querySelectorAll('.sb-scene-thumb').forEach(t => {\n t.addEventListener('click', () => { curId = t.dataset.sid; saveState(); renderAll(); });\n });\n }\n function renderMain() {\n const s = scenes.find(x => x.id === curId); if (!s) return;\n const v = s.versions[s.adopted];\n document.getElementById('sb-main-img').innerHTML = `<span class=\"ph-frame\">${s.name} · ${v.label}</span>`;\n document.getElementById('sb-side-scene').textContent = s.name.split(' · ')[0];\n const promptEdit = document.getElementById('sb-prompt-edit');\n promptEdit.textContent = s.prompt;\n promptEdit.oninput = () => {\n s.prompt = promptEdit.textContent.trim();\n saveState();\n };\n // history\n const ct = document.getElementById('sb-history-ct');\n const hist = document.getElementById('sb-history-row');\n ct.textContent = s.versions.length;\n if (s.versions.length === 0) {\n hist.innerHTML = '<div style=\"font-size: 11.5px; color: var(--black-alpha-48); padding: 12px 4px;\">// 暂无历史版本</div>';\n } else {\n hist.innerHTML = s.versions.map((vv, i) => `<div class=\"sb-history-thumb${i === s.adopted ? ' current' : ''}\" data-vi=\"${i}\">\n <div class=\"placeholder\"><span class=\"ph-frame\">${vv.label}</span></div>\n <div class=\"ts\">${vv.ts}</div>\n </div>`).join('');\n hist.querySelectorAll('.sb-history-thumb').forEach(t => {\n t.addEventListener('click', () => {\n s.adopted = +t.dataset.vi;\n ProjectStore.record('stage3.storyboard.version.selected', { scene: s.id, version: s.versions[s.adopted]?.label });\n saveState();\n renderMain();\n Shell.toast('已切换至 ' + s.versions[s.adopted].label, s.name);\n });\n });\n }\n }\n function renderAll() { renderRow(); renderMain(); }\n\n function init() {\n renderAll();\n document.getElementById('sb-rerun-btn')?.addEventListener('click', () => {\n const s = scenes.find(x => x.id === curId);\n const v = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (s.versions.length + 1) };\n s.versions.push(v);\n s.adopted = s.versions.length - 1;\n ProjectStore.record('stage3.storyboard.rerun', { scene: s.id, version: v.label });\n saveState();\n Shell.toast('整张重跑', s.name + ' · ' + v.label);\n renderAll();\n });\n }\n return { init };\n})();\nStage3.init();\n\n/* ============================================================\n STAGE 4 · 视频 · 详情 modal(大图 + 历史 + 采用)\n ============================================================ */\nconst Stage4 = (function () {\n const VIDEOS = {\n 'v1': { title: '场 1 · 深夜办公桌', time: '0-15s', info: [['场次', '场 1'], ['时长', '15.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:32', label: 'v1' }, { ts: '14:48', label: 'v2' }, { ts: '15:02', label: 'v3' }], adopted: 2, prompt: '中景 / 固定机位\\n光线:台灯暖光 + 屏幕冷光\\n演员:林夕(疲倦状态)\\n关键道具:面膜盒(从抽屉露半角)\\n氛围:午夜、安静、些许焦虑' },\n 'v2': { title: '场 2 · 面膜包装/特写', time: '15-27s', info: [['场次', '场 2'], ['时长', '12.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:35', label: 'v1' }, { ts: '14:52', label: 'v2' }], adopted: 1, prompt: '特写 / 缓推镜\\n光线:柔光顶打 + 背景虚化\\n关键道具:面膜包装、撕开瞬间\\n氛围:精致、放心、产品感' },\n 'v3': { title: '场 3 · 化妆台/产品定格', time: '27-40s', info: [['场次', '场 3'], ['时长', '13.0s'], ['分辨率', '1080×1920 · 9:16'], ['模型', 'Seedance v2'], ['成本', '¥0.45']], versions: [{ ts: '14:40', label: 'v1' }], adopted: 0, prompt: '中景 / 定格\\n光线:晨光 + 暖色滤镜\\n演员:林夕(精致妆面)\\n结尾:产品大图 + 价格 + 购物车浮动' },\n };\n const savedStage4 = ProjectStore.data.stage4;\n if (savedStage4?.videos) {\n Object.entries(savedStage4.videos).forEach(([id, sv]) => {\n if (!VIDEOS[id]) return;\n if (Array.isArray(sv.versions)) VIDEOS[id].versions = sv.versions;\n if (Number.isInteger(sv.adopted)) VIDEOS[id].adopted = sv.adopted;\n if (Number.isInteger(sv.preview)) VIDEOS[id].preview = sv.preview;\n if (typeof sv.prompt === 'string') VIDEOS[id].prompt = sv.prompt;\n });\n }\n let curVid = null;\n\n function saveState() {\n const videos = {};\n Object.entries(VIDEOS).forEach(([id, v]) => {\n videos[id] = { versions: v.versions, adopted: v.adopted, preview: v.preview, prompt: v.prompt };\n });\n ProjectStore.saveStage('stage4', { videos });\n }\n\n function getPreviewIndex(v) {\n const idx = Number.isInteger(v.preview) ? v.preview : v.adopted;\n return v.versions[idx] ? idx : Math.max(0, v.adopted || 0);\n }\n\n function openDetail(id) {\n const v = VIDEOS[id]; if (!v) return;\n curVid = id;\n document.getElementById('vd-title').textContent = v.title;\n document.getElementById('vd-sub').textContent = '// ' + v.title + ' · ' + v.time;\n const previewIdx = getPreviewIndex(v);\n const cur = v.versions[previewIdx];\n document.getElementById('vd-main-img').innerHTML = `<span class=\"ph-frame\">${v.title} · ${cur.label}</span>`;\n 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>`;\n const promptEl = document.getElementById('vd-prompt-edit');\n if (promptEl) {\n promptEl.textContent = v.prompt || '';\n promptEl.oninput = () => { v.prompt = promptEl.textContent.trim(); saveState(); };\n }\n document.getElementById('vd-history-ct').textContent = v.versions.length;\n const row = document.getElementById('vd-history-row');\n row.innerHTML = v.versions.map((vv, i) => `<div class=\"vd-history-thumb${i === previewIdx ? ' current' : ''}${i === v.adopted ? ' adopted' : ''}\" data-vi=\"${i}\">\n <div class=\"placeholder\"><span class=\"ph-frame\">${vv.label}</span></div>\n <div class=\"ts\">${vv.ts}</div>\n </div>`).join('');\n row.querySelectorAll('.vd-history-thumb').forEach(t => {\n t.addEventListener('click', () => {\n v.preview = +t.dataset.vi;\n ProjectStore.record('stage4.video.version.previewed', { id, version: v.versions[v.preview]?.label });\n saveState();\n openDetail(id);\n });\n });\n document.getElementById('video-detail-modal').classList.add('show');\n }\n\n function init() {\n // 根据各场实际时长(data-duration)计算总时长 + 单场平均(用于「每场 Seedance 约 n 秒」)\n const cards = document.querySelectorAll('.video-card[data-duration]');\n if (cards.length) {\n let total = 0;\n cards.forEach(c => { total += Number(c.dataset.duration) || 0; });\n const avg = Math.round(total / cards.length);\n const avgEl = document.getElementById('seedance-avg');\n const totalEl = document.getElementById('seedance-total');\n if (avgEl) avgEl.textContent = String(avg);\n if (totalEl) totalEl.textContent = String(total);\n }\n document.querySelectorAll('.video-card[data-video-id]').forEach(card => {\n card.addEventListener('click', (e) => {\n if (e.target.closest('[data-vstop]')) return;\n openDetail(card.dataset.videoId);\n });\n });\n document.getElementById('vd-adopt-btn')?.addEventListener('click', () => {\n if (!curVid) return;\n const v = VIDEOS[curVid];\n v.adopted = getPreviewIndex(v);\n v.preview = v.adopted;\n ProjectStore.record('stage4.video.version.adopted', { id: curVid, version: v.versions[v.adopted]?.label });\n saveState();\n Shell.toast('已采用 ' + v.versions[v.adopted].label, v.title + ' · 拼接将用此版');\n document.getElementById('video-detail-modal').classList.remove('show');\n });\n document.getElementById('vd-regen-btn')?.addEventListener('click', () => {\n if (!curVid) return;\n const v = VIDEOS[curVid];\n const promptEl = document.getElementById('vd-prompt-edit');\n const promptText = (promptEl?.textContent || '').trim();\n if (promptText) v.prompt = promptText;\n const nv = { ts: (new Date()).toTimeString().slice(0, 5), label: 'v' + (v.versions.length + 1) };\n v.versions.push(nv);\n v.preview = v.versions.length - 1;\n ProjectStore.record('stage4.video.rerun', { id: curVid, version: nv.label });\n saveState();\n Shell.toast('重跑中', v.title + ' · 约 30s · 新版预览中');\n openDetail(curVid);\n });\n }\n return { init };\n})();\nStage4.init();\n\n/* ============================================================\n Quota · 全局额度预检(PRD §10.3 四层)\n ============================================================ */\nwindow.Quota = (function () {\n // mock 团队/个人额度快照 - 与 team.html / account.html 数据保持一致\n const SNAP = {\n userDailyLeft: 499.55, // 个人日剩余\n userMonthlyLeft: 9837.40, // 个人月剩余\n teamMonthlyLeft: 2837.40, // 团队月剩余(月限额 3000 - 当月已用 162.60)\n teamBalance: 327.40, // 团队总余额\n };\n\n function buildChecks(est, demoBlock) {\n const need = est * 1.2; // PRD §10.3 任务预估 × 1.2\n const layers = [\n { name: '个人日剩余', left: SNAP.userDailyLeft, need },\n { name: '个人月剩余', left: SNAP.userMonthlyLeft, need },\n { name: '团队月剩余', left: SNAP.teamMonthlyLeft, need },\n { name: '团队总余额', left: demoBlock ? 0.50 : SNAP.teamBalance, need },\n ];\n return layers.map((l, i) => ({\n ...l,\n ok: l.left >= l.need,\n idx: i + 1,\n }));\n }\n\n function fmt(n) { return '¥' + n.toFixed(2).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); }\n\n function preflight({ stage, est = 0, hash = '', demo = '', force = false } = {}) {\n const isDemoBlock = demo === 'block';\n const checks = buildChecks(est, isDemoBlock);\n const allOk = checks.every(c => c.ok);\n\n // 渲染 stage 行\n document.getElementById('quota-stage-row').innerHTML =\n '<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>';\n\n // 渲染 4 行检查\n document.getElementById('quota-checks').innerHTML = checks.map(c => `\n <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);\">\n <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>\n <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>\n <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>\n </div>\n `).join('');\n\n // 标题 + footer 按钮\n const ic = document.getElementById('quota-ic');\n const title = document.getElementById('quota-title');\n const sub = document.getElementById('quota-sub');\n const tip = document.getElementById('quota-block-tip');\n const confirmBtn = document.getElementById('quota-confirm');\n const topupBtn = document.getElementById('quota-topup');\n\n if (allOk) {\n ic.style.background = 'rgba(66,195,102,.12)';\n ic.style.color = 'var(--accent-forest)';\n 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>';\n title.firstChild.textContent = '额度预检通过';\n sub.textContent = '// 4 层检查 · 全部通过';\n tip.style.display = 'none';\n confirmBtn.style.display = '';\n topupBtn.style.display = 'none';\n confirmBtn.onclick = () => {\n Shell.closeModal('quota-bg');\n Shell.toast('已确认扣费', stage + ' · 预估 ' + fmt(est));\n if (hash) location.hash = hash;\n };\n } else {\n ic.style.background = 'rgba(235,52,36,.12)';\n ic.style.color = 'var(--accent-crimson)';\n ic.innerHTML = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4M12 16h.01\"/></svg>';\n title.firstChild.textContent = '额度预检未通过';\n sub.textContent = '// ' + checks.filter(c => !c.ok).length + ' 层不通过 · 任务已拦截';\n tip.style.display = '';\n confirmBtn.style.display = 'none';\n topupBtn.style.display = '';\n }\n\n Shell.openModal('quota-bg');\n }\n\n return { preflight };\n})();\n\n/* ============================================================\n 三视图 · 放大查看 lightbox · setupProdPreview 共用\n ============================================================ */\nfunction openTriLightbox(ver, isAdopted, prodName) {\n let bg = document.getElementById('tri-lightbox-bg');\n if (!bg) {\n bg = document.createElement('div');\n bg.id = 'tri-lightbox-bg';\n bg.className = 'modal-bg';\n bg.innerHTML = `\n <div class=\"tri-lightbox\" role=\"dialog\" aria-label=\"三视图放大查看\">\n <button class=\"tri-lightbox-close\" type=\"button\" aria-label=\"关闭\">\n <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>\n </button>\n <div class=\"tri-lightbox-head\">\n // 三视图(正/侧/背) · <span class=\"lb-ver\" id=\"tri-lightbox-label\">v1</span>\n <span class=\"lb-tag\" id=\"tri-lightbox-tag\" hidden>已采用</span>\n </div>\n <div class=\"placeholder tri-lightbox-img\" id=\"tri-lightbox-img\"></div>\n <div class=\"tri-lightbox-foot\">\n <span id=\"tri-lightbox-meta\">// 生成于 --:--</span>\n <span class=\"spc\"></span>\n <span><kbd>Esc</kbd> 关闭</span>\n </div>\n </div>\n `;\n document.body.appendChild(bg);\n bg.addEventListener('click', (e) => {\n if (e.target === bg) Shell.closeModal('tri-lightbox-bg');\n });\n bg.querySelector('.tri-lightbox-close')?.addEventListener('click', () => {\n Shell.closeModal('tri-lightbox-bg');\n });\n }\n bg.querySelector('#tri-lightbox-img').innerHTML =\n `<span class=\"ph-frame\">${prodName} · 三视图(正/侧/背) · ${ver.label}</span>`;\n bg.querySelector('#tri-lightbox-label').textContent = ver.label;\n const tag = bg.querySelector('#tri-lightbox-tag');\n tag.hidden = !isAdopted;\n bg.querySelector('#tri-lightbox-meta').textContent = `// 生成于 ${ver.ts}`;\n Shell.openModal('tri-lightbox-bg');\n}\n\n/* ═══════════════════════════════════════════════════════════\n STAGE 5 · 时间轴编辑器交互(剪映式)\n select / seek / drag-playhead / play-pause / zoom / del / split / trim\n ═══════════════════════════════════════════════════════════ */\n(function initTimelineEditor() {\n const $tl = document.getElementById('ed-timeline');\n if (!$tl) return;\n const $ruler = document.getElementById('ed-ruler');\n const $playhead = document.getElementById('ed-playhead');\n const $laneV = document.getElementById('ed-lane-video');\n const $laneS = document.getElementById('ed-lane-subtitle');\n const $playBtn = document.getElementById('ed-play-btn');\n const $playIcon = document.getElementById('ed-play-icon');\n const $prevBtn = document.getElementById('ed-prev-btn');\n const $nextBtn = document.getElementById('ed-next-btn');\n const $splitBtn = document.getElementById('ed-split-btn');\n const $copyBtn = document.getElementById('ed-copy-btn');\n const $delBtn = document.getElementById('ed-del-btn');\n const $zoom = document.getElementById('ed-zoom-input');\n const $curTime = document.getElementById('ed-cur-time');\n const $totalTime= document.getElementById('ed-total-time');\n const $insName = document.getElementById('ed-inspect-name');\n const $insStart = document.getElementById('ed-inspect-start');\n const $insDur = document.getElementById('ed-inspect-dur');\n const $canvasLb = document.getElementById('ed-canvas-label');\n\n const TOTAL = 15;\n let currentTime = 0;\n let selectedEl = null;\n let playing = false;\n let rafId = null;\n let lastTs = 0;\n\n const PLAY_SVG = '<path d=\"M5 4l7 4-7 4z\" fill=\"currentColor\"/>';\n const PAUSE_SVG = '<path d=\"M4 3h3v10H4zM9 3h3v10H9z\" fill=\"currentColor\"/>';\n\n function fmtTime(s) {\n s = Math.max(0, Math.min(TOTAL, s));\n const m = Math.floor(s / 60);\n const sec = s - m * 60;\n return String(m).padStart(2,'0') + ':' + sec.toFixed(2).padStart(5,'0');\n }\n // 时间轴数据 · data-start(时间线起点) + data-dur(当前时长) + data-max(源最大) + data-in(源素材入点)\n const MIN_DUR = 0.2;\n const $laneB = document.querySelector('#ed-timeline .bgm-track .lane');\n const lanes = { video: $laneV, subtitle: $laneS, bgm: $laneB };\n const ALIGN_EPS = 0.12;\n const $alignGuide = document.createElement('span');\n $alignGuide.className = 'tl-align-guide';\n $tl.appendChild($alignGuide);\n const D = (c) => Number(c.dataset.dur || 1);\n const M = (c) => Number(c.dataset.max || 1);\n const IN = (c) => Number(c.dataset.in || 0);\n function clipDur(c) { return D(c); }\n function compactStart(c) {\n let acc = 0;\n for (const sib of c.parentElement.querySelectorAll('.clip')) {\n if (sib === c) return acc;\n acc += D(sib);\n }\n return 0;\n }\n function clipStart(c) {\n const start = Number(c.dataset.start);\n return Number.isFinite(start) ? start : compactStart(c);\n }\n function clipEnd(c) { return clipStart(c) + D(c); }\n function sourceDur(c) { return Math.max(M(c), D(c), MIN_DUR); }\n function clampSourceIn(c, sourceIn = IN(c)) {\n const maxIn = Math.max(0, sourceDur(c) - D(c));\n return Math.max(0, Math.min(maxIn, sourceIn));\n }\n function syncVideoPreview(c) {\n if (!c.classList.contains('video')) return;\n const dur = Math.max(D(c), MIN_DUR);\n const srcDur = Math.max(sourceDur(c), dur);\n const sourceIn = clampSourceIn(c);\n c.dataset.in = String(sourceIn);\n c.style.setProperty('--src-width', (srcDur / dur * 100) + '%');\n c.style.setProperty('--src-offset', (srcDur ? (-(sourceIn / srcDur) * 100) : 0) + '%');\n }\n function freezeLaneStarts(lane) {\n let acc = 0;\n for (const clip of lane.querySelectorAll('.clip')) {\n const explicit = Number(clip.dataset.start);\n const start = Number.isFinite(explicit) ? explicit : acc;\n clip.dataset.start = String(start);\n acc = start + D(clip);\n }\n }\n function compactLane(lane) {\n let acc = 0;\n for (const clip of lane.querySelectorAll('.clip')) {\n clip.dataset.start = String(acc);\n acc += D(clip);\n }\n layoutLane(lane);\n }\n function snapLane(lane) {\n lane.classList.add('is-snapping');\n compactLane(lane);\n window.setTimeout(() => lane.classList.remove('is-snapping'), 180);\n }\n function settleLane(lane) {\n if (lane === lanes.video) snapLane(lane);\n else layoutLane(lane);\n }\n // 绝对布局:默认紧贴排列;trim 后允许保留被裁剪端的可见空隙\n function layoutLane(lane) {\n let acc = 0;\n for (const clip of lane.querySelectorAll('.clip')) {\n const dur = D(clip);\n const explicit = Number(clip.dataset.start);\n const start = Number.isFinite(explicit) ? explicit : acc;\n clip.style.left = (start / TOTAL * 100) + '%';\n clip.style.width = (dur / TOTAL * 100) + '%';\n syncVideoPreview(clip);\n acc = start + dur;\n }\n }\n function videoBoundaries() {\n const points = [];\n $laneV.querySelectorAll('.clip.video').forEach(c => {\n points.push(clipStart(c), clipEnd(c));\n });\n return points;\n }\n function nearestVideoBoundary(edges) {\n let best = null;\n const points = videoBoundaries();\n edges.forEach((edge, edgeIndex) => {\n points.forEach(time => {\n const diff = Math.abs(edge - time);\n if (diff <= ALIGN_EPS && (!best || diff < best.diff)) {\n best = { edge, edgeIndex, time, diff };\n }\n });\n });\n return best;\n }\n function showAlignGuideAt(time) {\n const laneRect = $laneV.getBoundingClientRect();\n const tlRect = $tl.getBoundingClientRect();\n const rulerRect = $ruler.getBoundingClientRect();\n const lastLane = $laneB || $laneS || $laneV;\n const lastRect = lastLane.getBoundingClientRect();\n $alignGuide.style.left = (laneRect.left + (time / TOTAL) * laneRect.width - tlRect.left) + 'px';\n $alignGuide.style.top = (rulerRect.top - tlRect.top) + 'px';\n $alignGuide.style.height = (lastRect.bottom - rulerRect.top) + 'px';\n $alignGuide.classList.add('show');\n }\n function hideAlignGuide() {\n $alignGuide.classList.remove('show');\n }\n function guideEdgeToVideo(edge) {\n const match = nearestVideoBoundary([edge]);\n if (!match) {\n hideAlignGuide();\n return null;\n }\n showAlignGuideAt(match.time);\n return match.time;\n }\n function guideClipToVideo(start, dur) {\n const safeDur = Math.max(MIN_DUR, Math.min(dur, TOTAL));\n const clampedStart = Math.max(0, Math.min(TOTAL - safeDur, start));\n const match = nearestVideoBoundary([clampedStart, clampedStart + safeDur]);\n if (!match) {\n hideAlignGuide();\n return clampedStart;\n }\n const nextStart = match.edgeIndex === 0 ? match.time : match.time - safeDur;\n showAlignGuideAt(match.time);\n return Math.max(0, Math.min(TOTAL - safeDur, nextStart));\n }\n function ensureDropGhost(lane) {\n let ghost = lane.querySelector('.tl-insert-ghost');\n if (!ghost) {\n ghost = document.createElement('span');\n ghost.className = 'tl-insert-ghost';\n lane.appendChild(ghost);\n }\n return ghost;\n }\n function removeDropGhost(lane) {\n lane.querySelector('.tl-insert-ghost')?.remove();\n }\n function placeDropGhost(ghost, start, dur) {\n ghost.style.left = (start / TOTAL * 100) + '%';\n ghost.style.width = (dur / TOTAL * 100) + '%';\n }\n function insertIndexForCenter(siblings, centerT) {\n let acc = 0;\n for (let i = 0; i < siblings.length; i++) {\n const mid = acc + D(siblings[i]) / 2;\n if (centerT < mid) return i;\n acc += D(siblings[i]);\n }\n return siblings.length;\n }\n function previewReorder(lane, dragged, insertIndex) {\n const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== dragged);\n const ghost = ensureDropGhost(lane);\n let acc = 0;\n for (let i = 0; i <= siblings.length; i++) {\n if (i === insertIndex) {\n placeDropGhost(ghost, acc, D(dragged));\n acc += D(dragged);\n }\n const sibling = siblings[i];\n if (sibling) {\n sibling.dataset.start = String(acc);\n sibling.style.left = (acc / TOTAL * 100) + '%';\n sibling.style.width = (D(sibling) / TOTAL * 100) + '%';\n syncVideoPreview(sibling);\n acc += D(sibling);\n }\n }\n }\n function commitReorder(lane, dragged, insertIndex) {\n const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== dragged);\n dragged.remove();\n if (insertIndex >= siblings.length) lane.appendChild(dragged);\n else lane.insertBefore(dragged, siblings[insertIndex]);\n }\n function compactAll() {\n Object.values(lanes).forEach(l => l && compactLane(l));\n }\n function clipAtTimeOnTrack(track, t) {\n const lane = lanes[track];\n if (!lane) return null;\n for (const c of lane.querySelectorAll('.clip')) {\n if (t >= clipStart(c) && t < clipEnd(c)) return c;\n }\n return null;\n }\n\n function updateTimeUI() {\n $curTime.textContent = fmtTime(currentTime);\n $totalTime.textContent = fmtTime(TOTAL);\n $playhead.style.left = (currentTime / TOTAL * 100) + '%';\n const v = clipAtTimeOnTrack('video', currentTime);\n if ($canvasLb) {\n $canvasLb.textContent = v\n ? '9:16 预览 · 1080×1920 · ' + (v.dataset.label || '')\n : '9:16 预览 · 1080×1920';\n }\n updateActionButtons();\n }\n\n function selectClip(clip) {\n document.querySelectorAll('#ed-timeline .clip.selected').forEach(c => {\n c.classList.remove('selected');\n c.querySelectorAll('[data-trim]').forEach(t => t.remove());\n });\n selectedEl = clip;\n if (clip) {\n clip.classList.add('selected');\n // 三轨皆可剪 · 任意选中片段都加 trim 把手\n const l = document.createElement('span'); l.className = 'trim-l'; l.dataset.trim = 'l';\n const r = document.createElement('span'); r.className = 'trim-r'; r.dataset.trim = 'r';\n clip.prepend(l); clip.appendChild(r);\n }\n updateInspector();\n updateActionButtons();\n }\n function updateInspector() {\n if (!selectedEl) {\n $insName.textContent = '未选';\n $insStart.value = '—';\n $insDur.value = '—';\n return;\n }\n const t = selectedEl.dataset.track;\n const num = selectedEl.querySelector('.num')?.textContent.trim();\n const label = selectedEl.dataset.label || '';\n $insName.textContent = (t === 'video' ? '镜 ' + num : t === 'subtitle' ? '字幕' : 'BGM')\n + (label ? ' · ' + label.slice(0, 12) : '');\n $insStart.value = fmtTime(clipStart(selectedEl));\n $insDur.value = clipDur(selectedEl).toFixed(2) + 's';\n }\n function updateActionButtons() {\n const has = !!selectedEl;\n $delBtn.disabled = !has;\n $copyBtn.disabled = !has;\n if (has) {\n const s = clipStart(selectedEl), e = clipEnd(selectedEl);\n $splitBtn.disabled = !(currentTime > s + 0.05 && currentTime < e - 0.05);\n } else {\n $splitBtn.disabled = true;\n }\n }\n\n function bindClipClicks() {\n document.querySelectorAll('#ed-timeline .clip').forEach(clip => {\n if (clip.dataset.boundClick) return;\n clip.dataset.boundClick = '1';\n clip.addEventListener('click', (e) => {\n if (e.target.closest('[data-trim]')) return;\n if (_bodyDragMoved) return; // 拖动重排刚结束 · 抑制 click\n e.stopPropagation();\n selectClip(clip);\n if (clip.dataset.track === 'video') {\n currentTime = clipStart(clip);\n updateTimeUI();\n }\n });\n });\n }\n\n function laneSeek(lane, ev) {\n if (ev.target.closest('.clip') || ev.target.closest('.playhead') || ev.target.closest('[data-trim]')) return;\n const rect = lane.getBoundingClientRect();\n const pct = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width));\n currentTime = pct * TOTAL;\n updateTimeUI();\n }\n $laneV.addEventListener('click', e => laneSeek($laneV, e));\n\n $ruler.addEventListener('click', (e) => {\n const rect = $ruler.getBoundingClientRect();\n const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));\n currentTime = pct * TOTAL;\n updateTimeUI();\n });\n\n let dragging = false;\n let playheadDragOffset = 0;\n function playheadCenterX() {\n const rect = $laneS.getBoundingClientRect();\n return rect.left + (currentTime / TOTAL) * rect.width;\n }\n function seekPlayhead(clientX) {\n const rect = $laneS.getBoundingClientRect();\n const pct = Math.max(0, Math.min(1, (clientX - playheadDragOffset - rect.left) / rect.width));\n currentTime = pct * TOTAL;\n updateTimeUI();\n }\n function startPlayheadDrag(e) {\n e.preventDefault();\n e.stopPropagation();\n dragging = true;\n playheadDragOffset = e.clientX - playheadCenterX();\n if (playing) pause();\n $playhead.classList.add('is-dragging');\n $tl.classList.add('is-dragging-playhead');\n }\n $playhead.addEventListener('mousedown', startPlayheadDrag);\n document.addEventListener('mousemove', (e) => {\n if (!dragging) return;\n seekPlayhead(e.clientX);\n });\n document.addEventListener('mouseup', () => {\n if (!dragging) return;\n dragging = false;\n playheadDragOffset = 0;\n $playhead.classList.remove('is-dragging');\n $tl.classList.remove('is-dragging-playhead');\n });\n\n function play() {\n if (playing) return;\n if (currentTime >= TOTAL - 0.01) currentTime = 0;\n playing = true;\n $playIcon.innerHTML = PAUSE_SVG;\n $playBtn.classList.add('is-playing');\n lastTs = performance.now();\n rafId = requestAnimationFrame(tick);\n }\n function pause() {\n if (!playing) return;\n playing = false;\n $playIcon.innerHTML = PLAY_SVG;\n $playBtn.classList.remove('is-playing');\n if (rafId) cancelAnimationFrame(rafId);\n rafId = null;\n }\n function tick(ts) {\n if (!playing) return;\n const dt = (ts - lastTs) / 1000;\n lastTs = ts;\n currentTime = Math.min(TOTAL, currentTime + dt);\n updateTimeUI();\n if (currentTime >= TOTAL) { pause(); return; }\n rafId = requestAnimationFrame(tick);\n }\n $playBtn.addEventListener('click', () => playing ? pause() : play());\n\n const FRAME = 1 / 30;\n $prevBtn.addEventListener('click', () => {\n pause();\n currentTime = Math.max(0, currentTime - FRAME);\n updateTimeUI();\n });\n $nextBtn.addEventListener('click', () => {\n pause();\n currentTime = Math.min(TOTAL, currentTime + FRAME);\n updateTimeUI();\n });\n\n document.addEventListener('keydown', (e) => {\n const stage5 = document.querySelector('section.stage.active[data-stage-pane=\"5\"]');\n if (!stage5) return;\n if (e.target.matches('input, textarea, [contenteditable]')) return;\n if (e.code === 'Space') { e.preventDefault(); playing ? pause() : play(); }\n else if (e.key === 'ArrowLeft') { e.preventDefault(); $prevBtn.click(); }\n else if (e.key === 'ArrowRight') { e.preventDefault(); $nextBtn.click(); }\n else if (e.key === 'Delete' || e.key === 'Backspace') {\n if (selectedEl) { e.preventDefault(); deleteSelected(); }\n }\n });\n\n $zoom.addEventListener('input', () => {\n const pct = Number($zoom.value);\n document.querySelectorAll('#ed-timeline .lane, #ed-timeline #ed-ruler').forEach(l => {\n l.style.minWidth = (pct === 100 ? '' : (pct + '%'));\n });\n });\n\n function deleteSelected() {\n if (!selectedEl) return;\n const lane = selectedEl.parentElement;\n const next = selectedEl.nextElementSibling?.classList.contains('clip')\n ? selectedEl.nextElementSibling\n : selectedEl.previousElementSibling?.classList.contains('clip')\n ? selectedEl.previousElementSibling\n : null;\n selectedEl.remove();\n settleLane(lane);\n if (next) selectClip(next);\n else { selectedEl = null; updateInspector(); updateActionButtons(); }\n renumberVideo();\n updateTimeUI();\n }\n $delBtn.addEventListener('click', deleteSelected);\n\n $copyBtn.addEventListener('click', () => {\n if (!selectedEl) return;\n const dup = selectedEl.cloneNode(true);\n dup.classList.remove('selected');\n dup.querySelectorAll('[data-trim]').forEach(t => t.remove());\n delete dup.dataset.boundClick;\n selectedEl.after(dup);\n if (selectedEl.parentElement !== lanes.video) {\n dup.dataset.start = String(Math.max(0, Math.min(TOTAL - D(dup), clipEnd(selectedEl))));\n }\n settleLane(selectedEl.parentElement);\n bindClipClicks();\n renumberVideo();\n selectClip(dup);\n });\n\n $splitBtn.addEventListener('click', () => {\n if (!selectedEl) return;\n const s = clipStart(selectedEl), e = clipEnd(selectedEl);\n if (!(currentTime > s + 0.05 && currentTime < e - 0.05)) return;\n const cutOff = currentTime - s; // 距 clip 起始的偏移\n const origDur = D(selectedEl);\n const leftDur = cutOff;\n const rightDur = origDur - cutOff;\n // 左半:dur=max=leftDur(切开后双方各自独立 · 不能再恢复跨越切点)\n selectedEl.dataset.dur = String(leftDur);\n selectedEl.dataset.max = String(leftDur);\n selectedEl.dataset.in = '0';\n selectedEl.dataset.start = String(s);\n const right = selectedEl.cloneNode(true);\n right.classList.remove('selected');\n right.querySelectorAll('[data-trim]').forEach(t => t.remove());\n delete right.dataset.boundClick;\n right.dataset.dur = String(rightDur);\n right.dataset.max = String(rightDur);\n right.dataset.in = '0';\n right.dataset.start = String(s + leftDur);\n // 右半若是视频片段 · 重新生成胶卷帧条数量(按时长成比例)\n if (right.classList.contains('video')) {\n const oldFrames = right.querySelector('.frames');\n if (oldFrames) {\n const n = Math.max(2, Math.round(rightDur * 1.5));\n oldFrames.innerHTML = '<span class=\"fr\"></span>'.repeat(n);\n }\n const oldFramesL = selectedEl.querySelector('.frames');\n if (oldFramesL) {\n const n = Math.max(2, Math.round(leftDur * 1.5));\n oldFramesL.innerHTML = '<span class=\"fr\"></span>'.repeat(n);\n }\n }\n selectedEl.after(right);\n layoutLane(selectedEl.parentElement);\n bindClipClicks();\n renumberVideo();\n selectClip(right);\n });\n\n function renumberVideo() {\n $laneV.querySelectorAll('.clip.video').forEach((c, i) => {\n const num = c.querySelector('.num');\n if (num) num.textContent = String(i + 1);\n });\n }\n\n // Trim 把手 · 三轨通用 · 左右边界按真实裁剪方向反馈\n document.addEventListener('mousedown', (e) => {\n const handle = e.target.closest('[data-trim]');\n if (!handle || !selectedEl) return;\n e.preventDefault(); e.stopPropagation();\n const side = handle.dataset.trim;\n const lane = selectedEl.parentElement;\n const isVideoTrack = lane === lanes.video;\n freezeLaneStarts(lane);\n const laneRect = lane.getBoundingClientRect();\n const startMouseX = e.clientX;\n const startStart = clipStart(selectedEl);\n const startDur = D(selectedEl);\n const startEnd = startStart + startDur;\n const startIn = clampSourceIn(selectedEl);\n const startSourceDur = sourceDur(selectedEl);\n const startOut = Math.max(0, startSourceDur - startIn - startDur);\n const prevClip = selectedEl.previousElementSibling?.classList.contains('clip') ? selectedEl.previousElementSibling : null;\n const nextClip = selectedEl.nextElementSibling?.classList.contains('clip') ? selectedEl.nextElementSibling : null;\n const prevEnd = prevClip ? clipEnd(prevClip) : 0;\n const nextStart = nextClip ? clipStart(nextClip) : TOTAL;\n let didTrim = false;\n\n function onMove(ev) {\n const dx = ev.clientX - startMouseX;\n if (Math.abs(dx) > 1) didTrim = true;\n const dt = (dx / laneRect.width) * TOTAL;\n if (!isVideoTrack) {\n if (side === 'l') {\n const maxStart = startEnd - MIN_DUR;\n let newStart = Math.max(0, Math.min(maxStart, startStart + dt));\n const guidedStart = guideEdgeToVideo(newStart);\n if (guidedStart !== null) newStart = Math.max(0, Math.min(maxStart, guidedStart));\n selectedEl.dataset.start = String(newStart);\n selectedEl.dataset.dur = String(Math.max(MIN_DUR, startEnd - newStart));\n } else {\n const minEnd = startStart + MIN_DUR;\n let newEnd = Math.max(minEnd, Math.min(TOTAL, startEnd + dt));\n const guidedEnd = guideEdgeToVideo(newEnd);\n if (guidedEnd !== null) newEnd = Math.max(minEnd, Math.min(TOTAL, guidedEnd));\n selectedEl.dataset.start = String(startStart);\n selectedEl.dataset.dur = String(Math.max(MIN_DUR, newEnd - startStart));\n }\n layoutLane(lane);\n updateInspector();\n updateActionButtons();\n return;\n }\n hideAlignGuide();\n if (side === 'l') {\n const minStart = Math.max(prevEnd, startStart - startIn);\n const maxStart = startEnd - MIN_DUR;\n const newStart = Math.max(minStart, Math.min(maxStart, startStart + dt));\n const newDur = Math.max(MIN_DUR, startEnd - newStart);\n const newIn = Math.max(0, Math.min(startSourceDur - newDur, startIn + (newStart - startStart)));\n selectedEl.dataset.start = String(newStart);\n selectedEl.dataset.dur = String(newDur);\n selectedEl.dataset.in = String(newIn);\n } else {\n const minEnd = startStart + MIN_DUR;\n const maxEnd = Math.min(nextStart, startEnd + startOut, TOTAL);\n const newEnd = Math.max(minEnd, Math.min(maxEnd, startEnd + dt));\n selectedEl.dataset.start = String(startStart);\n selectedEl.dataset.dur = String(newEnd - startStart);\n selectedEl.dataset.in = String(startIn);\n }\n layoutLane(lane);\n updateInspector();\n updateActionButtons();\n }\n function onUp() {\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n document.body.style.cursor = '';\n hideAlignGuide();\n if (isVideoTrack) {\n snapLane(lane);\n if (didTrim && selectedEl) {\n currentTime = side === 'l' ? clipStart(selectedEl) : clipEnd(selectedEl);\n }\n updateInspector();\n updateTimeUI();\n } else {\n layoutLane(lane);\n updateInspector();\n updateActionButtons();\n }\n }\n document.body.style.cursor = 'ew-resize';\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n\n // 拖拽选中片段身体 · 在同轨内重排位置\n let _bodyDragMoved = false;\n document.addEventListener('mousedown', (e) => {\n const clip = e.target.closest('#ed-timeline .clip');\n if (!clip) return;\n if (e.target.closest('[data-trim]')) return;\n if (e.target.closest('.ph-grab')) return;\n e.preventDefault();\n _bodyDragMoved = false;\n if (clip !== selectedEl) selectClip(clip);\n const lane = clip.parentElement;\n const isVideoTrack = lane === lanes.video;\n freezeLaneStarts(lane);\n const laneRect = lane.getBoundingClientRect();\n const startMouseX = e.clientX;\n const startStartT = clipStart(clip);\n const clipDuration = D(clip);\n let dropIndex = null;\n\n function onMove(ev) {\n const dx = ev.clientX - startMouseX;\n if (!_bodyDragMoved && Math.abs(dx) < 5) return;\n _bodyDragMoved = true;\n clip.classList.add('dragging');\n const dt = (dx / laneRect.width) * TOTAL;\n if (!isVideoTrack) {\n const previewStart = guideClipToVideo(startStartT + dt, clipDuration);\n clip.dataset.start = String(previewStart);\n clip.style.left = (previewStart / TOTAL * 100) + '%';\n clip.style.width = (clipDuration / TOTAL * 100) + '%';\n updateInspector();\n updateActionButtons();\n document.body.style.cursor = 'grabbing';\n return;\n }\n hideAlignGuide();\n lane.classList.add('is-reordering');\n const newCenter = Math.max(clipDuration / 2, Math.min(TOTAL - clipDuration / 2, startStartT + clipDuration / 2 + dt));\n const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip);\n dropIndex = insertIndexForCenter(siblings, newCenter);\n previewReorder(lane, clip, dropIndex);\n const previewStart = Math.max(0, Math.min(TOTAL - clipDuration, newCenter - clipDuration / 2));\n clip.dataset.start = String(previewStart);\n clip.style.left = (previewStart / TOTAL * 100) + '%';\n clip.style.width = (clipDuration / TOTAL * 100) + '%';\n syncVideoPreview(clip);\n document.body.style.cursor = 'grabbing';\n }\n function onUp(ev) {\n document.removeEventListener('mousemove', onMove);\n document.removeEventListener('mouseup', onUp);\n removeDropGhost(lane);\n lane.classList.remove('is-reordering');\n clip.classList.remove('dragging');\n document.body.style.cursor = '';\n hideAlignGuide();\n if (!_bodyDragMoved) return;\n if (!isVideoTrack) {\n layoutLane(lane);\n selectClip(clip);\n updateInspector();\n updateActionButtons();\n setTimeout(() => { _bodyDragMoved = false; }, 50);\n return;\n }\n if (dropIndex === null) {\n const dx = ev.clientX - startMouseX;\n const dt = (dx / laneRect.width) * TOTAL;\n const newCenter = Math.max(clipDuration / 2, Math.min(TOTAL - clipDuration / 2, startStartT + clipDuration / 2 + dt));\n const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip);\n dropIndex = insertIndexForCenter(siblings, newCenter);\n }\n commitReorder(lane, clip, dropIndex);\n snapLane(lane);\n if (lane === lanes.video) renumberVideo();\n selectClip(clip);\n updateTimeUI();\n setTimeout(() => { _bodyDragMoved = false; }, 50);\n }\n document.addEventListener('mousemove', onMove);\n document.addEventListener('mouseup', onUp);\n });\n\n // 初始化:磁吸布局 + 绑定\n compactAll();\n bindClipClicks();\n updateTimeUI();\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"platformCover": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"platform-cover.html\">\n<meta charset=\"utf-8\">\n<title>平台套图 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .app { height: 100vh; overflow: hidden; }\n main { display: flex; flex-direction: column; min-height: 0; }\n #page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }\n\n .pc-layout {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 260px 1fr;\n transition: grid-template-columns var(--t-base);\n }\n .pc-layout.side-collapsed { grid-template-columns: 0 1fr; }\n @media (max-width: 1280px) {\n .pc-layout { grid-template-columns: 240px 1fr; }\n .pc-layout.side-collapsed { grid-template-columns: 0 1fr; }\n }\n\n /* ─── 主区: flat 双区 · 用 border 分隔 ─── */\n .pc-main {\n display: flex; flex-direction: column;\n min-height: 0;\n overflow: hidden;\n }\n /* 主区头部 · toolbar 风格 (跟 image-optimize 一致) */\n .pc-main-h {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 10px;\n padding: 12px 28px;\n border-bottom: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .pc-main-h .cur-title {\n display: flex; align-items: baseline; gap: 8px;\n min-width: 0; max-width: 50%;\n }\n .pc-main-h .cur-title .crumb {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .04em;\n flex-shrink: 0;\n }\n .pc-main-h .cur-title .nm {\n font-size: 15px; font-weight: 600;\n color: var(--accent-black);\n letter-spacing: -.005em;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .pc-main-h .cur-title .nm.placeholder {\n font-weight: 400; font-size: 13px;\n color: var(--black-alpha-48);\n }\n .pc-main-h .spacer { flex: 1; }\n .pc-main-h .side-restore-btn {\n height: 32px;\n padding: 0 10px;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n }\n .pc-main-h .side-restore-btn:hover { border-color: var(--heat-20); color: var(--heat); }\n .pc-main-h .side-restore-btn[hidden] { display: none; }\n .pc-main-h .side-restore-btn svg { width: 14px; height: 14px; }\n .pc-main-h .search-btn {\n width: 32px; height: 32px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n cursor: pointer;\n display: grid; place-items: center;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .pc-main-h .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }\n .pc-main-h .search-btn svg { width: 14px; height: 14px; }\n .pc-main-h .tb-chip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 32px; padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .pc-main-h .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }\n .pc-main-h .tb-chip svg { width: 10px; height: 10px; opacity: .6; }\n .pc-main-h .tb-chip.active {\n background: var(--heat-12);\n border-color: var(--heat-40);\n color: var(--heat);\n }\n .pc-main-h .tb-chip.active svg { opacity: .8; }\n\n /* search · 折叠图标态 + 展开输入框 */\n .pc-main-h .tb-search-wrap { display: flex; align-items: center; }\n .pc-main-h .tb-search-input {\n width: 0; height: 32px;\n padding: 0; margin: 0;\n border: 1px solid transparent;\n background: transparent;\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit; outline: none;\n transition: width var(--t-base), padding var(--t-base), border-color var(--t-base), margin var(--t-base), background var(--t-base);\n }\n .pc-main-h .tb-search-wrap.expanded .tb-search-input {\n width: 220px;\n padding: 0 10px;\n margin-left: 6px;\n background: var(--surface);\n border-color: var(--border-faint);\n }\n .pc-main-h .tb-search-wrap.expanded .tb-search-input:focus { border-color: var(--heat-40); }\n .pc-main-h .tb-search-wrap.expanded .search-btn { border-color: var(--heat-40); color: var(--heat); }\n\n /* dropdown · chip 下拉菜单 */\n .pc-main-h .tb-menu-wrap { position: relative; }\n .pc-main-h .tb-menu {\n position: absolute;\n top: calc(100% + 6px); right: 0;\n min-width: 160px; max-width: 260px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n box-shadow: 0 8px 24px rgba(0,0,0,.08);\n padding: 4px;\n z-index: 50;\n display: none;\n max-height: 320px; overflow-y: auto;\n }\n .pc-main-h .tb-menu-wrap.open .tb-menu { display: block; }\n .pc-main-h .tb-menu-item {\n display: flex; align-items: center; gap: 8px;\n width: 100%; padding: 7px 10px;\n background: transparent; border: 0;\n border-radius: 4px;\n font-size: 12.5px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer; text-align: left;\n transition: background var(--t-base), color var(--t-base);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .pc-main-h .tb-menu-item:hover { background: var(--background-lighter); color: var(--accent-black); }\n .pc-main-h .tb-menu-item.active {\n background: var(--heat-12); color: var(--heat); font-weight: 500;\n }\n .pc-main-h .tb-menu-empty {\n padding: 10px;\n font-size: 11.5px; color: var(--black-alpha-48);\n font-family: var(--font-mono); letter-spacing: .02em;\n text-align: center;\n }\n\n /* 批次被筛选隐藏 */\n .pv-platform-section[data-hidden=\"1\"] { display: none !important; }\n .pc-main-body {\n flex: 1; min-height: 0;\n display: grid;\n grid-template-columns: 320px 1fr;\n }\n @media (max-width: 1280px) {\n .pc-main-body { grid-template-columns: 300px 1fr; }\n }\n .pc-main-body > .pc-form {\n border: 0; border-radius: 0;\n border-right: 1px solid var(--border-faint);\n background: var(--surface);\n }\n .pc-main-body > .pc-preview {\n border: 0; border-radius: 0;\n background: var(--background-base);\n }\n\n /* ─── 商品空间 (最左 · 单选 · 当前商品决定结果区批次) ─── */\n .pc-prod-space {\n background: var(--surface);\n border-right: 1px solid var(--border-faint);\n display: flex; flex-direction: column;\n min-height: 0;\n overflow: hidden;\n transition: opacity var(--t-base), transform var(--t-base);\n }\n .pc-layout.side-collapsed .pc-prod-space {\n opacity: 0;\n transform: translateX(-8px);\n pointer-events: none;\n }\n /* 顶部工具栏: 返回 + 折叠 (跟 image-optimize 视觉一致) */\n .pc-side-top {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 8px;\n padding: 14px 14px 10px;\n border-bottom: 1px solid var(--border-faint);\n }\n .pc-side-top .back-pill {\n display: inline-flex; align-items: center; gap: 6px;\n height: 34px; padding: 0 13px 0 11px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n color: var(--accent-black);\n font-size: 13px; font-weight: 500;\n font-family: inherit;\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .pc-side-top .back-pill:hover {\n background: var(--black-alpha-4);\n border-color: var(--black-alpha-24);\n color: var(--accent-black);\n }\n .pc-side-top .back-pill svg { width: 14px; height: 14px; }\n .pc-side-top .fold {\n margin-left: auto;\n width: 26px; height: 26px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--black-alpha-48); cursor: pointer;\n transition: background var(--t-base), color var(--t-base);\n }\n .pc-side-top .fold:hover { background: var(--black-alpha-4); color: var(--accent-black); }\n .pc-side-top .fold svg { width: 14px; height: 14px; }\n .pc-ps-h {\n flex-shrink: 0;\n padding: 12px 14px 10px;\n }\n /* 商品列表 头部 (// 商品空间 + 新建商品 主 CTA · 右上显眼橙色) */\n .pc-list-h {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 8px;\n padding: 4px 14px 10px;\n }\n .pc-list-h .mono {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .06em;\n text-transform: uppercase;\n }\n .pc-list-h .new-prod {\n margin-left: auto;\n height: 28px; padding: 0 12px 0 10px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--heat); color: #fff;\n border: 1px solid var(--heat);\n border-radius: var(--r-sm);\n font-size: 12px; font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n box-shadow:\n inset 0 -2px 4px rgba(250, 93, 25, 0.20),\n 0 1px 1px rgba(250, 93, 25, 0.12),\n 0 2px 4px rgba(250, 93, 25, 0.10);\n transition: filter var(--t-base), transform var(--t-fast), box-shadow var(--t-base);\n }\n .pc-list-h .new-prod:hover {\n filter: brightness(.96);\n box-shadow:\n inset 0 -2px 4px rgba(250, 93, 25, 0.20),\n 0 1px 1px rgba(250, 93, 25, 0.16),\n 0 4px 8px rgba(250, 93, 25, 0.20);\n }\n .pc-list-h .new-prod:active { transform: scale(.98); }\n .pc-list-h .new-prod svg { width: 12px; height: 12px; }\n .pc-ps-search {\n position: relative;\n height: 32px;\n }\n .pc-ps-search svg {\n position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n width: 13px; height: 13px;\n color: var(--black-alpha-48);\n z-index: 2;\n pointer-events: none;\n }\n .pc-ps-search input {\n width: 100%; height: 100%;\n padding: 0 10px 0 30px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit;\n outline: none;\n transition: border-color var(--t-base), background var(--t-base);\n }\n .pc-ps-search input:focus { border-color: var(--heat-40); background: var(--surface); }\n .pc-ps-search input::placeholder { color: var(--black-alpha-48); }\n .pc-ps-list {\n flex: 1; min-height: 0;\n overflow-y: auto;\n padding: 4px 10px 10px;\n display: flex; flex-direction: column; gap: 4px;\n }\n .pc-ps-empty {\n padding: 24px 14px;\n text-align: center;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n line-height: 1.7;\n }\n .pc-prod-item {\n display: flex; align-items: center; gap: 10px;\n padding: 8px;\n border: 1px solid transparent;\n border-radius: var(--r-sm);\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pc-prod-item:hover { background: var(--black-alpha-4); }\n .pc-prod-item.active { background: var(--heat-12); border-color: var(--heat-20); }\n .pc-prod-item .thumb {\n flex-shrink: 0;\n width: 36px; height: 36px;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden;\n }\n .pc-prod-item.active .thumb { border-color: var(--heat); }\n .pc-prod-item .body { flex: 1; min-width: 0; }\n .pc-prod-item .nm {\n font-size: 12.5px;\n color: var(--accent-black); font-weight: 500;\n line-height: 1.3;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .pc-prod-item.active .nm { color: var(--heat); font-weight: 600; }\n .pc-prod-item .sub {\n margin-top: 2px;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .pc-ps-all {\n flex-shrink: 0;\n margin: 0 10px 12px;\n padding: 9px 12px;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n display: flex; align-items: center; gap: 8px;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .pc-ps-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .pc-ps-all .ct {\n margin-left: auto;\n color: var(--black-alpha-48);\n font-family: var(--font-mono); font-size: 10.5px;\n }\n .pc-ps-all:hover .ct { color: var(--heat); }\n .pc-ps-all svg { width: 12px; height: 12px; }\n /* 左栏 表单 */\n .pc-form {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow-y: auto;\n padding: 18px 20px;\n display: flex; flex-direction: column;\n }\n .pc-step { margin-bottom: 22px; }\n .pc-step:last-child { margin-bottom: 0; }\n .pc-step-h { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }\n .pc-step-h .num {\n width: 22px; height: 22px; border-radius: 50%;\n background: var(--heat-12); color: var(--heat);\n font-family: var(--font-mono); font-size: 11px; font-weight: 700;\n display: grid; place-items: center;\n }\n .pc-step-h .title { font-size: 14px; font-weight: 600; color: var(--accent-black); }\n\n /* 商品选择 · 已选 chip 列表 */\n .prod-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 6px; }\n .prod-row {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 10px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n transition: background var(--t-base), border-color var(--t-base);\n }\n .prod-row .thumb { width: 28px; height: 28px; border-radius: var(--r-sm); flex-shrink: 0; }\n .prod-row .info { flex: 1; min-width: 0; }\n .prod-row .nm {\n font-size: 12.5px; color: var(--accent-black); font-weight: 500;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .prod-row .meta {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n margin-top: 2px;\n }\n .prod-row .x {\n width: 22px; height: 22px;\n display: grid; place-items: center;\n background: transparent; border: 0;\n border-radius: var(--r-sm);\n cursor: pointer;\n color: var(--black-alpha-48);\n flex-shrink: 0;\n transition: background var(--t-base), color var(--t-base);\n }\n .prod-row .x:hover { background: var(--black-alpha-8); color: var(--accent-crimson); }\n .prod-row .x svg { width: 12px; height: 12px; }\n .prod-row .swap {\n width: 22px; height: 22px;\n display: grid; place-items: center;\n background: transparent; border: 0;\n border-radius: var(--r-sm);\n cursor: pointer;\n color: var(--black-alpha-48);\n flex-shrink: 0;\n transition: background var(--t-base), color var(--t-base);\n }\n .prod-row .swap:hover { background: var(--heat-12); color: var(--heat); }\n .prod-row .swap svg { width: 13px; height: 13px; }\n .prod-empty {\n padding: 14px 10px;\n text-align: center;\n background: var(--background-lighter);\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-48);\n font-size: 12px;\n margin-bottom: 6px;\n }\n .prod-empty .mono {\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: .02em;\n margin-top: 4px;\n }\n .prod-add {\n display: flex; align-items: center; justify-content: center; gap: 6px;\n padding: 8px 10px;\n background: transparent;\n border: 1px dashed var(--heat-40);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-size: 12.5px; color: var(--heat);\n font-family: inherit;\n transition: background var(--t-base);\n width: 100%;\n }\n .prod-add:hover { background: var(--heat-12); }\n .prod-add svg { width: 12px; height: 12px; }\n .prod-add[hidden] { display: none; }\n\n /* 商品库全屏弹窗 */\n .pl-modal-bg {\n position: fixed; inset: 0;\n background: var(--surface);\n z-index: 998;\n display: none;\n }\n .pl-modal-bg.show { display: flex; }\n .pl-modal {\n margin: 0; flex: 1;\n background: var(--surface);\n overflow: hidden;\n display: flex; flex-direction: column;\n }\n .pl-modal-h {\n display: flex; align-items: center; gap: 14px;\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n flex-shrink: 0;\n }\n .pl-modal-h h2 { font-size: 16px; font-weight: 600; }\n .pl-modal-h .ct {\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .pl-modal-h .actions { margin-left: auto; display: flex; gap: 10px; }\n .pl-modal-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 200px 1fr; }\n .pl-side {\n border-right: 1px solid var(--border-faint);\n padding: 18px 0;\n overflow-y: auto;\n }\n .pl-side .pl-side-h {\n padding: 0 20px 8px;\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .06em;\n }\n .pl-side .pl-side-item {\n display: flex; align-items: center; gap: 8px;\n padding: 9px 20px;\n cursor: pointer;\n color: var(--black-alpha-72);\n font-size: 13px;\n border-left: 3px solid transparent;\n transition: background var(--t-base), color var(--t-base);\n }\n .pl-side .pl-side-item:hover { background: var(--black-alpha-4); }\n .pl-side .pl-side-item.active {\n background: var(--heat-12); color: var(--accent-black);\n border-left-color: var(--heat); font-weight: 600;\n }\n .pl-side .pl-side-item .ct {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48);\n }\n .pl-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; }\n .pl-toolbar {\n padding: 14px 28px;\n border-bottom: 1px solid var(--border-faint);\n display: flex; align-items: center; gap: 12px;\n flex-shrink: 0;\n }\n .pl-toolbar .search { position: relative; flex: 1; max-width: 360px; }\n .pl-toolbar .search input {\n width: 100%; height: 32px;\n padding: 0 10px 0 32px;\n background: var(--background-lighter);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n font-size: 12.5px; font-family: inherit;\n color: var(--accent-black); outline: none;\n }\n .pl-toolbar .search svg {\n position: absolute; left: 10px; top: 50%; transform: translateY(-50%);\n width: 14px; height: 14px; color: var(--black-alpha-48);\n }\n .pl-toolbar .btn-new {\n height: 32px; padding: 0 14px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--surface);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n color: var(--accent-black);\n font-family: inherit; font-size: 12.5px;\n cursor: pointer;\n margin-left: auto;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pl-toolbar .btn-new:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }\n .pl-toolbar .btn-new svg { width: 13px; height: 13px; }\n .pl-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }\n .pl-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: 12px;\n }\n .pl-card {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 10px;\n cursor: pointer;\n display: flex; flex-direction: column; gap: 6px;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pl-card:hover { background: var(--surface); }\n .pl-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .pl-card .pl-thumb { aspect-ratio: 1; border-radius: var(--r-sm); }\n .pl-card .pl-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }\n .pl-card .pl-meta {\n font-family: var(--font-mono); font-size: 10.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .pl-card .pl-check {\n position: absolute; top: 16px; right: 16px;\n width: 22px; height: 22px;\n background: rgba(255,255,255,.95);\n border: 1.5px solid var(--black-alpha-24);\n border-radius: 50%;\n display: grid; place-items: center;\n z-index: 2;\n color: var(--accent-white);\n }\n .pl-card .pl-check svg { width: 11px; height: 11px; opacity: 0; }\n .pl-card.selected .pl-check { background: var(--heat); border-color: var(--heat); }\n .pl-card.selected .pl-check svg { opacity: 1; }\n /* 卡片右上 actions: 编辑 + 删除 (hover 显示, check 左侧) */\n .pl-card .pl-card-actions {\n position: absolute;\n top: 14px; right: 44px;\n display: flex; gap: 4px;\n z-index: 2;\n opacity: 0;\n transition: opacity var(--t-base);\n }\n .pl-card:hover .pl-card-actions { opacity: 1; }\n .pl-card .pl-act {\n width: 26px; height: 26px;\n background: rgba(255,255,255,.95);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n cursor: pointer;\n color: var(--black-alpha-72);\n transition: color var(--t-base), border-color var(--t-base), background var(--t-base);\n }\n .pl-card .pl-act:hover { color: var(--heat); border-color: var(--heat); background: var(--surface); }\n .pl-card .pl-act.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }\n .pl-card .pl-act svg { width: 12px; height: 12px; }\n\n /* 编辑商品 drawer */\n .pc-drawer { width: 720px; max-width: 100vw; z-index: 1101; }\n .pc-drawer .drawer-b { padding: 24px 28px; }\n #pc-drawer-bg.drawer-bg { z-index: 1100; }\n .pc-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }\n .pc-field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); }\n .pc-field-label .req { color: var(--accent-crimson); margin-left: 2px; }\n .pc-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 18px; }\n .pc-field-row > div { display: flex; flex-direction: column; gap: 6px; }\n .pc-bullets {\n list-style: none; padding: 0; margin: 0;\n display: flex; flex-direction: column; gap: 6px;\n }\n .pc-bullets li {\n display: flex; align-items: center; gap: 8px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n padding: 0 10px;\n height: 36px;\n }\n .pc-bullets li.add { border-style: dashed; border-color: var(--heat-40); }\n .pc-bullets li .num {\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); width: 18px; text-align: center;\n flex-shrink: 0;\n }\n .pc-bullets li.add .num { color: var(--heat); }\n .pc-bullets li input {\n flex: 1; border: 0; background: transparent; outline: none;\n font-size: 13px; color: var(--accent-black);\n font-family: inherit;\n }\n .pc-bullets li input::placeholder { color: var(--black-alpha-48); }\n .pc-bullets li .rm {\n width: 22px; height: 22px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n color: var(--black-alpha-48); cursor: pointer;\n display: grid; place-items: center;\n }\n .pc-bullets li .rm:hover { color: var(--accent-crimson); background: var(--black-alpha-4); }\n .pc-bullets li .rm svg { width: 11px; height: 11px; }\n /* 商品图片 grid (对齐 product-detail .ov-images-grid) */\n .pc-imgs {\n display: grid;\n grid-template-columns: repeat(6, 1fr);\n gap: 8px;\n }\n .pc-imgs .thumb {\n aspect-ratio: 1 / 1;\n border-radius: var(--r-sm);\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n position: relative;\n overflow: hidden;\n }\n .pc-imgs .thumb .ph-frame {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 10px;\n color: var(--black-alpha-32);\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .pc-imgs .thumb .rm {\n position: absolute; top: 4px; right: 4px;\n width: 18px; height: 18px;\n background: rgba(0,0,0,.5);\n color: #fff; border: 0;\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n cursor: pointer;\n opacity: 0;\n transition: opacity var(--t-base);\n }\n .pc-imgs .thumb:hover .rm { opacity: 1; }\n .pc-imgs .thumb .rm svg { width: 10px; height: 10px; }\n .pc-imgs .img-upload {\n aspect-ratio: 1 / 1;\n border-radius: var(--r-sm);\n background: var(--heat-12);\n border: 1.5px dashed var(--heat-40);\n display: grid; place-items: center;\n color: var(--heat);\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .pc-imgs .img-upload:hover { background: var(--heat-20); border-color: var(--heat); }\n .pc-imgs .img-upload svg { width: 18px; height: 18px; }\n .pl-modal-f {\n padding: 14px 28px;\n border-top: 1px solid var(--border-faint);\n display: flex; justify-content: flex-end; align-items: center; gap: 10px;\n flex-shrink: 0;\n }\n .pl-modal-f .summary {\n margin-right: auto;\n font-family: var(--font-mono); font-size: 12px;\n color: var(--black-alpha-56); letter-spacing: .02em;\n }\n .pl-modal-f .summary b { color: var(--heat); font-weight: 700; }\n\n /* 平台多选卡片 */\n .platform-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n }\n .platform-card {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 10px 6px;\n cursor: pointer;\n text-align: center;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .platform-card:hover { background: var(--surface); }\n .platform-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .platform-card .p-logo {\n width: 32px; height: 32px;\n margin: 0 auto 4px;\n border-radius: var(--r-md);\n display: grid; place-items: center;\n color: var(--accent-white);\n font-family: var(--font-mono);\n font-size: 11px; font-weight: 700;\n }\n .platform-card .p-name {\n font-size: 11.5px;\n color: var(--accent-black);\n font-weight: 500;\n }\n .platform-card.selected .p-name { color: var(--heat); }\n .platform-card .p-check {\n position: absolute; top: 4px; right: 4px;\n width: 16px; height: 16px;\n border-radius: 50%;\n background: transparent;\n border: 1.5px solid var(--black-alpha-24);\n }\n .platform-card.selected .p-check {\n background: var(--heat); border-color: var(--heat);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.5'%3E%3Cpolyline points='3 8 7 12 13 4'/%3E%3C/svg%3E\");\n background-position: center;\n background-size: 10px 10px;\n background-repeat: no-repeat;\n border: 0;\n }\n /* 平台 logo 配色 */\n .p-logo.dy { background: #000; }\n .p-logo.tb { background: #ff6f00; }\n .p-logo.tm { background: #ff0036; }\n .p-logo.jd { background: #e1251b; }\n .p-logo.pdd { background: #e02e24; }\n .p-logo.xhs { background: #ff2741; }\n .p-logo.ks { background: #ff4906; }\n .p-logo.sph { background: #07c160; }\n .p-logo.amz { background: #ff9900; }\n .p-logo.al { background: #2c4af1; }\n\n /* pill row */\n .pill-row { display: flex; gap: 6px; }\n .pill-row .opt {\n flex: 1; height: 32px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72); font-size: 12.5px;\n cursor: pointer; font-family: inherit;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .pill-row .opt:hover { color: var(--accent-black); }\n .pill-row .opt.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); font-weight: 600; }\n .pc-sub-h { font-size: 12px; color: var(--black-alpha-48); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }\n .pc-sub { margin-bottom: 12px; }\n .pc-sub:last-child { margin-bottom: 0; }\n\n /* CTA */\n .pc-cta { margin-top: auto; padding-top: 14px; }\n .pc-cta .btn-gen {\n width: 100%; justify-content: center;\n padding: 12px; font-size: 14px; font-weight: 600;\n }\n .pc-cta .btn-gen.disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }\n .pc-cta-hint {\n margin-top: 8px;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n text-align: center; line-height: 1.5;\n }\n\n /* 右栏 预览 */\n .pc-preview {\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 18px 22px;\n display: flex; flex-direction: column;\n overflow-y: auto;\n }\n /* prompt-style summary 卡片 (引号 icon + 灰底 + 右上 meta) */\n .pc-pv-h {\n position: relative;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 14px 18px 14px 44px;\n margin-bottom: 14px;\n }\n .pc-pv-h .quote-icon {\n position: absolute;\n top: 13px; left: 16px;\n width: 18px; height: 18px;\n color: var(--black-alpha-24);\n }\n .pc-pv-h .pv-meta {\n float: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n line-height: 1.5;\n }\n .pc-pv-h .pv-meta b { color: var(--accent-black); font-weight: 600; }\n .pc-pv-h .pv-line {\n font-size: 13px;\n color: var(--accent-black);\n line-height: 1.6;\n display: flex; align-items: center;\n }\n .pc-pv-h .pv-line .k {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n margin-right: 8px;\n min-width: 36px;\n }\n .pc-pv-h .pv-line .v { font-weight: 500; }\n .pc-pv-h .pv-line .swap {\n margin-left: 10px;\n font-size: 11.5px; color: var(--heat);\n cursor: pointer;\n }\n .pc-pv-h .pv-line .swap:hover { text-decoration: underline; }\n\n .pv-platform-section { margin-bottom: 24px; }\n .pv-platform-section:last-child { margin-bottom: 0; }\n .pv-platform-section .ps-h {\n display: flex; align-items: center; gap: 8px;\n margin-bottom: 10px;\n font-size: 13px; font-weight: 600; color: var(--accent-black);\n }\n .pv-platform-section .ps-h .ct {\n margin-left: auto; font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em; font-weight: 400;\n }\n .pv-platform-section .ps-h .batch-lab {\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n padding: 2px 8px; border-radius: var(--r-pill); letter-spacing: .04em;\n }\n .pv-platform-section .ps-h .batch-lab.gen { background: var(--background-lighter); color: var(--black-alpha-56); }\n .pv-platform-section .ps-h .batch-lab.rerun { background: var(--heat-12); color: var(--heat); }\n .pv-platform-section .ps-h .sep { color: var(--black-alpha-32); font-weight: 400; }\n .pv-platform-section .ps-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 12px;\n }\n @media (max-width: 1400px) {\n .pv-platform-section .ps-grid { gap: 10px; }\n }\n /* 结果卡片 · 与图片创作 .io-cell 视觉一致 */\n .pv-platform-section .ps-grid .mp-result {\n position: relative;\n aspect-ratio: 1;\n border-radius: var(--r-md);\n overflow: hidden;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n cursor: pointer;\n transition: border-color var(--t-base);\n }\n .pv-platform-section .ps-grid .mp-result:hover { border-color: var(--black-alpha-32); }\n .pv-platform-section .ps-grid .mp-result .mp-r-thumb { position: absolute; inset: 0; }\n .pv-platform-section .ps-grid .mp-result .mp-r-thumb .ph-frame {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-32); letter-spacing: .02em;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n }\n .pv-platform-section .ps-grid .mp-result.gen .mp-r-thumb .ph-frame { animation: pc-pulse 1.4s ease-in-out infinite; }\n @keyframes pc-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: .55; }\n }\n .pv-platform-section .ps-grid .mp-result.err { border-color: var(--accent-crimson, #c43d3d); }\n .pv-platform-section .ps-grid .mp-result.err .mp-r-thumb .ph-frame {\n color: var(--accent-crimson, #c43d3d);\n background: rgba(196, 61, 61, .05);\n }\n /* 右上 hover 操作组 · 同 spec §4.18 .gen-image-actions */\n .pv-platform-section .ps-grid .mp-result .cell-ops {\n position: absolute; top: 8px; right: 8px;\n display: flex; gap: 2px;\n padding: 2px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 2px 8px rgba(0,0,0,.08);\n opacity: 0;\n transition: opacity var(--t-base);\n z-index: 2;\n }\n .pv-platform-section .ps-grid .mp-result:hover .cell-ops { opacity: 1; }\n .pv-platform-section .ps-grid .mp-result .cell-ops button {\n width: 28px; height: 28px;\n background: transparent;\n border: 0;\n border-radius: 6px;\n color: var(--black-alpha-56);\n cursor: pointer;\n display: grid; place-items: center;\n transition: background var(--t-base), color var(--t-base);\n }\n .pv-platform-section .ps-grid .mp-result .cell-ops button:hover {\n background: var(--black-alpha-4);\n color: var(--accent-black);\n }\n .pv-platform-section .ps-grid .mp-result .cell-ops button svg { width: 14px; height: 14px; }\n .pv-platform-section .ps-grid .mp-result.adopted .cell-ops .r-check { display: none; }\n /* 中央反馈 toast · 同 spec §4.18 .gen-image-feedback */\n .pv-platform-section .ps-grid .mp-result .cell-feedback {\n position: absolute; inset: 0;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;\n background: rgba(38, 38, 38, .88);\n color: var(--accent-white);\n border-radius: var(--r-md);\n opacity: 0;\n pointer-events: none;\n transition: opacity .2s var(--t-base, ease);\n z-index: 3;\n }\n .pv-platform-section .ps-grid .mp-result.show-feedback .cell-feedback { opacity: 1; }\n .pv-platform-section .ps-grid .mp-result .cell-feedback svg { width: 20px; height: 20px; }\n .pv-platform-section .ps-grid .mp-result .cell-feedback span { font-size: 12.5px; font-weight: 500; letter-spacing: .02em; }\n .pv-platform-section .ps-grid .mp-result .cell-more-wrap { position: relative; }\n .pv-platform-section .ps-grid .mp-result .cell-more-menu {\n position: absolute; top: calc(100% + 4px); right: 0;\n min-width: 132px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .pv-platform-section .ps-grid .mp-result .cell-more-wrap.open .cell-more-menu { display: block; }\n .pv-platform-section .ps-grid .mp-result .cell-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n backdrop-filter: none !important;\n height: auto !important;\n justify-content: flex-start !important;\n }\n .pv-platform-section .ps-grid .mp-result .cell-more-menu button:hover {\n background: var(--background-lighter) !important;\n color: var(--heat) !important;\n }\n .pv-platform-section .ps-grid .mp-result .cell-more-menu button.danger:hover {\n color: var(--accent-crimson) !important;\n background: var(--crimson-bg, #fdebea) !important;\n }\n .pv-platform-section .ps-grid .mp-result .cell-more-menu button svg { width: 13px !important; height: 13px !important; }\n /* 已采用角标 */\n .pv-platform-section .ps-grid .mp-result .adopt-badge {\n position: absolute; top: 6px; left: 6px;\n background: var(--accent-forest, #42c366);\n color: #fff;\n font-family: var(--font-mono); font-size: 10px; font-weight: 600;\n letter-spacing: .04em;\n padding: 1px 6px;\n border-radius: var(--r-pill);\n z-index: 3;\n display: none;\n }\n .pv-platform-section .ps-grid .mp-result.adopted .adopt-badge { display: inline-block; }\n\n /* 每批次下方的批量操作 (胶囊按钮组 · 左对齐) */\n .pc-pv-batch {\n margin-top: 10px;\n display: flex; gap: 10px; align-items: center; justify-content: flex-start;\n }\n .pc-pv-batch[hidden] { display: none; }\n .pv-platform-section .pc-pv-batch.batch-foot { margin-top: 10px; }\n\n /* 预览区空态 (新任务且未生成) */\n .pc-pv-empty-state {\n flex: 1;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n text-align: center;\n padding: 40px 24px;\n gap: 6px;\n }\n .pc-pv-empty-state[hidden] { display: none; }\n .pc-pv-empty-state .mono {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .06em;\n margin-bottom: 4px;\n }\n .pc-pv-empty-state .title {\n font-size: 14px;\n font-weight: 600;\n color: var(--accent-black);\n }\n .pc-pv-empty-state .hint {\n font-size: 12.5px;\n color: var(--black-alpha-48);\n line-height: 1.6;\n max-width: 320px;\n }\n .pc-pv-empty-state .hint b { color: var(--heat); font-weight: 600; }\n /* 预览区 hidden 时收起所有内容元素 */\n #pv-summary[hidden], #pv-results[hidden], #pv-foot[hidden] { display: none; }\n .pc-pv-batch .summary {\n margin-right: auto;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .pc-pv-batch .summary b { color: var(--heat); font-weight: 700; }\n /* 底栏按钮 · 同 spec §4.18 + image-optimize .io-msg-ops button */\n .pc-pv-batch .pill-btn {\n height: 30px;\n padding: 0 12px;\n display: inline-flex; align-items: center; gap: 6px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n color: var(--accent-black);\n font-family: inherit; font-size: 12.5px;\n cursor: pointer;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .pc-pv-batch .pill-btn:hover {\n border-color: var(--heat-20); color: var(--heat); background: var(--heat-12);\n }\n .pc-pv-batch .pill-btn svg { width: 13px; height: 13px; }\n .pc-pv-batch .pill-btn.icon { width: 30px; padding: 0; justify-content: center; }\n /* 批次更多气泡 */\n .pc-pv-batch .batch-more-wrap { position: relative; display: inline-flex; }\n .pc-pv-batch .batch-more-menu {\n position: absolute; bottom: calc(100% + 6px); right: 0;\n min-width: 168px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 24px rgba(0,0,0,.10);\n padding: 4px;\n display: none;\n z-index: 12;\n }\n .pc-pv-batch .batch-more-wrap.open .batch-more-menu { display: block; }\n .pc-pv-batch .batch-more-menu button {\n width: 100%;\n display: inline-flex !important; align-items: center; gap: 8px;\n height: auto !important;\n padding: 7px 10px !important;\n background: transparent !important;\n border: 0 !important; border-radius: var(--r-sm) !important;\n font-size: 12.5px;\n color: var(--accent-black) !important;\n text-align: left;\n justify-content: flex-start !important;\n cursor: pointer; font-family: inherit;\n }\n .pc-pv-batch .batch-more-menu button:hover {\n background: var(--background-lighter) !important;\n color: var(--heat) !important;\n }\n .pc-pv-batch .batch-more-menu button.danger:hover {\n color: var(--accent-crimson) !important;\n background: var(--crimson-bg, #fdebea) !important;\n }\n .pc-pv-batch .batch-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }\n\n .pc-pv-foot {\n margin-top: 14px; padding-top: 12px;\n border-top: 1px solid var(--border-faint);\n font-family: var(--font-mono); font-size: 11.5px;\n color: var(--black-alpha-56); letter-spacing: .02em;\n line-height: 1.6;\n }\n .pc-pv-foot a { color: var(--heat); }\n\n /* 空状态 */\n .pc-pv-empty {\n flex: 1;\n display: grid; place-items: center;\n color: var(--black-alpha-48);\n font-size: 13px;\n text-align: center;\n }\n .pc-pv-empty .mono {\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); margin-top: 6px;\n letter-spacing: .04em;\n }\n\n @media (max-width: 1100px) {\n .pc-layout { grid-template-columns: 1fr; }\n }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n <div class=\"pc-layout\">\n\n <!-- ===== 最左栏 · 商品空间 (单选 · 当前商品决定结果区批次) ===== -->\n <aside class=\"pc-prod-space\" id=\"prod-space\">\n <!-- 顶部 · 返回 + 折叠 (跟图片创作风格一致) -->\n <div class=\"pc-side-top\">\n <button class=\"back-pill\" type=\"button\" onclick=\"history.length > 1 ? history.back() : location.href='asset-factory.html'\" title=\"返回\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 18l-6-6 6-6\"/></svg>\n <span>返回</span>\n </button>\n <button class=\"fold\" type=\"button\" title=\"折叠侧栏\" style=\"margin-left:auto\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M9 3v18\"/></svg>\n </button>\n </div>\n <div class=\"pc-ps-h\">\n <div class=\"pc-ps-search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" id=\"ps-search-input\" placeholder=\"搜索商品 / 分类\">\n </div>\n </div>\n <!-- 商品列表 标题行 · 右上显眼新建按钮 -->\n <div class=\"pc-list-h\">\n <span class=\"mono\">// 商品空间</span>\n <button class=\"new-prod\" type=\"button\" id=\"ps-new-btn\" title=\"新建商品\">\n <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>\n <span>新建商品</span>\n </button>\n </div>\n <div class=\"pc-ps-list\" id=\"ps-list\"></div>\n <button class=\"pc-ps-all\" type=\"button\" id=\"ps-all-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\"/><path d=\"M3 9h18M9 4v16\"/></svg>\n <span>全部商品</span>\n <span class=\"ct\" id=\"ps-all-ct\">0 个</span>\n <svg style=\"margin-left:0\" 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>\n </button>\n </aside>\n\n <!-- ===== 主区 · 头部 + 参数/结果 双栏 ===== -->\n <section class=\"pc-main\">\n\n <!-- 主区顶部 · toolbar (商品标题 + 搜索 + 筛选 · 跟图片创作一致) -->\n <div class=\"pc-main-h\">\n <button class=\"side-restore-btn\" type=\"button\" id=\"pc-side-restore\" hidden title=\"展开商品空间\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M9 3v18\"/></svg>\n 商品\n </button>\n <div class=\"cur-title\">\n <span class=\"crumb\">// 商品空间</span>\n <span class=\"nm placeholder\" id=\"cur-prod-nm\">未选择 · 请在左侧商品空间选一个</span>\n </div>\n <span class=\"spacer\"></span>\n <div class=\"tb-search-wrap\" id=\"pc-search-wrap\">\n <button class=\"search-btn\" type=\"button\" title=\"搜索批次/平台\" id=\"pc-search-toggle\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </button>\n <input type=\"text\" class=\"tb-search-input\" id=\"pc-search-input\" placeholder=\"搜索批次/平台…\" autocomplete=\"off\">\n </div>\n <div class=\"tb-menu-wrap\" data-filter=\"time\">\n <button class=\"tb-chip\" type=\"button\" id=\"pc-chip-time\">\n <span class=\"lbl\">时间</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"tb-menu\" id=\"pc-menu-time\" role=\"listbox\" aria-labelledby=\"pc-chip-time\">\n <button class=\"tb-menu-item active\" type=\"button\" data-val=\"all\">全部时间</button>\n <button class=\"tb-menu-item\" type=\"button\" data-val=\"today\">今天</button>\n <button class=\"tb-menu-item\" type=\"button\" data-val=\"1h\">1 小时内</button>\n <button class=\"tb-menu-item\" type=\"button\" data-val=\"10min\">10 分钟内</button>\n </div>\n </div>\n <div class=\"tb-menu-wrap\" data-filter=\"platform\">\n <button class=\"tb-chip\" type=\"button\" id=\"pc-chip-platform\">\n <span class=\"lbl\">平台</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"tb-menu\" id=\"pc-menu-platform\" role=\"listbox\" aria-labelledby=\"pc-chip-platform\">\n <button class=\"tb-menu-item active\" type=\"button\" data-val=\"all\">全部平台</button>\n <div class=\"tb-menu-empty\">暂无批次,生成后可按平台筛选</div>\n </div>\n </div>\n </div>\n\n <div class=\"pc-main-body\">\n\n <!-- 左 · 参数 -->\n <div class=\"pc-form\">\n\n <!-- ① 选择平台 (单选, 10 个热门电商平台) -->\n <div class=\"pc-step\">\n <div class=\"pc-step-h\">\n <span class=\"num\">1</span>\n <span class=\"title\">选择平台</span>\n </div>\n <div class=\"platform-grid\" id=\"platform-grid\">\n <div class=\"platform-card\" data-id=\"dy\" data-name=\"抖音电商\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo dy\">抖</div>\n <div class=\"p-name\">抖音电商</div>\n </div>\n <div class=\"platform-card\" data-id=\"tb\" data-name=\"淘宝\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo tb\">淘</div>\n <div class=\"p-name\">淘宝</div>\n </div>\n <div class=\"platform-card\" data-id=\"tm\" data-name=\"天猫\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo tm\">猫</div>\n <div class=\"p-name\">天猫</div>\n </div>\n <div class=\"platform-card\" data-id=\"jd\" data-name=\"京东\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo jd\">京</div>\n <div class=\"p-name\">京东</div>\n </div>\n <div class=\"platform-card\" data-id=\"pdd\" data-name=\"拼多多\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo pdd\">拼</div>\n <div class=\"p-name\">拼多多</div>\n </div>\n <div class=\"platform-card\" data-id=\"xhs\" data-name=\"小红书\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo xhs\">红</div>\n <div class=\"p-name\">小红书</div>\n </div>\n <div class=\"platform-card\" data-id=\"ks\" data-name=\"快手\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo ks\">快</div>\n <div class=\"p-name\">快手</div>\n </div>\n <div class=\"platform-card\" data-id=\"sph\" data-name=\"视频号\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo sph\">视</div>\n <div class=\"p-name\">视频号</div>\n </div>\n <div class=\"platform-card\" data-id=\"amz\" data-name=\"亚马逊\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo amz\">a</div>\n <div class=\"p-name\">亚马逊</div>\n </div>\n <div class=\"platform-card\" data-id=\"al\" data-name=\"1688\">\n <div class=\"p-check\"></div>\n <div class=\"p-logo al\">阿</div>\n <div class=\"p-name\">1688</div>\n </div>\n </div>\n </div>\n\n <!-- ② 生成设置 -->\n <div class=\"pc-step\">\n <div class=\"pc-step-h\">\n <span class=\"num\">2</span>\n <span class=\"title\">生成设置</span>\n </div>\n <div class=\"pc-sub\">\n <div class=\"pc-sub-h\">// 生成数量</div>\n <div class=\"pill-row\" data-key=\"count\">\n <button type=\"button\" class=\"opt active\" data-val=\"4\">4 张</button>\n <button type=\"button\" class=\"opt\" data-val=\"8\">8 张</button>\n <button type=\"button\" class=\"opt\" data-val=\"12\">12 张</button>\n </div>\n </div>\n </div>\n\n <!-- 底部 立即生成 -->\n <div class=\"pc-cta\">\n <button class=\"btn btn-primary btn-gen\" id=\"pc-go-btn\" type=\"button\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 3l14 9-14 9V3z\"/></svg>\n 立即生成 (预估 <span id=\"cost-total\">¥4.00</span>)\n </button>\n <div class=\"pc-cta-hint\">// 满意后点 [入资产库] 才扣费 · 失败不扣</div>\n </div>\n\n </div>\n\n <!-- ===== 右栏 · 预览 ===== -->\n <div class=\"pc-preview\" id=\"pc-preview\">\n <!-- 空态(新任务态 & 还没立即生成时显示) -->\n <div class=\"pc-pv-empty-state\" id=\"pv-empty\">\n <div class=\"mono\">// EMPTY STATE</div>\n <div class=\"title\">还没有生成结果</div>\n <div class=\"hint\">先选商品、选平台,点击 <b>立即生成</b> 后,效果图会出现在这里</div>\n </div>\n\n <div class=\"pc-pv-h\" id=\"pv-summary\">\n <svg class=\"quote-icon\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M9 7c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32C5.99 17.36 4.99 18 4 18v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1zm9 0c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32-.49 1.04-1.49 1.68-2.48 1.68v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1z\"/></svg>\n <div class=\"pv-meta\"><b id=\"pv-count\">4 张</b></div>\n <div class=\"pv-line\"><span class=\"k\">商品</span><span class=\"v\" id=\"pv-prod\">未选择</span></div>\n <div class=\"pv-line\"><span class=\"k\">平台</span><span class=\"v\" id=\"pv-platforms\">未选择</span></div>\n </div>\n <div id=\"pv-results\">\n <!-- 默认占位 -->\n </div>\n\n <div class=\"pc-pv-foot\" id=\"pv-foot\">\n // 采用即扣费并入对应商品的 <a href=\"products.html\">AI 素材库 →</a>;未采用的图不扣费、不保存\n <br>// 切换左侧商品空间 · 查看其他商品的批次记录\n </div>\n </div>\n\n </div><!-- /.pc-main-body -->\n </section><!-- /.pc-main -->\n\n </div>\n</div>\n\n<!-- ===== 编辑商品 drawer (在商品库内点编辑触发,prefilled) ===== -->\n<div class=\"drawer-bg\" id=\"pc-drawer-bg\"></div>\n<aside class=\"drawer pc-drawer\" id=\"pc-drawer\" role=\"dialog\" aria-label=\"编辑商品\" aria-hidden=\"true\">\n <div class=\"drawer-h\">\n <h3 id=\"pc-drawer-title\">编辑商品</h3>\n <button class=\"x\" type=\"button\" id=\"pc-drawer-close\" aria-label=\"关闭\">\n <svg width=\"14\" height=\"14\" 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>\n </button>\n </div>\n <div class=\"drawer-b\">\n <div class=\"pc-field\">\n <label class=\"pc-field-label\">商品名称<span class=\"req\">*</span></label>\n <input class=\"input\" id=\"pcf-name\" placeholder=\"请输入商品名称(必填)\" maxlength=\"100\">\n </div>\n <div class=\"pc-field-row\">\n <div>\n <label class=\"pc-field-label\">品类<span class=\"req\">*</span></label>\n <select class=\"select\" id=\"pcf-cat\">\n <option>美妆个护</option>\n <option>服饰内衣</option>\n <option>食品饮料</option>\n <option>家居家电</option>\n <option>数码 3C</option>\n <option>个护清洁</option>\n <option>运动户外</option>\n <option>母婴亲子</option>\n </select>\n </div>\n <div>\n <label class=\"pc-field-label\">目标人群<span style=\"color:var(--black-alpha-48);margin-left:2px\">(选填)</span></label>\n <input class=\"input\" id=\"pcf-target\" placeholder=\"例: 22-32 岁女性、敏感肌、办公室通勤\">\n </div>\n </div>\n <div class=\"pc-field\">\n <label class=\"pc-field-label\">商品图片<span style=\"color:var(--black-alpha-48);margin-left:2px\">(<span id=\"pcf-imgs-ct\">6</span>)</span></label>\n <div class=\"pc-imgs\" id=\"pcf-imgs\"></div>\n </div>\n <div class=\"pc-field\">\n <label class=\"pc-field-label\">核心卖点<span class=\"req\">*</span></label>\n <ul class=\"pc-bullets\" id=\"pcf-bullets\">\n <li class=\"add\"><span class=\"num\">+</span><input id=\"pcf-add-input\" placeholder=\"添加新卖点 · 回车确认\"></li>\n </ul>\n </div>\n </div>\n <div class=\"drawer-f\">\n <button class=\"btn\" type=\"button\" id=\"pc-cancel-btn\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"pc-save-btn\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n 保存修改\n </button>\n </div>\n</aside>\n\n<!-- ===== 商品库 全屏(无遮罩自适应,多选) ===== -->\n<div class=\"pl-modal-bg\" id=\"pl-modal-bg\">\n <div class=\"pl-modal\">\n <div class=\"pl-modal-h\">\n <h2>商品库</h2>\n <span class=\"ct\" id=\"pl-total-ct\">// 共 7 个商品</span>\n <div class=\"actions\">\n <button class=\"x\" type=\"button\" id=\"pl-close-btn\" aria-label=\"关闭\" style=\"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)\">\n <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>\n </button>\n </div>\n </div>\n <div class=\"pl-modal-body\">\n <aside class=\"pl-side\">\n <div class=\"pl-side-h\">分类</div>\n <div class=\"pl-side-item active\" data-cat=\"\">全部 <span class=\"ct\">7</span></div>\n <div class=\"pl-side-item\" data-cat=\"美妆个护\">美妆个护 <span class=\"ct\">2</span></div>\n <div class=\"pl-side-item\" data-cat=\"数码 3C\">数码 3C <span class=\"ct\">1</span></div>\n <div class=\"pl-side-item\" data-cat=\"食品饮料\">食品饮料 <span class=\"ct\">2</span></div>\n <div class=\"pl-side-item\" data-cat=\"家居家电\">家居家电 <span class=\"ct\">1</span></div>\n <div class=\"pl-side-item\" data-cat=\"运动户外\">运动户外 <span class=\"ct\">1</span></div>\n </aside>\n <div class=\"pl-main\">\n <div class=\"pl-toolbar\">\n <div class=\"search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" id=\"pl-search-input\" placeholder=\"搜索商品名\">\n </div>\n <button class=\"btn-new\" type=\"button\" id=\"pl-new-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n 新建商品\n </button>\n </div>\n <div class=\"pl-scroll\">\n <div class=\"pl-grid\" id=\"pl-grid\"></div>\n </div>\n </div>\n </div>\n <div class=\"pl-modal-f\">\n <div class=\"summary\">// 已选 <b id=\"pl-sel-ct\">0</b> 个商品</div>\n <button class=\"btn\" type=\"button\" id=\"pl-cancel-btn\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"pl-confirm-btn\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M5 12l5 5L20 7\"/></svg>\n 确认选择\n </button>\n </div>\n </div>\n</div>\n\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script src=\"/exact/assets/new-product-drawer.js?v=202605211643\"></script>\n<script>\nShell.render({\n active: 'asset-factory',\n crumbs: [\n { label: '工作台', href: 'index.html' },\n { label: '图片生成', href: 'asset-factory.html' },\n { label: '平台套图' }\n ]\n});\n\n// ─── 商品库数据 (mock,与 products.html 7 个商品对齐) ───\nconst PRODUCTS = [\n { id: 'p1', name: '透真玻尿酸补水面膜', cat: '美妆个护', meta: '熬夜党 · 124 素材' },\n { id: 'p2', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', meta: '通勤 · 96 素材' },\n { id: 'p3', name: '滋啦速食牛肉面 6 桶装', cat: '食品饮料', meta: '加班 · 96 素材' },\n { id: 'p4', name: '透真清透物理防晒霜', cat: '美妆个护', meta: 'SPF50 · 76 素材' },\n { id: 'p5', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', meta: '提神 · 68 素材' },\n { id: 'p6', name: '小熊 4L 可视空气炸锅', cat: '家居家电', meta: '小户型 · 54 素材' },\n { id: 'p7', name: '露露同款裸感瑜伽裤', cat: '运动户外', meta: '健身房 · 42 素材' },\n];\n\n// State (单选 · 默认全空)\nconst state = {\n selectedProd: null, // string | null\n selectedPlatform: null, // string | null\n count: 4,\n};\nconst UNIT_PRICE = 0.50;\n\n// ─── 商品空间 (左侧栏) 渲染 ───\nlet _psQuery = '';\nfunction renderProdSpace() {\n const listEl = document.getElementById('ps-list');\n const ctEl = document.getElementById('ps-count');\n const allCtEl = document.getElementById('ps-all-ct');\n if (!listEl) return;\n if (ctEl) ctEl.textContent = PRODUCTS.length;\n if (allCtEl) allCtEl.textContent = PRODUCTS.length + ' 个';\n const q = _psQuery.trim();\n const filtered = q\n ? PRODUCTS.filter(p => p.name.includes(q) || p.cat.includes(q))\n : PRODUCTS;\n if (!filtered.length) {\n listEl.innerHTML = `<div class=\"pc-ps-empty\">// NO MATCH<br>试试其他关键词</div>`;\n return;\n }\n listEl.innerHTML = filtered.map(p => `\n <div class=\"pc-prod-item${state.selectedProd === p.id ? ' active' : ''}\" data-id=\"${p.id}\">\n <div class=\"placeholder thumb\"></div>\n <div class=\"body\">\n <div class=\"nm\">${p.name}</div>\n <div class=\"sub\">// ${p.cat}</div>\n </div>\n </div>\n `).join('');\n listEl.querySelectorAll('.pc-prod-item').forEach(el => {\n el.addEventListener('click', () => selectProduct(el.dataset.id));\n });\n}\n\n// 选中商品 (sidebar 单选 · 同步更新表单/预览/Cost)\nfunction selectProduct(id) {\n state.selectedProd = id;\n document.querySelectorAll('.pc-prod-item').forEach(el => {\n el.classList.toggle('active', el.dataset.id === id);\n });\n updateCurProdHeader();\n if (typeof renderBatchesForCurrentProd === 'function') renderBatchesForCurrentProd();\n const p = PRODUCTS.find(x => x.id === id);\n document.getElementById('pv-prod').textContent = p ? p.name : '未选择';\n updateCost();\n renderPreviewSections();\n}\n\n// 当前商品 header strip\nfunction updateCurProdHeader() {\n const nmEl = document.getElementById('cur-prod-nm');\n const statsEl = document.getElementById('cur-prod-stats');\n const batchesEl = document.getElementById('cur-prod-batches');\n if (!nmEl) return;\n const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;\n if (!p) {\n nmEl.textContent = '未选择 · 请在左侧商品空间选一个';\n nmEl.classList.add('placeholder');\n if (statsEl) statsEl.hidden = true;\n } else {\n nmEl.textContent = p.name;\n nmEl.classList.remove('placeholder');\n const ct = (window._countBatchesForProd ? window._countBatchesForProd(p.id) : 0);\n if (batchesEl) batchesEl.textContent = ct;\n if (statsEl) statsEl.hidden = false;\n }\n}\n\n// 保留旧函数名 alias (兼容旧 call site)\nfunction renderSelectedProds() {\n renderProdSpace();\n updateCurProdHeader();\n const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;\n document.getElementById('pv-prod').textContent = p ? p.name : '未选择';\n updateCost();\n renderPreviewSections();\n}\n\n// ─── 商品库全屏弹窗 (单选) ───\nlet _plDraft = null;\nlet _plCatFilter = '';\nlet _plQuery = '';\n\nfunction renderProdLib() {\n const grid = document.getElementById('pl-grid');\n let list = PRODUCTS;\n if (_plCatFilter) list = list.filter(p => p.cat === _plCatFilter);\n if (_plQuery) list = list.filter(p => p.name.includes(_plQuery));\n grid.innerHTML = list.map(p => `\n <div class=\"pl-card${_plDraft === p.id ? ' selected' : ''}\" data-id=\"${p.id}\">\n <div class=\"pl-card-actions\">\n <button class=\"pl-act\" type=\"button\" data-edit=\"${p.id}\" title=\"编辑商品\" aria-label=\"编辑\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4 12.5-12.5z\"/></svg>\n </button>\n <button class=\"pl-act danger\" type=\"button\" data-del=\"${p.id}\" title=\"删除商品\" aria-label=\"删除\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </button>\n </div>\n <div class=\"pl-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder pl-thumb\"><span class=\"ph-frame\">${p.name}</span></div>\n <div class=\"pl-name\">${p.name}</div>\n <div class=\"pl-meta\">${p.cat} · ${p.meta}</div>\n </div>\n `).join('');\n grid.querySelectorAll('.pl-card').forEach(card => {\n card.addEventListener('click', e => {\n if (e.target.closest('[data-edit]') || e.target.closest('[data-del]')) return;\n const id = card.dataset.id;\n // 单选: 选中当前,取消其他\n _plDraft = (_plDraft === id) ? null : id;\n grid.querySelectorAll('.pl-card').forEach(c => c.classList.toggle('selected', c.dataset.id === _plDraft));\n document.getElementById('pl-sel-ct').textContent = _plDraft ? 1 : 0;\n });\n });\n grid.querySelectorAll('[data-edit]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n openEditProductDrawer(btn.dataset.edit);\n });\n });\n grid.querySelectorAll('[data-del]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const id = btn.dataset.del;\n const p = PRODUCTS.find(x => x.id === id);\n if (!p) return;\n if (!confirm('确认删除「' + p.name + '」?\\n该操作不可撤销,商品下生成的素材记录也会一并清理。')) return;\n const idx = PRODUCTS.findIndex(x => x.id === id);\n if (idx >= 0) PRODUCTS.splice(idx, 1);\n if (_plDraft === id) _plDraft = null;\n if (state.selectedProd === id) state.selectedProd = null;\n renderProdLib();\n renderSelectedProds();\n Shell.toast('已删除', p.name);\n });\n });\n document.getElementById('pl-sel-ct').textContent = _plDraft ? 1 : 0;\n}\n\n// ─── 编辑商品 drawer (在商品库内 prefill 数据) ───\nconst PRODUCT_EXTRA = {\n p1: { target: '熬夜党 · 25-35 岁女性 · 敏感肌', bullets: ['72h 长效补水', '官方授权正品', '通勤补妆神器'], imgs: 6 },\n p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'], imgs: 6 },\n p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'], imgs: 6 },\n p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'], imgs: 6 },\n p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'], imgs: 6 },\n p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'], imgs: 6 },\n p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'], imgs: 6 },\n};\n\n// 渲染商品图片 grid · n 张占位 + 上传按钮\nfunction renderProdImgs(n) {\n const grid = document.getElementById('pcf-imgs');\n const ct = document.getElementById('pcf-imgs-ct');\n if (!grid) return;\n if (ct) ct.textContent = n;\n let html = '';\n for (let i = 0; i < n; i++) {\n html += `<div class=\"thumb\"><span class=\"ph-frame\">1:1</span><button class=\"rm\" type=\"button\" title=\"删除\" data-idx=\"${i}\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button></div>`;\n }\n html += `<div class=\"img-upload\" id=\"pcf-img-add\" title=\"上传图片\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg></div>`;\n grid.innerHTML = html;\n grid.querySelectorAll('.thumb .rm').forEach(btn => {\n btn.addEventListener('click', () => {\n btn.closest('.thumb').remove();\n if (ct) ct.textContent = grid.querySelectorAll('.thumb').length;\n });\n });\n const addBtn = document.getElementById('pcf-img-add');\n if (addBtn) addBtn.addEventListener('click', () => {\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('上传图片', '// 演示版暂不支持真实上传');\n });\n}\n\nlet _editingProdId = null;\n\nfunction openEditProductDrawer(id) {\n const p = PRODUCTS.find(x => x.id === id);\n if (!p) return;\n _editingProdId = id;\n document.getElementById('pc-drawer-title').textContent = '编辑商品 · ' + p.name;\n document.getElementById('pcf-name').value = p.name;\n document.getElementById('pcf-cat').value = p.cat;\n const extra = PRODUCT_EXTRA[id] || { target: '', bullets: [], imgs: 0 };\n document.getElementById('pcf-target').value = extra.target || '';\n // 渲染商品图片 (n 张占位)\n renderProdImgs(typeof extra.imgs === 'number' ? extra.imgs : 6);\n const ul = document.getElementById('pcf-bullets');\n ul.querySelectorAll('li:not(.add)').forEach(li => li.remove());\n const addLi = ul.querySelector('.add');\n (extra.bullets || []).forEach((b, i) => {\n const li = document.createElement('li');\n li.innerHTML = `\n <span class=\"num\">${i + 1}</span>\n <input value=\"${b.replace(/\"/g, '&quot;')}\">\n <button class=\"rm\" type=\"button\" aria-label=\"删除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button>\n `;\n ul.insertBefore(li, addLi);\n li.querySelector('.rm').addEventListener('click', () => { li.remove(); renumberBullets(); });\n });\n document.getElementById('pcf-add-input').value = '';\n document.getElementById('pc-drawer-bg').classList.add('show');\n document.getElementById('pc-drawer').classList.add('show');\n document.getElementById('pc-drawer').setAttribute('aria-hidden', 'false');\n}\n\nfunction renumberBullets() {\n const ul = document.getElementById('pcf-bullets');\n [...ul.querySelectorAll('li:not(.add) .num')].forEach((s, i) => { s.textContent = i + 1; });\n}\n\nfunction closeEditProductDrawer() {\n document.getElementById('pc-drawer-bg').classList.remove('show');\n document.getElementById('pc-drawer').classList.remove('show');\n document.getElementById('pc-drawer').setAttribute('aria-hidden', 'true');\n _editingProdId = null;\n}\n\ndocument.getElementById('pcf-add-input').addEventListener('keydown', e => {\n if (e.key !== 'Enter') return;\n const v = e.target.value.trim();\n if (!v) return;\n const ul = document.getElementById('pcf-bullets');\n const addLi = ul.querySelector('.add');\n const li = document.createElement('li');\n li.innerHTML = `\n <span class=\"num\">0</span>\n <input value=\"${v.replace(/\"/g, '&quot;')}\">\n <button class=\"rm\" type=\"button\" aria-label=\"删除\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 6l12 12M6 18L18 6\"/></svg></button>\n `;\n ul.insertBefore(li, addLi);\n li.querySelector('.rm').addEventListener('click', () => { li.remove(); renumberBullets(); });\n e.target.value = '';\n renumberBullets();\n});\n\ndocument.getElementById('pc-drawer-close').addEventListener('click', closeEditProductDrawer);\ndocument.getElementById('pc-cancel-btn').addEventListener('click', closeEditProductDrawer);\ndocument.getElementById('pc-drawer-bg').addEventListener('click', closeEditProductDrawer);\ndocument.getElementById('pc-save-btn').addEventListener('click', () => {\n if (!_editingProdId) return;\n const newName = document.getElementById('pcf-name').value.trim();\n const newCat = document.getElementById('pcf-cat').value;\n const newTarget = document.getElementById('pcf-target').value.trim();\n if (!newName) { Shell.toast('请填写商品名称'); return; }\n const p = PRODUCTS.find(x => x.id === _editingProdId);\n if (p) { p.name = newName; p.cat = newCat; }\n const bullets = [...document.querySelectorAll('#pcf-bullets li:not(.add) input')].map(i => i.value.trim()).filter(Boolean);\n const imgs = document.querySelectorAll('#pcf-imgs .thumb').length;\n PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets, imgs };\n Shell.toast('已保存', newName);\n closeEditProductDrawer();\n renderProdLib();\n renderSelectedProds();\n});\n\n// 全部商品 入口 (左侧栏底部 · 打开商品库 modal)\nfunction openProdLibModal() {\n _plDraft = state.selectedProd;\n _plCatFilter = '';\n _plQuery = '';\n document.getElementById('pl-search-input').value = '';\n document.querySelectorAll('.pl-side-item').forEach(x => x.classList.toggle('active', x.dataset.cat === ''));\n renderProdLib();\n document.getElementById('pl-modal-bg').classList.add('show');\n}\ndocument.getElementById('ps-all-btn').addEventListener('click', openProdLibModal);\n\n// 商品空间 · 搜索框 · 新建按钮\ndocument.getElementById('ps-search-input').addEventListener('input', e => {\n _psQuery = e.target.value;\n renderProdSpace();\n});\ndocument.getElementById('ps-new-btn').addEventListener('click', () => {\n if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }\n window.NewProductDrawer.open({\n onSave: function (p) {\n const product = {\n id: p.id,\n name: p.name,\n cat: p.cat,\n meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',\n };\n PRODUCTS.unshift(product);\n renderProdSpace();\n selectProduct(product.id);\n Shell.toast('已加入商品库', '+ ' + product.name);\n }\n });\n});\ndocument.getElementById('pl-close-btn').addEventListener('click', () => {\n document.getElementById('pl-modal-bg').classList.remove('show');\n});\ndocument.getElementById('pl-cancel-btn').addEventListener('click', () => {\n document.getElementById('pl-modal-bg').classList.remove('show');\n});\ndocument.getElementById('pl-confirm-btn').addEventListener('click', () => {\n if (!_plDraft) { Shell.toast('请先选择商品', '只能选 1 个'); return; }\n document.getElementById('pl-modal-bg').classList.remove('show');\n selectProduct(_plDraft);\n});\ndocument.getElementById('pl-new-btn').addEventListener('click', () => {\n if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }\n window.NewProductDrawer.open({\n onSave: function (p) {\n const product = {\n id: p.id,\n name: p.name,\n cat: p.cat,\n meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',\n };\n PRODUCTS.unshift(product);\n _plDraft = product.id;\n _plCatFilter = '';\n _plQuery = '';\n const searchInput = document.getElementById('pl-search-input');\n if (searchInput) searchInput.value = '';\n renderProdLib();\n renderProdSpace();\n selectProduct(product.id);\n Shell.toast('已加入商品库', '+ ' + product.name);\n }\n });\n});\ndocument.querySelectorAll('.pl-side-item').forEach(item => {\n item.addEventListener('click', () => {\n document.querySelectorAll('.pl-side-item').forEach(x => x.classList.remove('active'));\n item.classList.add('active');\n _plCatFilter = item.dataset.cat;\n renderProdLib();\n });\n});\ndocument.getElementById('pl-search-input').addEventListener('input', e => {\n _plQuery = e.target.value.trim();\n renderProdLib();\n});\n\n// 平台单选\nfunction updatePlatforms() {\n const id = state.selectedPlatform;\n const c = id ? document.querySelector('.platform-card[data-id=\"' + id + '\"]') : null;\n document.getElementById('pv-platforms').textContent = c ? c.dataset.name : '未选择';\n updateCost();\n renderPreviewSections();\n}\nfunction updateCost() {\n const hasProd = !!state.selectedProd;\n const hasPlat = !!state.selectedPlatform;\n const total = (hasProd && hasPlat ? 1 : 0) * state.count * UNIT_PRICE;\n document.getElementById('cost-total').textContent = '¥' + total.toFixed(2);\n const btn = document.getElementById('pc-go-btn');\n if (!hasProd || !hasPlat) btn.classList.add('disabled');\n else btn.classList.remove('disabled');\n document.getElementById('pv-count').textContent = state.count + ' 张';\n}\ndocument.querySelectorAll('.platform-card').forEach(card => {\n card.addEventListener('click', () => {\n const id = card.dataset.id;\n state.selectedPlatform = (state.selectedPlatform === id) ? null : id;\n document.querySelectorAll('.platform-card').forEach(c =>\n c.classList.toggle('selected', c.dataset.id === state.selectedPlatform)\n );\n updatePlatforms();\n });\n});\n\n// 数量\ndocument.querySelectorAll('.pill-row').forEach(row => {\n row.addEventListener('click', e => {\n const btn = e.target.closest('.opt');\n if (!btn) return;\n row.querySelectorAll('.opt').forEach(o => o.classList.remove('active'));\n btn.classList.add('active');\n state.count = +btn.dataset.val;\n updateCost();\n renderPreviewSections();\n });\n});\n\n// ─── 预览区空态 / 内容 切换 ───\nfunction showPreviewEmpty() {\n const empty = document.getElementById('pv-empty');\n const sum = document.getElementById('pv-summary');\n const results = document.getElementById('pv-results');\n const foot = document.getElementById('pv-foot');\n if (empty) empty.hidden = false;\n if (sum) sum.hidden = true;\n if (results) results.hidden = true;\n if (foot) foot.hidden = true;\n}\nfunction showPreviewContent() {\n const empty = document.getElementById('pv-empty');\n const sum = document.getElementById('pv-summary');\n const results = document.getElementById('pv-results');\n const foot = document.getElementById('pv-foot');\n if (empty) empty.hidden = true;\n if (sum) sum.hidden = false;\n if (results) results.hidden = false;\n if (foot) foot.hidden = false;\n // pv-batch 由 renderResultCards 单独控制\n}\n\n// 渲染右侧预览 (占位 — 切平台/数量时显示)\nfunction renderPreviewSections() {\n const container = document.getElementById('pv-results');\n const id = state.selectedPlatform;\n const c = id ? document.querySelector('.platform-card[data-id=\"' + id + '\"]') : null;\n if (!c) {\n container.innerHTML = '<div class=\"pc-pv-empty\">请先选择平台<div class=\"mono\">// PICK A PLATFORM</div></div>';\n return;\n }\n container.innerHTML = `\n <div class=\"pv-platform-section\">\n <div class=\"ps-h\">\n <span>${c.dataset.name} 套图预览</span>\n <span class=\"ct\">${state.count} 张</span>\n </div>\n <div class=\"ps-grid\">\n ${Array(state.count).fill(0).map(() => `\n <div class=\"mp-result placeholder-only\">\n <div class=\"mp-r-thumb\"><span class=\"ph-frame\">待生成</span></div>\n </div>\n `).join('')}\n </div>\n </div>\n `;\n}\n\n// 渲染生成结果 (点立即生成时调,带 hover overlay + 批量 bar)\nconst RERUN_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"23 4 23 10 17 10\"/><polyline points=\"1 20 1 14 7 14\"/><path d=\"M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15\"/></svg>';\nconst ADOPT_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>';\nconst CELL_RERUN_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>';\nconst CELL_DL_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3\"/></svg>';\nconst CELL_MORE_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"19\" cy=\"12\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\"/></svg>';\nconst CELL_ADOPT_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z\"/></svg>';\nconst CELL_DEL_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>';\nconst CELL_EDIT_SVG = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z\"/></svg>';\n\nlet _batchSeq = 0;\nfunction appendBatch(n, kind) {\n const container = document.getElementById('pv-results');\n const id = state.selectedPlatform;\n const c = id ? document.querySelector('.platform-card[data-id=\"' + id + '\"]') : null;\n if (!c) return;\n _batchSeq += 1;\n const ts = new Date();\n const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`;\n const labCls = kind === 'gen' ? 'gen' : 'rerun';\n const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`);\n const section = document.createElement('div');\n section.className = 'pv-platform-section';\n section.dataset.kind = kind;\n section.dataset.ts = String(ts.getTime());\n section.dataset.platformId = id || '';\n section.dataset.platformName = c.dataset.name || '';\n section.dataset.search = [labTxt, c.dataset.name || '', n + '张'].join(' ').toLowerCase();\n section.innerHTML = `\n <div class=\"ps-h\">\n <span class=\"batch-lab ${labCls}\">${labTxt}</span>\n <span class=\"sep\">·</span>\n <span>${c.dataset.name} 套图</span>\n <span class=\"ct\">${n} 张 · ${tsStr}</span>\n </div>\n <div class=\"ps-grid\">\n ${Array(n).fill(0).map((_, i) => `\n <div class=\"mp-result gen\" data-idx=\"${i}\">\n <div class=\"mp-r-thumb\"><span class=\"ph-frame\">${c.dataset.name}</span></div>\n <span class=\"adopt-badge\">已采用</span>\n <div class=\"cell-feedback\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n <span>已采用</span>\n </div>\n <div class=\"cell-ops\">\n <button class=\"r-rerun\" type=\"button\" title=\"再次生成\" aria-label=\"再次生成\">${CELL_RERUN_SVG}</button>\n <button class=\"r-dl\" type=\"button\" title=\"下载\" aria-label=\"下载\">${CELL_DL_SVG}</button>\n <div class=\"cell-more-wrap\">\n <button class=\"r-more\" type=\"button\" title=\"更多\" aria-label=\"更多\">${CELL_MORE_SVG}</button>\n <div class=\"cell-more-menu\">\n <button class=\"r-adopt\" type=\"button\">${CELL_ADOPT_SVG}<span>加入资产库</span></button>\n <button class=\"r-del danger\" type=\"button\">${CELL_DEL_SVG}<span>删除</span></button>\n </div>\n </div>\n </div>\n </div>\n `).join('')}\n </div>\n <div class=\"pc-pv-batch batch-foot\">\n <button class=\"pill-btn edit-batch\" type=\"button\" title=\"重新编辑\">\n ${CELL_EDIT_SVG}\n <span>重新编辑</span>\n </button>\n <button class=\"pill-btn rerun-batch\" type=\"button\" title=\"再次生成这一批\">\n ${CELL_RERUN_SVG}\n <span>再次生成</span>\n </button>\n <div class=\"batch-more-wrap\">\n <button class=\"pill-btn icon batch-more\" type=\"button\" title=\"更多\" aria-label=\"更多\">${CELL_MORE_SVG}</button>\n <div class=\"batch-more-menu\" role=\"menu\">\n <button class=\"batch-save-all\" type=\"button\">${CELL_ADOPT_SVG}<span>全部加入资产库</span></button>\n <button class=\"batch-del danger\" type=\"button\">${CELL_DEL_SVG}<span>删除该批结果</span></button>\n </div>\n </div>\n </div>\n `;\n container.appendChild(section);\n section.querySelectorAll('.mp-result.gen').forEach(card => {\n setTimeout(() => card.classList.remove('gen'), 1200);\n });\n section.querySelectorAll('.r-rerun').forEach(b => b.addEventListener('click', e => {\n e.stopPropagation();\n rerunOne(b.closest('.mp-result'));\n }));\n section.querySelectorAll('.r-dl').forEach(b => b.addEventListener('click', e => {\n e.stopPropagation();\n Shell.toast('下载', '已开始下载 · MOCK');\n }));\n // 更多 menu 开/合\n section.querySelectorAll('.r-more').forEach(b => b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n const willOpen = !wrap.classList.contains('open');\n document.querySelectorAll('.pv-platform-section .cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (willOpen) wrap.classList.add('open');\n }));\n section.querySelectorAll('.r-adopt').forEach(b => b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n if (wrap) wrap.classList.remove('open');\n adoptOne(b.closest('.mp-result'));\n }));\n section.querySelectorAll('.r-del').forEach(b => b.addEventListener('click', e => {\n e.stopPropagation();\n const wrap = b.closest('.cell-more-wrap');\n if (wrap) wrap.classList.remove('open');\n const card = b.closest('.mp-result');\n const sec = card.closest('.pv-platform-section');\n card.remove();\n if (sec && !sec.querySelectorAll('.mp-result:not(.placeholder-only)').length) sec.remove();\n else updateBatchSummary();\n Shell.toast('已删除');\n }));\n section.querySelector('.edit-batch').addEventListener('click', () => {\n const form = document.querySelector('.pc-form');\n if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });\n Shell.toast('重新编辑', '请在左侧调整平台 / 张数后再生成');\n });\n section.querySelector('.rerun-batch').addEventListener('click', () => {\n appendBatch(n, 'rerun-all');\n Shell.toast('再次生成', n + ' 张图重新生成中 · 新批次已追加');\n });\n const _adoptAll = () => {\n const cards = section.querySelectorAll('.mp-result:not(.adopted)');\n if (!cards.length) { Shell.toast('该批次已全部采用'); return; }\n cards.forEach(c => { c.classList.remove('gen'); c.classList.add('adopted'); });\n updateBatchSummary();\n Shell.toast('已全部加入资产库', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));\n };\n // 批次「更多」按钮 → 开/合 menu\n const _bMoreBtn = section.querySelector('.batch-more');\n const _bMoreWrap = section.querySelector('.batch-more-wrap');\n if (_bMoreBtn && _bMoreWrap) {\n _bMoreBtn.addEventListener('click', e => {\n e.stopPropagation();\n const willOpen = !_bMoreWrap.classList.contains('open');\n document.querySelectorAll('.pc-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));\n if (willOpen) _bMoreWrap.classList.add('open');\n });\n }\n section.querySelector('.batch-save-all').addEventListener('click', e => {\n e.stopPropagation();\n if (_bMoreWrap) _bMoreWrap.classList.remove('open');\n _adoptAll();\n });\n section.querySelector('.batch-del').addEventListener('click', e => {\n e.stopPropagation();\n if (_bMoreWrap) _bMoreWrap.classList.remove('open');\n section.remove();\n updateBatchSummary();\n Shell.toast('已删除该批结果');\n });\n section.scrollIntoView({ behavior: 'smooth', block: 'end' });\n updateBatchSummary();\n if (typeof _refreshPlatformMenu === 'function') _refreshPlatformMenu();\n if (typeof applyPvFilters === 'function') applyPvFilters();\n}\nfunction renderResultCards() {\n const container = document.getElementById('pv-results');\n // 首次生成清掉占位 placeholder-batch,保留已有真实批次(再次「立即生成」追加新批次)\n container.querySelectorAll('.placeholder-batch').forEach(el => el.remove());\n appendBatch(state.count, 'gen');\n}\n\nfunction rerunOne(card) {\n if (!card) return;\n appendBatch(1, 'rerun-one');\n}\nfunction adoptOne(card) {\n if (!card || card.classList.contains('adopted')) return;\n card.classList.remove('gen');\n card.classList.add('adopted');\n // spec §4.18 · 就地中央反馈\n card.classList.add('show-feedback');\n setTimeout(() => card.classList.remove('show-feedback'), 1500);\n updateBatchSummary();\n}\n// 点击页面其它位置 → 关闭单图/批次 more menu\ndocument.addEventListener('click', e => {\n if (!e.target.closest('.pv-platform-section .cell-more-wrap')) {\n document.querySelectorAll('.pv-platform-section .cell-more-wrap.open').forEach(w => w.classList.remove('open'));\n }\n if (!e.target.closest('.pc-pv-batch .batch-more-wrap')) {\n document.querySelectorAll('.pc-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));\n }\n});\nfunction updateBatchSummary() {\n document.querySelectorAll('#pv-results .pv-platform-section').forEach(section => {\n const cards = section.querySelectorAll('.mp-result:not(.placeholder-only)');\n const adopted = section.querySelectorAll('.mp-result.adopted').length;\n const adoptedEl = section.querySelector('.adopt-batch .adopted');\n const totalEl = section.querySelector('.adopt-batch .total');\n if (adoptedEl) adoptedEl.textContent = adopted;\n if (totalEl) totalEl.textContent = cards.length;\n });\n}\n\n// 立即生成\ndocument.getElementById('pc-go-btn').addEventListener('click', () => {\n if (!state.selectedPlatform || !state.selectedProd) return;\n const container = document.getElementById('pv-results');\n const hasReal = container && container.querySelector('.mp-result-batch:not(.placeholder-batch)');\n const prod = PRODUCTS.find(p => p.id === state.selectedProd);\n if (hasReal) {\n Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');\n } else {\n Shell.toast('已提交任务', (prod ? prod.name + ' · ' : '') + state.count + ' 张图生成中');\n }\n showPreviewContent();\n renderResultCards();\n});\n\n// ============================================================\n// 工具台头部 · 搜索 / 时间 / 平台 筛选\n// ============================================================\nconst _pvFilter = { time: 'all', platform: 'all', search: '' };\n\nfunction _pvTimeMatch(ts, key) {\n if (key === 'all') return true;\n const now = Date.now();\n const diff = now - Number(ts);\n if (key === '10min') return diff <= 10 * 60 * 1000;\n if (key === '1h') return diff <= 60 * 60 * 1000;\n if (key === 'today') {\n const a = new Date(now); const b = new Date(Number(ts));\n return a.toDateString() === b.toDateString();\n }\n return true;\n}\n\nfunction applyPvFilters() {\n const container = document.getElementById('pv-results');\n if (!container) return;\n const q = (_pvFilter.search || '').trim().toLowerCase();\n container.querySelectorAll('.pv-platform-section:not(.placeholder-batch)').forEach(sec => {\n let ok = true;\n if (!_pvTimeMatch(sec.dataset.ts, _pvFilter.time)) ok = false;\n if (ok && _pvFilter.platform !== 'all' && sec.dataset.platformId !== _pvFilter.platform) ok = false;\n if (ok && q && !(sec.dataset.search || '').includes(q)) ok = false;\n sec.dataset.hidden = ok ? '0' : '1';\n });\n const hasReal = !!container.querySelector('.pv-platform-section:not(.placeholder-batch)');\n const filterActive = _pvFilter.time !== 'all' || _pvFilter.platform !== 'all' || q.length > 0;\n container.querySelectorAll('.placeholder-batch').forEach(ph => {\n ph.dataset.hidden = (hasReal || filterActive) ? '1' : '0';\n });\n}\n\nfunction _refreshPlatformMenu() {\n const menu = document.getElementById('pc-menu-platform');\n if (!menu) return;\n const container = document.getElementById('pv-results');\n const used = new Map();\n if (container) {\n container.querySelectorAll('.pv-platform-section:not(.placeholder-batch)').forEach(s => {\n const id = s.dataset.platformId; const nm = s.dataset.platformName;\n if (id) used.set(id, nm || id);\n });\n }\n const items = ['<button class=\"tb-menu-item' + (_pvFilter.platform === 'all' ? ' active' : '') + '\" type=\"button\" data-val=\"all\">全部平台</button>'];\n if (used.size === 0) {\n items.push('<div class=\"tb-menu-empty\">暂无批次,生成后可按平台筛选</div>');\n } else {\n used.forEach((nm, id) => {\n items.push(`<button class=\"tb-menu-item${_pvFilter.platform === id ? ' active' : ''}\" type=\"button\" data-val=\"${id}\">${nm}</button>`);\n });\n }\n menu.innerHTML = items.join('');\n}\n\nfunction _setChipLabel(chipId, baseLabel, val) {\n const chip = document.getElementById(chipId);\n if (!chip) return;\n if (val === 'all' || !val) chip.classList.remove('active');\n else chip.classList.add('active');\n}\n\nfunction _closeAllMenus(except) {\n document.querySelectorAll('.pc-main-h .tb-menu-wrap.open').forEach(w => {\n if (w !== except) w.classList.remove('open');\n });\n}\n\ndocument.querySelectorAll('.pc-main-h .tb-menu-wrap').forEach(wrap => {\n const chip = wrap.querySelector('.tb-chip');\n chip.addEventListener('click', e => {\n e.stopPropagation();\n const willOpen = !wrap.classList.contains('open');\n _closeAllMenus(wrap);\n wrap.classList.toggle('open', willOpen);\n if (willOpen && wrap.dataset.filter === 'platform') _refreshPlatformMenu();\n });\n});\ndocument.querySelectorAll('#pc-menu-time, #pc-menu-platform').forEach(menu => {\n menu.addEventListener('click', e => {\n const btn = e.target.closest('.tb-menu-item');\n if (!btn) return;\n const val = btn.dataset.val;\n const wrap = menu.closest('.tb-menu-wrap');\n const key = wrap.dataset.filter;\n _pvFilter[key] = val;\n menu.querySelectorAll('.tb-menu-item').forEach(it => it.classList.toggle('active', it === btn));\n wrap.classList.remove('open');\n _setChipLabel(key === 'time' ? 'pc-chip-time' : 'pc-chip-platform', key === 'time' ? '时间' : '平台', val);\n applyPvFilters();\n });\n});\ndocument.addEventListener('click', e => {\n if (!e.target.closest('.pc-main-h .tb-menu-wrap')) _closeAllMenus(null);\n});\n\n(function setupSearch() {\n const wrap = document.getElementById('pc-search-wrap');\n const toggle = document.getElementById('pc-search-toggle');\n const input = document.getElementById('pc-search-input');\n if (!wrap || !toggle || !input) return;\n toggle.addEventListener('click', e => {\n e.stopPropagation();\n const willExpand = !wrap.classList.contains('expanded');\n wrap.classList.toggle('expanded', willExpand);\n if (willExpand) setTimeout(() => input.focus(), 50);\n else {\n input.value = '';\n _pvFilter.search = '';\n applyPvFilters();\n }\n });\n input.addEventListener('input', () => {\n _pvFilter.search = input.value;\n applyPvFilters();\n });\n input.addEventListener('keydown', e => {\n if (e.key === 'Escape') {\n input.value = '';\n _pvFilter.search = '';\n wrap.classList.remove('expanded');\n applyPvFilters();\n }\n });\n})();\n\n// URL ?product=商品名 → 替换默认选中(从 products.html 跳过来时携带)\n(function applyUrlProduct() {\n const q = new URLSearchParams(location.search);\n const productName = q.get('product');\n if (!productName) return;\n let p = PRODUCTS.find(x => x.name === productName);\n if (!p) {\n p = { id: 'np-' + Date.now().toString(36), name: productName, cat: '美妆个护', meta: '新建 · 待补充' };\n PRODUCTS.unshift(p);\n }\n state.selectedProd = p.id;\n})();\n\n/* ============================================================\n 生成批次 (localStorage) · 按当前商品过滤 · 立即生成时追加\n ============================================================ */\n(function () {\n 'use strict';\n const TASK_TYPE = 'platform';\n const KEY = 'fs-image-tasks-' + TASK_TYPE;\n\n let tasks = [];\n\n function load() {\n try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; }\n }\n function save(arr) {\n try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch (e) {}\n }\n function buildSnapshot() {\n return {\n selectedProd: state.selectedProd,\n selectedPlatform: state.selectedPlatform,\n count: state.count,\n };\n }\n function timeNow() {\n const d = new Date();\n return ('0'+(d.getMonth()+1)).slice(-2) + '.' + ('0'+d.getDate()).slice(-2) + ' ' + ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2);\n }\n\n // 暴露给上层 (给 cur-prod header 用)\n window._countBatchesForProd = function (prodId) {\n return tasks.filter(t => t.snap && t.snap.selectedProd === prodId).length;\n };\n\n // 切换商品 → 清空 pv-results (历史批次只在当前 session 持有)\n window.renderBatchesForCurrentProd = function () {\n const container = document.getElementById('pv-results');\n if (!container) return;\n container.innerHTML = '';\n showPreviewEmpty();\n };\n\n // 立即生成 → push 新批次 + persist + 刷新 cur-prod 计数\n document.getElementById('pc-go-btn').addEventListener('click', () => {\n if (!state.selectedPlatform || !state.selectedProd) return;\n const snap = buildSnapshot();\n const _prod = PRODUCTS.find(p => p.id === state.selectedProd);\n const _platCard = document.querySelector('.platform-card[data-id=\"' + state.selectedPlatform + '\"]');\n const _platName = (_platCard && _platCard.dataset.name) || '平台';\n const _name = ((_prod && _prod.name) || '商品') + ' × ' + _platName;\n const task = {\n id: 'task-' + Date.now(),\n type: TASK_TYPE,\n name: _name,\n snap,\n status: 'ok',\n time: timeNow(),\n createdAt: Date.now(),\n };\n tasks.push(task);\n save(tasks);\n updateCurProdHeader();\n });\n\n tasks = load();\n})();\n\n/* ---------- 商品空间折叠 / 展开 ---------- */\n(function () {\n const layout = document.querySelector('.pc-layout');\n const foldBtn = document.querySelector('.pc-side-top .fold');\n const restoreBtn = document.getElementById('pc-side-restore');\n function setSideCollapsed(collapsed) {\n layout?.classList.toggle('side-collapsed', collapsed);\n if (restoreBtn) restoreBtn.hidden = !collapsed;\n if (foldBtn) foldBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');\n try { localStorage.setItem('fs-pc-side-collapsed', collapsed ? '1' : '0'); } catch (e) {}\n }\n foldBtn?.addEventListener('click', () => setSideCollapsed(true));\n restoreBtn?.addEventListener('click', () => setSideCollapsed(false));\n try { setSideCollapsed(localStorage.getItem('fs-pc-side-collapsed') === '1'); } catch (e) {}\n})();\n\n// 初始\nrenderProdSpace();\nrenderSelectedProds();\nupdatePlatforms();\nshowPreviewEmpty(); // 默认 → 右侧显示空态\n// 默认选中: URL ?product= 优先, 否则选 PRODUCTS 首位 (= 最近编辑)\nconst defaultProdId = state.selectedProd || (PRODUCTS[0] && PRODUCTS[0].id);\nif (defaultProdId) selectProduct(defaultProdId);\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"productCreate": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"product-create.html\">\n<meta charset=\"utf-8\">\n<title>新建商品 · Airshelf</title>\n<meta http-equiv=\"Cache-Control\" content=\"no-store, must-revalidate\">\n<meta http-equiv=\"Pragma\" content=\"no-cache\">\n<meta http-equiv=\"Expires\" content=\"0\">\n<!-- 双保险:JS 没跑也能跳走 (1.5s 后强制回 products.html) -->\n<meta http-equiv=\"refresh\" content=\"1.5; url=products.html?npd=1\">\n<style>\n html,body{background:#f9f9f9;margin:0;height:100%;font-family:'Inter',system-ui,sans-serif;}\n .stub-msg{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:#999;font-size:13px;}\n .stub-msg .ttl{font-size:14px;color:#262626;font-weight:500;}\n .stub-msg a{color:#fa5d19;text-decoration:underline;}\n</style>\n</head>\n<body>\n<div class=\"stub-msg\">\n <div class=\"ttl\">正在打开「新建商品」…</div>\n <div>如未自动跳转,<a href=\"products.html?npd=1\">点击这里</a></div>\n</div>\n<script>\n/* ============================================================\n 新建商品 · 重定向 stub\n ----------------------------------------------------------\n 旧版本是一个 3883 行的全屏 drawer 页(legacy 已归档到 _archive/deprecated-pages-20260528/)。\n 现在「新建商品」改为 drawer 形态在原页面弹出,直接访问此 URL 没有意义。\n\n 行为:\n 1. 设置 sessionStorage 标志,通知落地页自动打开 drawer\n 2. 优先回退到 referrer(用户来源页),否则去 products.html\n 3. cache-bust 参数 ?npd=1 强制浏览器重新拉新版宿主页\n ============================================================ */\n(function () {\n try { sessionStorage.setItem('npd-auto-open', '1'); } catch (e) {}\n try { sessionStorage.setItem('auto-open-new-product', '1'); } catch (e) {}\n\n const ref = document.referrer || '';\n const here = location.origin + location.pathname;\n\n // 同源 referrer 且不是自身才回跳, 否则去 products.html\n let target = 'products.html';\n if (ref && ref.indexOf(location.origin) === 0 && ref.split('#')[0].split('?')[0] !== here) {\n target = ref;\n }\n // 加 cache-bust query · 强制浏览器拉新版宿主页,避免再次命中缓存旧版\n const sep = target.indexOf('?') >= 0 ? '&' : '?';\n target = target + sep + 'npd=' + Date.now().toString(36);\n location.replace(target);\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"productCreateUpload": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"product-create-upload.html\">\n<meta charset=\"utf-8\">\n<title>新建商品 · Airshelf</title>\n<script>\n // 已废弃 · 新建商品改为 products.html 上的居中弹窗(Shell.openNewProduct)\n // 直接访问此 URL 时,跳回商品库并自动打开弹窗\n sessionStorage.setItem('auto-open-new-product', '1');\n location.replace('products.html');\n</script>\n</head>\n<body><script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"productDetail": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"product-detail.html\">\n<meta charset=\"utf-8\">\n<meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\">\n<meta http-equiv=\"Pragma\" content=\"no-cache\">\n<meta http-equiv=\"Expires\" content=\"0\">\n<title>商品详情 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── 顶部 标题 + 状态 ─── */\n .pd-title {\n display: flex; align-items: center; gap: 12px;\n margin-bottom: 22px;\n }\n .pd-title h1 {\n font-size: 24px; font-weight: 600;\n letter-spacing: -.015em;\n color: var(--accent-black);\n line-height: 1.25;\n }\n .pd-title .status {\n display: inline-flex; align-items: center; gap: 4px;\n padding: 3px 10px;\n background: var(--accent-emerald-bg, #e6f4ec);\n color: var(--accent-emerald, #1f8a51);\n border: 1px solid var(--accent-emerald-bd, #c4e3d1);\n border-radius: var(--r-sm);\n font-size: 11.5px;\n font-weight: 500;\n }\n\n /* ─── 商品信息(含图片) + 快速操作(辅助) · 3 : 2 两栏 · 高度对齐 ─── */\n .pd-overview {\n display: grid;\n grid-template-columns: 3fr 2fr;\n gap: 16px;\n margin-bottom: 24px;\n align-items: stretch;\n }\n .pd-overview .ov-card { height: 100%; box-sizing: border-box; }\n .pd-overview .ov-card {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 20px 22px;\n min-width: 0;\n position: relative;\n }\n /* 编辑按钮 · 放在 .ov-h 标题行右侧 (flex item, 不再 absolute) */\n .pd-overview .ov-h { align-items: center; }\n .ov-edit {\n display: inline-flex; align-items: center; gap: 5px;\n height: 28px;\n padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 12px;\n font-family: inherit;\n cursor: pointer;\n white-space: nowrap;\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .ov-edit-single { margin-left: auto; }\n .ov-edit:hover {\n border-color: var(--heat-40);\n color: var(--heat);\n background: var(--heat-12);\n }\n .ov-edit svg { width: 12px; height: 12px; }\n .ov-edit.primary {\n background: var(--heat);\n color: var(--accent-white);\n border-color: var(--heat);\n white-space: nowrap;\n }\n .ov-edit.primary:hover { filter: brightness(1.05); background: var(--heat); color: var(--accent-white); }\n .ov-edit:disabled { cursor: not-allowed; color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); opacity: 1; }\n .ov-edit:disabled:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }\n .ov-edit:disabled svg { color: var(--heat); }\n\n /* 编辑模式按钮组 (重置 + 取消 + 保存) */\n .ov-edit-group {\n display: none;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n }\n .ov-card.editing .ov-edit-single { display: none; }\n .ov-card.editing .ov-edit-group { display: inline-flex; }\n\n /* AI 生成三视图 · 按钮 + 弹出 panel(布局复刻 pipeline.html stage 2 三视图预览) */\n .ov-tri-wrap { position: relative; margin-left: auto; }\n /* 当 AI 入口存在时,编辑信息按钮不再独占 ml-auto,与 AI 按钮紧贴 */\n .ov-tri-wrap + .ov-edit-single { margin-left: 0; }\n .ov-tri-trigger { white-space: nowrap; }\n .ov-tri-trigger.is-open { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .ov-card.editing .ov-tri-wrap { display: none; }\n\n .ov-tri-pop {\n position: absolute;\n top: calc(100% + 6px); right: 0;\n width: 360px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 8px 24px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.06);\n padding: 14px 14px 12px;\n display: none;\n flex-direction: column;\n gap: 10px;\n z-index: 40;\n }\n .ov-tri-pop.show { display: flex; }\n .ov-tri-pop::before {\n content: ''; position: absolute;\n top: -5px; right: 36px;\n width: 9px; height: 9px;\n background: var(--surface);\n border-left: 1px solid var(--border-faint);\n border-top: 1px solid var(--border-faint);\n transform: rotate(45deg);\n }\n .ov-tri-close {\n position: absolute;\n top: 8px; right: 8px;\n width: 22px; height: 22px;\n display: grid; place-items: center;\n background: transparent;\n border: 1px solid transparent;\n border-radius: var(--r-sm);\n color: var(--black-alpha-56);\n cursor: pointer;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n z-index: 2;\n }\n .ov-tri-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--border-faint); }\n .ov-tri-close svg { width: 12px; height: 12px; }\n\n /* 复刻 pipeline.html .prod-preview-* 内部样式 */\n .ov-tri-pop .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; padding-right: 28px; }\n .ov-tri-pop .prod-preview-img { aspect-ratio: 16/9; }\n .ov-tri-pop .prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }\n .ov-tri-pop .prod-preview-history { display: none; flex-direction: column; gap: 6px; }\n .ov-tri-pop .prod-preview-history.show { display: flex; }\n .ov-tri-pop .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; }\n .ov-tri-pop .prod-preview-history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }\n .ov-tri-pop .prod-preview-history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; scrollbar-width: thin; }\n .ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar { height: 4px; }\n .ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }\n .ov-tri-pop .prod-preview-history .h-thumb {\n flex: 0 0 auto;\n width: 72px; aspect-ratio: 16/9;\n background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);\n position: relative; cursor: pointer; transition: border-color var(--t-base);\n display: grid; place-items: center; overflow: hidden;\n }\n .ov-tri-pop .prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }\n /* 已采用版本:主橙描边 + 「已采用」徽标 */\n .ov-tri-pop .prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }\n /* 仅预览(未采用):黑色描边,无徽标 */\n .ov-tri-pop .prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }\n .ov-tri-pop .prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .ov-tri-pop .prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }\n .ov-tri-pop .prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }\n .ov-tri-pop .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; }\n .ov-tri-pop .prod-preview-history .h-thumb.adopted .badge { display: block; }\n\n /* 主图可点击放大 */\n .ov-tri-pop .prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }\n .ov-tri-pop .prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }\n .ov-tri-pop .prod-preview-img.is-zoomable::after {\n content: '';\n position: absolute; top: 8px; right: 8px;\n width: 22px; height: 22px;\n 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;\n border-radius: var(--r-sm);\n opacity: 0; transition: opacity var(--t-base);\n pointer-events: none;\n }\n .ov-tri-pop .prod-preview-img.is-zoomable:hover::after { opacity: 1; }\n\n /* 三视图放大查看 lightbox */\n #ov-tri-lightbox-bg { z-index: 80; }\n #ov-tri-lightbox-bg .tri-lightbox {\n position: relative;\n width: min(1100px, 92vw);\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 18px 20px 20px;\n display: flex; flex-direction: column; gap: 12px;\n box-shadow: 0 24px 64px rgba(0,0,0,.24);\n }\n .tri-lightbox-head {\n display: flex; align-items: center; gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px; letter-spacing: .04em; text-transform: uppercase;\n color: var(--black-alpha-56);\n padding-right: 32px;\n }\n .tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }\n .tri-lightbox-head .lb-tag {\n margin-left: 6px;\n padding: 2px 6px;\n background: var(--heat-12); color: var(--heat);\n border-radius: 3px;\n font-size: 10px;\n }\n .tri-lightbox-close {\n position: absolute;\n top: 12px; right: 12px;\n width: 28px; height: 28px;\n display: grid; place-items: center;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-56);\n cursor: pointer;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n z-index: 2;\n }\n .tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }\n .tri-lightbox-close svg { width: 14px; height: 14px; }\n .tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }\n .tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }\n .tri-lightbox-foot .spc { flex: 1; }\n .tri-lightbox-foot kbd {\n display: inline-block;\n padding: 1px 5px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-bottom-width: 2px;\n border-radius: 3px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--black-alpha-72);\n }\n\n /* 字段 view ↔ edit 状态切换 */\n .v-edit { display: none; }\n .ov-card.editing .v-static { display: none; }\n .ov-card.editing .v-edit { display: block; }\n\n /* 输入控件 · 对齐新建表单 V2.1 规范 */\n .v-input,\n .v-select {\n width: 100%;\n max-width: 100%;\n height: 38px;\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-md);\n padding: 0 14px;\n font-size: 13.5px;\n color: var(--accent-black);\n background: var(--background-lighter);\n font-family: inherit;\n outline: none;\n transition: border-color var(--t-base), box-shadow var(--t-base);\n }\n .v-input:focus,\n .v-select:focus {\n border-color: var(--heat-40);\n box-shadow: inset 0 0 0 1px var(--heat-40);\n }\n\n /* 编辑模式下 · 核心卖点 bullet-list (与新建表单完全一致) */\n .v-bullet-list {\n list-style: none;\n padding: 0; margin: 0;\n }\n .v-bullet-list .bl-item,\n .v-bullet-list .bl-add {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 12px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n margin-bottom: 6px;\n font-size: 13.5px;\n }\n .v-bullet-list .bl-add { background: transparent; border-style: dashed; }\n .v-bullet-list .bl-add:focus-within { border-color: var(--heat-40); background: var(--surface); }\n .v-bullet-list .num {\n width: 22px; height: 22px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--heat);\n font-weight: 700;\n display: grid; place-items: center;\n flex-shrink: 0;\n }\n .v-bullet-list .bl-add .num {\n background: transparent;\n color: var(--heat);\n border-color: var(--heat-40);\n }\n .v-bullet-list .bl-text { flex: 1; color: var(--accent-black); }\n .v-bullet-list .bl-input {\n flex: 1;\n background: transparent; border: 0; outline: none;\n font-size: 13.5px;\n color: var(--accent-black);\n font-family: inherit;\n }\n .v-bullet-list .bl-input::placeholder { color: var(--black-alpha-48); }\n .v-bullet-list .bl-x {\n width: 22px; height: 22px;\n color: var(--black-alpha-48);\n cursor: pointer;\n display: grid; place-items: center;\n border-radius: var(--r-sm);\n transition: color var(--t-base), background var(--t-base);\n }\n .v-bullet-list .bl-x:hover { color: var(--accent-crimson, #c43d3d); background: var(--crimson-bg, #fdebea); }\n .v-bullet-list .bl-x svg { width: 11px; height: 11px; }\n\n /* 编辑模式下,商品图片显示一个 [+ 上传] 占位 */\n .img-upload {\n display: none;\n aspect-ratio: 1;\n border: 1.5px dashed var(--black-alpha-24);\n border-radius: var(--r-sm);\n cursor: pointer;\n place-items: center;\n color: var(--black-alpha-48);\n background: var(--background-lighter);\n transition: border-color var(--t-base), color var(--t-base);\n }\n .img-upload:hover { border-color: var(--heat); color: var(--heat); }\n .img-upload svg { width: 18px; height: 18px; }\n .ov-card.editing .img-upload { display: grid; }\n .ov-card.editing .ov-images-sub .thumb { cursor: pointer; }\n .ov-card.editing .ov-images-sub .thumb::after {\n content: '×';\n position: absolute;\n top: 4px; right: 4px;\n width: 18px; height: 18px;\n background: rgba(0,0,0,.7);\n color: var(--accent-white);\n border-radius: 50%;\n display: grid; place-items: center;\n font-size: 13px;\n line-height: 1;\n }\n .ov-images-sub .thumb { position: relative; }\n .pd-overview .ov-h {\n display: flex; align-items: baseline; gap: 8px;\n margin-bottom: 14px;\n }\n .pd-overview .ov-h .ti {\n font-size: 14px; font-weight: 600;\n color: var(--accent-black);\n }\n .pd-overview .ov-h .ct {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n }\n .pd-overview .ov-h .more {\n margin-left: auto;\n font-size: 12px;\n color: var(--heat);\n cursor: pointer;\n }\n .pd-overview .ov-h .more:hover { text-decoration: underline; }\n\n /* 商品信息卡片内 · 上信息 / 下图片 (堆叠, 图片铺满卡片) */\n .ov-main-grid {\n display: flex;\n flex-direction: column;\n gap: 18px;\n }\n .ov-main-grid > .ov-images-sub {\n padding-top: 18px;\n border-top: 1px solid var(--border-faint);\n }\n .ov-info .row {\n display: flex; gap: 12px;\n margin-bottom: 10px;\n font-size: 13px;\n }\n .ov-info .row:last-child { margin-bottom: 0; }\n .ov-info .k {\n width: 64px;\n flex-shrink: 0;\n color: var(--black-alpha-48);\n font-size: 12.5px;\n }\n .ov-info .v {\n flex: 1; min-width: 0;\n color: var(--accent-black);\n line-height: 1.6;\n }\n .ov-info .v .bullet { display: block; }\n .ov-info .v .bullet::before {\n content: '·';\n color: var(--heat);\n margin-right: 6px;\n font-weight: 700;\n }\n\n /* 商品图片 · 卡片内子 section */\n .ov-images-sub .sub-h {\n display: flex; align-items: baseline; gap: 6px;\n margin-bottom: 10px;\n }\n .ov-images-sub .sub-h .ti {\n font-size: 12.5px; font-weight: 500;\n color: var(--black-alpha-72);\n }\n .ov-images-sub .sub-h .ct {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n }\n .ov-images-sub .sub-h .more {\n margin-left: auto;\n font-size: 11.5px;\n color: var(--heat);\n cursor: pointer;\n }\n .ov-images-sub .sub-h .more:hover { text-decoration: underline; }\n .ov-images-sub .grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));\n gap: 8px;\n }\n .ov-images-sub .thumb {\n aspect-ratio: 1;\n border-radius: var(--r-sm);\n overflow: hidden;\n cursor: pointer;\n }\n .ov-images-sub .thumb img { width: 100%; height: 100%; object-fit: cover; }\n\n /* 快速操作 · 2 段:图片生成(3 等比 CTA)+ 视频生成(1 CTA) · 两段等高填充容器 */\n .ov-actions { display: flex; flex-direction: column; }\n .ov-actions .qa-section {\n margin-bottom: 14px;\n display: flex; flex-direction: column;\n flex: 1 1 0; min-height: 0;\n }\n .ov-actions .qa-section:last-child { margin-bottom: 0; }\n .ov-actions .qa-section-h {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .06em;\n text-transform: uppercase;\n margin-bottom: 8px;\n }\n .ov-actions .qa-row-3 {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n flex: 1 1 0; min-height: 0;\n }\n .ov-actions .qa-row-1 {\n display: flex;\n flex: 1 1 0; min-height: 0;\n }\n .ov-actions .qa-row-1 .qa-item { width: 100%; }\n .qa-item {\n display: flex; flex-direction: column;\n align-items: center; justify-content: center; gap: 8px;\n padding: 14px 10px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n cursor: pointer;\n font-size: 12.5px;\n color: var(--accent-black);\n text-align: center;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .qa-item:hover { border-color: var(--heat); background: var(--heat-12); color: var(--heat); }\n .qa-item .ic {\n width: 32px; height: 32px;\n display: grid; place-items: center;\n color: var(--heat);\n flex-shrink: 0;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n }\n .qa-item:hover .ic { border-color: var(--heat-20); background: var(--surface); }\n .qa-item .ic svg { width: 16px; height: 16px; }\n .qa-item.primary {\n background: var(--heat);\n color: var(--accent-white);\n border-color: var(--heat);\n }\n .qa-item.primary .ic { background: rgba(255,255,255,.16); color: var(--accent-white); border-color: rgba(255,255,255,.24); }\n .qa-item.primary:hover { color: var(--accent-white); box-shadow: var(--shadow-cta-hover); }\n\n /* 状态 pill 三态(通过/不通过/归档) */\n .asset-card .meta .pill.pass {\n background: var(--accent-emerald-bg, #e6f4ec);\n color: var(--accent-emerald, #1f8a51);\n border: 1px solid var(--accent-emerald-bd, #c4e3d1);\n cursor: pointer;\n }\n .asset-card .meta .pill.fail {\n background: var(--crimson-bg, #fdebea);\n color: var(--accent-crimson, #c43d3d);\n border: 1px solid var(--crimson-bd, #f5c2bf);\n cursor: pointer;\n }\n .asset-card .meta .pill.archive {\n background: var(--background-lighter);\n color: var(--black-alpha-56);\n border: 1px solid var(--border-faint);\n cursor: pointer;\n }\n\n /* ─── Tabs ─── */\n .pd-tabs {\n display: flex; gap: 4px;\n border-bottom: 1px solid var(--border-faint);\n margin-bottom: 18px;\n }\n .pd-tabs .tab {\n padding: 10px 14px;\n font-size: 13.5px;\n color: var(--black-alpha-56);\n background: transparent;\n border: 0;\n border-bottom: 2px solid transparent;\n cursor: pointer;\n font-family: inherit;\n font-weight: 500;\n transition: color var(--t-base), border-color var(--t-base);\n }\n .pd-tabs .tab:hover { color: var(--accent-black); }\n .pd-tabs .tab.active {\n color: var(--accent-black);\n border-bottom-color: var(--heat);\n font-weight: 600;\n }\n .tab-pane { display: none; }\n .tab-pane.active { display: block; }\n\n /* ─── AI 素材 工具栏 ─── */\n .pd-toolbar {\n display: flex; align-items: center; gap: 10px;\n margin-bottom: 14px;\n flex-wrap: wrap;\n }\n .pd-toolbar .total {\n font-size: 14px; font-weight: 600;\n color: var(--accent-black);\n }\n .pd-toolbar .total .ct {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n margin-left: 4px;\n font-weight: 500;\n }\n .pd-toolbar .filter {\n display: inline-flex; align-items: center; gap: 4px;\n height: 30px;\n padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-size: 12.5px;\n color: var(--black-alpha-72);\n font-family: inherit;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .pd-toolbar .filter:hover { border-color: var(--black-alpha-24); }\n .pd-toolbar .filter.open, .pd-toolbar .filter.filtered { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .pd-toolbar .filter.open svg, .pd-toolbar .filter.filtered svg { opacity: 1; }\n .pd-toolbar .filter svg { width: 10px; height: 10px; opacity: .6; transition: transform var(--t-base); }\n .pd-toolbar .filter.open svg { transform: rotate(180deg); }\n\n /* 筛选下拉 · 挂在 body 上避免被祖先 overflow 裁切 */\n .filter-pop {\n position: fixed;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 4px;\n box-shadow: 0 6px 20px var(--black-alpha-12);\n z-index: 1500;\n min-width: 130px;\n display: none;\n flex-direction: column;\n }\n .filter-pop.show { display: flex; }\n .filter-pop button {\n background: transparent; border: 0;\n padding: 8px 12px;\n text-align: left;\n font-size: 12.5px;\n color: var(--accent-black);\n cursor: pointer;\n border-radius: var(--r-sm);\n font-family: inherit;\n white-space: nowrap;\n transition: background var(--t-base);\n }\n .filter-pop button:hover { background: var(--background-lighter); }\n .filter-pop button.selected { background: var(--heat-12); color: var(--heat); font-weight: 600; }\n .filter-pop button.selected::before { content: '✓ '; }\n .pd-toolbar .right { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }\n .pd-toolbar .view-tog {\n display: inline-flex;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n padding: 2px;\n }\n .pd-toolbar .view-tog button {\n width: 28px; height: 26px;\n display: grid; place-items: center;\n border: 0;\n background: transparent;\n color: var(--black-alpha-48);\n cursor: pointer;\n border-radius: 4px;\n }\n .pd-toolbar .view-tog button.active {\n background: var(--accent-black);\n color: var(--accent-white);\n }\n .pd-toolbar .view-tog button svg { width: 13px; height: 13px; }\n\n /* ─── AI 素材 网格 ─── */\n .asset-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n gap: 12px;\n }\n /* 列表视图:卡片横排,缩略图缩到 88px */\n .asset-grid.list-view {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .asset-grid.list-view .asset-card {\n display: grid;\n grid-template-columns: 88px minmax(0, 1fr);\n gap: 0;\n align-items: center;\n }\n .asset-grid.list-view .asset-card .thumb {\n aspect-ratio: 1;\n width: 88px;\n border-right: 1px solid var(--border-faint);\n }\n .asset-grid.list-view .asset-card .thumb .type-pill {\n font-size: 9.5px;\n padding: 2px 6px;\n top: 4px;\n left: 4px;\n }\n .asset-grid.list-view .asset-card .thumb .ph-frame { font-size: 10px; }\n .asset-grid.list-view .asset-card .meta {\n padding: 10px 14px;\n }\n\n /* 空筛选结果 */\n .empty-filter {\n padding: 56px 24px;\n text-align: center;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n font-size: 12.5px;\n letter-spacing: .02em;\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-md);\n background: var(--background-lighter);\n }\n .empty-filter .reset {\n display: inline-block; margin-top: 12px;\n color: var(--heat); cursor: pointer; text-decoration: underline;\n }\n .asset-card {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow: hidden;\n cursor: pointer;\n transition: border-color var(--t-base), transform var(--t-fast);\n }\n .asset-card:hover { border-color: var(--black-alpha-24); transform: translateY(-1px); }\n .asset-card .thumb {\n aspect-ratio: 3/4;\n position: relative;\n overflow: hidden;\n }\n .asset-card .thumb .type-pill {\n position: absolute; top: 8px; left: 8px;\n padding: 3px 8px;\n background: rgba(0,0,0,.65);\n color: var(--accent-white);\n border-radius: var(--r-sm);\n font-size: 11px;\n font-weight: 500;\n backdrop-filter: blur(4px);\n }\n .asset-card .meta {\n padding: 10px 12px;\n display: flex; align-items: center; gap: 8px;\n }\n .asset-card .meta .pill {\n padding: 2px 8px;\n border-radius: var(--r-sm);\n font-size: 10.5px;\n font-weight: 500;\n }\n .asset-card .meta .date {\n margin-left: auto;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n }\n .pd-more {\n text-align: center;\n padding: 18px 0 32px;\n }\n .pd-more button {\n height: 32px;\n padding: 0 18px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n color: var(--black-alpha-72);\n font-size: 12.5px;\n font-family: inherit;\n cursor: pointer;\n }\n .pd-more button:hover { border-color: var(--heat-40); color: var(--heat); }\n\n /* ─── 任务记录 · 表格 ─── */\n .task-stats {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 12px;\n margin-bottom: 18px;\n }\n .task-stat {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 14px 18px;\n }\n .task-stat .lbl {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n margin-bottom: 6px;\n }\n .task-stat .v {\n font-size: 22px; font-weight: 600;\n color: var(--accent-black);\n letter-spacing: -.01em;\n }\n .task-stat .v small {\n font-size: 13px;\n color: var(--black-alpha-48);\n font-weight: 400;\n margin-left: 4px;\n }\n .task-stat.ok .v { color: var(--accent-emerald, #1f8a51); }\n .task-stat.gen .v { color: var(--heat); }\n .task-stat.err .v { color: var(--accent-crimson, #c43d3d); }\n\n .task-table {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow: hidden;\n }\n .task-row {\n display: grid;\n grid-template-columns: 36px 1.8fr 0.7fr 1fr 1.1fr 1.1fr 0.7fr 100px;\n align-items: center;\n gap: 12px;\n padding: 12px 18px;\n border-bottom: 1px solid var(--border-faint);\n font-size: 13px;\n }\n .task-row:last-child { border-bottom: 0; }\n .task-row.head {\n background: var(--background-lighter);\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n letter-spacing: .04em;\n font-weight: 500;\n padding: 10px 18px;\n }\n .task-row .ph {\n width: 36px; height: 36px;\n border-radius: var(--r-sm);\n flex-shrink: 0;\n }\n .task-row .nm {\n color: var(--accent-black);\n font-weight: 500;\n display: flex; align-items: center; gap: 8px;\n }\n .task-row .nm .id-mono {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n font-weight: 400;\n }\n .task-row .qty { color: var(--black-alpha-72); font-family: var(--font-mono); }\n .task-row .time {\n color: var(--black-alpha-72);\n font-family: var(--font-mono);\n font-size: 12px;\n letter-spacing: .01em;\n }\n .task-row .dur {\n color: var(--black-alpha-56);\n font-family: var(--font-mono);\n font-size: 12px;\n }\n .task-row .pill {\n display: inline-flex; align-items: center; gap: 5px;\n padding: 3px 9px;\n border-radius: var(--r-sm);\n font-size: 11.5px;\n font-weight: 500;\n width: fit-content;\n }\n .task-row .pill .dot {\n width: 6px; height: 6px;\n border-radius: 50%;\n }\n .task-row .pill.ok {\n background: var(--accent-emerald-bg, #e6f4ec);\n color: var(--accent-emerald, #1f8a51);\n border: 1px solid var(--accent-emerald-bd, #c4e3d1);\n }\n .task-row .pill.ok .dot { background: var(--accent-emerald, #1f8a51); }\n .task-row .pill.gen {\n background: var(--heat-12);\n color: var(--heat);\n border: 1px solid var(--heat-20);\n }\n .task-row .pill.gen .dot { background: var(--heat); animation: pulse 1.6s ease-in-out infinite; }\n .task-row .pill.err {\n background: var(--crimson-bg, #fdebea);\n color: var(--accent-crimson, #c43d3d);\n border: 1px solid var(--crimson-bd, #f5c2bf);\n }\n .task-row .pill.err .dot { background: var(--accent-crimson, #c43d3d); }\n .task-row .pill.wait {\n background: var(--background-lighter);\n color: var(--black-alpha-56);\n border: 1px solid var(--border-faint);\n }\n .task-row .pill.wait .dot { background: var(--black-alpha-32); }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: .3; }\n }\n .task-row .status-cell { display: flex; flex-direction: column; gap: 4px; }\n .task-row .progress {\n width: 100%; height: 3px;\n background: var(--black-alpha-12);\n border-radius: 2px;\n overflow: hidden;\n }\n .task-row .progress > span {\n display: block;\n height: 100%;\n background: var(--heat);\n }\n .task-row .ops {\n display: inline-flex; gap: 4px;\n justify-self: end;\n }\n .task-row .ops button {\n padding: 4px 10px;\n height: 26px;\n background: transparent;\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n color: var(--black-alpha-72);\n font-size: 11.5px;\n font-family: inherit;\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .task-row .ops button:hover { border-color: var(--heat-40); color: var(--heat); }\n .task-row .ops button.danger:hover { border-color: var(--crimson-bd, #f5c2bf); color: var(--accent-crimson, #c43d3d); }\n\n @media (max-width: 1100px) {\n .pd-overview { grid-template-columns: 1fr; }\n .ov-actions .qa-grid {\n display: grid !important;\n grid-template-columns: repeat(3, 1fr);\n }\n }\n @media (max-width: 900px) {\n .ov-actions .qa-grid { grid-template-columns: 1fr 1fr; }\n .task-stats { grid-template-columns: repeat(2, 1fr); }\n }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n <!-- 顶部 标题 + 状态 -->\n <div class=\"pd-title\">\n <h1 id=\"pd-name\">补水保湿精华液</h1>\n </div>\n\n <!-- 商品信息(含图片) + 快速操作 · 主辅两栏 -->\n <div class=\"pd-overview\">\n\n <div class=\"ov-card ov-main\" id=\"ov-main-card\">\n <div class=\"ov-h\">\n <span class=\"ti\">商品信息</span>\n <!-- AI 生成三视图 · 按钮 + 弹出 panel(view 模式可见) -->\n <div class=\"ov-tri-wrap\">\n <button class=\"ov-edit ov-tri-trigger\" type=\"button\" id=\"ov-tri-btn\" title=\"AI 生成商品三视图\" aria-haspopup=\"dialog\" aria-expanded=\"false\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z\"/><path d=\"M19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9L19 14z\"/></svg>\n AI 生成三视图\n </button>\n <div class=\"ov-tri-pop\" id=\"ov-tri-pop\" role=\"dialog\" aria-label=\"AI 生成三视图\">\n <button class=\"ov-tri-close\" type=\"button\" id=\"ov-tri-close\" aria-label=\"关闭\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg>\n </button>\n <div class=\"prod-preview-h\">// 三视图预览 · <span id=\"ov-tri-status\">待生成</span></div>\n <div class=\"placeholder prod-preview-img\" id=\"ov-tri-img\"><span class=\"ph-frame\">// 尚未生成 · 点击下方按钮开始</span></div>\n <div class=\"prod-preview-foot\" id=\"ov-tri-foot\">\n <button class=\"ov-edit primary\" type=\"button\" id=\"ov-tri-start\" style=\"height:28px;\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z\"/></svg>\n 生成\n </button>\n <span style=\"flex:1;\"></span>\n <span class=\"mono\" style=\"font-size:11px; color: var(--black-alpha-56);\">~¥0.30 / 次</span>\n </div>\n <div class=\"prod-preview-history\" id=\"ov-tri-history\">\n <div class=\"h-lbl\">// 历史版本 · <span class=\"ct\" id=\"ov-tri-history-count\">0</span> 版</div>\n <div class=\"h-row\" id=\"ov-tri-history-row\"></div>\n </div>\n </div>\n </div>\n <!-- view 模式: 单个 [编辑信息] -->\n <button class=\"ov-edit ov-edit-single\" type=\"button\" id=\"ov-edit-btn\" title=\"编辑商品信息\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z\"/></svg>\n 编辑信息\n </button>\n <!-- edit 模式: [重置] [取消] [保存] -->\n <div class=\"ov-edit-group\">\n <button class=\"ov-edit\" type=\"button\" id=\"ov-reset-btn\" title=\"重置为修改前\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg>\n 重置\n </button>\n <button class=\"ov-edit\" type=\"button\" id=\"ov-cancel-btn\">取消</button>\n <button class=\"ov-edit primary\" type=\"button\" id=\"ov-save-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n 保存\n </button>\n </div>\n </div>\n\n <div class=\"ov-main-grid\">\n\n <div class=\"ov-info\">\n <div class=\"row\" data-field=\"name\">\n <div class=\"k\">商品名称</div>\n <div class=\"v\">\n <span class=\"v-static\">补水保湿精华液</span>\n <input class=\"v-edit v-input\" type=\"text\" value=\"补水保湿精华液\" maxlength=\"100\">\n </div>\n </div>\n <div class=\"row\" data-field=\"cat\">\n <div class=\"k\">品类</div>\n <div class=\"v\">\n <span class=\"v-static\">美妆个护 / 精华液</span>\n <select class=\"v-edit v-select\">\n <option>美妆个护 / 精华液</option>\n <option>美妆个护</option>\n <option>服饰内衣</option>\n <option>食品饮料</option>\n <option>家居家电</option>\n <option>数码 3C</option>\n <option>个护清洁</option>\n <option>运动户外</option>\n <option>母婴亲子</option>\n </select>\n </div>\n </div>\n <div class=\"row\" data-field=\"target\">\n <div class=\"k\">目标人群</div>\n <div class=\"v\">\n <span class=\"v-static\">22-32 岁女性、敏感肌、办公室通勤</span>\n <input class=\"v-edit v-input\" type=\"text\" value=\"22-32 岁女性、敏感肌、办公室通勤\">\n </div>\n </div>\n <div class=\"row\" data-field=\"bullets\">\n <div class=\"k\">核心卖点</div>\n <div class=\"v\">\n <div class=\"v-static\">\n <span class=\"bullet\">透明质酸 + B5,敷完不黏不闷</span>\n <span class=\"bullet\">30g 大容量精华液</span>\n <span class=\"bullet\">0 香精 0 酒精,敏感肌可用</span>\n </div>\n <ul class=\"v-edit v-bullet-list\" id=\"v-bullets-list\">\n <!-- li.bl-item × N + li.bl-add 由 JS 在进入编辑模式时渲染 -->\n </ul>\n </div>\n </div>\n </div>\n\n <div class=\"ov-images-sub\">\n <div class=\"sub-h\">\n <span class=\"ti\">商品图片</span>\n <span class=\"ct\">(6)</span>\n </div>\n <div class=\"grid\" id=\"ov-images-grid\">\n <div class=\"thumb placeholder\"><span class=\"ph-frame\">1:1</span></div>\n <div class=\"thumb placeholder\"><span class=\"ph-frame\">1:1</span></div>\n <div class=\"thumb placeholder\"><span class=\"ph-frame\">1:1</span></div>\n <div class=\"thumb placeholder\"><span class=\"ph-frame\">1:1</span></div>\n <div class=\"thumb placeholder\"><span class=\"ph-frame\">1:1</span></div>\n <div class=\"thumb placeholder\"><span class=\"ph-frame\">1:1</span></div>\n <div class=\"img-upload\" id=\"ov-img-add\" title=\"上传图片\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n </div>\n </div>\n </div>\n\n </div>\n </div>\n\n <div class=\"ov-card ov-actions\">\n <div class=\"ov-h\"><span class=\"ti\">快速操作</span></div>\n <div class=\"qa-section\">\n <div class=\"qa-section-h\">// 图片生成</div>\n <div class=\"qa-row-3\">\n <div class=\"qa-item\" data-go=\"model-photo\" role=\"button\" tabindex=\"0\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21v-2a4 4 0 014-4h8a4 4 0 014 4v2\"/></svg></span>\n 模特上身图\n </div>\n <div class=\"qa-item\" data-go=\"platform-cover\" role=\"button\" tabindex=\"0\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M3 9h18M9 3v18\"/></svg></span>\n 平台套图\n </div>\n <div class=\"qa-item\" data-go=\"image-optimize\" role=\"button\" tabindex=\"0\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3v18M3 12h18M5 5l14 14M5 19l14-14\"/></svg></span>\n 图片创作\n </div>\n </div>\n </div>\n <div class=\"qa-section\">\n <div class=\"qa-section-h\">// 视频生成</div>\n <div class=\"qa-row-1\">\n <div class=\"qa-item primary\" data-go=\"projects-new\" role=\"button\" tabindex=\"0\">\n <span class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg></span>\n 生成视频\n </div>\n </div>\n </div>\n </div>\n\n </div>\n\n <!-- Tabs -->\n <div class=\"pd-tabs\">\n <button class=\"tab active\" type=\"button\" data-tab=\"assets\">AI 生成素材</button>\n <button class=\"tab\" type=\"button\" data-tab=\"videos\">视频项目</button>\n <button class=\"tab\" type=\"button\" data-tab=\"tasks\" hidden>任务记录</button>\n </div>\n\n <!-- ===== AI 生成素材 ===== -->\n <div class=\"tab-pane active\" data-pane=\"assets\">\n\n <div class=\"pd-toolbar\">\n <div class=\"total\">全部 AI 素材 <span class=\"ct\">(32)</span></div>\n <button class=\"filter\" type=\"button\" data-key=\"type\">\n 全部类型\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <button class=\"filter\" type=\"button\" data-key=\"status\">\n 通过\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"right\">\n <div class=\"view-tog\">\n <button type=\"button\" class=\"active\" title=\"网格视图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><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\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"/></svg>\n </button>\n <button type=\"button\" title=\"列表视图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 6h18M3 12h18M3 18h18\"/></svg>\n </button>\n </div>\n <button class=\"filter\" type=\"button\" data-key=\"sort\">\n 最新生成\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n </div>\n </div>\n\n <div class=\"asset-grid\">\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">模特上身图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">模特上身图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">模特上身图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill fail\" data-status=\"fail\" title=\"点击切换状态\">不通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">模特上身图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">模特上身图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill archive\" data-status=\"archive\" title=\"点击切换状态\">归档</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">平台套图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">平台套图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">平台套图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill fail\" data-status=\"fail\" title=\"点击切换状态\">不通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">平台套图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill archive\" data-status=\"archive\" title=\"点击切换状态\">归档</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">平台套图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">三视图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill pass\" data-status=\"pass\" title=\"点击切换状态\">通过</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n <div class=\"asset-card\"><div class=\"thumb placeholder\"><span class=\"type-pill\">三视图</span><span class=\"ph-frame\">3:4</span></div><div class=\"meta\"><span class=\"pill archive\" data-status=\"archive\" title=\"点击切换状态\">归档</span><span class=\"date\">2026-05-19 15:30</span></div></div>\n </div>\n\n <div class=\"pd-more\"><button type=\"button\">加载更多</button></div>\n </div>\n\n <!-- ===== 视频项目 ===== -->\n <div class=\"tab-pane\" data-pane=\"videos\">\n <div class=\"pd-toolbar\">\n <div class=\"total\">该商品视频项目 <span class=\"ct\">(4)</span></div>\n <div class=\"right\">\n <button class=\"filter\" type=\"button\" data-key=\"sort\">\n 最新导出\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n </div>\n </div>\n\n <div class=\"asset-grid\">\n <div class=\"asset-card\" data-proj-status=\"done\"><div class=\"thumb placeholder\" style=\"aspect-ratio: 9/16;\"><span class=\"type-pill\">视频 · 9:16</span><span class=\"ph-frame\">补水面膜 · v3</span></div><div class=\"meta\"><span class=\"pill ok\"><span class=\"dot\"></span>已完成</span><span class=\"date\">2026-05-20 12:08</span></div></div>\n <div class=\"asset-card\" data-proj-status=\"wip\"><div class=\"thumb placeholder\" style=\"aspect-ratio: 9/16;\"><span class=\"type-pill\">视频 · 9:16</span><span class=\"ph-frame\">补水面膜 · v2</span></div><div class=\"meta\"><span class=\"pill info\"><span class=\"dot\"></span>视频生成 4/6</span><span class=\"date\">2026-05-19 10:24</span></div></div>\n <div class=\"asset-card\" data-proj-status=\"archived\"><div class=\"thumb placeholder\" style=\"aspect-ratio: 9/16;\"><span class=\"type-pill\">视频 · 9:16</span><span class=\"ph-frame\">熬夜急救 · v1</span></div><div class=\"meta\"><span class=\"pill neutral\"><span class=\"dot\"></span>已归档</span><span class=\"date\">2026-05-18 21:42</span></div></div>\n <div class=\"asset-card\" data-proj-status=\"fail\"><div class=\"thumb placeholder\" style=\"aspect-ratio: 9/16;\"><span class=\"type-pill\">视频 · 9:16</span><span class=\"ph-frame\">补水面膜 · v1</span></div><div class=\"meta\"><span class=\"pill err\"><span class=\"dot\"></span>故事板失败</span><span class=\"date\">2026-05-17 16:00</span></div></div>\n </div>\n\n <div class=\"pd-more\"><button type=\"button\">加载更多</button></div>\n </div>\n\n <!-- ===== 任务记录 ===== -->\n <div class=\"tab-pane\" data-pane=\"tasks\">\n\n <!-- 顶部统计概览 -->\n <div class=\"task-stats\">\n <div class=\"task-stat\">\n <div class=\"lbl\">// TOTAL</div>\n <div class=\"v\">12 <small>个任务</small></div>\n </div>\n <div class=\"task-stat ok\">\n <div class=\"lbl\">// SUCCESS</div>\n <div class=\"v\">9</div>\n </div>\n <div class=\"task-stat gen\">\n <div class=\"lbl\">// RUNNING</div>\n <div class=\"v\">2</div>\n </div>\n <div class=\"task-stat err\">\n <div class=\"lbl\">// FAILED</div>\n <div class=\"v\">1</div>\n </div>\n </div>\n\n <!-- 工具栏 -->\n <div class=\"pd-toolbar\">\n <div class=\"total\">任务记录 <span class=\"ct\">(12)</span></div>\n <button class=\"filter\" type=\"button\" data-key=\"type\">\n 全部类型\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <button class=\"filter\" type=\"button\" data-key=\"status\">\n 全部状态\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <button class=\"filter\" type=\"button\" data-key=\"date\">\n 近 7 天\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"right\">\n <button class=\"filter\" type=\"button\" data-key=\"sort\">\n 提交时间倒序\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n </div>\n </div>\n\n <!-- 任务表格 -->\n <div class=\"task-table\">\n <div class=\"task-row head\">\n <span></span>\n <span>任务 / 编号</span>\n <span>数量</span>\n <span>状态</span>\n <span>提交时间</span>\n <span>完成时间</span>\n <span>耗时</span>\n <span style=\"justify-self:end\">操作</span>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">视频素材 <span class=\"id-mono\">// T-2026-0519-0007</span></div>\n <div class=\"qty\">1 个</div>\n <div class=\"status-cell\">\n <span class=\"pill gen\"><span class=\"dot\"></span>生成中 60%</span>\n <span class=\"progress\"><span style=\"width:60%\"></span></span>\n </div>\n <div class=\"time\">2026-05-19 16:00</div>\n <div class=\"time\">—</div>\n <div class=\"dur\">—</div>\n <div class=\"ops\">\n <button type=\"button\">查看</button>\n <button type=\"button\" class=\"danger\">取消</button>\n </div>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">模特上身图 <span class=\"id-mono\">// T-2026-0519-0006</span></div>\n <div class=\"qty\">3 张</div>\n <div class=\"status-cell\">\n <span class=\"pill wait\"><span class=\"dot\"></span>排队中</span>\n </div>\n <div class=\"time\">2026-05-19 15:58</div>\n <div class=\"time\">—</div>\n <div class=\"dur\">—</div>\n <div class=\"ops\">\n <button type=\"button\">查看</button>\n <button type=\"button\" class=\"danger\">取消</button>\n </div>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">模特上身图 <span class=\"id-mono\">// T-2026-0519-0005</span></div>\n <div class=\"qty\">5 张</div>\n <div class=\"status-cell\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n </div>\n <div class=\"time\">2026-05-19 15:30</div>\n <div class=\"time\">2026-05-19 15:32</div>\n <div class=\"dur\">2m 14s</div>\n <div class=\"ops\">\n <button type=\"button\">查看</button>\n </div>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">平台套图 <span class=\"id-mono\">// T-2026-0519-0004</span></div>\n <div class=\"qty\">4 张</div>\n <div class=\"status-cell\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n </div>\n <div class=\"time\">2026-05-19 14:20</div>\n <div class=\"time\">2026-05-19 14:23</div>\n <div class=\"dur\">3m 02s</div>\n <div class=\"ops\">\n <button type=\"button\">查看</button>\n </div>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">模特上身图 <span class=\"id-mono\">// T-2026-0519-0003</span></div>\n <div class=\"qty\">4 张</div>\n <div class=\"status-cell\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n </div>\n <div class=\"time\">2026-05-19 13:10</div>\n <div class=\"time\">2026-05-19 13:13</div>\n <div class=\"dur\">2m 50s</div>\n <div class=\"ops\">\n <button type=\"button\">查看</button>\n </div>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">三视图 <span class=\"id-mono\">// T-2026-0519-0002</span></div>\n <div class=\"qty\">3 张</div>\n <div class=\"status-cell\">\n <span class=\"pill err\"><span class=\"dot\"></span>失败</span>\n </div>\n <div class=\"time\">2026-05-19 12:00</div>\n <div class=\"time\">2026-05-19 12:01</div>\n <div class=\"dur\">30s</div>\n <div class=\"ops\">\n <button type=\"button\">重试</button>\n <button type=\"button\">日志</button>\n </div>\n </div>\n\n <div class=\"task-row\">\n <div class=\"ph placeholder\"></div>\n <div class=\"nm\">平台套图 <span class=\"id-mono\">// T-2026-0518-0001</span></div>\n <div class=\"qty\">6 张</div>\n <div class=\"status-cell\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n </div>\n <div class=\"time\">2026-05-18 18:42</div>\n <div class=\"time\">2026-05-18 18:46</div>\n <div class=\"dur\">4m 10s</div>\n <div class=\"ops\">\n <button type=\"button\">查看</button>\n </div>\n </div>\n\n </div>\n\n <div class=\"pd-more\"><button type=\"button\">加载更多</button></div>\n </div>\n\n</div>\n\n<!-- 三视图 · 放大查看 lightbox -->\n<div class=\"modal-bg\" id=\"ov-tri-lightbox-bg\" onclick=\"if(event.target===this)Shell.closeModal('ov-tri-lightbox-bg')\">\n <div class=\"tri-lightbox\" role=\"dialog\" aria-label=\"三视图放大查看\">\n <button class=\"tri-lightbox-close\" type=\"button\" onclick=\"Shell.closeModal('ov-tri-lightbox-bg')\" aria-label=\"关闭\">\n <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>\n </button>\n <div class=\"tri-lightbox-head\">\n // 三视图(正/侧/背) · <span class=\"lb-ver\" id=\"ov-tri-lightbox-label\">v1</span>\n <span class=\"lb-tag\" id=\"ov-tri-lightbox-tag\" hidden>已采用</span>\n </div>\n <div class=\"placeholder tri-lightbox-img\" id=\"ov-tri-lightbox-img\"></div>\n <div class=\"tri-lightbox-foot\">\n <span id=\"ov-tri-lightbox-meta\">// 生成于 --:--</span>\n <span class=\"spc\"></span>\n <span><kbd>Esc</kbd> 关闭</span>\n </div>\n </div>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\n // 从 URL ?product= 读出商品名,注入 crumb / h1 / 商品名字段\n const _urlProductName = (function () {\n try {\n const q = new URLSearchParams(location.search);\n const v = q.get('product') || q.get('name');\n return v ? decodeURIComponent(v) : '';\n } catch (e) { return ''; }\n })();\n const _productDisplayName = _urlProductName || '补水保湿精华液';\n Shell.render({\n active: 'products',\n crumbs: [\n { label: '工作台', href: 'index.html' },\n { label: '商品库', href: 'products.html' },\n { label: _productDisplayName }\n ]\n });\n if (_urlProductName) {\n const h1 = document.getElementById('pd-name');\n if (h1) h1.textContent = _urlProductName;\n const nameRow = document.querySelector('[data-field=\"name\"] .v-static');\n if (nameRow) nameRow.textContent = _urlProductName;\n const nameInput = document.querySelector('[data-field=\"name\"] .v-input');\n if (nameInput) nameInput.value = _urlProductName;\n }\n\n // 快速操作 · 跳转至对应工作台/wizard 并携带商品名\n (function bindQuickActions() {\n const productName = (document.querySelector('.pd-title h1, .pd-title, .ov-h .ti, h1') || {}).textContent || '';\n const crumbName = (document.querySelector('.crumb-current') || {}).textContent\n || (document.querySelectorAll('.crumb-item').length ? document.querySelectorAll('.crumb-item')[document.querySelectorAll('.crumb-item').length-1].textContent : '')\n || '补水保湿精华液';\n const name = (crumbName || productName || '').trim();\n document.querySelectorAll('.qa-item[data-go]').forEach(item => {\n item.style.cursor = 'pointer';\n let go = item.dataset.go;\n // 图片创作 → 独立工作台 image-optimize.html(自由创作),带 product 作为提示词种子\n let url;\n if (go === 'image-optimize') {\n url = 'image-optimize.html?t=' + Date.now() + '&prompt=' + encodeURIComponent(name);\n } else {\n url = go + '.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);\n }\n item.addEventListener('click', () => { location.href = url; });\n item.addEventListener('keydown', e => {\n if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); location.href = url; }\n });\n });\n })();\n\n // 任务记录 · 「查看」按钮 / 行点击 → 跳对应工作台\n (function bindTaskRowJump() {\n const TYPE_MAP = {\n '模特上身图': 'model-photo.html',\n '平台套图': 'platform-cover.html',\n '视频素材': 'projects-new.html',\n '视频': 'projects-new.html',\n '三视图': 'model-photo.html?mode=tri',\n };\n const productName = (document.getElementById('pd-name')?.textContent || '').trim();\n document.querySelectorAll('.task-table .task-row:not(.head)').forEach(row => {\n const nameCell = row.querySelector('.nm');\n if (!nameCell) return;\n // 任务类型 = nm 的第一段文本(去掉 id-mono)\n const type = nameCell.firstChild?.nodeValue?.trim() || '';\n let url = TYPE_MAP[type];\n if (!url) return;\n url += (url.includes('?') ? '&' : '?') + 't=' + Date.now()\n + (productName ? '&product=' + encodeURIComponent(productName) : '');\n row.style.cursor = 'pointer';\n row.addEventListener('click', e => {\n if (e.target.closest('.ops button')) return;\n location.href = url;\n });\n // 「查看」按钮\n row.querySelectorAll('.ops button').forEach(btn => {\n if (btn.textContent.trim() === '查看') {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n location.href = url;\n });\n }\n });\n });\n })();\n\n // 状态 pill · 三态循环(通过 → 不通过 → 归档 → 通过)\n (function bindStatusPills() {\n const labels = { pass: '通过', fail: '不通过', archive: '归档' };\n const order = ['pass', 'fail', 'archive'];\n document.addEventListener('click', e => {\n const pill = e.target.closest('.asset-card .meta .pill[data-status]');\n if (!pill) return;\n e.stopPropagation();\n if (pill.closest('.asset-card[data-tri-version]')) {\n window.ProductDetailFilters?.applyAssets?.();\n Shell.toast('三视图状态由采用版本决定', '请在三视图面板选择「采用此版本」');\n return;\n }\n const cur = pill.dataset.status;\n const next = order[(order.indexOf(cur) + 1) % order.length];\n pill.dataset.status = next;\n pill.classList.remove('pass', 'fail', 'archive');\n pill.classList.add(next);\n pill.textContent = labels[next];\n window.ProductDetailFilters?.applyAssets?.();\n Shell.toast('状态已更新', labels[next]);\n });\n })();\n\n // Tab 切换\n document.querySelectorAll('.pd-tabs .tab').forEach(t => {\n t.onclick = () => {\n document.querySelectorAll('.pd-tabs .tab').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n const target = t.dataset.tab;\n document.querySelectorAll('.tab-pane').forEach(p => {\n p.classList.toggle('active', p.dataset.pane === target);\n });\n };\n });\n\n // ─── 工具栏:筛选 / 排序 / 视图 / 加载更多(per pane)───\n (function setupToolbars() {\n // 每个 (pane, key) 的可选项 + 默认值(数组首项)\n const OPTIONS = {\n 'assets:type': ['全部类型', '模特上身图', '平台套图', '三视图'],\n // 状态:默认 通过 · 没有「全部状态」选项,只能切换 通过 / 不通过 / 归档\n 'assets:status': ['通过', '不通过', '归档'],\n 'assets:sort': ['最新生成', '最早生成'],\n 'videos:sort': ['最新导出', '最早导出'],\n 'tasks:type': ['全部类型', '模特上身图', '平台套图', '视频素材', '三视图'],\n 'tasks:status': ['全部状态', '已完成', '生成中', '排队中', '失败'],\n 'tasks:date': ['全部', '今天', '近 7 天', '近 30 天'],\n 'tasks:sort': ['提交时间倒序', '提交时间正序'],\n };\n // 「状态」永远视为已生效筛选(没有\"全部\"选项),即便选了默认值也走过滤逻辑\n const ALWAYS_APPLY_KEYS = new Set(['status']);\n // 任务行 status pill 的 class → 中文标签\n const TASK_STATUS_MAP = { ok: '已完成', gen: '生成中', wait: '排队中', err: '失败', fail: '失败' };\n // assets 卡片 data-status → 中文标签\n const ASSET_STATUS_MAP = { pass: '通过', fail: '不通过', archive: '归档' };\n\n let openPop = null;\n function closeOpenPop() {\n if (!openPop) return;\n openPop.pop.classList.remove('show');\n openPop.btn.classList.remove('open');\n openPop = null;\n }\n document.addEventListener('click', closeOpenPop);\n window.addEventListener('resize', closeOpenPop);\n // .content 区域滚动时也关\n (document.querySelector('.content') || document).addEventListener('scroll', closeOpenPop, true);\n\n document.querySelectorAll('.tab-pane').forEach(paneEl => {\n const paneId = paneEl.dataset.pane;\n\n paneEl.querySelectorAll('.pd-toolbar .filter[data-key]').forEach(btn => {\n const key = btn.dataset.key;\n const opts = OPTIONS[paneId + ':' + key];\n if (!opts) return;\n btn.dataset.value = opts[0];\n btn.dataset.default = opts[0];\n\n const pop = document.createElement('div');\n pop.className = 'filter-pop';\n pop.innerHTML = opts.map(o =>\n `<button type=\"button\" data-val=\"${o}\">${o}</button>`\n ).join('');\n document.body.appendChild(pop);\n\n btn.addEventListener('click', e => {\n e.stopPropagation();\n if (openPop && openPop.btn === btn) { closeOpenPop(); return; }\n closeOpenPop();\n const r = btn.getBoundingClientRect();\n pop.style.top = (r.bottom + 4) + 'px';\n pop.style.left = r.left + 'px';\n pop.style.minWidth = r.width + 'px';\n pop.classList.add('show');\n btn.classList.add('open');\n pop.querySelectorAll('button').forEach(b =>\n b.classList.toggle('selected', b.dataset.val === btn.dataset.value));\n openPop = { btn, pop };\n });\n pop.addEventListener('click', e => {\n e.stopPropagation();\n const opt = e.target.closest('button[data-val]');\n if (!opt) return;\n setFilterValue(btn, opt.dataset.val);\n closeOpenPop();\n applyFilters(paneEl);\n });\n });\n\n // 视图切换:网格 / 列表\n const viewTog = paneEl.querySelector('.view-tog');\n if (viewTog) {\n const btns = viewTog.querySelectorAll('button');\n btns.forEach((b, i) => {\n b.onclick = () => {\n btns.forEach(x => x.classList.remove('active'));\n b.classList.add('active');\n const grid = paneEl.querySelector('.asset-grid');\n if (grid) grid.classList.toggle('list-view', i === 1);\n };\n });\n }\n\n // 加载更多:复用前 N 个卡片克隆,演示用\n const moreBtn = paneEl.querySelector('.pd-more button');\n if (moreBtn) {\n let loaded = 0;\n moreBtn.onclick = () => {\n loaded++;\n if (loaded > 2) {\n moreBtn.textContent = '已加载全部';\n moreBtn.disabled = true;\n moreBtn.style.opacity = '.5';\n moreBtn.style.cursor = 'not-allowed';\n Shell.toast('已到末尾', '没有更多素材了');\n return;\n }\n const grid = paneEl.querySelector('.asset-grid');\n if (!grid) return;\n const src = [...grid.querySelectorAll('.asset-card')].slice(0, 4);\n src.forEach(c => grid.appendChild(c.cloneNode(true)));\n applyFilters(paneEl);\n Shell.toast('已加载更多', '+' + src.length + ' 个');\n };\n }\n\n // 首次 apply 一遍(同步 count)\n applyFilters(paneEl);\n });\n\n function setFilterValue(btn, val) {\n btn.dataset.value = val;\n [...btn.childNodes].forEach(n => { if (n.nodeType === 3) n.remove(); });\n btn.insertBefore(document.createTextNode(val + ' '), btn.firstChild);\n // 仅在切到非默认值时才高亮(状态 chip 即便永远过滤,视觉也保持中性,与「全部类型」一致)\n btn.classList.toggle('filtered', val !== btn.dataset.default);\n }\n\n function applyFilters(paneEl) {\n const paneId = paneEl.dataset.pane;\n const f = {};\n paneEl.querySelectorAll('.pd-toolbar .filter[data-key]').forEach(b => {\n f[b.dataset.key] = b.dataset.value;\n });\n const isDefault = (key) => {\n const btn = paneEl.querySelector('.pd-toolbar .filter[data-key=\"' + key + '\"]');\n if (!btn) return true; // 该 pane 没这个 filter → 等同默认,不过滤\n if (ALWAYS_APPLY_KEYS.has(key)) return false; // 状态有按钮时,永远走过滤\n return btn.dataset.value === btn.dataset.default;\n };\n\n if (paneId === 'assets' || paneId === 'videos') {\n const cards = [...paneEl.querySelectorAll('.asset-card')];\n let visible = 0;\n cards.forEach(c => {\n let show = true;\n if (paneId === 'assets' && !isDefault('type')) {\n const t = c.querySelector('.type-pill')?.textContent?.trim() || '';\n if (t !== f.type) show = false;\n }\n if (!isDefault('status')) {\n const s = c.querySelector('.pill[data-status]')?.dataset.status || '';\n if ((ASSET_STATUS_MAP[s] || '') !== f.status) show = false;\n }\n c.style.display = show ? '' : 'none';\n if (show) visible++;\n });\n if (!isDefault('sort')) {\n const grid = paneEl.querySelector('.asset-grid');\n const asc = (f.sort || '').includes('最早');\n [...grid.querySelectorAll('.asset-card')]\n .filter(c => c.style.display !== 'none')\n .sort((a, b) => {\n const ad = a.querySelector('.date')?.textContent || '';\n const bd = b.querySelector('.date')?.textContent || '';\n return asc ? ad.localeCompare(bd) : bd.localeCompare(ad);\n })\n .forEach(c => grid.appendChild(c));\n }\n updateCount(paneEl, visible);\n toggleEmpty(paneEl, visible === 0);\n // 不足 2 行时不显示「加载更多」按钮(布局后用 offsetTop 统计行数)\n const moreEl = paneEl.querySelector('.pd-more');\n if (moreEl) {\n requestAnimationFrame(() => {\n const visCards = [...paneEl.querySelectorAll('.asset-card')].filter(c => c.style.display !== 'none');\n const rows = new Set(visCards.map(c => c.offsetTop)).size;\n moreEl.style.display = rows >= 2 ? '' : 'none';\n });\n }\n } else if (paneId === 'tasks') {\n const rows = [...paneEl.querySelectorAll('.task-table .task-row:not(.head)')];\n let visible = 0;\n rows.forEach(row => {\n let show = true;\n if (!isDefault('type')) {\n const t = row.querySelector('.nm')?.firstChild?.nodeValue?.trim() || '';\n if (t !== f.type) show = false;\n }\n if (!isDefault('status')) {\n const pill = row.querySelector('.status-cell .pill');\n const cls = pill?.className || '';\n const matched = Object.entries(TASK_STATUS_MAP)\n .some(([k, v]) => cls.split(/\\s+/).includes(k) && v === f.status);\n if (!matched) show = false;\n }\n // date 过滤 · mock 数据都是 5/19,简单按天数差近似\n if (!isDefault('date')) {\n const submitTxt = row.querySelectorAll('.time')[0]?.textContent || '';\n const m = submitTxt.match(/(\\d{4})-(\\d{2})-(\\d{2})/);\n if (m) {\n const d = new Date(m[1] + '-' + m[2] + '-' + m[3]);\n const now = new Date();\n const days = Math.floor((now - d) / 86400000);\n if (f.date === '今天' && days > 0) show = false;\n if (f.date === '近 7 天' && days > 7) show = false;\n if (f.date === '近 30 天' && days > 30) show = false;\n }\n }\n row.style.display = show ? '' : 'none';\n if (show) visible++;\n });\n if (!isDefault('sort')) {\n const table = paneEl.querySelector('.task-table');\n const asc = (f.sort || '').includes('正序');\n [...table.querySelectorAll('.task-row:not(.head)')]\n .filter(r => r.style.display !== 'none')\n .sort((a, b) => {\n const ad = a.querySelectorAll('.time')[0]?.textContent || '';\n const bd = b.querySelectorAll('.time')[0]?.textContent || '';\n return asc ? ad.localeCompare(bd) : bd.localeCompare(ad);\n })\n .forEach(r => table.appendChild(r));\n }\n updateCount(paneEl, visible);\n toggleEmpty(paneEl, visible === 0);\n }\n }\n\n function updateCount(paneEl, n) {\n const ct = paneEl.querySelector('.pd-toolbar .total .ct');\n if (ct) ct.textContent = '(' + n + ')';\n }\n\n function toggleEmpty(paneEl, isEmpty) {\n let empty = paneEl.querySelector('.empty-filter');\n const container = paneEl.querySelector('.asset-grid, .task-table');\n const more = paneEl.querySelector('.pd-more');\n if (isEmpty) {\n if (!empty) {\n empty = document.createElement('div');\n empty.className = 'empty-filter';\n empty.innerHTML = '// 当前筛选下没有结果<br><a class=\"reset\">点这里重置筛选</a>';\n container?.after(empty);\n empty.querySelector('.reset').addEventListener('click', () => {\n paneEl.querySelectorAll('.pd-toolbar .filter[data-key]').forEach(b => {\n setFilterValue(b, b.dataset.default);\n });\n applyFilters(paneEl);\n });\n }\n empty.style.display = '';\n if (more) more.style.display = 'none';\n } else if (empty) {\n empty.style.display = 'none';\n if (more) more.style.display = '';\n }\n }\n\n window.ProductDetailFilters = {\n apply(pane = 'assets') {\n const paneEl = document.querySelector(`.tab-pane[data-pane=\"${pane}\"]`);\n if (paneEl) applyFilters(paneEl);\n },\n applyAssets() {\n this.apply('assets');\n }\n };\n })();\n\n // 编辑商品信息 · 在卡片内 inline 切换 view ↔ edit\n (function initEdit() {\n const card = document.getElementById('ov-main-card');\n const editBtn = document.getElementById('ov-edit-btn');\n const cancelBtn = document.getElementById('ov-cancel-btn');\n const saveBtn = document.getElementById('ov-save-btn');\n if (!card || !editBtn || !cancelBtn || !saveBtn) return;\n\n // 同时同步顶部 h1 标题 — 商品名称改动后,顶部大标题也跟着更新\n const pdName = document.getElementById('pd-name');\n\n function escapeHtml(s) {\n return s.replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n }\n const X_SVG = '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>';\n\n // bullet-list 编辑模式 · 操作 helpers\n function renumberBullets(list) {\n [...list.querySelectorAll('.bl-item .num')].forEach((n, i) => n.textContent = i + 1);\n }\n function bindBulletX(x, list) {\n x.addEventListener('click', () => {\n x.closest('.bl-item').remove();\n renumberBullets(list);\n });\n }\n function makeBulletLi(text) {\n const li = document.createElement('li');\n li.className = 'bl-item';\n const safe = (text || '').replace(/\"/g, '&quot;');\n li.innerHTML = `<span class=\"num\"></span><input class=\"bl-input bl-edit\" type=\"text\" value=\"${safe}\" placeholder=\"卖点内容\"><span class=\"bl-x\" title=\"删除\">${X_SVG}</span>`;\n return li;\n }\n function addBulletItem(list, text) {\n const li = makeBulletLi(text);\n list.querySelector('.bl-add').before(li);\n bindBulletX(li.querySelector('.bl-x'), list);\n renumberBullets(list);\n }\n function renderBulletEditor(list, items) {\n list.innerHTML = '';\n // 已有项 (每条都是 input 可直接修改)\n items.forEach(text => list.appendChild(makeBulletLi(text)));\n // 添加行\n const addLi = document.createElement('li');\n addLi.className = 'bl-add';\n addLi.innerHTML = `<span class=\"num\">+</span><input class=\"bl-input\" placeholder=\"添加新卖点 · 回车确认\">`;\n list.appendChild(addLi);\n // 绑定已有项的删除\n list.querySelectorAll('.bl-item .bl-x').forEach(x => bindBulletX(x, list));\n // 绑定回车追加\n const addInput = addLi.querySelector('.bl-input');\n addInput.addEventListener('keydown', e => {\n if (e.key === 'Enter') {\n e.preventDefault();\n const v = addInput.value.trim();\n if (!v) return;\n addBulletItem(list, v);\n addInput.value = '';\n addInput.focus();\n }\n });\n renumberBullets(list);\n }\n\n // 进入编辑模式: 把当前静态值灌进 input\n function enterEdit() {\n card.querySelectorAll('[data-field]').forEach(row => {\n const stat = row.querySelector('.v-static');\n const inp = row.querySelector('.v-edit');\n if (!stat || !inp) return;\n if (row.dataset.field === 'bullets') {\n const items = [...stat.querySelectorAll('.bullet')].map(b => b.textContent.trim());\n renderBulletEditor(inp, items);\n } else if (inp.tagName === 'SELECT') {\n const cur = stat.textContent.trim();\n [...inp.options].forEach((o, i) => { if (o.textContent.trim() === cur) inp.selectedIndex = i; });\n } else {\n inp.value = stat.textContent.trim();\n }\n });\n card.classList.add('editing');\n }\n function exitEdit() {\n card.classList.remove('editing');\n }\n function save() {\n card.querySelectorAll('[data-field]').forEach(row => {\n const stat = row.querySelector('.v-static');\n const inp = row.querySelector('.v-edit');\n if (!stat || !inp) return;\n if (row.dataset.field === 'bullets') {\n // 从 .bl-item .bl-edit input 读值 (允许修改已有条目)\n const items = [...inp.querySelectorAll('.bl-item .bl-edit')]\n .map(t => t.value.trim()).filter(Boolean);\n stat.innerHTML = items.map(s => `<span class=\"bullet\">${escapeHtml(s)}</span>`).join('');\n } else {\n const val = (inp.tagName === 'SELECT') ? inp.options[inp.selectedIndex].textContent : inp.value;\n stat.textContent = val.trim();\n if (row.dataset.field === 'name' && pdName) {\n pdName.textContent = val.trim() || pdName.textContent;\n }\n }\n });\n card.classList.remove('editing');\n Shell.toast('已保存', '商品信息已更新');\n }\n // 重置 · 在编辑模式下,把所有输入框回退到当前静态值 (相当于重新进入 edit)\n function resetEdit() {\n card.querySelectorAll('[data-field]').forEach(row => {\n const stat = row.querySelector('.v-static');\n const inp = row.querySelector('.v-edit');\n if (!stat || !inp) return;\n if (row.dataset.field === 'bullets') {\n const items = [...stat.querySelectorAll('.bullet')].map(b => b.textContent.trim());\n renderBulletEditor(inp, items);\n } else if (inp.tagName === 'SELECT') {\n const cur = stat.textContent.trim();\n [...inp.options].forEach((o, i) => { if (o.textContent.trim() === cur) inp.selectedIndex = i; });\n } else {\n inp.value = stat.textContent.trim();\n }\n });\n Shell.toast('已重置');\n }\n\n const resetBtn = document.getElementById('ov-reset-btn');\n // 防御性: 先清空可能存在的 inline onclick, 再用 addEventListener 绑定\n editBtn.onclick = null;\n cancelBtn.onclick = null;\n saveBtn.onclick = null;\n if (resetBtn) resetBtn.onclick = null;\n editBtn.addEventListener('click', (e) => { e.preventDefault(); enterEdit(); });\n cancelBtn.addEventListener('click', (e) => { e.preventDefault(); exitEdit(); });\n saveBtn.addEventListener('click', (e) => { e.preventDefault(); save(); });\n if (resetBtn) resetBtn.addEventListener('click', (e) => { e.preventDefault(); resetEdit(); });\n\n // 编辑模式下,点缩略图 → 删除\n const grid = document.getElementById('ov-images-grid');\n if (grid) {\n grid.addEventListener('click', e => {\n if (!card.classList.contains('editing')) return;\n const thumb = e.target.closest('.thumb');\n if (thumb && !thumb.classList.contains('img-upload')) {\n thumb.remove();\n }\n });\n // [+] 上传占位\n const addBtn = document.getElementById('ov-img-add');\n if (addBtn) addBtn.onclick = () => Shell.toast('上传图片', '请选择本地图片 (占位)');\n }\n })();\n\n // AI 生成三视图 · 按钮悬浮 panel(可重复打开 / X 关闭 / 点击外部关闭 / Esc 关闭)\n (function initTriView() {\n const btn = document.getElementById('ov-tri-btn');\n const pop = document.getElementById('ov-tri-pop');\n const closeBtn = document.getElementById('ov-tri-close');\n const startBtn = document.getElementById('ov-tri-start');\n const img = document.getElementById('ov-tri-img');\n const statusEl = document.getElementById('ov-tri-status');\n const foot = document.getElementById('ov-tri-foot');\n const history = document.getElementById('ov-tri-history');\n const historyRow = document.getElementById('ov-tri-history-row');\n const historyCount = document.getElementById('ov-tri-history-count');\n if (!btn || !pop || !closeBtn || !startBtn) return;\n\n const versions = []; // [{ ts, label }]\n let previewIdx = -1; // 主图当前正在「预览」哪一版(浏览态)\n let adoptedIdx = -1; // 真正被「采用」的那一版 · 与素材库通过状态联动\n let generating = false;\n\n function prodName() {\n return (document.getElementById('pd-name')?.textContent || '商品').trim();\n }\n\n function refreshAssetFilters() {\n window.ProductDetailFilters?.applyAssets?.();\n }\n\n function open() {\n pop.classList.add('show');\n btn.classList.add('is-open');\n btn.setAttribute('aria-expanded', 'true');\n }\n function close() {\n pop.classList.remove('show');\n btn.classList.remove('is-open');\n btn.setAttribute('aria-expanded', 'false');\n }\n function toggle() {\n if (pop.classList.contains('show')) close(); else open();\n }\n\n function renderHistory() {\n if (versions.length === 0) { history.classList.remove('show'); return; }\n history.classList.add('show');\n historyCount.textContent = versions.length;\n historyRow.innerHTML = versions.map((ver, i) => {\n const isAdopted = i === adoptedIdx;\n const isPreview = i === previewIdx;\n const cls = [\n isAdopted ? 'adopted' : '',\n isPreview && !isAdopted ? 'previewing' : '',\n ].filter(Boolean).join(' ');\n const titleParts = [ver.label, ver.ts];\n if (isAdopted) titleParts.push('已采用');\n else if (isPreview) titleParts.push('预览中');\n return `\n <div class=\"h-thumb ${cls}\" data-idx=\"${i}\" title=\"${titleParts.join(' · ')}\">\n <span class=\"badge\">已采用</span>\n <span class=\"v\">${ver.label}</span>\n </div>\n `;\n }).join('');\n historyRow.querySelectorAll('.h-thumb').forEach(el => {\n el.addEventListener('click', () => {\n const idx = Number(el.dataset.idx);\n if (idx === previewIdx) return;\n setPreview(idx);\n });\n });\n }\n\n function renderMain() {\n if (previewIdx < 0) return;\n const ver = versions[previewIdx];\n const isAdopted = previewIdx === adoptedIdx;\n img.innerHTML = `<span class=\"ph-frame\">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;\n img.classList.add('is-zoomable');\n img.title = '点击放大查看';\n statusEl.textContent = isAdopted\n ? `${ver.label} · 已采用,不满意可重跑`\n : `${ver.label} · 预览中(未采用)`;\n foot.innerHTML = `\n <button class=\"ov-edit\" type=\"button\" id=\"ov-tri-rerun\" style=\"height:28px;\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg>\n 重跑\n </button>\n <button class=\"ov-edit ${isAdopted ? '' : 'primary'}\" type=\"button\" id=\"ov-tri-adopt\" style=\"height:28px;\" ${isAdopted ? 'disabled title=\"此版本已采用\"' : 'title=\"将此版本设为唯一通过版本,其他版本变为不通过\"'}>\n ${isAdopted\n ? '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg> 已采用'\n : '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg> 采用此版本'}\n </button>\n <span style=\"flex:1;\"></span>\n <span class=\"mono\" style=\"font-size:11px; color: var(--black-alpha-56);\">~¥0.30 / 次</span>\n `;\n document.getElementById('ov-tri-rerun')?.addEventListener('click', start);\n document.getElementById('ov-tri-adopt')?.addEventListener('click', adoptPreview);\n }\n\n function syncLibraryStatus() {\n const grid = document.querySelector('.tab-pane[data-pane=\"assets\"] .asset-grid');\n if (!grid) return;\n const adoptedLabel = versions[adoptedIdx]?.label;\n grid.querySelectorAll('.asset-card[data-tri-version]').forEach(c => {\n const pill = c.querySelector('.pill');\n if (!pill) return;\n const isAdopted = c.dataset.triVersion === adoptedLabel;\n pill.className = 'pill ' + (isAdopted ? 'pass' : 'fail');\n pill.textContent = isAdopted ? '通过' : '不通过';\n pill.setAttribute('data-status', isAdopted ? 'pass' : 'fail');\n pill.setAttribute('title', isAdopted ? '当前采用版本' : '未被采用');\n });\n refreshAssetFilters();\n }\n\n function appendLibraryCard(ver) {\n const grid = document.querySelector('.tab-pane[data-pane=\"assets\"] .asset-grid');\n if (!grid) return;\n const now = new Date();\n const pad = n => String(n).padStart(2, '0');\n const dateStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;\n const card = document.createElement('div');\n card.className = 'asset-card';\n card.dataset.triVersion = ver.label;\n // 新生成的版本默认 `不通过`,等用户点「采用此版本」才转为通过\n card.innerHTML = `\n <div class=\"thumb placeholder\"><span class=\"type-pill\">三视图</span><span class=\"ph-frame\">${prodName()} · ${ver.label}</span></div>\n <div class=\"meta\"><span class=\"pill fail\" data-status=\"fail\" title=\"未被采用\">不通过</span><span class=\"date\">${dateStr}</span></div>\n `;\n grid.prepend(card);\n // 更新「全部 AI 素材 (N)」计数\n const ct = document.querySelector('.pd-toolbar .total .ct');\n if (ct) {\n const m = ct.textContent.match(/(\\d+)/);\n const n = m ? Number(m[1]) + 1 : 1;\n ct.textContent = `(${n})`;\n }\n refreshAssetFilters();\n }\n\n // 切换主图预览(不动采用状态、不动素材库)\n function setPreview(idx) {\n previewIdx = idx;\n renderHistory();\n renderMain();\n }\n\n // 显式「采用」当前预览版本 · 同步素材库通过/不通过\n function adoptPreview() {\n if (previewIdx < 0) return;\n if (previewIdx === adoptedIdx) return;\n adoptedIdx = previewIdx;\n renderHistory();\n renderMain();\n syncLibraryStatus();\n if (window.Shell?.toast) {\n Shell.toast('已采用 ' + versions[adoptedIdx].label,\n prodName() + ' · 该版本通过,其余版本转为不通过');\n }\n }\n\n function renderLoading() {\n 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>`;\n img.classList.remove('is-zoomable');\n img.removeAttribute('title');\n statusEl.textContent = '生成中 · 约 12s';\n foot.innerHTML = '<span class=\"mono\" style=\"font-size:11px; color: var(--black-alpha-48);\">// POST /assets/tri-view</span>';\n }\n\n function openLightbox() {\n if (previewIdx < 0) return;\n const ver = versions[previewIdx];\n const isAdopted = previewIdx === adoptedIdx;\n const lbImg = document.getElementById('ov-tri-lightbox-img');\n const lbLabel = document.getElementById('ov-tri-lightbox-label');\n const lbTag = document.getElementById('ov-tri-lightbox-tag');\n const lbMeta = document.getElementById('ov-tri-lightbox-meta');\n if (lbImg) lbImg.innerHTML = `<span class=\"ph-frame\">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;\n if (lbLabel) lbLabel.textContent = ver.label;\n if (lbTag) {\n lbTag.hidden = !isAdopted;\n lbTag.textContent = '已采用';\n }\n if (lbMeta) lbMeta.textContent = `// 生成于 ${ver.ts}`;\n window.Shell?.openModal?.('ov-tri-lightbox-bg');\n }\n\n function start() {\n if (generating) return;\n generating = true;\n open();\n renderLoading();\n setTimeout(() => {\n generating = false;\n const now = new Date();\n const ts = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');\n const newVer = { ts, label: 'v' + (versions.length + 1) };\n versions.push(newVer);\n appendLibraryCard(newVer);\n const newIdx = versions.length - 1;\n previewIdx = newIdx;\n // 第一次生成 · 自动采用新版本(无选择可言);之后只切预览,不动采用\n if (adoptedIdx === -1) {\n adoptedIdx = newIdx;\n syncLibraryStatus();\n }\n renderHistory();\n renderMain();\n if (window.Shell?.toast) {\n const tip = (adoptedIdx === newIdx)\n ? `${newVer.label} · 已采用并同步到素材库`\n : `${newVer.label} · 预览中 · 满意请点「采用此版本」`;\n Shell.toast('三视图已生成', `${prodName()} · ${tip}`);\n }\n }, 1800);\n }\n\n btn.addEventListener('click', (e) => { e.stopPropagation(); toggle(); });\n closeBtn.addEventListener('click', (e) => { e.stopPropagation(); close(); });\n startBtn.addEventListener('click', (e) => { e.stopPropagation(); start(); });\n pop.addEventListener('click', (e) => e.stopPropagation());\n img.addEventListener('click', (e) => {\n if (!img.classList.contains('is-zoomable')) return;\n e.stopPropagation();\n openLightbox();\n });\n document.addEventListener('click', (e) => {\n if (!pop.classList.contains('show')) return;\n if (pop.contains(e.target) || btn.contains(e.target)) return;\n close();\n });\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && pop.classList.contains('show')) close();\n });\n })();\n\n // 从 create 跳来时显示 toast + 清空三个 tab 数据(新商品没有素材/项目/任务)\n if (location.search.includes('id=new')) {\n setTimeout(() => Shell.toast('商品已创建', '开始创建 AI 资产'), 200);\n\n // 从 sessionStorage 读出 drawer 刚保存的完整 product,注入到「商品信息」卡\n (function injectFromSession() {\n let p = null;\n try {\n const raw = sessionStorage.getItem('npd-last-created');\n if (raw) p = JSON.parse(raw);\n sessionStorage.removeItem('npd-last-created'); // 读完即清,避免污染\n } catch (e) { /* ignore */ }\n if (!p) return;\n\n const esc = s => String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n\n // 商品名称(URL 已经设过,这里兜底)\n if (p.name) {\n const nm = document.querySelector('[data-field=\"name\"] .v-static');\n if (nm) nm.textContent = p.name;\n const nmI = document.querySelector('[data-field=\"name\"] .v-input');\n if (nmI) nmI.value = p.name;\n }\n // 品类\n if (p.cat) {\n const catRow = document.querySelector('[data-field=\"cat\"] .v-static');\n if (catRow) catRow.textContent = p.cat;\n const catSel = document.querySelector('[data-field=\"cat\"] .v-select');\n if (catSel) {\n // 找到匹配 option 并 select;若没有则插入到首位\n let matched = false;\n [...catSel.options].forEach(o => { if (o.value === p.cat || o.textContent === p.cat) { o.selected = true; matched = true; } });\n if (!matched) {\n const opt = document.createElement('option');\n opt.value = p.cat; opt.textContent = p.cat; opt.selected = true;\n catSel.insertBefore(opt, catSel.firstChild);\n }\n }\n }\n // 目标人群(可空)\n const tgtRow = document.querySelector('[data-field=\"target\"] .v-static');\n const tgtIn = document.querySelector('[data-field=\"target\"] .v-input');\n if (tgtRow) tgtRow.textContent = p.target || '—';\n if (tgtIn) tgtIn.value = p.target || '';\n // 卖点\n if (Array.isArray(p.points)) {\n const blStatic = document.querySelector('[data-field=\"bullets\"] .v-static');\n if (blStatic) {\n blStatic.innerHTML = p.points.length\n ? p.points.map(t => `<span class=\"bullet\">${esc(t)}</span>`).join('')\n : '<span class=\"bullet\" style=\"color:var(--black-alpha-48)\">—</span>';\n }\n }\n // 商品图片 — 替换 6 张占位为真实 dataUrl;数量按上传数定\n const grid = document.getElementById('ov-images-grid');\n const addBtn = document.getElementById('ov-img-add');\n if (grid) {\n // 移除现有所有 .thumb 占位(保留末尾 #ov-img-add)\n [...grid.querySelectorAll('.thumb')].forEach(t => t.remove());\n if (Array.isArray(p.images) && p.images.length) {\n p.images.forEach(img => {\n const t = document.createElement('div');\n t.className = 'thumb';\n t.style.cssText = 'background-image:url(' + img.dataUrl + ');background-size:cover;background-position:center;';\n if (addBtn) grid.insertBefore(t, addBtn);\n else grid.appendChild(t);\n });\n }\n // 同步计数\n const ct = document.querySelector('.ov-images-sub .sub-h .ct');\n if (ct) ct.textContent = '(' + ((p.images || []).length) + ')';\n }\n })();\n\n const EMPTY_HTML = (txt) => `<div class=\"empty-filter\">// NO DATA<br><span style=\"margin-top:6px;display:inline-block\">${txt}</span></div>`;\n\n // AI 生成素材\n const assetsPane = document.querySelector('.tab-pane[data-pane=\"assets\"]');\n if (assetsPane) {\n const grid = assetsPane.querySelector('.asset-grid');\n if (grid) grid.outerHTML = EMPTY_HTML('还没有 AI 素材,使用右上角「图片生成」开始创建');\n const ct = assetsPane.querySelector('.total .ct');\n if (ct) ct.textContent = '(0)';\n const more = assetsPane.querySelector('.pd-more');\n if (more) more.remove();\n }\n\n // 视频项目\n const videosPane = document.querySelector('.tab-pane[data-pane=\"videos\"]');\n if (videosPane) {\n const grid = videosPane.querySelector('.asset-grid');\n if (grid) grid.outerHTML = EMPTY_HTML('还没有视频项目,前往工作台「新建项目」开始');\n const ct = videosPane.querySelector('.total .ct');\n if (ct) ct.textContent = '(0)';\n const more = videosPane.querySelector('.pd-more');\n if (more) more.remove();\n }\n\n // 任务记录\n const tasksPane = document.querySelector('.tab-pane[data-pane=\"tasks\"]');\n if (tasksPane) {\n tasksPane.querySelectorAll('.task-stat .v').forEach(el => {\n const small = el.querySelector('small');\n el.textContent = '0';\n if (small) el.appendChild(small);\n });\n const tbl = tasksPane.querySelector('.task-table');\n if (tbl) tbl.outerHTML = EMPTY_HTML('暂无任务记录');\n const ct = tasksPane.querySelector('.total .ct');\n if (ct) ct.textContent = '(0)';\n const more = tasksPane.querySelector('.pd-more');\n if (more) more.remove();\n }\n }\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"products": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"products.html\">\n<meta charset=\"utf-8\">\n<title>商品库 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── 全局 viewport 高度链 (让右侧 toolbar/分页吸顶吸底) ─── */\n /* 整页滚动 · 头部 H1+actions sticky 固定 · 其他随页面滚 */\n .page-head {\n position: sticky;\n top: 0;\n z-index: 5;\n background: var(--background-base);\n padding-top: 4px;\n margin-top: -4px;\n }\n\n /* ─── 主区 (普通文档流) ─── */\n .products-main { display: flex; flex-direction: column; }\n .products-main .result-meta {\n margin-bottom: 12px;\n display: flex;\n align-items: center;\n gap: 10px;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n }\n\n /* ─── 商品分类多选 chip · menu 内多选项 + 计数 + 全选/全清 ─── */\n .chip-wrap[data-key=\"cat\"] .chip-menu {\n min-width: 220px;\n padding: 4px;\n }\n .chip-wrap[data-key=\"cat\"] .mi-all {\n border-bottom: 1px solid var(--border-faint);\n margin-bottom: 4px;\n padding-bottom: 4px;\n }\n .chip-wrap[data-key=\"cat\"] .mi .cat-count {\n margin-left: auto;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n .chip-wrap[data-key=\"cat\"] .mi.selected .cat-count { color: var(--heat); }\n .chip .chip-count {\n display: inline-flex; align-items: center; justify-content: center;\n height: 18px; min-width: 18px; padding: 0 5px;\n background: var(--heat-12); color: var(--heat);\n border: 1px solid var(--heat-20);\n border-radius: var(--r-pill);\n font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;\n letter-spacing: .02em;\n margin-left: 2px;\n }\n .product-grid-wrap {\n /* 滚动区与外栏视觉对齐 */\n margin: 0 -8px;\n padding: 2px 8px 24px;\n }\n\n /* ─── 生成类型选择 modal ─── */\n .gen-choice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n .gen-choice-card {\n display: flex; flex-direction: column; gap: 10px;\n padding: 18px 16px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n cursor: pointer;\n text-align: left;\n font-family: inherit;\n transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);\n }\n .gen-choice-card:hover {\n border-color: var(--heat);\n background: var(--surface);\n transform: translateY(-1px);\n }\n .gen-choice-card .gc-ic {\n width: 36px; height: 36px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n display: grid; place-items: center;\n color: var(--black-alpha-72);\n }\n .gen-choice-card:hover .gc-ic { color: var(--heat); border-color: var(--heat-20); background: var(--heat-12); }\n .gen-choice-card .gc-ic svg { width: 18px; height: 18px; }\n .gen-choice-card .gc-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }\n .gen-choice-card .gc-d { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.45; }\n\n .product-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n gap: 16px;\n }\n .product-card {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n cursor: pointer;\n transition: background .15s, border-color .15s;\n position: relative;\n overflow: hidden;\n display: flex; flex-direction: column;\n }\n .product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }\n .product-thumb { aspect-ratio: 1.4 / 1; }\n\n .product-body {\n padding: 14px 14px 12px;\n flex: 1;\n }\n .product-name {\n font-size: 14px; font-weight: 600;\n color: var(--accent-black);\n line-height: 1.3;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .product-cat {\n display: inline-flex; align-items: center;\n margin-top: 8px;\n padding: 2px 8px;\n background: var(--background-lighter);\n color: var(--black-alpha-72);\n border-radius: var(--r-sm);\n font-size: 11.5px;\n }\n .product-date {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--black-alpha-48);\n margin-top: 10px;\n letter-spacing: .02em;\n }\n\n /* ─── 卡片底栏 · V2.1 克制版 (线图标 + mono 文本) ─── */\n .product-footer {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n column-gap: 8px;\n padding: 10px 12px;\n border-top: 1px solid var(--border-faint);\n font-size: 11.5px;\n color: #6f6f6f;\n background: var(--background-base);\n }\n .product-footer .stat {\n display: inline-flex; align-items: center; justify-content: center; gap: 5px;\n padding: 3px 8px;\n border-radius: var(--r-sm);\n font-family: var(--font-mono);\n letter-spacing: .02em;\n white-space: nowrap; /* 防止\"素材\"等中文被挤压成竖排 */\n border: 1px solid transparent;\n transition: background var(--t-base), color var(--t-base), border-color var(--t-base);\n }\n .product-footer .stat { justify-self: center; }\n .product-footer .stat[data-type] { cursor: pointer; }\n .product-footer .stat[data-type]:hover {\n background: var(--heat-12);\n color: var(--heat);\n border-color: var(--heat-20);\n }\n .product-footer .stat[data-type]:hover svg { color: var(--heat); }\n .product-footer .stat[data-type]:hover b { color: var(--heat); }\n .product-footer .stat svg {\n width: 14px; height: 14px;\n color: currentColor;\n flex-shrink: 0;\n stroke-width: 1.25;\n transition: color var(--t-base);\n }\n .product-footer .stat b {\n color: var(--accent-black);\n font-weight: 600;\n transition: color var(--t-base);\n }\n /* default ↔ hover 文本切换 */\n .product-footer .stat .stat-hover { display: none; }\n .product-footer .stat[data-type]:hover > svg { display: none; }\n .product-footer .stat[data-type]:hover .stat-default { display: none; }\n .product-footer .stat[data-type]:hover .stat-hover { display: inline; }\n .product-footer .sep {\n color: #b8b8b8;\n font-family: var(--font-mono);\n flex-shrink: 0;\n }\n\n /* ─── 底部分页 (吸底) ─── */\n .pagination {\n flex-shrink: 0;\n display: flex; align-items: center; gap: 16px;\n padding: 14px 0;\n border-top: 1px solid var(--border-faint);\n background: var(--background-base);\n font-size: 12.5px;\n color: var(--black-alpha-56);\n }\n .pagination[hidden] { display: none; }\n .pagination .page-size { transition: border-color var(--t-base), color var(--t-base); }\n .pagination .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pagination .pages .ellipsis {\n min-width: 22px; height: 30px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--black-alpha-48);\n font-family: var(--font-mono);\n }\n .pagination .total { font-family: var(--font-mono); letter-spacing: .02em; }\n .pagination .page-size {\n display: inline-flex; align-items: center; gap: 4px;\n height: 30px;\n padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-family: inherit;\n font-size: 12.5px;\n color: var(--black-alpha-72);\n }\n .pagination .page-size svg { width: 10px; height: 10px; opacity: .6; }\n .pagination .pages {\n display: inline-flex; gap: 4px;\n margin-left: auto;\n }\n .pagination .pages button {\n min-width: 30px; height: 30px;\n padding: 0 8px;\n border: 1px solid var(--border-faint);\n background: var(--surface);\n border-radius: var(--r-sm);\n cursor: pointer;\n font-size: 12.5px;\n color: var(--black-alpha-72);\n font-family: inherit;\n transition: background var(--t-base), border-color var(--t-base), color var(--t-base);\n }\n .pagination .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pagination .pages button.active {\n background: var(--heat);\n color: var(--accent-white);\n border-color: var(--heat);\n font-weight: 600;\n }\n .pagination .pages button:disabled { opacity: .4; cursor: not-allowed; }\n .pagination .jump {\n display: inline-flex; align-items: center; gap: 6px;\n color: var(--black-alpha-56);\n }\n .pagination .jump input {\n width: 44px; height: 30px;\n border: 1px solid var(--border-faint);\n background: var(--surface);\n border-radius: var(--r-sm);\n text-align: center;\n font-size: 12.5px;\n color: var(--accent-black);\n font-family: inherit;\n outline: none;\n transition: border-color var(--t-base);\n }\n .pagination .jump input:focus { border-color: var(--heat-40); }\n\n /* 响应式 */\n @media (max-width: 900px) {\n .toolbar .search-inline { max-width: 100%; flex-basis: 100%; }\n }\n\n /* ============================================================\n 新建商品 drawer · 复用 .drawer (restraint.css)\n 在 products.html 上原地打开, 后面保留商品网格作为上下文\n ============================================================ */\n .pc-drawer { width: 820px; max-width: 100vw; }\n .pc-drawer .drawer-h h3 { font-size: 16px; font-weight: 600; }\n .pc-drawer .drawer-b { padding: 24px 28px; }\n .pc-drawer .drawer-b .form-card {\n background: transparent; border: 0; padding: 0; border-radius: 0;\n }\n .pc-drawer .drawer-f {\n padding: 14px 24px; background: var(--surface); align-items: center;\n }\n .pc-drawer .drawer-f .btn-guide {\n margin-right: auto;\n display: inline-flex; align-items: center; gap: 6px;\n font-size: 13px; color: var(--black-alpha-56);\n background: transparent; border: 0; cursor: pointer;\n padding: 8px 10px; border-radius: var(--r-md);\n font-family: inherit;\n transition: background var(--t-base), color var(--t-base);\n }\n .pc-drawer .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }\n .pc-drawer .drawer-f .btn-guide svg { width: 14px; height: 14px; }\n\n /* form-card · 表单容器(drawer 内被去外观,直接铺) */\n .form-card .form-h {\n font-size: 15px; font-weight: 600; color: var(--accent-black);\n margin-bottom: 18px; padding-bottom: 12px;\n border-bottom: 1px solid var(--border-faint);\n }\n .form-card .field { margin-bottom: 16px; }\n .form-card .field:last-child { margin-bottom: 0; }\n .form-card .field-row {\n display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px;\n }\n .form-card .field-label {\n display: block; font-size: 13px; font-weight: 500;\n color: var(--accent-black); margin-bottom: 6px;\n }\n .form-card .field-label .req { color: var(--heat); margin-left: 2px; }\n .form-card .field-label .opt {\n color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;\n }\n .form-card .input,\n .form-card .select {\n width: 100%; height: 38px;\n background: var(--background-lighter);\n border: 1px solid var(--black-alpha-12);\n border-radius: var(--r-md);\n padding: 0 14px;\n font-size: 13.5px; color: var(--accent-black);\n outline: none; font-family: inherit;\n transition: border-color var(--t-base);\n }\n .form-card .input:focus,\n .form-card .select:focus {\n border-color: var(--heat-40);\n box-shadow: inset 0 0 0 1px var(--heat-40);\n }\n\n /* 商品主图 · 上传(左) + 示例(右) */\n .form-card .pf-upload-row {\n display: grid;\n grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);\n gap: 16px; align-items: stretch;\n }\n .form-card .pf-upload-zone {\n border: 1.5px dashed var(--black-alpha-24);\n border-radius: var(--r-md);\n padding: 28px 20px;\n background: var(--background-lighter);\n cursor: pointer; text-align: center;\n transition: border-color var(--t-base), background var(--t-base);\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n min-height: 180px;\n }\n .form-card .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }\n .form-card .pf-upload-zone .uz-ic {\n width: 44px; height: 44px;\n margin: 0 auto 10px;\n background: var(--surface);\n border: 1px solid var(--heat-20);\n border-radius: var(--r-md);\n color: var(--heat);\n display: grid; place-items: center;\n }\n .form-card .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }\n .form-card .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }\n .form-card .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }\n .form-card .pf-upload-zone .uz-d {\n margin-top: 8px;\n font-family: var(--font-mono); font-size: 11.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n }\n /* 示例图 · 纵向卡片 */\n .form-card .pf-example {\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n padding: 16px;\n display: flex; flex-direction: column; gap: 10px;\n }\n .form-card .pf-example .ex-h {\n font-size: 13px; font-weight: 600; color: var(--accent-black);\n }\n .form-card .pf-example .ex-grid {\n display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;\n }\n .form-card .pf-example .ex-grid .ex-thumb {\n aspect-ratio: 1;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n overflow: hidden; position: relative;\n display: grid; place-items: center;\n color: var(--black-alpha-32);\n }\n .form-card .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }\n .form-card .pf-example .ex-grid .ex-thumb::after {\n content: ''; position: absolute; inset: 0;\n background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);\n pointer-events: none;\n }\n .form-card .pf-example .ex-d {\n font-size: 12px; color: var(--black-alpha-56); line-height: 1.5;\n }\n\n .form-card .pf-grid {\n display: grid; grid-template-columns: repeat(5, 1fr);\n gap: 8px; margin-top: 12px;\n }\n .form-card .pf-grid:empty { display: none; }\n .form-card .pf-thumb {\n aspect-ratio: 1;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n position: relative; overflow: hidden; cursor: pointer;\n }\n .form-card .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }\n .form-card .pf-thumb .pf-x {\n position: absolute; top: 4px; right: 4px;\n width: 22px; height: 22px;\n background: rgba(0,0,0,.7); color: var(--accent-white);\n border: 0; border-radius: 50%; cursor: pointer;\n display: grid; place-items: center;\n opacity: 0; transition: opacity var(--t-base);\n }\n .form-card .pf-thumb:hover .pf-x { opacity: 1; }\n .form-card .pf-thumb .pf-x svg { width: 11px; height: 11px; }\n\n /* 核心卖点 · bullet-list */\n .form-card .bullet-list { list-style: none; padding: 0; margin: 0; }\n .form-card .bullet-list .bl-item,\n .form-card .bullet-list .bl-add {\n display: flex; align-items: center; gap: 10px;\n padding: 8px 12px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n margin-bottom: 6px;\n font-size: 13.5px;\n }\n .form-card .bullet-list .bl-add { background: transparent; border-style: dashed; }\n .form-card .bullet-list .num {\n width: 22px; height: 22px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n font-family: var(--font-mono);\n font-size: 11px; color: var(--heat); font-weight: 700;\n display: grid; place-items: center; flex-shrink: 0;\n }\n .form-card .bullet-list .bl-text { flex: 1; color: var(--accent-black); }\n .form-card .bullet-list .bl-input {\n flex: 1; background: transparent; border: 0; outline: none;\n font-size: 13.5px; color: var(--accent-black); font-family: inherit;\n }\n .form-card .bullet-list .bl-x {\n width: 22px; height: 22px;\n color: var(--black-alpha-48);\n cursor: pointer; display: grid; place-items: center;\n border-radius: var(--r-sm);\n transition: color var(--t-base), background var(--t-base);\n }\n .form-card .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }\n .form-card .bullet-list .bl-x svg { width: 11px; height: 11px; }\n @media (max-width: 900px) {\n .pc-drawer .drawer-b .pf-upload-row { grid-template-columns: 1fr; }\n }\n\n /* ============================================================\n 视图切换 · 网格 / 列表\n ============================================================ */\n .view-tog {\n display: inline-flex;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n padding: 2px;\n flex-shrink: 0;\n }\n .view-tog button {\n width: 30px; height: 28px;\n display: grid; place-items: center;\n border: 0;\n background: transparent;\n color: var(--black-alpha-48);\n cursor: pointer;\n border-radius: 4px;\n transition: background var(--t-base), color var(--t-base);\n }\n .view-tog button:hover { color: var(--accent-black); }\n .view-tog button.active {\n background: var(--accent-black);\n color: var(--accent-white);\n }\n .view-tog button svg { width: 13px; height: 13px; }\n\n /* 列表视图: 把网格改为单列, 每行横向布局 */\n .product-grid.list-view { display: flex; flex-direction: column; gap: 8px; }\n .product-grid.list-view .product-card {\n display: grid;\n grid-template-columns: 100px 1fr auto;\n gap: 16px;\n padding: 12px 16px;\n align-items: center;\n }\n .product-grid.list-view .product-thumb {\n width: 100px; height: 70px; aspect-ratio: auto;\n margin: 0; border-radius: var(--r-sm);\n }\n .product-grid.list-view .product-body {\n padding: 0;\n display: flex; align-items: center; gap: 14px;\n }\n .product-grid.list-view .product-name { font-size: 14px; }\n .product-grid.list-view .product-cat,\n .product-grid.list-view .product-date {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--black-alpha-48);\n letter-spacing: .02em;\n margin-top: 0;\n }\n .product-grid.list-view .product-footer {\n border-top: 0;\n padding: 0;\n background: transparent;\n gap: 6px;\n }\n\n /* ============================================================\n 批量编辑模式\n ============================================================ */\n .product-card .card-check {\n position: absolute;\n top: 10px; left: 10px;\n width: 22px; height: 22px;\n border-radius: 50%;\n background: var(--surface);\n border: 2px solid var(--black-alpha-32);\n display: none;\n place-items: center;\n color: var(--accent-white);\n z-index: 5;\n pointer-events: none;\n }\n .product-card .card-check svg { width: 11px; height: 11px; opacity: 0; }\n body.edit-mode .product-card { cursor: pointer; }\n body.edit-mode .product-card .card-check { display: grid; }\n body.edit-mode .product-card.selected .card-check {\n background: var(--heat);\n border-color: var(--heat);\n }\n body.edit-mode .product-card.selected .card-check svg { opacity: 1; }\n body.edit-mode .product-card.selected {\n border-color: var(--heat);\n box-shadow: 0 0 0 1px var(--heat) inset;\n }\n /* 编辑模式下,卡片底部 stat 和 more-btn 静默 (不能点击跳转) */\n body.edit-mode .product-footer .stat,\n body.edit-mode .product-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; }\n\n /* 浮动 action bar */\n .bulk-bar {\n position: fixed;\n bottom: 24px;\n left: 50%;\n transform: translateX(-50%);\n background: var(--accent-black);\n color: var(--accent-white);\n border-radius: var(--r-md);\n padding: 10px 14px 10px 18px;\n display: none;\n align-items: center;\n gap: 16px;\n box-shadow: 0 8px 24px rgba(0,0,0,.18);\n z-index: 100;\n font-size: 13px;\n }\n body.edit-mode .bulk-bar { display: inline-flex; }\n .bulk-bar .ct { font-family: var(--font-mono); letter-spacing: .02em; }\n .bulk-bar .ct b { color: var(--heat); font-weight: 700; padding: 0 3px; }\n .bulk-bar .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); }\n .bulk-bar button {\n height: 30px;\n padding: 0 12px;\n background: transparent;\n border: 1px solid rgba(255,255,255,.24);\n border-radius: var(--r-sm);\n color: var(--accent-white);\n font-size: 12.5px;\n font-family: inherit;\n cursor: pointer;\n display: inline-flex; align-items: center; gap: 5px;\n transition: background var(--t-base), border-color var(--t-base);\n }\n .bulk-bar button:hover { background: rgba(255,255,255,.08); }\n .bulk-bar button.danger {\n background: var(--accent-crimson, #c43d3d);\n border-color: var(--accent-crimson, #c43d3d);\n }\n .bulk-bar button.danger:hover { filter: brightness(1.06); }\n .bulk-bar button svg { width: 12px; height: 12px; }\n .bulk-bar .clear-sel {\n color: rgba(255,255,255,.6);\n font-size: 12px;\n cursor: pointer;\n background: none;\n border: 0;\n padding: 4px 6px;\n }\n .bulk-bar .clear-sel:hover { color: var(--accent-white); }\n\n /* edit-mode 下「编辑商品」按钮变成「完成」 */\n .btn-edit-toggle.active {\n background: var(--accent-black);\n color: var(--accent-white);\n border-color: var(--accent-black);\n }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>商品库</h1>\n <div class=\"sub\"><span class=\"mono\">// <span id=\"sku-count\">0</span> SKU</span> · 商品信息会作为脚本和资产生成的素材</div>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-edit-toggle\" type=\"button\" id=\"edit-toggle-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m3 7 2 2 4-4\"/><path d=\"m3 17 2 2 4-4\"/><path d=\"M13 6h8\"/><path d=\"M13 12h8\"/><path d=\"M13 18h8\"/></svg>\n <span class=\"btn-edit-label\">管理商品</span>\n </button>\n <button class=\"btn btn-primary btn-create\" type=\"button\" id=\"open-new-product\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22V12\"/><path d=\"M16 17h6\"/><path d=\"M19 14v6\"/><path d=\"M21 10.5V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l1.7-1\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"m7.5 4.3 9 5.1\"/></svg>\n 新建商品\n </button>\n </div>\n</div>\n\n<!-- ===== 主区 (三段式) ===== -->\n<div class=\"products-main\">\n\n <!-- 顶部固定: toolbar + meta -->\n <div class=\"toolbar\">\n <div class=\"search-inline\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input class=\"input\" id=\"search-input\" placeholder=\"搜索商品名称、品牌\">\n </div>\n <div class=\"chip-wrap\" data-key=\"cat\">\n <button class=\"chip\" type=\"button\">\n <span class=\"chip-label\">商品分类</span>\n <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"chip-menu\"></div>\n </div>\n <div class=\"chip-wrap\" data-key=\"date\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">创建时间</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <button class=\"clear-filters\" id=\"clear-filters\" type=\"button\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n 清空筛选\n </button>\n </div>\n\n <div class=\"result-meta\" id=\"result-meta\">\n <span>// 显示 <span class=\"count\">7</span> / 7 个商品</span>\n <div class=\"view-tog\" style=\"margin-left:auto\" id=\"view-tog\">\n <button type=\"button\" class=\"active\" data-view=\"grid\" title=\"网格视图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><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\"/><rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"/></svg>\n </button>\n <button type=\"button\" data-view=\"list\" title=\"列表视图\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 6h18M3 12h18M3 18h18\"/></svg>\n </button>\n </div>\n </div>\n\n <!-- 中间滚动: 商品网格 -->\n <div class=\"product-grid-wrap\">\n <div class=\"product-grid\" id=\"product-grid\">\n <div class=\"product-card\" data-cat=\"美妆个护\" data-name=\"透真玻尿酸补水面膜\" data-tags=\"熬夜党 敏感肌\" data-added=\"1\" data-assets=\"124\" data-videos=\"36\" data-date=\"2026-05-15\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">补水面膜 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">透真玻尿酸补水面膜</div>\n <div class=\"product-cat\">美妆个护</div>\n <div class=\"product-date\">2026-05-15 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>124</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>36</b>\n </span>\n \n </div>\n </div>\n\n <div class=\"product-card\" data-cat=\"数码 3C\" data-name=\"南卡 Lite Pro 蓝牙耳机\" data-tags=\"通勤 运动\" data-added=\"2\" data-assets=\"96\" data-videos=\"28\" data-date=\"2026-05-12\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">蓝牙耳机 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">南卡 Lite Pro 蓝牙耳机</div>\n <div class=\"product-cat\">数码 3C</div>\n <div class=\"product-date\">2026-05-12 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>96</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>28</b>\n </span>\n \n </div>\n </div>\n\n <div class=\"product-card\" data-cat=\"食品饮料\" data-name=\"滋啦速食牛肉面 6 桶装\" data-tags=\"加班 独居\" data-added=\"3\" data-assets=\"96\" data-videos=\"24\" data-date=\"2026-05-10\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">速食牛肉面 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">滋啦速食牛肉面 · 6 桶装</div>\n <div class=\"product-cat\">食品饮料</div>\n <div class=\"product-date\">2026-05-10 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>96</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>24</b>\n </span>\n \n </div>\n </div>\n\n <div class=\"product-card\" data-cat=\"美妆个护\" data-name=\"透真清透物理防晒霜\" data-tags=\"SPF50 通勤\" data-added=\"4\" data-assets=\"76\" data-videos=\"18\" data-date=\"2026-05-08\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">防晒霜 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">透真清透物理防晒霜</div>\n <div class=\"product-cat\">美妆个护</div>\n <div class=\"product-date\">2026-05-08 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>76</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>18</b>\n </span>\n \n </div>\n </div>\n\n <div class=\"product-card\" data-cat=\"食品饮料\" data-name=\"三顿半同款冻干咖啡粉\" data-tags=\"提神 早八\" data-added=\"5\" data-assets=\"68\" data-videos=\"21\" data-date=\"2026-05-05\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">咖啡冻干粉 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">三顿半同款冻干咖啡粉</div>\n <div class=\"product-cat\">食品饮料</div>\n <div class=\"product-date\">2026-05-05 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>68</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>21</b>\n </span>\n \n </div>\n </div>\n\n <div class=\"product-card\" data-cat=\"家居家电\" data-name=\"小熊 4L 可视空气炸锅\" data-tags=\"小户型 健康\" data-added=\"6\" data-assets=\"54\" data-videos=\"16\" data-date=\"2026-05-03\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">空气炸锅 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">小熊 4L 可视空气炸锅</div>\n <div class=\"product-cat\">家居家电</div>\n <div class=\"product-date\">2026-05-03 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>54</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>16</b>\n </span>\n \n </div>\n </div>\n\n <div class=\"product-card\" data-cat=\"运动户外\" data-name=\"露露同款裸感瑜伽裤\" data-tags=\"健身房 通勤\" data-added=\"7\" data-assets=\"42\" data-videos=\"12\" data-date=\"2026-04-30\" onclick=\"location.href='product-detail.html?t='+Date.now()\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">瑜伽裤 · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">露露同款裸感瑜伽裤</div>\n <div class=\"product-cat\">运动户外</div>\n <div class=\"product-date\">2026-04-30 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>42</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>12</b>\n </span>\n \n </div>\n </div>\n </div>\n\n <div class=\"empty-state\" id=\"empty\">\n <div class=\"ic-empty\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </div>\n <h3>没有匹配的商品</h3>\n <p>// 试试切换分类或修改搜索词</p>\n </div>\n </div>\n\n <!-- 底部固定: 分页 -->\n <div class=\"pagination\" id=\"pagination\" hidden>\n <span class=\"total\">共 <b id=\"page-total\">0</b> 条</span>\n <button class=\"page-size\" type=\"button\" id=\"page-size-btn\" title=\"切换每页条数\">\n <span id=\"page-size-label\">12 条/页</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <span class=\"pages\" id=\"page-list\"></span>\n <span class=\"jump\">跳至 <input type=\"number\" min=\"1\" value=\"1\" id=\"page-jump\"> 页</span>\n </div>\n\n</div><!-- /.products-main -->\n\n</div><!-- /#page -->\n\n<!-- ============================================================\n 新建商品 · 右侧 Drawer · 在商品库页面原地打开\n ============================================================ -->\n<div class=\"drawer-bg\" id=\"pc-drawer-bg\"></div>\n<aside class=\"drawer pc-drawer\" id=\"pc-drawer\" role=\"dialog\" aria-label=\"新建商品\" aria-hidden=\"true\">\n <div class=\"drawer-h\">\n <h3>新建商品</h3>\n <button class=\"x\" type=\"button\" id=\"pc-drawer-close\" aria-label=\"关闭\">\n <svg width=\"14\" height=\"14\" 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>\n </button>\n </div>\n\n <div class=\"drawer-b\">\n <div class=\"form-card\">\n <div class=\"field\">\n <label class=\"field-label\">商品名称<span class=\"req\">*</span></label>\n <input class=\"input\" id=\"pf-name\" placeholder=\"请输入商品名称(必填)\" maxlength=\"100\">\n </div>\n\n <div class=\"field-row\">\n <div>\n <label class=\"field-label\">品类<span class=\"req\">*</span></label>\n <select class=\"select\" id=\"pf-cat\">\n <option>美妆个护</option>\n <option>服饰内衣</option>\n <option>食品饮料</option>\n <option>家居家电</option>\n <option>数码 3C</option>\n <option>个护清洁</option>\n <option>运动户外</option>\n <option>母婴亲子</option>\n </select>\n </div>\n <div>\n <label class=\"field-label\">目标人群<span class=\"opt\">(选填)</span></label>\n <input class=\"input\" id=\"pf-target\" placeholder=\"例: 22-32 岁女性、敏感肌、办公室通勤\">\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\">商品主图<span class=\"req\">*</span></label>\n <input type=\"file\" id=\"pf-file\" accept=\"image/*\" multiple hidden>\n <div class=\"pf-upload-row\">\n <div class=\"pf-upload-zone\" id=\"pf-zone\">\n <div class=\"uz-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12\"/></svg>\n </div>\n <div class=\"uz-t\">点击上传或<strong>拖拽图片</strong>到此处</div>\n <div class=\"uz-d\">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>\n </div>\n <div class=\"pf-example\">\n <div class=\"ex-h\">示例图</div>\n <div class=\"ex-grid\">\n <div class=\"ex-thumb\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M7 4h10l1 4v12H6V8l1-4z\"/><path d=\"M9 4v3M15 4v3M9 11h6M9 14h6\"/></svg></div>\n <div class=\"ex-thumb\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"6\" y=\"5\" width=\"12\" height=\"15\" rx=\"2\"/><path d=\"M9 9h6M9 12h6M9 15h4\"/></svg></div>\n <div class=\"ex-thumb\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 3h8l1 5v12H7V8l1-5z\"/><circle cx=\"12\" cy=\"13\" r=\"2.5\"/></svg></div>\n </div>\n <div class=\"ex-d\">优质的商品图有助于生成更好的素材效果</div>\n </div>\n </div>\n <div class=\"pf-grid\" id=\"pf-grid\"></div>\n </div>\n\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">核心卖点<span class=\"req\">*</span></label>\n <ul class=\"bullet-list\" id=\"pf-bullets\" data-bl>\n <li class=\"bl-add\"><span class=\"num\">+</span><input class=\"bl-input\" placeholder=\"添加新卖点 · 回车确认\"></li>\n </ul>\n </div>\n </div>\n </div>\n\n <div class=\"drawer-f\">\n <button class=\"btn-guide\" type=\"button\" onclick=\"Shell.toast('使用指南', '点击查看完整填写指南')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01\"/></svg>\n 使用指南\n </button>\n <button class=\"btn\" type=\"button\" id=\"pc-cancel-btn\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"pc-save-btn\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n 创建商品\n </button>\n </div>\n</aside>\n\n<!-- ===== 生成类型选择 modal ===== -->\n<div class=\"modal-bg\" id=\"gen-choice-bg\">\n <div class=\"modal\" role=\"dialog\" aria-labelledby=\"gen-choice-ti\" style=\"max-width:560px\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"background:var(--heat-12);color:var(--heat)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83\"/></svg>\n </div>\n <div class=\"ti\" id=\"gen-choice-ti\"><span id=\"gen-choice-target\">—</span><span>// PICK ONE</span></div>\n </div>\n <div class=\"modal-b\" style=\"padding-top:12px\">\n <div class=\"gen-choice-grid\">\n <button type=\"button\" class=\"gen-choice-card\" data-go=\"model-photo\">\n <div class=\"gc-ic\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8\"/></svg>\n </div>\n <div class=\"gc-t\">模特上身图</div>\n <div class=\"gc-d\">// 选模特 + 商品 → AI 生成穿搭/试用图</div>\n </button>\n <button type=\"button\" class=\"gen-choice-card\" data-go=\"platform-cover\">\n <div class=\"gc-ic\">\n <svg 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 9h18M9 5v14\"/></svg>\n </div>\n <div class=\"gc-t\">平台套图</div>\n <div class=\"gc-d\">// 多平台尺寸自动生成 (抖音/淘宝/小红书等)</div>\n </button>\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" id=\"gen-choice-cancel\">取消</button>\n </div>\n </div>\n</div>\n\n<!-- ===== 删除确认 modal ===== -->\n<div class=\"modal-bg\" id=\"del-confirm-bg\">\n <div class=\"modal\" role=\"dialog\" aria-labelledby=\"del-confirm-ti\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </div>\n <div class=\"ti\" id=\"del-confirm-ti\">确认删除商品<span>// CONFIRM DELETE</span></div>\n </div>\n <div class=\"modal-b\" id=\"del-confirm-body\">即将删除 <span class=\"mono-acc\" id=\"del-confirm-target\">—</span>,此操作无法撤销,商品下生成的素材记录也将一并清理。</div>\n <div class=\"modal-f\" id=\"del-confirm-foot\">\n <button class=\"btn\" type=\"button\" id=\"del-confirm-cancel\">取消</button>\n <button class=\"btn\" type=\"button\" id=\"del-confirm-ok\" style=\"background:var(--accent-crimson,#c43d3d);color:var(--accent-white);border-color:var(--accent-crimson,#c43d3d)\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/></svg>\n 确认删除\n </button>\n </div>\n </div>\n</div>\n\n<!-- ===== 批量编辑 浮动 action bar ===== -->\n<div class=\"bulk-bar\" id=\"bulk-bar\">\n <span class=\"ct\">已选 <b id=\"bulk-count\">0</b> 项</span>\n <button class=\"clear-sel\" type=\"button\" id=\"bulk-clear\">清空</button>\n <span class=\"sep\"></span>\n <button class=\"danger\" type=\"button\" id=\"bulk-del\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n 删除所选\n </button>\n <button type=\"button\" id=\"bulk-exit\">完成</button>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });\n\n// ============== 注入用户新建的商品(来自工作台 / 商品库 drawer,写入 localStorage)==============\n// 必须在 const cards / TOTAL / CAT_COUNT 之前执行,让后续逻辑把它们当普通商品处理\n(function injectExtraProducts() {\n // 一次性清掉历史遗留的 localStorage 旧数据(用户上次会话误持久化的占位商品)\n try { localStorage.removeItem('fs-extra-products'); } catch (e) {}\n let pending;\n try {\n pending = JSON.parse(sessionStorage.getItem('fs-extra-products') || '[]');\n } catch (e) { return; }\n if (!Array.isArray(pending) || !pending.length) return;\n const grid = document.getElementById('product-grid');\n if (!grid) return;\n const esc = s => String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n\n // 按 createdAt 升序排,然后逐个 insertBefore firstChild → 最新的排最上\n pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));\n pending.forEach(p => {\n const card = document.createElement('div');\n card.className = 'product-card';\n card.dataset.cat = p.cat || '美妆个护';\n card.dataset.name = p.name || '';\n card.dataset.tags = p.tags || '';\n card.dataset.added = '0';\n card.dataset.assets = String(p.assets || 0);\n card.dataset.videos = String(p.videos || 0);\n card.dataset.date = p.date || new Date(p.createdAt || Date.now()).toISOString().slice(0, 10);\n card.setAttribute('onclick', `location.href='product-detail.html?t='+Date.now()+'&product=${encodeURIComponent(p.name || '')}'`);\n card.dataset.triview = '0';\n card.innerHTML = `\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除商品\" onclick=\"event.stopPropagation();\" data-action=\"delete-product\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder product-thumb\">\n <span class=\"tri-missing-badge\" tabindex=\"0\" role=\"button\" aria-label=\"缺三视图,查看说明\">\n <span class=\"ico\" aria-hidden=\"true\"></span>\n <span class=\"lbl-mono\">缺三视图</span>\n <span class=\"tri-missing-pop\" role=\"tooltip\">\n <span class=\"pop-h\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 9v4M12 17h.01\"/><path d=\"M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/></svg>\n MISSING TRI-VIEW\n </span>\n <span class=\"pop-body\">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>\n <span class=\"pop-tip\">建议:进入 <b>商品详情</b> 先补齐三视图,再发起后续生成。</span>\n </span>\n </span>\n <span class=\"ph-frame\">${esc(p.name)} · 新建</span>\n </div>\n <div class=\"product-body\">\n <div class=\"product-name\">${esc(p.name)}</div>\n <div class=\"product-cat\">${esc(p.cat || '美妆个护')}</div>\n <div class=\"product-date\">${esc(p.date || '')} 创建</div>\n </div>\n <div class=\"product-footer\">\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"M21 17l-5-5-9 9\"/></svg>\n 素材 <b>${p.assets || 0}</b>\n </span>\n <span class=\"sep\">·</span>\n <span class=\"stat\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"6\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"M16 10l6-3v10l-6-3z\"/></svg>\n 视频 <b>${p.videos || 0}</b>\n </span>\n </div>\n `;\n grid.insertBefore(card, grid.firstChild);\n });\n})();\n\n// ============== Products: 商品分类多选 + 搜索 + 创建时间筛选 ==============\n// state.cats: Set<string>,空集 = 全部\nconst PAGE_SIZES = [12, 24, 48, 96];\nconst state = { cats: new Set(), search: '', date: 'all', page: 1, pageSize: 12 };\n\n// 抖音爆款品类 · TOP 8 (全部显示,即使没商品)\nconst CATEGORIES = ['美妆个护', '服饰内衣', '食品饮料', '家居家电', '数码 3C', '个护清洁', '运动户外', '母婴亲子'];\n\nconst DATE_LABEL = {\n 'all': '全部时间',\n '7d': '最近 7 天',\n '30d': '最近 30 天',\n '90d': '最近 90 天',\n};\n\nconst grid = document.getElementById('product-grid');\nconst cards = [...grid.querySelectorAll('.product-card')];\nconst TOTAL = cards.length;\nconst CAT_COUNT = Object.fromEntries(CATEGORIES.map(c =>\n [c, cards.filter(card => card.dataset.cat === c).length]\n));\n\n// 同步计数: 大标题 SKU + 侧栏徽章\ndocument.getElementById('sku-count').textContent = TOTAL;\nconst sidebarBadge = document.querySelector('aside.sidebar a[href=\"products.html\"] .pill-mini');\nif (sidebarBadge) sidebarBadge.textContent = TOTAL;\n\n// ─── 卡片底部 stat 增强 · hover 切换文案 + 直跳生成入口 ───\ncards.forEach(card => {\n card.querySelectorAll('.product-footer .stat').forEach(stat => {\n const text = stat.textContent.trim();\n const m = text.match(/^(素材|视频)\\s*(\\d+)/);\n if (!m) return;\n const label = m[1];\n const num = m[2];\n const isAsset = label === '素材';\n const cta = isAsset ? '去生成素材' : '去生成视频';\n const svg = stat.querySelector('svg');\n const icon = window.IconKit\n ? window.IconKit.svg(isAsset ? 'images' : 'video', {\n size: 14,\n strokeWidth: 1.25,\n className: 'product-stat-icon'\n })\n : (svg ? svg.outerHTML : '');\n // 重组结构: <svg> <span.stat-default>素材 <b>N</b></span> <span.stat-hover>去生成素材</span>\n stat.innerHTML = '';\n if (icon) stat.insertAdjacentHTML('beforeend', icon);\n const dft = document.createElement('span');\n dft.className = 'stat-default';\n dft.innerHTML = `${label} <b>${num}</b>`;\n const hv = document.createElement('span');\n hv.className = 'stat-hover';\n hv.textContent = cta.replace(/^\\s*\\+\\s*/, '');\n stat.appendChild(dft);\n stat.appendChild(hv);\n stat.dataset.type = isAsset ? 'asset' : 'video';\n stat.title = isAsset ? '去生成 AI 素材' : '去生成视频项目';\n stat.addEventListener('click', e => {\n e.stopPropagation();\n const name = card.dataset.name || '';\n if (isAsset) {\n // 弹出选择: 模特上身图 / 平台套图\n openGenAssetChoice(name);\n } else {\n location.href = 'projects-new.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);\n }\n });\n });\n});\n\n// ─── 生成类型选择 modal (素材生成 → 选 模特上身图 / 平台套图) ───\nconst genChoiceBg = document.getElementById('gen-choice-bg');\nconst genChoiceTarget = document.getElementById('gen-choice-target');\nconst genChoiceCancel = document.getElementById('gen-choice-cancel');\nlet _genChoiceProduct = '';\nfunction openGenAssetChoice(productName) {\n _genChoiceProduct = productName || '';\n genChoiceTarget.textContent = _genChoiceProduct || '该商品';\n genChoiceBg.classList.add('show');\n document.body.style.overflow = 'hidden';\n}\nfunction closeGenAssetChoice() {\n genChoiceBg.classList.remove('show');\n document.body.style.overflow = '';\n}\ngenChoiceCancel.addEventListener('click', closeGenAssetChoice);\ngenChoiceBg.addEventListener('click', e => {\n if (e.target === genChoiceBg) closeGenAssetChoice();\n});\ngenChoiceBg.querySelectorAll('.gen-choice-card').forEach(btn => {\n btn.addEventListener('click', () => {\n const go = btn.dataset.go;\n const target = (go === 'model-photo') ? 'model-photo.html' : 'platform-cover.html';\n const url = target + '?t=' + Date.now() + '&product=' + encodeURIComponent(_genChoiceProduct);\n closeGenAssetChoice();\n location.href = url;\n });\n});\n\n// ─── 删除确认 modal (PRD §6.3: 已被项目引用时禁止强删) ───\n// 模拟: 某些商品被项目引用 (data-refs=\"项目A,项目B\")\n// 真实环境从后台查 product → projects 映射\nconst PRODUCT_REFS = {\n '透真玻尿酸补水面膜': ['夏日水嫩计划', '七夕主题推广'],\n '南卡 Lite Pro 蓝牙耳机': ['通勤好物']\n};\nfunction getRefs(card) {\n const name = card.dataset.name || '';\n return PRODUCT_REFS[name] || [];\n}\n\nconst delBg = document.getElementById('del-confirm-bg');\nconst delBody = document.getElementById('del-confirm-body');\nconst delFoot = document.getElementById('del-confirm-foot');\nconst delCancel = document.getElementById('del-confirm-cancel');\nconst delOk = document.getElementById('del-confirm-ok');\nlet _delQueue = [];\n\nfunction setFootDeletable() {\n delFoot.innerHTML = '';\n delFoot.appendChild(delCancel);\n delFoot.appendChild(delOk);\n}\nfunction setFootBlocked() {\n delFoot.innerHTML = '';\n const okBtn = document.createElement('button');\n okBtn.className = 'btn btn-primary';\n okBtn.type = 'button';\n okBtn.textContent = '我知道了';\n okBtn.addEventListener('click', closeDelConfirm);\n delFoot.appendChild(okBtn);\n}\n\nfunction openDelConfirm(targets) {\n // 分组: 可删除 / 被引用 (PRD §6.3 软删除规则)\n const blocked = targets.filter(c => getRefs(c).length > 0);\n const deletable = targets.filter(c => getRefs(c).length === 0);\n\n // 全部被引用 → 阻断式提示\n if (deletable.length === 0 && blocked.length > 0) {\n const c = blocked[0];\n const refs = getRefs(c);\n if (blocked.length === 1) {\n delBody.innerHTML = `<span class=\"mono-acc\">${c.dataset.name}</span> 当前被 <b>${refs.length}</b> 个项目使用,无法直接删除。请先在以下项目中解除引用:<br><br>` +\n refs.map(r => `<span class=\"mono-acc\" style=\"margin-right:6px\">${r}</span>`).join('');\n } else {\n delBody.innerHTML = `所选 <b>${blocked.length}</b> 个商品均被项目引用,无法直接删除。请先解除引用后重试。`;\n }\n setFootBlocked();\n delBg.classList.add('show');\n _delQueue = [];\n return;\n }\n\n _delQueue = deletable;\n if (deletable.length === 1 && blocked.length === 0) {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + deletable[0].dataset.name + '</span>,此操作无法撤销,商品下生成的素材记录也将一并清理。';\n } else if (blocked.length > 0) {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + deletable.length + ' 个商品</span>,其中 <b>' + blocked.length + '</b> 个被项目引用已跳过。可删除的将一并清理素材记录。';\n } else {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + deletable.length + ' 个商品</span>,此操作无法撤销,这些商品下生成的素材记录也将一并清理。';\n }\n setFootDeletable();\n delBg.classList.add('show');\n}\nfunction closeDelConfirm() { delBg.classList.remove('show'); _delQueue = []; }\ndelCancel.addEventListener('click', closeDelConfirm);\ndelBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });\ndelOk.addEventListener('click', () => {\n const n = _delQueue.length;\n _delQueue.forEach(card => card.remove());\n closeDelConfirm();\n const remaining = document.querySelectorAll('.product-card').length;\n document.getElementById('sku-count').textContent = remaining;\n const meta = document.querySelector('#result-meta .count');\n if (meta) meta.textContent = remaining;\n const sidebar = document.querySelector('aside.sidebar a[href=\"products.html\"] .pill-mini');\n if (sidebar) sidebar.textContent = remaining;\n Shell.toast('已删除', n === 1 ? '商品已移除' : '已删除 ' + n + ' 个商品');\n updateBulkBar();\n});\n\n// ─── 单卡片删除 (新统一 card-del-btn) ───\ndocument.querySelectorAll('.product-card .card-del-btn').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const card = btn.closest('.product-card');\n if (!card) return;\n openDelConfirm([card]);\n });\n});\n\n// ─── 视图切换 网格 / 列表 ───\nconst productGrid = document.getElementById('product-grid');\ndocument.querySelectorAll('#view-tog button').forEach(b => {\n b.addEventListener('click', () => {\n document.querySelectorAll('#view-tog button').forEach(x => x.classList.remove('active'));\n b.classList.add('active');\n if (b.dataset.view === 'list') productGrid.classList.add('list-view');\n else productGrid.classList.remove('list-view');\n });\n});\n\n// ─── 批量编辑模式 ───\nconst editToggleBtn = document.getElementById('edit-toggle-btn');\nconst bulkBar = document.getElementById('bulk-bar');\nconst bulkCount = document.getElementById('bulk-count');\nconst bulkClear = document.getElementById('bulk-clear');\nconst bulkDel = document.getElementById('bulk-del');\nconst bulkExit = document.getElementById('bulk-exit');\nconst editLabel = editToggleBtn.querySelector('.btn-edit-label');\n\nfunction getSelectedCards() {\n return [...document.querySelectorAll('.product-card.selected')];\n}\nfunction updateBulkBar() {\n const sel = getSelectedCards();\n bulkCount.textContent = sel.length;\n bulkDel.disabled = sel.length === 0;\n bulkDel.style.opacity = sel.length === 0 ? '.4' : '1';\n bulkDel.style.cursor = sel.length === 0 ? 'not-allowed' : 'pointer';\n}\nfunction enterEditMode() {\n document.body.classList.add('edit-mode');\n editToggleBtn.classList.add('active');\n editLabel.textContent = '完成';\n updateBulkBar();\n}\nfunction exitEditMode() {\n document.body.classList.remove('edit-mode');\n editToggleBtn.classList.remove('active');\n editLabel.textContent = '管理商品';\n document.querySelectorAll('.product-card.selected').forEach(c => c.classList.remove('selected'));\n}\neditToggleBtn.addEventListener('click', () => {\n if (document.body.classList.contains('edit-mode')) exitEditMode();\n else enterEditMode();\n});\nbulkExit.addEventListener('click', exitEditMode);\nbulkClear.addEventListener('click', () => {\n document.querySelectorAll('.product-card.selected').forEach(c => c.classList.remove('selected'));\n updateBulkBar();\n});\nbulkDel.addEventListener('click', () => {\n const sel = getSelectedCards();\n if (!sel.length) return;\n openDelConfirm(sel);\n});\n\n// 编辑模式下,卡片点击切换 selected (不再跳详情)\ndocument.querySelectorAll('.product-card').forEach(card => {\n card.addEventListener('click', e => {\n if (!document.body.classList.contains('edit-mode')) return; // 非编辑模式走原 onclick (跳详情)\n // capture 阶段必须用 stopImmediatePropagation 才能阻止 element.onclick (inline onclick=\"\")\n e.stopImmediatePropagation();\n e.preventDefault();\n card.classList.toggle('selected');\n updateBulkBar();\n }, true); // capture: 早于 inline onclick\n});\n\nconst checkSvg = '<svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg>';\n\n// ====== 商品分类 chip · 多选 ======\nconst catWrap = document.querySelector('.chip-wrap[data-key=\"cat\"]');\nconst catMenu = catWrap.querySelector('.chip-menu');\n\nfunction renderCatMenu() {\n const allSelected = state.cats.size === 0;\n const allRow = `<div class=\"mi mi-all${allSelected ? ' selected' : ''}\" data-value=\"__all__\">${checkSvg}<span>全部商品</span><span class=\"cat-count\">${TOTAL}</span></div>`;\n const items = CATEGORIES\n .filter(c => CAT_COUNT[c] > 0)\n .map(c => {\n const sel = state.cats.has(c);\n const n = CAT_COUNT[c];\n return `<div class=\"mi${sel ? ' selected' : ''}\" data-value=\"${c}\">${checkSvg}<span>${c}</span><span class=\"cat-count\">${n}</span></div>`;\n }).join('');\n catMenu.innerHTML = allRow + items;\n\n catMenu.querySelectorAll('.mi').forEach(mi => {\n mi.addEventListener('click', e => {\n e.stopPropagation();\n const v = mi.dataset.value;\n if (v === '__all__') {\n state.cats.clear();\n } else {\n if (state.cats.has(v)) state.cats.delete(v);\n else state.cats.add(v);\n }\n renderCatMenu(); // 不关菜单,只刷新 selected 态\n syncCatChip();\n state.page = 1;\n applyFilter();\n });\n });\n}\n\nfunction syncCatChip() {\n const label = catWrap.querySelector('.chip-label');\n const chip = catWrap.querySelector('.chip');\n // 先清掉旧的 chip-count\n chip.querySelectorAll('.chip-count').forEach(n => n.remove());\n if (state.cats.size === 0) {\n label.textContent = '商品分类';\n chip.classList.remove('active');\n } else if (state.cats.size === 1) {\n label.textContent = [...state.cats][0];\n chip.classList.add('active');\n } else {\n label.textContent = '商品分类';\n const count = document.createElement('span');\n count.className = 'chip-count';\n count.textContent = state.cats.size;\n // 把计数徽标插在 caret 之前\n chip.insertBefore(count, chip.querySelector('.caret'));\n chip.classList.add('active');\n }\n}\n\ncatWrap.querySelector('.chip').addEventListener('click', e => {\n e.stopPropagation();\n const isOpen = catWrap.classList.contains('open');\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) catWrap.classList.add('open');\n});\n// menu 内点击不冒泡到 document(防止误关)\ncatMenu.addEventListener('click', e => e.stopPropagation());\n\nrenderCatMenu();\n\n// ====== 创建时间 chip ======\nconst dateWrap = document.querySelector('.chip-wrap[data-key=\"date\"]');\ndateWrap.querySelector('.chip-menu').innerHTML = Object.entries(DATE_LABEL).map(([v, l]) =>\n `<div class=\"mi${v === 'all' ? ' selected' : ''}\" data-value=\"${v}\">${checkSvg}<span>${l}</span></div>`\n).join('');\n\nfunction syncDateChip() {\n const label = dateWrap.querySelector('.chip-label');\n const chip = dateWrap.querySelector('.chip');\n if (state.date === 'all') {\n label.textContent = '创建时间';\n chip.classList.remove('active');\n } else {\n label.textContent = DATE_LABEL[state.date];\n chip.classList.add('active');\n }\n dateWrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.date));\n}\n\ndateWrap.querySelector('.chip').addEventListener('click', e => {\n e.stopPropagation();\n const isOpen = dateWrap.classList.contains('open');\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) dateWrap.classList.add('open');\n});\ndateWrap.querySelectorAll('.mi').forEach(mi => {\n mi.addEventListener('click', e => {\n e.stopPropagation();\n state.date = mi.dataset.value;\n syncDateChip();\n dateWrap.classList.remove('open');\n state.page = 1;\n applyFilter();\n });\n});\ndocument.addEventListener('click', () => {\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n});\n\n// ====== 筛选应用 ======\nconst NOW = new Date('2026-05-19'); // 当前日期参考 (Shell.today)\nfunction withinDate(dateStr) {\n if (state.date === 'all') return true;\n const days = { '7d': 7, '30d': 30, '90d': 90 }[state.date];\n if (!days) return true;\n const d = new Date(dateStr);\n const diffMs = NOW - d;\n return diffMs <= days * 24 * 60 * 60 * 1000;\n}\n\nfunction applyFilter() {\n let visible = 0;\n const visibleCards = [];\n cards.forEach(c => {\n const matchCat = state.cats.size === 0 || state.cats.has(c.dataset.cat);\n const q = state.search.toLowerCase();\n const matchSearch = !q\n || (c.dataset.name || '').toLowerCase().includes(q)\n || (c.dataset.tags || '').toLowerCase().includes(q)\n || (c.dataset.cat || '').toLowerCase().includes(q);\n const matchDate = withinDate(c.dataset.date);\n const show = matchCat && matchSearch && matchDate;\n c.style.display = show ? '' : 'none';\n if (show) { visible++; visibleCards.push(c); }\n });\n\n // 默认按 added desc(最新在前)\n visibleCards.sort((a, b) => +b.dataset.added - +a.dataset.added);\n visibleCards.forEach(c => grid.appendChild(c));\n\n // 分页 · 排序后裁页, 把非当前页的卡片隐藏\n const totalPages = Math.max(1, Math.ceil(visible / state.pageSize));\n if (state.page > totalPages) state.page = totalPages;\n if (state.page < 1) state.page = 1;\n const start = (state.page - 1) * state.pageSize;\n const end = start + state.pageSize;\n visibleCards.forEach((c, i) => {\n if (i < start || i >= end) c.style.display = 'none';\n });\n\n const empty = document.getElementById('empty');\n if (visible === 0) {\n empty.classList.add('show');\n grid.style.display = 'none';\n } else {\n empty.classList.remove('show');\n grid.style.display = '';\n }\n\n document.getElementById('result-meta').innerHTML = `// 显示 <span class=\"count\">${visible}</span> / ${TOTAL} 个商品`;\n document.getElementById('clear-filters').hidden = !(state.cats.size > 0 || state.search || state.date !== 'all');\n\n renderPagination(visible, totalPages);\n}\n\n// ============== 分页器渲染 ==============\nfunction pageNumberList(cur, total) {\n if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);\n const pages = new Set([1, total, cur, cur - 1, cur + 1]);\n if (cur <= 4) [2, 3, 4, 5].forEach(p => pages.add(p));\n if (cur >= total - 3) [total - 4, total - 3, total - 2, total - 1].forEach(p => pages.add(p));\n const sorted = [...pages].filter(p => p >= 1 && p <= total).sort((a, b) => a - b);\n const out = [];\n for (let i = 0; i < sorted.length; i++) {\n if (i > 0 && sorted[i] - sorted[i - 1] > 1) out.push('…');\n out.push(sorted[i]);\n }\n return out;\n}\n\nfunction renderPagination(totalVisible, totalPages) {\n const root = document.getElementById('pagination');\n if (!root) return;\n // 空结果或只有一页且 ≤ pageSize → 不显示\n if (totalVisible === 0 || (totalPages <= 1 && totalVisible <= state.pageSize)) {\n root.hidden = true;\n return;\n }\n root.hidden = false;\n document.getElementById('page-total').textContent = totalVisible;\n document.getElementById('page-size-label').textContent = `${state.pageSize} 条/页`;\n document.getElementById('page-jump').value = state.page;\n document.getElementById('page-jump').max = totalPages;\n\n const list = document.getElementById('page-list');\n const items = pageNumberList(state.page, totalPages);\n let html = `<button type=\"button\" data-page=\"prev\" ${state.page <= 1 ? 'disabled' : ''} aria-label=\"上一页\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 12L6 8l4-4\"/></svg>\n </button>`;\n items.forEach(p => {\n if (p === '…') html += `<span class=\"ellipsis\">…</span>`;\n else html += `<button type=\"button\" data-page=\"${p}\" ${p === state.page ? 'class=\"active\"' : ''}>${p}</button>`;\n });\n html += `<button type=\"button\" data-page=\"next\" ${state.page >= totalPages ? 'disabled' : ''} aria-label=\"下一页\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 4l4 4-4 4\"/></svg>\n </button>`;\n list.innerHTML = html;\n}\n\n// 搜索\ndocument.getElementById('search-input').addEventListener('input', e => {\n state.search = e.target.value.trim();\n state.page = 1;\n applyFilter();\n});\n\n// 清空筛选\ndocument.getElementById('clear-filters').addEventListener('click', () => {\n state.cats.clear();\n state.search = '';\n state.date = 'all';\n document.getElementById('search-input').value = '';\n renderCatMenu();\n syncCatChip();\n syncDateChip();\n state.page = 1;\n applyFilter();\n Shell.toast('已清空筛选');\n});\n\n// ============== 分页器控件 ==============\n// 翻页按钮(事件委托)\ndocument.getElementById('page-list').addEventListener('click', e => {\n const btn = e.target.closest('button[data-page]');\n if (!btn || btn.disabled) return;\n const v = btn.dataset.page;\n const totalPages = +document.getElementById('page-jump').max || 1;\n if (v === 'prev') state.page = Math.max(1, state.page - 1);\n else if (v === 'next') state.page = Math.min(totalPages, state.page + 1);\n else state.page = +v;\n applyFilter();\n // 翻页后把网格滚回顶, 让首张卡立刻可见\n const wrap = document.querySelector('.product-grid-wrap');\n if (wrap) wrap.scrollTo({ top: 0, behavior: 'smooth' });\n});\n\n// 每页条数(循环切换 12 → 24 → 48 → 96 → 12)\ndocument.getElementById('page-size-btn').addEventListener('click', () => {\n const i = PAGE_SIZES.indexOf(state.pageSize);\n state.pageSize = PAGE_SIZES[(i + 1) % PAGE_SIZES.length];\n state.page = 1;\n applyFilter();\n});\n\n// 跳转\nconst _jumpEl = document.getElementById('page-jump');\nfunction _doJump() {\n let v = parseInt(_jumpEl.value, 10);\n const max = +_jumpEl.max || 1;\n if (!Number.isFinite(v)) v = 1;\n v = Math.max(1, Math.min(max, v));\n state.page = v;\n applyFilter();\n}\n_jumpEl.addEventListener('change', _doJump);\n_jumpEl.addEventListener('blur', _doJump);\n_jumpEl.addEventListener('keydown', e => {\n if (e.key === 'Enter') { e.preventDefault(); _doJump(); _jumpEl.blur(); }\n});\n\napplyFilter();\n\n// ============================================================\n// 新建商品 · Drawer 控制 + 多图上传 + 卖点 bullet-list\n// ============================================================\nconst drawerBg = document.getElementById('pc-drawer-bg');\nconst drawerEl = document.getElementById('pc-drawer');\nconst openBtn = document.getElementById('open-new-product');\nconst closeBtn = document.getElementById('pc-drawer-close');\nconst cancelBtn = document.getElementById('pc-cancel-btn');\nconst saveBtn = document.getElementById('pc-save-btn');\n\nfunction openNewProductDrawer() {\n if (drawerEl.classList.contains('show')) return;\n drawerBg.classList.add('show');\n drawerEl.classList.add('show');\n drawerEl.setAttribute('aria-hidden', 'false');\n if (typeof Shell !== 'undefined' && Shell.lockScroll) Shell.lockScroll();\n setTimeout(() => document.getElementById('pf-name')?.focus(), 280);\n}\nfunction closeNewProductDrawer() {\n if (!drawerEl.classList.contains('show')) return;\n drawerBg.classList.remove('show');\n drawerEl.classList.remove('show');\n drawerEl.setAttribute('aria-hidden', 'true');\n if (typeof Shell !== 'undefined' && Shell.unlockScroll) Shell.unlockScroll();\n}\n\nopenBtn.addEventListener('click', openNewProductDrawer);\ncloseBtn.addEventListener('click', closeNewProductDrawer);\ncancelBtn.addEventListener('click', closeNewProductDrawer);\ndrawerBg.addEventListener('click', closeNewProductDrawer);\ndocument.addEventListener('keydown', e => {\n if (e.key === 'Escape' && drawerEl.classList.contains('show')) closeNewProductDrawer();\n});\n\n// 来自其它页面的\"新建商品\"链接 → 跳到 products.html 后自动开\nif (sessionStorage.getItem('auto-open-new-product') === '1') {\n sessionStorage.removeItem('auto-open-new-product');\n setTimeout(openNewProductDrawer, 100);\n}\n\n// ─── 多图上传 ───\nconst PF_MAX = 5;\nconst pfFiles = []; // {id, dataUrl, name}\nconst pfFile = document.getElementById('pf-file');\nconst pfZone = document.getElementById('pf-zone');\nconst pfGrid = document.getElementById('pf-grid');\n\nconst pfUid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 5);\nconst pfEsc = s => (s || '').replace(/[<>&\"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','\"':'&quot;' })[c]);\n\nfunction pfRender() {\n pfGrid.innerHTML = pfFiles.map(u => `\n <div class=\"pf-thumb\" data-id=\"${u.id}\">\n <img src=\"${u.dataUrl}\" alt=\"${pfEsc(u.name)}\">\n <button class=\"pf-x\" type=\"button\" title=\"删除\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n </button>\n </div>\n `).join('');\n pfGrid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {\n b.onclick = e => {\n e.stopPropagation();\n const id = b.closest('.pf-thumb').dataset.id;\n const i = pfFiles.findIndex(f => f.id === id);\n if (i >= 0) { pfFiles.splice(i, 1); pfRender(); }\n };\n });\n}\n\nfunction pfAdd(fileList) {\n const room = PF_MAX - pfFiles.length;\n if (room <= 0) { Shell.toast('已达上限', `${PF_MAX} / ${PF_MAX} 张`); return; }\n const incoming = [...fileList].filter(f => f.type.startsWith('image/')).slice(0, room);\n let done = 0;\n incoming.forEach(f => {\n const r = new FileReader();\n r.onload = e => {\n pfFiles.push({ id: pfUid(), dataUrl: e.target.result, name: f.name });\n if (++done === incoming.length) {\n pfRender();\n Shell.toast('已上传', `+ ${done} 张 · 共 ${pfFiles.length} / ${PF_MAX}`);\n }\n };\n r.readAsDataURL(f);\n });\n}\n\npfFile.addEventListener('change', e => { pfAdd(e.target.files); e.target.value = ''; });\npfZone.addEventListener('click', () => { if (pfFiles.length < PF_MAX) pfFile.click(); });\npfZone.addEventListener('dragover', e => { e.preventDefault(); pfZone.style.borderColor = 'var(--heat)'; });\npfZone.addEventListener('dragleave', () => { pfZone.style.borderColor = ''; });\npfZone.addEventListener('drop', e => {\n e.preventDefault(); pfZone.style.borderColor = '';\n if (e.dataTransfer?.files?.length) pfAdd(e.dataTransfer.files);\n});\n\n// ─── 核心卖点 bullet-list ───\nconst blList = document.getElementById('pf-bullets');\nconst blInput = blList.querySelector('.bl-add .bl-input');\nconst blXSvg = '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>';\n\nfunction blRenumber() {\n [...blList.querySelectorAll('.bl-item')].forEach((li, i) => {\n li.querySelector('.num').textContent = i + 1;\n });\n}\nfunction blBindX(x) {\n x.addEventListener('click', () => {\n const li = x.closest('li');\n li.style.transition = 'opacity .15s, transform .15s';\n li.style.opacity = 0;\n li.style.transform = 'translateX(-8px)';\n setTimeout(() => { li.remove(); blRenumber(); }, 150);\n });\n}\nfunction blAdd(text) {\n const t = (text || '').trim();\n if (!t) return;\n const li = document.createElement('li');\n li.className = 'bl-item';\n li.innerHTML = `<span class=\"num\">0</span><span class=\"bl-text\">${pfEsc(t)}</span><span class=\"bl-x\" title=\"删除\">${blXSvg}</span>`;\n blList.querySelector('.bl-add').before(li);\n blBindX(li.querySelector('.bl-x'));\n blRenumber();\n}\nblInput.addEventListener('keydown', e => {\n if (e.key === 'Enter') {\n e.preventDefault();\n blAdd(blInput.value);\n blInput.value = '';\n }\n});\n\n// 创建商品 · 持久化到 localStorage + 立即跳详情(中间不闪商品库)\nsaveBtn.addEventListener('click', () => {\n const name = document.getElementById('pf-name').value.trim();\n const cat = document.getElementById('pf-cat').value;\n if (!name) {\n Shell.toast('请填写商品名称', '必填项');\n document.getElementById('pf-name').focus();\n return;\n }\n if (pfFiles.length === 0) {\n Shell.toast('请上传商品主图', '至少 1 张 · 必填');\n return;\n }\n const confirmedBullets = [...document.querySelectorAll('#pf-bullets .bl-item .bl-text')]\n .map(el => el.textContent.trim()).filter(Boolean);\n const pendingInput = document.querySelector('#pf-bullets .bl-add .bl-input');\n const pendingText = (pendingInput?.value || '').trim();\n if (pendingText) confirmedBullets.push(pendingText);\n if (confirmedBullets.length === 0) {\n Shell.toast('请填写核心卖点', '至少 1 条 · 回车确认');\n pendingInput?.focus();\n return;\n }\n const bullets = confirmedBullets;\n // 持久化到 sessionStorage('fs-extra-products') · 仅在当前标签页生命周期内有效\n // 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留\n try {\n const KEY = 'fs-extra-products';\n const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');\n list.push({\n id: 'pp-' + Date.now(),\n name, cat,\n tags: '',\n assets: 0,\n videos: 0,\n bullets,\n date: new Date().toISOString().slice(0, 10),\n createdAt: Date.now(),\n });\n sessionStorage.setItem(KEY, JSON.stringify(list));\n } catch (e) { /* storage 不可用降级到只跳转 */ }\n\n // 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底消除\"闪商品库\"\n // 浏览器导航开始后,整页会被新页面替换,drawer 自然消失\n Shell.toast('商品已创建 · 跳转详情', `+ ${name} · ${bullets.length} 条卖点`);\n location.href = 'product-detail.html?t=' + Date.now() + '&product=' + encodeURIComponent(name) + '&id=new';\n});\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"projectWizard": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"projects-new.html\">\n<meta charset=\"utf-8\">\n<title>新建项目 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n .wizard { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 36px; align-items: start; max-width: 1400px; }\n @media (max-width: 1180px) { .wizard { grid-template-columns: 200px minmax(0, 1fr); } }\n .steps {\n position: sticky;\n top: calc(64px + 24px);\n align-self: start;\n max-height: calc(100vh - 64px - 48px);\n overflow-y: auto;\n z-index: 2;\n }\n .wiz-preview { display: none !important; }\n /* 顶部胶囊 + 商品横向单行 */\n .pick-actions { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }\n .cap-pill { 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); }\n .cap-pill:hover { background: var(--background-lighter); border-color: var(--heat-20); color: var(--heat); }\n .cap-pill svg { width: 13px; height: 13px; }\n .product-pick-row { display: flex; gap: 10px; overflow-x: auto; overflow-y: hidden; padding: 2px 2px 12px; scrollbar-width: thin; }\n .product-pick-row::-webkit-scrollbar { height: 8px; }\n .product-pick-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }\n .product-pick-row .product-pick { flex: 0 0 240px; min-width: 240px; }\n .product-pick-row .product-pick.lib { flex: 0 0 152px; min-width: 152px; min-height: 96px; }\n /* 底部开始 CTA */\n .wiz-start-bar { display: flex; justify-content: flex-end; padding: 20px 0 8px; }\n .wiz-start-bar .btn-start { height: 44px; padding: 0 36px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: 999px; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: var(--shadow-cta); display: inline-flex; align-items: center; gap: 8px; font-family: inherit; transition: box-shadow var(--t-base), opacity var(--t-base); }\n .wiz-start-bar .btn-start:hover:not(.disabled) { box-shadow: var(--shadow-cta-hover); }\n .wiz-start-bar .btn-start.disabled { opacity: .4; cursor: not-allowed; }\n .wiz-start-bar .btn-start svg { width: 14px; height: 14px; }\n /* 单页式: 所有 step pane 同时显示, 不再切换 */\n #wiz-body { display: flex; flex-direction: column; gap: 14px; }\n .wiz-pane.active::before, .wiz-pane.active::after { display: none; } /* 去掉 corner-mark, 不需要 \"当前激活\" 视觉强突出 */\n .wiz-pane.active { padding: 22px 24px; }\n .wiz-foot { display: none !important; } /* 底部上一步/下一步彻底隐藏 */\n /* 计费明细 */\n .pv-list.pv-bill li {\n display: flex; justify-content: space-between;\n padding: 5px 0;\n font-size: 12px;\n color: var(--black-alpha-72);\n }\n .pv-list.pv-bill li .ai { color: var(--accent-black); font-family: var(--font-mono); font-size: 11.5px; }\n .pv-list.pv-bill .pv-bill-total {\n border-top: 1px solid var(--border-faint);\n margin-top: 6px; padding-top: 8px;\n font-weight: 600; color: var(--accent-black);\n }\n .pv-list.pv-bill .pv-bill-total .ai b { color: var(--heat); font-size: 14px; font-weight: 700; }\n .step { display: flex; gap: 12px; padding: 12px 0; position: relative; }\n .step:not(:last-child)::after { content: ''; position: absolute; left: 11px; top: 36px; width: 1px; height: calc(100% - 24px); background: var(--border-faint); }\n .step .num { width: 24px; height: 24px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); display: grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--black-alpha-48); flex-shrink: 0; z-index: 1; font-family: var(--font-mono); }\n .step.done .num { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }\n .step.active .num { background: var(--heat); border-color: var(--heat); color: var(--accent-white); }\n .step .label { font-size: 13.5px; font-weight: 500; color: var(--black-alpha-56); padding-top: 2px; }\n .step .desc { font-size: 11.5px; color: var(--black-alpha-48); padding-top: 3px; line-height: 1.4; font-family: var(--font-mono); letter-spacing: .02em; }\n .step.active .label { color: var(--accent-black); font-weight: 600; }\n .step.done .label { color: var(--black-alpha-56); }\n .step.done:not(:last-child)::after { background: var(--accent-black); }\n .step.clickable { cursor: pointer; }\n .step.clickable:hover .label { color: var(--heat); }\n .step.clickable:hover .num { border-color: var(--heat); }\n\n .wiz-pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 22px 24px; margin-bottom: 14px; }\n .wiz-pane.active { padding: 26px 28px; position: relative; }\n .wiz-pane.active::before, .wiz-pane.active::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; }\n .wiz-pane.active::before { top: -7px; left: -7px; }\n .wiz-pane.active::after { bottom: -7px; right: -7px; }\n .wiz-pane.collapsed { padding: 16px 20px; }\n .wiz-pane-h { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }\n .wiz-pane-h h3 { font-size: 14px; font-weight: 600; }\n .wiz-step-h { margin-bottom: 18px; }\n .wiz-step-h h2 { font-size: 20px; font-weight: 600; letter-spacing: -.015em; }\n .wiz-step-h p { font-size: 13px; color: var(--black-alpha-56); margin-top: 6px; }\n\n .opt-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n .opt-row.cols-4 { grid-template-columns: repeat(4, 1fr); }\n .opt-row.cols-6 { grid-template-columns: repeat(3, 1fr); }\n @media (min-width: 1280px) { .opt-row.cols-6 { grid-template-columns: repeat(6, 1fr); } }\n .opt-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; background: var(--surface); cursor: pointer; position: relative; display: flex; flex-direction: column; min-width: 0; transition: background var(--t-base), border-color var(--t-base); }\n .opt-card:hover { background: var(--background-lighter); }\n .opt-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .opt-card.selected::after { content: ''; position: absolute; top: 8px; right: 10px; width: 16px; height: 16px; background-color: var(--heat); 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: 10px 10px; border-radius: var(--r-sm); }\n .opt-card h4 { font-size: 13px; font-weight: 600; }\n .opt-card .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 3px; letter-spacing: .02em; }\n .opt-card .note { font-size: 11.5px; color: var(--black-alpha-56); margin-top: 6px; line-height: 1.5; }\n .opt-card .metric { margin-top: auto; padding-top: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .opt-card .metric .val { color: var(--accent-black); font-weight: 500; }\n .opt-card.selected .metric .val { color: var(--heat); }\n .opt-card .badge { font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-48); display: inline-block; margin-top: 8px; letter-spacing: .04em; align-self: flex-start; }\n .opt-card.selected .badge { color: var(--heat); border-color: var(--heat-20); }\n\n .theme-pill-row { display: flex; gap: 8px; flex-wrap: wrap; }\n .theme-pill { display: inline-flex; gap: 6px; align-items: center; height: 36px; padding: 0 16px; border: 1px solid var(--border-faint); border-radius: 999px; background: var(--surface); font-size: 13px; font-weight: 500; font-family: inherit; cursor: pointer; color: var(--accent-black); transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }\n .theme-pill:hover { background: var(--background-lighter); }\n .theme-pill.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat); font-weight: 600; }\n .theme-pill svg { width: 13px; height: 13px; }\n\n .reco-bubble { position: relative; margin-top: 10px; padding: 10px 14px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-md); display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--accent-black); }\n .reco-bubble::before { content: ''; position: absolute; top: -5px; left: 28px; width: 9px; height: 9px; background: var(--heat-12); border-left: 1px solid var(--heat-20); border-top: 1px solid var(--heat-20); transform: rotate(45deg); }\n .reco-bubble .ic { color: var(--heat); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }\n .reco-bubble .ic svg, .reco-bubble .dismiss svg { display: block; }\n .reco-bubble .txt { flex: 1; line-height: 1.5; }\n .reco-bubble .txt strong { color: var(--heat); font-weight: 600; }\n .reco-bubble .txt .meta { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }\n .reco-bubble .btn-apply { height: 28px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0; box-shadow: var(--shadow-cta); transition: box-shadow var(--t-base); }\n .reco-bubble .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }\n .reco-bubble .dismiss { background: transparent; color: var(--black-alpha-48); border: 0; width: 24px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }\n .reco-bubble .dismiss:hover { color: var(--accent-black); }\n\n .wiz-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border-faint); }\n .btn:disabled, .btn.disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }\n\n /* ── pick toolbar (Step 1) ── */\n .pick-toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }\n .pick-toolbar .search-input { position: relative; flex: 1; max-width: 320px; min-width: 200px; }\n .pick-toolbar .search-input svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--black-alpha-48); width: 14px; height: 14px; }\n .pick-toolbar .search-input input { width: 100%; height: 32px; padding: 0 12px 0 34px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 12.5px; color: var(--accent-black); font-family: inherit; transition: border-color var(--t-base); }\n .pick-toolbar .search-input input:focus { outline: none; border-color: var(--heat); }\n .cat-chip { height: 32px; padding: 0 12px; border: 1px solid var(--border-faint); background: var(--surface); border-radius: var(--r-md); font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }\n .cat-chip:hover { background: var(--background-lighter); }\n .cat-chip.active { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n\n .pick-section-h { display: flex; align-items: baseline; gap: 8px; margin: 14px 0 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; }\n .pick-section-h .count { background: var(--background-lighter); border: 1px solid var(--border-faint); padding: 1px 6px; color: var(--black-alpha-48); font-size: 10px; }\n\n /* ============================================================\n 第 1 步 · 商品选择器(沿用 products.html 商品库的卡片与 toolbar 视觉)\n ─ 命名空间 .pp- 前缀,避免与 products.html 冲突\n ============================================================ */\n .pp-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }\n .pp-toolbar .search-inline {\n flex: 1; min-width: 220px; max-width: 340px;\n display: inline-flex; align-items: center; gap: 8px;\n height: 34px; padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n transition: border-color var(--t-base);\n }\n .pp-toolbar .search-inline:focus-within { border-color: var(--heat-40); }\n .pp-toolbar .search-inline svg { width: 14px; height: 14px; color: var(--black-alpha-48); flex-shrink: 0; }\n .pp-toolbar .search-inline input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; font-size: 13px; color: var(--accent-black); font-family: inherit; }\n .pp-toolbar .pp-chip-wrap { position: relative; }\n .pp-toolbar .pp-chip {\n display: inline-flex; align-items: center; gap: 6px;\n height: 34px; padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n font-size: 13px; font-family: inherit;\n color: var(--black-alpha-72);\n cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .pp-toolbar .pp-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pp-toolbar .pp-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }\n .pp-toolbar .pp-chip svg { width: 11px; height: 11px; opacity: .6; }\n .pp-toolbar .pp-menu {\n position: absolute; top: calc(100% + 4px); left: 0;\n min-width: 160px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 6px 20px rgba(0,0,0,.06);\n padding: 4px;\n display: none;\n z-index: 20;\n }\n .pp-toolbar .pp-chip-wrap.open .pp-menu { display: block; }\n .pp-toolbar .pp-menu .mi {\n display: flex; align-items: center; gap: 6px;\n padding: 7px 10px;\n border-radius: var(--r-sm);\n font-size: 12.5px;\n color: var(--accent-black);\n cursor: pointer;\n }\n .pp-toolbar .pp-menu .mi:hover { background: var(--background-lighter); }\n .pp-toolbar .pp-menu .mi.selected { color: var(--heat); font-weight: 600; }\n .pp-toolbar .pp-menu .mi-check { width: 12px; height: 12px; opacity: 0; }\n .pp-toolbar .pp-menu .mi.selected .mi-check { opacity: 1; }\n .pp-toolbar .pp-clear {\n display: inline-flex; align-items: center; gap: 4px;\n height: 30px; padding: 0 10px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n color: var(--black-alpha-56); font-size: 12.5px; font-family: inherit;\n cursor: pointer;\n }\n .pp-toolbar .pp-clear:hover { color: var(--accent-crimson); background: var(--crimson-bg, #fdebea); }\n .pp-toolbar .pp-clear svg { width: 11px; height: 11px; }\n .pp-toolbar .spacer { flex: 1; }\n .pp-view-tog { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }\n .pp-view-tog button {\n width: 34px; height: 34px;\n background: var(--surface);\n border: 0; border-right: 1px solid var(--border-faint);\n cursor: pointer;\n color: var(--black-alpha-48);\n display: grid; place-items: center;\n transition: background var(--t-base), color var(--t-base);\n }\n .pp-view-tog button:last-child { border-right: 0; }\n .pp-view-tog button:hover { background: var(--background-lighter); color: var(--accent-black); }\n .pp-view-tog button.active { background: var(--heat-12); color: var(--heat); }\n .pp-view-tog button svg { width: 14px; height: 14px; }\n\n .pp-result-meta {\n font-family: var(--font-mono); font-size: 11.5px;\n color: var(--black-alpha-48); letter-spacing: .02em;\n margin: 4px 0 12px;\n }\n\n /* 网格 — 沿用 products.html 商品库卡片样式(复制必要部分)\n 固定 4 列 → 每页 8 tile(createCard + 7 商品)正好两行 */\n .pp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }\n @media (max-width: 1100px) { .pp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }\n @media (max-width: 800px) { .pp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }\n .pp-grid .product-card {\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n cursor: pointer; position: relative; overflow: hidden;\n display: flex; flex-direction: column;\n transition: background .15s, border-color .15s, transform .15s;\n }\n .pp-grid .product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }\n .pp-grid .product-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .pp-grid .product-card.selected::after {\n content: ''; position: absolute; top: 0; right: 0;\n width: 0; height: 0;\n border-top: 28px solid var(--heat);\n border-left: 28px solid transparent;\n z-index: 2;\n }\n .pp-grid .product-card.selected::before {\n content: ''; position: absolute; top: 4px; right: 4px;\n width: 10px; height: 10px;\n background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 8 7 12 13 4'/%3E%3C/svg%3E\") no-repeat center / contain;\n z-index: 3;\n }\n .pp-grid .product-thumb { aspect-ratio: 1.4 / 1; }\n .pp-grid .product-body { padding: 14px 14px 12px; flex: 1; }\n .pp-grid .product-name {\n font-size: 14px; font-weight: 600; color: var(--accent-black);\n line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .pp-grid .product-cat {\n display: inline-flex; align-items: center;\n margin-top: 8px; padding: 2px 8px;\n background: var(--background-lighter); color: var(--black-alpha-72);\n border-radius: var(--r-sm); font-size: 11.5px;\n }\n .pp-grid .product-date {\n font-family: var(--font-mono);\n font-size: 11px; color: var(--black-alpha-48);\n margin-top: 10px; letter-spacing: .02em;\n }\n .pp-grid .product-card.selected .product-cat { background: var(--surface); color: var(--heat); }\n\n /* 创建新商品 空卡 */\n .pp-grid .pp-create-card {\n border: 1.5px dashed var(--black-alpha-24);\n border-radius: var(--r-md);\n background: transparent;\n cursor: pointer;\n display: flex; flex-direction: column; align-items: center; justify-content: center;\n gap: 10px; min-height: 220px;\n color: var(--black-alpha-48);\n transition: border-color var(--t-base), color var(--t-base), background var(--t-base);\n }\n .pp-grid .pp-create-card:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }\n .pp-grid .pp-create-card .pc-plus {\n width: 44px; height: 44px;\n border-radius: 50%;\n background: var(--heat); color: var(--accent-white);\n display: grid; place-items: center;\n transition: filter var(--t-base);\n }\n .pp-grid .pp-create-card:hover .pc-plus { filter: brightness(1.06); }\n .pp-grid .pp-create-card .pc-plus svg { width: 18px; height: 18px; }\n .pp-grid .pp-create-card .pc-t { font-size: 13px; font-weight: 600; }\n .pp-grid .pp-create-card .pc-d { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }\n\n /* 列表视图 */\n .pp-grid.list-view { display: flex; flex-direction: column; gap: 6px; }\n .pp-grid.list-view .product-card {\n flex-direction: row; align-items: center;\n }\n .pp-grid.list-view .product-thumb { width: 96px; aspect-ratio: 1.4 / 1; flex-shrink: 0; }\n .pp-grid.list-view .product-body { flex: 1; padding: 10px 14px; }\n .pp-grid.list-view .pp-create-card { flex-direction: row; min-height: 56px; gap: 12px; }\n .pp-grid.list-view .pp-create-card .pc-plus { width: 32px; height: 32px; }\n\n /* 空筛选结果 */\n .pp-empty {\n grid-column: 1 / -1;\n padding: 48px 24px; text-align: center;\n border: 1px dashed var(--border-faint);\n border-radius: var(--r-md);\n background: var(--background-lighter);\n color: var(--black-alpha-48);\n font-size: 12.5px; font-family: var(--font-mono); letter-spacing: .02em;\n }\n .pp-empty .reset { display: inline-block; margin-top: 8px; color: var(--heat); cursor: pointer; text-decoration: underline; }\n\n /* 分页 */\n .pp-pager {\n display: flex; align-items: center; gap: 16px;\n margin-top: 18px; padding-top: 14px;\n border-top: 1px solid var(--border-faint);\n font-size: 12.5px; color: var(--black-alpha-56);\n }\n .pp-pager .total { font-family: var(--font-mono); letter-spacing: .02em; }\n .pp-pager .pages { display: inline-flex; gap: 4px; margin-left: auto; }\n .pp-pager .pages button {\n min-width: 28px; height: 28px; padding: 0 8px;\n border: 1px solid var(--border-faint); background: var(--surface);\n border-radius: var(--r-sm);\n cursor: pointer; font-size: 12.5px; color: var(--black-alpha-72); font-family: inherit;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .pp-pager .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pp-pager .pages button.active { background: var(--heat); color: var(--accent-white); border-color: var(--heat); font-weight: 600; }\n .pp-pager .pages button:disabled { opacity: .4; cursor: not-allowed; }\n .pp-pager .pages .ellipsis {\n min-width: 22px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--black-alpha-48); font-family: var(--font-mono);\n }\n .pp-pager .page-size {\n display: inline-flex; align-items: center; gap: 4px;\n height: 28px; padding: 0 10px;\n background: var(--surface); border: 1px solid var(--border-faint);\n border-radius: var(--r-sm);\n cursor: pointer; font-family: inherit; font-size: 12.5px; color: var(--black-alpha-72);\n }\n .pp-pager .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }\n .pp-pager .page-size svg { width: 10px; height: 10px; opacity: .6; }\n\n /* 底部提示 */\n .pp-bottom-tip {\n margin-top: 14px;\n padding: 10px 14px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n font-size: 12.5px; color: var(--black-alpha-56);\n display: flex; align-items: center; gap: 8px;\n }\n .pp-bottom-tip svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-48); }\n .pp-bottom-tip a { color: var(--heat); cursor: pointer; text-decoration: none; }\n .pp-bottom-tip a:hover { text-decoration: underline; }\n\n /* ── Step 1 · product picker grid ── */\n .product-pick-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n @media (max-width: 1100px) { .product-pick-grid { grid-template-columns: repeat(2, 1fr); } }\n .product-pick { display: flex; gap: 12px; padding: 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); cursor: pointer; position: relative; transition: background var(--t-base), border-color var(--t-base); min-width: 0; }\n .product-pick:hover { background: var(--background-lighter); }\n .product-pick.selected { border-color: var(--heat); background: var(--heat-12); }\n .product-pick.selected::after { content: ''; position: absolute; top: 8px; right: 10px; width: 16px; height: 16px; background-color: var(--heat); 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: 10px 10px; border-radius: var(--r-sm); }\n .product-pick .thumb { width: 56px; height: 72px; flex-shrink: 0; }\n .product-pick .body { flex: 1; min-width: 0; padding-right: 18px; }\n .product-pick .name { font-weight: 600; font-size: 13px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }\n .product-pick .meta { margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .product-pick .meta b { color: var(--accent-black); font-weight: 500; }\n .product-pick .tags { margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap; }\n .product-pick .tag-s { font-size: 10.5px; color: var(--black-alpha-56); background: var(--background-lighter); padding: 1px 6px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); }\n .product-pick.selected .tag-s { background: var(--surface); border-color: var(--heat-20); color: var(--heat); }\n\n .product-pick.add { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 6px; border-style: dashed; color: var(--black-alpha-48); min-height: 96px; }\n .product-pick.add:hover { color: var(--heat); border-color: var(--heat); background: var(--heat-12); }\n .product-pick.add .pc { width: 32px; height: 32px; border: 1px solid currentColor; display: grid; place-items: center; border-radius: var(--r-sm); }\n .product-pick.add svg { width: 16px; height: 16px; }\n .product-pick.add .hint { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .product-pick.add.lib:hover { color: var(--heat); border-color: var(--heat); background: var(--heat-12); }\n\n /* ===== 商品库全屏弹窗 (单选,与 model-photo/platform-cover 风格统一) ===== */\n .pl-modal-bg { position: fixed; inset: 0; background: var(--surface); z-index: 998; display: none; }\n .pl-modal-bg.show { display: flex; }\n .pl-modal { margin: 0; flex: 1; background: var(--surface); overflow: hidden; display: flex; flex-direction: column; }\n .pl-modal-h { display: flex; align-items: center; gap: 14px; padding: 14px 28px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; }\n .pl-modal-h h2 { font-size: 16px; font-weight: 600; }\n .pl-modal-h .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .pl-modal-h .actions { margin-left: auto; display: flex; gap: 10px; }\n .pl-modal-h .x { 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); }\n .pl-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }\n .pl-modal-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 200px 1fr; }\n .pl-side { border-right: 1px solid var(--border-faint); padding: 18px 0; overflow-y: auto; }\n .pl-side .pl-side-h { padding: 0 20px 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; }\n .pl-side .pl-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); }\n .pl-side .pl-side-item:hover { background: var(--black-alpha-4); }\n .pl-side .pl-side-item.active { background: var(--heat-12); color: var(--accent-black); border-left-color: var(--heat); font-weight: 600; }\n .pl-side .pl-side-item .ct { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }\n .pl-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; }\n .pl-toolbar { padding: 14px 28px; border-bottom: 1px solid var(--border-faint); display: flex; align-items: center; gap: 12px; flex-shrink: 0; }\n .pl-toolbar .search { position: relative; flex: 1; max-width: 360px; }\n .pl-toolbar .search input { width: 100%; height: 32px; padding: 0 10px 0 32px; background: var(--background-lighter); border: 1px solid var(--black-alpha-12); border-radius: var(--r-sm); font-size: 12.5px; font-family: inherit; color: var(--accent-black); outline: none; box-sizing: border-box; }\n .pl-toolbar .search svg { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--black-alpha-48); }\n .pl-toolbar .btn-new { 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; margin-left: auto; }\n .pl-toolbar .btn-new:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }\n .pl-toolbar .btn-new svg { width: 13px; height: 13px; }\n .pl-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }\n .pl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }\n .pl-card { position: relative; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px; cursor: pointer; display: flex; flex-direction: column; gap: 6px; transition: background var(--t-base), border-color var(--t-base); }\n .pl-card:hover { background: var(--surface); }\n .pl-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .pl-card .pl-thumb { aspect-ratio: 1; border-radius: var(--r-sm); }\n .pl-card .pl-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }\n .pl-card .pl-meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .pl-card .pl-check { position: absolute; top: 16px; right: 16px; width: 22px; height: 22px; background: rgba(255,255,255,.95); border: 1.5px solid var(--black-alpha-24); border-radius: 50%; display: grid; place-items: center; z-index: 2; color: var(--accent-white); }\n .pl-card .pl-check svg { width: 11px; height: 11px; opacity: 0; }\n .pl-card.selected .pl-check { background: var(--heat); border-color: var(--heat); }\n .pl-card.selected .pl-check svg { opacity: 1; }\n .pl-modal-f { padding: 14px 28px; border-top: 1px solid var(--border-faint); display: flex; justify-content: flex-end; align-items: center; gap: 10px; flex-shrink: 0; }\n .pl-modal-f .summary { margin-right: auto; font-family: var(--font-mono); font-size: 12px; color: var(--black-alpha-56); letter-spacing: .02em; }\n .pl-modal-f .summary b { color: var(--heat); font-weight: 700; }\n\n /* ── Step 2 · source-type cards ── */\n .source-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }\n .source-card { display: flex; flex-direction: column; gap: 8px; padding: 16px 16px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); cursor: pointer; position: relative; transition: background var(--t-base), border-color var(--t-base); min-height: 132px; }\n .source-card:hover { background: var(--background-lighter); }\n .source-card.selected { border-color: var(--heat); background: var(--heat-12); }\n .source-card.selected::after { content: ''; position: absolute; top: 10px; right: 12px; width: 16px; height: 16px; background-color: var(--heat); 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: 10px 10px; border-radius: var(--r-sm); }\n .source-card .src-ic { width: 32px; height: 32px; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: var(--r-sm); display: grid; place-items: center; }\n .source-card .src-ic svg { width: 16px; height: 16px; }\n .source-card.selected .src-ic { background: var(--surface); color: var(--heat); border-color: var(--heat-20); }\n .source-card h4 { font-size: 14px; font-weight: 600; }\n .source-card .src-tag { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .06em; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 1px 6px; align-self: flex-start; }\n .source-card.selected .src-tag { color: var(--heat); border-color: var(--heat-20); }\n .source-card .src-desc { font-size: 12px; color: var(--black-alpha-56); line-height: 1.55; margin-top: auto; }\n\n .source-detail { margin-top: 16px; padding: 18px 20px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); }\n .source-detail .sd-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 10px; }\n .source-detail .sd-h b { color: var(--accent-black); font-weight: 500; }\n\n /* ── shared field styles ── */\n .field { display: block; margin-bottom: 16px; }\n .config-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: end; margin-bottom: 16px; }\n .config-row .field { margin-bottom: 0; }\n .duration-select { cursor: pointer; }\n @media (max-width: 900px) { .config-row { grid-template-columns: 1fr; } }\n .field-label { display: block; font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; margin-bottom: 6px; }\n .field-label .req { color: var(--heat); margin-left: 2px; }\n .field-hint { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 4px; }\n .input, .textarea { width: 100%; height: 36px; padding: 0 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 13px; color: var(--accent-black); font-family: inherit; transition: border-color var(--t-base); }\n .input:focus, .textarea:focus { outline: none; border-color: var(--heat); }\n .textarea { height: auto; padding: 10px 12px; resize: vertical; min-height: 120px; line-height: 1.55; }\n\n /* ── Step 4 · confirm grid / billing / balance / eta / tos ── */\n .confirm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 18px; }\n .confirm-card { position: relative; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); padding: 14px 16px; }\n .confirm-card .cc-h { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 10px; }\n .confirm-card .cc-edit { font-size: 11.5px; color: var(--black-alpha-56); letter-spacing: 0; font-family: var(--font-sans, 'Inter'); text-transform: none; padding: 2px 8px; border: 1px solid var(--border-faint); background: var(--surface); cursor: pointer; border-radius: var(--r-sm); }\n .confirm-card .cc-edit:hover { color: var(--heat); border-color: var(--heat-20); }\n .confirm-card .cc-body { font-size: 13px; color: var(--accent-black); }\n .confirm-card .cc-body .ln { display: flex; align-items: center; gap: 8px; padding: 2px 0; font-size: 12.5px; color: var(--black-alpha-56); flex-wrap: wrap; }\n .confirm-card .cc-body .ln b { color: var(--accent-black); font-weight: 500; }\n\n .section-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin: 18px 0 10px; }\n\n .bill-list { border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }\n .bill-row { display: grid; grid-template-columns: 1fr auto 80px; align-items: baseline; gap: 12px; padding: 11px 16px; border-bottom: 1px solid var(--border-faint); }\n .bill-row:last-child { border-bottom: 0; }\n .bill-row .l { font-size: 12.5px; color: var(--accent-black); }\n .bill-row .l .l-sub { color: var(--black-alpha-48); font-size: 11.5px; margin-left: 6px; }\n .bill-row .qty { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; text-align: right; }\n .bill-row .amt { font-family: var(--font-mono); font-size: 12.5px; color: var(--accent-black); font-variant-numeric: tabular-nums; text-align: right; }\n .bill-row.subtotal { background: var(--background-lighter); }\n .bill-row.subtotal .l { color: var(--black-alpha-56); font-size: 12px; }\n .bill-row.total { background: var(--background-lighter); border-top: 1px solid var(--black-alpha-12); }\n .bill-row.total .l { font-weight: 600; font-size: 13px; }\n .bill-row.total .amt { font-size: 16px; font-weight: 600; color: var(--accent-black); }\n .bill-row.total .amt small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; margin-left: 2px; }\n\n .balance-row { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-top: 10px; }\n .balance-row .bl { display: flex; align-items: center; gap: 8px; flex: 1; flex-wrap: wrap; }\n .balance-row .bl svg { width: 14px; height: 14px; color: var(--black-alpha-56); }\n .balance-row .bl .lbl { font-size: 12.5px; color: var(--black-alpha-56); }\n .balance-row .bl .val { font-family: var(--font-mono); font-size: 14px; color: var(--accent-black); font-variant-numeric: tabular-nums; font-weight: 500; }\n .balance-row .bl .arrow { color: var(--black-alpha-48); font-family: var(--font-mono); }\n .balance-row.low .bl .val.after { color: var(--accent-crimson); }\n .balance-row .pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 11.5px; font-weight: 500; border: 1px solid; white-space: nowrap; margin-left: auto; }\n .balance-row .pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }\n .balance-row .pill.ok { background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest); }\n .balance-row .pill.err { background: var(--crimson-bg); border-color: var(--crimson-bd); color: var(--accent-crimson); }\n .balance-row .pill.err a { margin-left: 4px; text-decoration: underline; cursor: pointer; }\n\n .eta-block { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }\n .eta-tile { padding: 14px 16px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); }\n .eta-tile .lbl { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 6px; }\n .eta-tile .v { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }\n .eta-tile .v small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; margin-left: 2px; }\n .eta-tile .desc { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 6px; }\n\n /* ── SVG checkbox · per design spec (no CSS hack) ── */\n .check-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 12.5px; color: var(--black-alpha-56); cursor: pointer; user-select: none; }\n .check-row:hover .check-box { border-color: var(--black-alpha-56); }\n .check-box { width: 16px; height: 16px; background: var(--surface); border: 1px solid var(--black-alpha-24); flex-shrink: 0; border-radius: var(--r-sm); display: grid; place-items: center; transition: background var(--t-base), border-color var(--t-base); }\n .check-row.on .check-box { background: var(--heat); border-color: var(--heat); 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: 11px 11px; }\n .check-row.on .lab { color: var(--accent-black); }\n .check-row .lab b { color: var(--accent-black); font-weight: 500; }\n .check-row .lab .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); margin-left: 6px; letter-spacing: .02em; }\n\n .tos-row { display: flex; align-items: center; gap: 10px; padding: 14px 16px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-top: 14px; cursor: pointer; font-size: 12.5px; color: var(--black-alpha-56); user-select: none; }\n .tos-row:hover .check-box { border-color: var(--black-alpha-56); }\n .tos-row.on { background: var(--heat-12); border-color: var(--heat-20); }\n .tos-row.on .check-box { background: var(--heat); border-color: var(--heat); 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: 11px 11px; }\n .tos-row.on .lab { color: var(--accent-black); }\n .tos-row .lab a { color: var(--heat); text-decoration: underline; cursor: pointer; }\n\n /* preview panel */\n .wiz-preview { position: sticky; top: 24px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px; }\n .wiz-preview input,\n .wiz-preview textarea,\n .wiz-preview select { box-sizing: border-box; max-width: 100%; }\n .wiz-preview::before, .wiz-preview::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; }\n .wiz-preview::before { top: -7px; left: -7px; }\n .wiz-preview::after { bottom: -7px; right: -7px; }\n .pv-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; margin-bottom: 12px; text-transform: uppercase; display: flex; justify-content: space-between; }\n .pv-h .live { display: inline-flex; align-items: center; gap: 5px; color: var(--heat); }\n .pv-h .live::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--heat); animation: pulse 1.6s ease-in-out infinite; }\n @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }\n .pv-title { font-size: 14px; font-weight: 600; line-height: 1.3; margin-bottom: 14px; word-break: break-all; }\n .pv-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border-faint); border: 1px solid var(--border-faint); margin-bottom: 14px; }\n .pv-metric { padding: 10px 12px; background: var(--surface); }\n .pv-metric .l { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }\n .pv-metric .v { font-size: 18px; font-weight: 600; margin-top: 3px; font-variant-numeric: tabular-nums; color: var(--accent-black); }\n .pv-metric .v small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; }\n .pv-metric.accent .v { color: var(--heat); }\n .pv-section { margin-top: 14px; }\n .pv-section .lbl { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 8px; }\n .pv-flow { display: flex; flex-wrap: wrap; gap: 4px 0; font-size: 11.5px; color: var(--black-alpha-56); align-items: center; line-height: 1.7; }\n .pv-flow .node { padding: 2px 7px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--accent-black); font-weight: 500; }\n .pv-flow .arrow { color: var(--heat); margin: 0 5px; display: inline-flex; align-items: center; }\n .pv-flow .arrow svg { display: block; }\n .pv-list { list-style: none; padding: 0; margin: 0; }\n .pv-list li { font-size: 11.5px; color: var(--black-alpha-56); padding: 4px 0; display: flex; align-items: center; gap: 6px; }\n .pv-list li::before { content: ''; width: 11px; height: 11px; flex-shrink: 0; background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FA5D19' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12l5 5L20 6'/%3E%3C/svg%3E\") no-repeat center; background-size: contain; }\n .pv-foot { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border-faint); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); display: flex; justify-content: space-between; }\n .pv-foot strong { color: var(--accent-black); font-weight: 500; }\n /* 精简版计费 + 余额 */\n .pv-bill-summary { margin-top: 14px; padding: 10px 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); display: flex; flex-direction: column; gap: 6px; }\n .pv-bill-summary .row { display: flex; align-items: baseline; justify-content: space-between; font-size: 12px; }\n .pv-bill-summary .row .k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .pv-bill-summary .row .v { font-variant-numeric: tabular-nums; color: var(--accent-black); }\n .pv-bill-summary .row .v b { color: var(--heat); font-size: 14px; font-weight: 700; }\n .pv-bill-summary .row .v.low { color: var(--accent-crimson); }\n .pv-agree-row { margin-top: 10px; }\n .pv-agree-row label { display: flex; align-items: flex-start; gap: 6px; cursor: pointer; font-size: 11.5px; color: var(--black-alpha-56); line-height: 1.5; }\n .pv-agree-row input[type=\"checkbox\"] { margin-top: 2px; cursor: pointer; flex-shrink: 0; }\n .pv-agree-row a { color: var(--heat); }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>新建项目</h1>\n <div class=\"sub\"><span class=\"mono\">// 商品 → 配置 · 2 步开始生成</span></div>\n </div>\n <div class=\"actions\">\n <a class=\"btn btn-ghost\" href=\"projects.html\">退出</a>\n </div>\n</div>\n\n<div class=\"wizard\">\n <nav class=\"steps\" id=\"rail\"></nav>\n <div id=\"wiz-body\"></div>\n <aside class=\"wiz-preview\" id=\"preview\"></aside>\n</div>\n\n</div>\n\n<!-- ===== 商品库 全屏弹窗 (单选,新建项目用) ===== -->\n<div class=\"pl-modal-bg\" id=\"pl-modal-bg\">\n <div class=\"pl-modal\">\n <div class=\"pl-modal-h\">\n <h2>商品库</h2>\n <span class=\"ct\" id=\"pl-total-ct\"></span>\n <div class=\"actions\">\n <button class=\"x\" type=\"button\" id=\"pl-close-btn\" aria-label=\"关闭\">\n <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>\n </button>\n </div>\n </div>\n <div class=\"pl-modal-body\">\n <aside class=\"pl-side\" id=\"pl-side\">\n <!-- JS 渲染分类 (动态从 PRODUCTS) -->\n </aside>\n <div class=\"pl-main\">\n <div class=\"pl-toolbar\">\n <div class=\"search\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" id=\"pl-search-input\" placeholder=\"搜索商品名\">\n </div>\n <button class=\"btn-new\" type=\"button\" id=\"pl-new-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>\n 新建商品\n </button>\n </div>\n <div class=\"pl-scroll\">\n <div class=\"pl-grid\" id=\"pl-grid\"></div>\n </div>\n </div>\n </div>\n <div class=\"pl-modal-f\">\n <div class=\"summary\">// 单选:点击商品即选用,自动关闭</div>\n <button class=\"btn\" type=\"button\" id=\"pl-cancel-btn\">取消</button>\n </div>\n </div>\n</div>\n\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script src=\"/exact/assets/new-product-drawer.js?v=202605211643\"></script>\n<script>Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: '新建项目' }] });</script>\n\n<script>\n/* ============================================================\n 新建项目 · 4 步动态向导 (vanilla JS state machine)\n ============================================================ */\n(function () {\n 'use strict';\n\n /* ---------- data ---------- */\n\n const PRODUCTS = [\n { id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'], date: '2026-05-15' },\n { id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'], date: '2026-05-12' },\n { id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'], date: '2026-05-10' },\n { id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'], date: '2026-05-08' },\n { id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'], date: '2026-05-05' },\n { id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'], date: '2026-05-03' },\n { id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'], date: '2026-04-30' },\n ];\n\n const RECENT_IDS = ['mask', 'sun', 'coffee', 'earphone'];\n const CATS = ['全部', '美妆个护', '数码 3C', '食品饮料', '家居家电', '运动户外'];\n\n const SOURCES = [\n { id: 'ai', name: 'AI 全生', tag: '最常用', desc: 'LLM 全权决定脚本走向,最省事。后续仍可在故事板阶段微调。',\n icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z\"/><path d=\"M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z\"/></svg>' },\n { id: 'theme', name: '一句话主题', tag: '轻引导', desc: '你给一句切入主题,AI 按此扩写。推荐 530 字。',\n icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/><path d=\"M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14\"/></svg>' },\n { id: 'manual', name: '自带脚本', tag: '我已有稿', desc: '粘贴或上传完整脚本,系统按镜头自动切分并适配商品。',\n icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><path d=\"M14 2v6h6M9 13h6M9 17h6\"/></svg>' },\n ];\n\n const DURATIONS = [\n { id: '0-10', label: '0-10 秒', shots: [3, 4], tag: '黄金完播', completion: 52, conversion: 1.6 },\n { id: '0-15', label: '0-15 秒', shots: [4, 5], tag: '完播率最佳', completion: 42, conversion: 1.8 },\n { id: '0-30', label: '0-30 秒', shots: [6, 8], tag: '卖点详解', completion: 32, conversion: 2.1 },\n { id: '0-60', label: '0-60 秒', shots: [10, 12], tag: '故事化', completion: 26, conversion: 2.4 },\n ];\n\n const STYLES = [\n { id: 'pain', name: '痛点种草', note: '用户痛点切入,以「我懂你」的口吻引出产品。', tag: '最常用', flow: ['痛点', '共鸣', '产品', '效果', '引导'] },\n { id: 'review', name: '开箱测评', note: '朋友式分享,从开箱到使用感受娓娓道来。', flow: ['开箱', '首印象', '试用', '对比', '结论'] },\n { id: 'compare', name: '对比展示', note: '「用前 vs 用后 / 同类 vs 本品」直观呈现。', flow: ['对照', '差距', '本品', '数据', '购买'] },\n ];\n\n const PERSONAS = [\n { id: 'urban', name: '都市白领女性', sub: '25-30 岁', metric: '大盘消费力', defaults: { duration: '0-15', style: 'pain' } },\n { id: 'bestie', name: '闺蜜种草', sub: '邻家女孩', metric: '复购最高', defaults: { duration: '0-15', style: 'pain' } },\n { id: 'ceo', name: '总裁亲选', sub: '创始人 IP', metric: '30 万销额案例', defaults: { duration: '0-30', style: 'pain' } },\n { id: 'reviewer', name: '专业测评师', sub: '垂类达人', metric: '互动 +30%', defaults: { duration: '0-30', style: 'review' } },\n { id: 'mom', name: '实用宝妈', sub: '家庭决策者', metric: '母婴/家清稳', defaults: { duration: '0-30', style: 'pain' } },\n { id: 'genz', name: '学生党', sub: 'Z 世代 18-24', metric: '平价快消', defaults: { duration: '0-10', style: 'compare' } },\n ];\n\n /* ---------- 合并其它页面创建的商品 ---------- */\n // 商品库 / 工作台 / 新建项目 都共用同一 drawer(assets/new-product-drawer.js),\n // 该 drawer 在 save() 时把新商品写入 sessionStorage['fs-extra-products']。\n // 这里在 PRODUCTS hardcoded 数据后,把 storage 中尚不在 PRODUCTS 的商品 unshift\n // 到列表头部 → 用户跨页面新建的商品在 step 1 也能立即看到。\n (function mergeExtraProducts() {\n try {\n const raw = sessionStorage.getItem('fs-extra-products');\n if (!raw) return;\n const list = JSON.parse(raw);\n if (!Array.isArray(list) || !list.length) return;\n const existingIds = new Set(PRODUCTS.map(p => p.id));\n // 按 createdAt 倒序(最新在前),逐个 unshift\n list.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)).forEach(x => {\n if (existingIds.has(x.id)) return;\n existingIds.add(x.id);\n PRODUCTS.unshift({\n id: x.id,\n name: x.name || '未命名商品',\n cat: x.cat || '未分类',\n price: null, // 表单未采集价格\n imgs: Math.max(1, x.assets || 0), // 用素材数兜底,至少 1\n points: Array.isArray(x.bullets) ? x.bullets : [],\n tags: x.target ? String(x.target).split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],\n date: x.date || (x.createdAt ? new Date(x.createdAt).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10)),\n createdAt: x.createdAt || Date.now(),\n });\n });\n } catch (e) { /* storage 不可用就静默放弃 */ }\n })();\n\n const USER_EMAIL = 'li@shop.com';\n const ACCOUNT_BALANCE = 327.40;\n\n /* ---------- state ---------- */\n\n const state = {\n currentStep: 1,\n productId: null,\n pickSearch: '',\n pickCat: '全部',\n pickSort: 'recent', // recent | name | added\n pickView: 'grid', // grid | list\n pickPage: 1,\n pickPageSize: 7, // 固定 4 列 × 2 行 = 8 tile,首页含 createCard 占 1 位 → 7 商品\n sourceId: null,\n themeText: '',\n manualScript: '',\n projectName: '',\n duration: null,\n scriptStyle: null,\n persona: null,\n points: {},\n recoDismissed: false,\n notifyEmail: true,\n notifyWeChat: false,\n agreed: false,\n };\n\n /* ---------- helpers ---------- */\n\n function $(sel) { return document.querySelector(sel); }\n function esc(s) {\n return String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n }\n function getProduct() { return PRODUCTS.find(p => p.id === state.productId) || null; }\n function getSource() { return SOURCES.find(s => s.id === state.sourceId) || null; }\n function getPersona() { return PERSONAS.find(p => p.id === state.persona) || null; }\n function getDuration() { return DURATIONS.find(d => d.id === state.duration) || null; }\n function getStyle() { return STYLES.find(s => s.id === state.scriptStyle) || null; }\n function getShots() { const d = getDuration(); return d ? (d.shots[0] + d.shots[1]) / 2 : 0; }\n function getCost() {\n // 全部返回 number,使用方各自 toFixed(2),避免 stringify 后再调 toFixed 报错\n const p = getProduct();\n const script = 0.20;\n const assets = p ? p.imgs * 0.30 : 0;\n const sb = 0.40; // 故事板 storyboard\n const video = getShots() * 0.30; // 视频片段\n const exp = 0.10; // 拼接导出\n const subtotal = script + assets + sb + video + exp;\n const fee = +(subtotal * 0.05).toFixed(2);\n const total = +(subtotal + fee).toFixed(2);\n return {\n script, assets, sb, video, export: exp,\n storyboard: sb, render: video, // 别名,兼容旧调用点\n subtotal, fee, total,\n };\n }\n function balanceAfter() { return +(ACCOUNT_BALANCE - getCost().total).toFixed(2); }\n function etaMinutes() {\n const p = getProduct();\n return Math.max(3, Math.round(2 + getShots() * 0.4 + (p ? p.imgs * 0.2 : 0)));\n }\n function canPass1() { return !!state.productId; }\n function canPass2() {\n const s = getSource(); if (!s) return false;\n if (s.id === 'theme') return state.themeText.trim().length >= 4;\n if (s.id === 'manual') return state.manualScript.trim().length >= 20;\n return true;\n }\n function canPass3() { return state.projectName.trim().length >= 2; }\n function canFinish() { return canPass1() && canPass2() && canPass3() && state.agreed && balanceAfter() >= 5; }\n\n /* ---------- icons ---------- */\n\n const icon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';\n const ICONS = {\n check: icon('check'),\n search: icon('search'),\n plus: icon('plus'),\n x: icon('x'),\n bulb: icon('info'),\n arrow: icon('arrowRight'),\n wallet: icon('wallet'),\n };\n\n /* ---------- actions ---------- */\n\n function selectProduct(id) {\n state.productId = id;\n const p = getProduct();\n if (!state.projectName) {\n state.projectName = p.name.split(' ')[0] + ' · 痛点种草 · v1';\n }\n state.points = {};\n p.points.forEach(pt => { state.points[pt] = false; });\n render();\n }\n\n function selectSource(id) {\n state.sourceId = id;\n render();\n }\n\n function goNext() {\n if (state.currentStep === 1 && !canPass1()) return;\n if (state.currentStep === 2 && !canPass2()) return;\n if (state.currentStep === 3 && !canPass3()) return;\n if (state.currentStep < 4) state.currentStep++;\n render();\n window.scrollTo({ top: 0, behavior: 'smooth' });\n }\n function goPrev() {\n if (state.currentStep > 1) state.currentStep--;\n render();\n window.scrollTo({ top: 0, behavior: 'smooth' });\n }\n function jumpTo(n) {\n if (n < state.currentStep) {\n state.currentStep = n;\n render();\n window.scrollTo({ top: 0, behavior: 'smooth' });\n }\n }\n\n function applyPreset() {\n const p = getPersona();\n state.duration = p.defaults.duration;\n state.scriptStyle = p.defaults.style;\n state.recoDismissed = false;\n render();\n }\n\n function startGenerate() {\n const p = getProduct(), d = getDuration(), st = getStyle();\n if (!p || !d || !st || state.projectName.trim().length < 2) return;\n // 持久化项目, 让 projects.html 下次加载时自动 prepend 到列表\n try {\n const seconds = (d.id.split('-')[1] || '15');\n const baseName = state.projectName.trim();\n const project = {\n id: 'proj-' + Date.now(),\n name: baseName + (/\\sv\\d+$/.test(baseName) ? '' : ' · v1'),\n product: p.name,\n source: '',\n shots: Math.round(getShots()),\n durationLabel: '0-' + seconds + 's',\n status: 'wip',\n stage: 1,\n pillText: '脚本生成中',\n createdAt: Date.now(),\n };\n const KEY = 'fs-extra-projects';\n const list = JSON.parse(localStorage.getItem(KEY) || '[]');\n list.push(project);\n localStorage.setItem(KEY, JSON.stringify(list));\n } catch (e) { /* localStorage 不可用时降级到只跳转 */ }\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('项目已创建', '进入 pipeline · Stage 1 脚本');\n setTimeout(() => { location.href = 'pipeline.html?stage=1&product=' + encodeURIComponent(p.name); }, 300);\n }\n\n function openNewProduct() {\n if (!window.NewProductDrawer) {\n if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('Drawer 未加载', '检查 new-product-drawer.js');\n return;\n }\n window.NewProductDrawer.open({\n onSave: function (p) {\n // p = { id, name, cat, target, points: string[], images: [...], imgs: N }\n // 适配 wizard 数据结构\n const now = Date.now();\n const product = {\n id: p.id,\n name: p.name,\n cat: p.cat,\n price: null, // 表单暂未收集价格,显示时跳过\n imgs: p.imgs,\n points: p.points,\n tags: p.target ? p.target.split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],\n date: new Date(now).toISOString().slice(0, 10),\n createdAt: now,\n };\n // 置顶插入,让用户立刻看到\n PRODUCTS.unshift(product);\n // 自动选中(同时种子项目名 / 卖点),触发 render() 刷新 step 1\n selectProduct(product.id);\n // 商品库若开着 → 强制 reset filter/query/搜索框,确保新商品一定可见\n const libBg = document.getElementById('pl-modal-bg');\n if (libBg && libBg.classList.contains('show')) {\n _plCatFilter = '';\n _plQuery = '';\n const searchInput = document.getElementById('pl-search-input');\n if (searchInput) searchInput.value = '';\n document.getElementById('pl-total-ct').textContent = '// 共 ' + PRODUCTS.length + ' 个商品';\n renderProdLibSide();\n renderProdLib();\n }\n }\n });\n }\n\n // ─── 商品库全屏弹窗 (单选) ───\n let _plQuery = '';\n let _plCatFilter = '';\n function renderProdLib() {\n const grid = document.getElementById('pl-grid');\n let list = PRODUCTS;\n if (_plCatFilter) list = list.filter(x => x.cat === _plCatFilter);\n if (_plQuery) list = list.filter(x => x.name.includes(_plQuery));\n grid.innerHTML = list.map(p => `\n <div class=\"pl-card${state.productId === p.id ? ' selected' : ''}\" data-id=\"${p.id}\">\n <div class=\"pl-check\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></div>\n <div class=\"placeholder pl-thumb\"><span class=\"ph-frame\">${esc(p.name)}</span></div>\n <div class=\"pl-name\">${esc(p.name)}</div>\n <div class=\"pl-meta\">${esc(p.cat)}${p.price != null ? ' · ¥' + p.price : ''} · ${p.imgs} 张图</div>\n </div>\n `).join('');\n grid.querySelectorAll('.pl-card').forEach(card => {\n card.addEventListener('click', () => {\n selectProduct(card.dataset.id);\n closeProdLib();\n });\n });\n }\n function renderProdLibSide() {\n // 动态分类(从 PRODUCTS 统计)\n const counts = { '全部': PRODUCTS.length };\n PRODUCTS.forEach(p => { counts[p.cat] = (counts[p.cat] || 0) + 1; });\n const cats = ['全部', ...Object.keys(counts).filter(k => k !== '全部')];\n const side = document.getElementById('pl-side');\n side.innerHTML = '<div class=\"pl-side-h\">分类</div>' +\n cats.map(c => `<div class=\"pl-side-item${(_plCatFilter === '' && c === '全部') || _plCatFilter === c ? ' active' : ''}\" data-cat=\"${c === '全部' ? '' : esc(c)}\">${esc(c)} <span class=\"ct\">${counts[c]}</span></div>`).join('');\n side.querySelectorAll('.pl-side-item').forEach(item => {\n item.addEventListener('click', () => {\n side.querySelectorAll('.pl-side-item').forEach(x => x.classList.remove('active'));\n item.classList.add('active');\n _plCatFilter = item.dataset.cat;\n renderProdLib();\n });\n });\n }\n function openProdLib() {\n _plQuery = '';\n _plCatFilter = '';\n document.getElementById('pl-search-input').value = '';\n document.getElementById('pl-total-ct').textContent = '// 共 ' + PRODUCTS.length + ' 个商品';\n renderProdLibSide();\n renderProdLib();\n document.getElementById('pl-modal-bg').classList.add('show');\n }\n function closeProdLib() {\n document.getElementById('pl-modal-bg').classList.remove('show');\n }\n\n // expose for inline onclick\n window._wiz = {\n selectProduct, selectSource, goNext, goPrev, jumpTo, applyPreset, startGenerate, openNewProduct,\n openProdLib, closeProdLib,\n setSearch: v => { state.pickSearch = v; state.pickPage = 1; renderStep1Only(); },\n setCat: v => { state.pickCat = v; state.pickPage = 1; renderStep1Only(); },\n setSort: v => { state.pickSort = v; state.pickPage = 1; renderStep1Only(); },\n setView: v => { state.pickView = v; renderStep1Only(); },\n setPage: v => { state.pickPage = Math.max(1, v); renderStep1Only(); },\n setPageSize: v => { state.pickPageSize = v; state.pickPage = 1; renderStep1Only(); },\n clearPickFilters: () => { state.pickSearch=''; state.pickCat='全部'; state.pickPage=1; renderStep1Only(); },\n setTheme: v => { state.themeText = v; updateFootOnly(); updatePreviewLive(); },\n setScript: v => { state.manualScript = v; updateFootOnly(); },\n setName: v => { state.projectName = v; updatePreviewLive(); updateFootOnly(); updateRailOnly(); },\n setDur: v => { state.duration = v; render(); },\n setStyle: v => { state.scriptStyle = v; render(); },\n setPersona:v => { state.persona = v; state.recoDismissed = false; render(); },\n togglePt: k => { state.points[k] = !state.points[k]; render(); },\n dismissReco: () => { state.recoDismissed = true; render(); },\n toggleEmail: () => { state.notifyEmail = !state.notifyEmail; render(); },\n toggleWeChat: () => { state.notifyWeChat = !state.notifyWeChat; render(); },\n toggleTos: () => { state.agreed = !state.agreed; render(); },\n toggleAgree: v => { state.agreed = v; render(); },\n setProjectName: v => {\n state.projectName = v;\n // 仅更新预览的标题, 不重新 render (避免 input 失焦)\n const titleEl = $('#preview .pv-title');\n if (titleEl) {\n // 仅替换标题文字部分,保留 input\n const p = getProduct();\n const text = state.projectName || (p ? p.name + ' · 待命名' : '未命名项目');\n const node = titleEl.childNodes[0];\n if (node && node.nodeType === 3) node.nodeValue = text;\n }\n },\n };\n\n /* ============================================================\n RENDER\n ============================================================ */\n\n function railConfig() {\n const p = getProduct(), du = getDuration(), st = getStyle();\n const cfgReady = !!(du && st);\n return [\n { n: 1, label: '选择商品', desc: p ? p.name : '未选择', done: canPass1() },\n { n: 2, label: '项目配置', desc: cfgReady ? (du.label + ' · ' + st.name) : '时长 · 风格 · 人物', done: cfgReady && canPass3() },\n ];\n }\n\n // 平滑滚动到 step section\n function scrollToStep(n) {\n const target = document.querySelector('#step-pane-' + n);\n if (!target) return;\n target.scrollIntoView({ behavior: 'smooth', block: 'start' });\n }\n // 暴露 (rail 内 onclick 引用)\n window._wiz_scrollToStep = scrollToStep;\n\n function renderRail() {\n const cfg = railConfig();\n const html = cfg.map(s => {\n const stt = s.done ? 'done' : '';\n const numContent = s.done ? ICONS.check : s.n;\n return `<div class=\"step ${stt} clickable\" onclick=\"_wiz_scrollToStep(${s.n})\">\n <div class=\"num\">${numContent}</div>\n <div>\n <div class=\"label\">${esc(s.label)}</div>\n <div class=\"desc\">${esc(s.desc)}</div>\n </div>\n </div>`;\n }).join('');\n $('#rail').innerHTML = html;\n }\n\n function productCardHTML(p) {\n const selected = state.productId === p.id;\n return `<div class=\"product-card${selected ? ' selected' : ''}\" onclick=\"_wiz.selectProduct('${p.id}')\">\n <div class=\"placeholder product-thumb\"><span class=\"ph-frame\">${esc(p.name)} · 1200×800</span></div>\n <div class=\"product-body\">\n <div class=\"product-name\">${esc(p.name)}</div>\n <div class=\"product-cat\">${esc(p.cat)}</div>\n <div class=\"product-date\">${esc(p.date || '')} 创建</div>\n </div>\n </div>`;\n }\n\n function pickerFilteredProducts() {\n const q = (state.pickSearch || '').trim().toLowerCase();\n let list = PRODUCTS.filter(p => {\n if (state.pickCat !== '全部' && p.cat !== state.pickCat) return false;\n if (q) {\n const blob = (p.name + ' ' + p.cat + ' ' + (p.tags || []).join(' ')).toLowerCase();\n if (!blob.includes(q)) return false;\n }\n return true;\n });\n if (state.pickSort === 'name') {\n list = list.slice().sort((a, b) => a.name.localeCompare(b.name, 'zh'));\n } else if (state.pickSort === 'added') {\n list = list.slice();\n } else {\n // recent 排序优先级:\n // 1) 用户新建的商品(有 createdAt) — 按 createdAt 倒序,最新在最前(紧挨 createCard 按钮)\n // 2) RECENT_IDS 命中的商品 — 按命中顺序\n // 3) 其余 — 保持原始顺序\n const recent = new Map(RECENT_IDS.map((id, i) => [id, i]));\n function weight(p) {\n if (p.createdAt) return -p.createdAt; // 负值 → 最大的负数(最近创建)排最前\n if (recent.has(p.id)) return recent.get(p.id);\n return Number.POSITIVE_INFINITY;\n }\n list = list.slice().sort((a, b) => weight(a) - weight(b));\n }\n return list;\n }\n\n function pickerPagerHTML(total) {\n const pageSize = state.pickPageSize;\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n const cur = Math.min(state.pickPage, totalPages);\n function pageBtn(n) {\n const act = n === cur ? ' active' : '';\n return `<button type=\"button\" class=\"${act.trim()}\" onclick=\"_wiz.setPage(${n})\">${n}</button>`;\n }\n const items = [];\n items.push(`<button type=\"button\"${cur === 1 ? ' disabled' : ''} onclick=\"_wiz.setPage(${cur - 1})\"></button>`);\n if (totalPages <= 7) {\n for (let i = 1; i <= totalPages; i++) items.push(pageBtn(i));\n } else {\n items.push(pageBtn(1));\n if (cur > 3) items.push('<span class=\"ellipsis\">…</span>');\n const lo = Math.max(2, cur - 1), hi = Math.min(totalPages - 1, cur + 1);\n for (let i = lo; i <= hi; i++) items.push(pageBtn(i));\n if (cur < totalPages - 2) items.push('<span class=\"ellipsis\">…</span>');\n items.push(pageBtn(totalPages));\n }\n items.push(`<button type=\"button\"${cur === totalPages ? ' disabled' : ''} onclick=\"_wiz.setPage(${cur + 1})\"></button>`);\n return `<div class=\"pp-pager\">\n <span class=\"total\">共 ${total} 条</span>\n <div class=\"pages\">${items.join('')}</div>\n <span class=\"page-size\">每页 ${pageSize} 条</span>\n </div>`;\n }\n\n function renderStep1() {\n const list = pickerFilteredProducts();\n const total = list.length;\n const pageSize = state.pickPageSize;\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n const cur = Math.min(state.pickPage, totalPages);\n const pageList = list.slice((cur - 1) * pageSize, cur * pageSize);\n\n const cats = ['全部', ...new Set(PRODUCTS.map(p => p.cat))];\n const catChipActive = state.pickCat !== '全部';\n const sortLabels = { recent: '最近使用', name: '商品名称', added: '添加顺序' };\n const hasFilter = !!state.pickSearch || state.pickCat !== '全部';\n\n const catMenu = cats.map(c => `<div class=\"mi${state.pickCat === c ? ' selected' : ''}\" onclick=\"_wiz.setCat('${esc(c)}')\">\n <svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\"><polyline points=\"3 8 7 12 13 4\"/></svg>\n <span>${esc(c)}</span>\n </div>`).join('');\n\n const sortMenu = Object.keys(sortLabels).map(k => `<div class=\"mi${state.pickSort === k ? ' selected' : ''}\" onclick=\"_wiz.setSort('${k}')\">\n <svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.6\"><polyline points=\"3 8 7 12 13 4\"/></svg>\n <span>${sortLabels[k]}</span>\n </div>`).join('');\n\n const cards = pageList.map(productCardHTML).join('');\n const createCard = `<div class=\"pp-create-card\" onclick=\"_wiz.openNewProduct()\">\n <div class=\"pc-plus\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg></div>\n <div class=\"pc-t\">创建新商品</div>\n <div class=\"pc-d\">// 在此添加一个新商品</div>\n </div>`;\n const gridContent = total === 0\n ? (createCard + `<div class=\"pp-empty\">// NO MATCH<br>没有符合筛选条件的商品 <span class=\"reset\" onclick=\"_wiz.clearPickFilters()\">[ 清空筛选 ]</span></div>`)\n : (createCard + cards);\n\n return `<div class=\"wiz-pane active\" data-step=\"1\">\n <div class=\"wiz-step-h\">\n <h2>第 1 步 · 选择商品</h2>\n <p>从商品库选一个 SKU。它的主图与卖点会被 LLM 作为脚本/资产生成的素材。</p>\n </div>\n\n <div class=\"pp-toolbar\">\n <div class=\"search-inline\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input type=\"text\" placeholder=\"搜索商品名称、标签\" value=\"${esc(state.pickSearch)}\" oninput=\"_wiz.setSearch(this.value)\">\n </div>\n <div class=\"pp-chip-wrap\" data-key=\"cat\">\n <button class=\"pp-chip${catChipActive ? ' active' : ''}\" type=\"button\" onclick=\"event.stopPropagation(); this.parentElement.classList.toggle('open'); document.querySelectorAll('.pp-chip-wrap.open').forEach(w=>{if(w!==this.parentElement)w.classList.remove('open')})\">\n <span>${state.pickCat === '全部' ? '全部分类' : esc(state.pickCat)}</span>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg>\n </button>\n <div class=\"pp-menu\">${catMenu}</div>\n </div>\n ${hasFilter ? `<button class=\"pp-clear\" type=\"button\" onclick=\"_wiz.clearPickFilters()\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n 清空筛选\n </button>` : ''}\n </div>\n\n <div class=\"pp-result-meta\">// 显示 ${pageList.length} / ${total} 个商品${hasFilter ? ' (已筛选)' : ''}</div>\n\n <div class=\"pp-grid${state.pickView === 'list' ? ' list-view' : ''}\">${gridContent}</div>\n\n ${total > pageSize ? pickerPagerHTML(total) : ''}\n\n <div class=\"pp-bottom-tip\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v5M12 16h.01\"/></svg>\n <span>找不到想要的商品?可<a onclick=\"_wiz.openNewProduct()\">创建新商品</a>,或前往 <a href=\"products.html\">商品库 · 管理商品</a></span>\n </div>\n </div>`;\n }\n\n function renderStep2() {\n const s = getSource();\n let detail = '';\n if (s) {\n if (s.id === 'ai') {\n detail = `<div class=\"source-detail\">\n <div class=\"sd-h\">// 已选 · <b>${esc(s.name)}</b></div>\n <div class=\"field-hint\" style=\"font-size: 12.5px; color: var(--black-alpha-72);\">AI 全生模式无需额外输入。下一步选定时长 / 风格 / 人设后,LLM 会自动决定切入点和卖点权重。</div>\n </div>`;\n } else if (s.id === 'theme') {\n detail = `<div class=\"source-detail\">\n <div class=\"sd-h\">// 已选 · <b>${esc(s.name)}</b></div>\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">一句话主题<span class=\"req\">*</span></label>\n <input class=\"input\" placeholder=\"例:熬夜党的急救面膜 / 加班吃啥不内疚\" value=\"${esc(state.themeText)}\" oninput=\"_wiz.setTheme(this.value)\">\n <div class=\"field-hint\">推荐 530 字。这句话会作为 LLM 扩写的锚点,越具体越聚焦。</div>\n </div>\n </div>`;\n } else if (s.id === 'manual') {\n detail = `<div class=\"source-detail\">\n <div class=\"sd-h\">// 已选 · <b>${esc(s.name)}</b></div>\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">粘贴脚本内容<span class=\"req\">*</span></label>\n <textarea class=\"textarea\" placeholder=\"粘贴你的脚本内容(旁白 / 镜头描述均可,系统会自动切分镜头)\" oninput=\"_wiz.setScript(this.value)\">${esc(state.manualScript)}</textarea>\n <div class=\"field-hint\">最少 20 字。镜头数由你的脚本自然段落决定,时长 / 风格仍会影响后期渲染节奏。</div>\n </div>\n </div>`;\n }\n }\n return `<div class=\"wiz-pane active\" data-step=\"2\">\n <div class=\"wiz-step-h\">\n <h2>第 2 步 · 脚本来源</h2>\n <p>决定 LLM 如何获得初稿脚本。三种方式由「最省事」到「最保真原意」。</p>\n </div>\n <div class=\"source-row\">\n ${SOURCES.map(s => `<div class=\"source-card${state.sourceId === s.id ? ' selected' : ''}\" onclick=\"_wiz.selectSource('${s.id}')\">\n <span class=\"src-ic\">${s.icon}</span>\n <h4>${esc(s.name)}</h4>\n <span class=\"src-tag\">[ ${esc(s.tag)} ]</span>\n <p class=\"src-desc\">${esc(s.desc)}</p>\n </div>`).join('')}\n </div>\n ${detail}\n </div>`;\n }\n\n function renderStep3() {\n const personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();\n let showReco = false, recoDur = null, recoStyle = null;\n if (personaObj && state.duration && state.scriptStyle) {\n const recoMismatch = personaObj.defaults.duration !== state.duration || personaObj.defaults.style !== state.scriptStyle;\n showReco = recoMismatch && !state.recoDismissed;\n recoDur = DURATIONS.find(d => d.id === personaObj.defaults.duration);\n recoStyle = STYLES.find(s => s.id === personaObj.defaults.style);\n }\n\n return `<div class=\"wiz-pane active\" data-step=\"2\">\n <div class=\"wiz-step-h\">\n <h2>第 2 步 · 项目配置</h2>\n <p>这些设置会影响 LLM 生成脚本的方向,确认后会进入流水线第 1 步(脚本生成)。</p>\n </div>\n\n <div class=\"config-row\">\n <div class=\"field\">\n <label class=\"field-label\">项目名称<span class=\"req\">*</span></label>\n <input class=\"input\" value=\"${esc(state.projectName)}\" oninput=\"_wiz.setName(this.value)\">\n </div>\n <div class=\"field\">\n <label class=\"field-label\">视频时长<span class=\"req\">*</span></label>\n <select class=\"input duration-select\" onchange=\"_wiz.setDur(this.value)\">\n <option value=\"\" disabled ${state.duration ? '' : 'selected'}>选择时长</option>\n ${DURATIONS.map(d => `<option value=\"${esc(d.id)}\" ${state.duration === d.id ? 'selected' : ''}>${esc(d.label)} · ${d.shots[0]}-${d.shots[1]} 镜</option>`).join('')}\n </select>\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\">脚本风格</label>\n <div class=\"opt-row cols-4\">\n ${STYLES.map(s => `<div class=\"opt-card${state.scriptStyle === s.id ? ' selected' : ''}\" onclick=\"_wiz.setStyle('${s.id}')\">\n <h4>${esc(s.name)}</h4>\n <div class=\"note\">${esc(s.note)}</div>\n ${s.tag ? `<span class=\"badge\">[ ${esc(s.tag)} ]</span>` : ''}\n </div>`).join('')}\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\">人物设定</label>\n <div class=\"opt-row cols-6\">\n ${PERSONAS.map(p => `<div class=\"opt-card${state.persona === p.id ? ' selected' : ''}\" onclick=\"_wiz.setPersona('${p.id}')\">\n <h4>${esc(p.name)}</h4>\n <div class=\"sub\">${esc(p.sub)}</div>\n <div class=\"metric\"><span class=\"val\">${esc(p.metric)}</span></div>\n </div>`).join('')}\n </div>\n\n ${showReco ? `<div class=\"reco-bubble\">\n <span class=\"ic\">${ICONS.bulb}</span>\n <div class=\"txt\">\n <span>抖音同人设 TOP 视频更常用 <strong>${esc(recoDur.label)}</strong> + <strong>${esc(recoStyle.name)}</strong></span>\n <span class=\"meta\">当前 ${esc(durObj.label)} · ${esc(styleObj.name)} → 推荐换为同人设最优组合</span>\n </div>\n <button class=\"btn-apply\" onclick=\"_wiz.applyPreset()\">一键套用</button>\n <button class=\"dismiss\" onclick=\"_wiz.dismissReco()\" aria-label=\"忽略\">${ICONS.x}</button>\n </div>` : ''}\n </div>\n\n ${Object.keys(state.points).length > 0 ? `<div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">关键卖点(可勾选要重点突出的)</label>\n <div class=\"theme-pill-row\">\n ${Object.entries(state.points).map(([k, v]) => `<button class=\"theme-pill${v ? ' active' : ''}\" type=\"button\" aria-pressed=\"${v ? 'true' : 'false'}\" onclick=\"_wiz.togglePt('${esc(k).replace(/'/g, \"\\\\'\")}')\">${v ? ICONS.check : ''}<span>${esc(k)}</span></button>`).join('')}\n </div>\n </div>` : ''}\n </div>`;\n }\n\n function renderStep4() {\n const p = getProduct(), s = getSource(), personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();\n const c = getCost();\n const ba = balanceAfter();\n const low = ba < 5;\n const eta = etaMinutes();\n const pointsList = Object.entries(state.points).filter(([, v]) => v).map(([k]) => k).join(' / ') || '未选';\n\n return `<div class=\"wiz-pane active\" data-step=\"4\">\n <div class=\"wiz-step-h\">\n <h2>第 4 步 · 确认与计费</h2>\n <p>核对前 3 步的选择 + 计费明细。点击「开始生成」会立刻扣款并进入流水线。</p>\n </div>\n\n <div class=\"confirm-grid\">\n <div class=\"confirm-card\">\n <div class=\"cc-h\"><span>// 商品</span><button class=\"cc-edit\" onclick=\"_wiz.jumpTo(1)\">修改</button></div>\n ${p ? `<div style=\"display:flex; gap:12px; align-items:flex-start;\">\n <div class=\"placeholder\" style=\"width:44px; height:56px; flex-shrink:0;\"><span class=\"ph-frame\">9:16</span></div>\n <div class=\"cc-body\" style=\"min-width:0;\">\n <div style=\"font-weight:600; font-size:13px;\">${esc(p.name)}</div>\n <div class=\"ln\">${esc(p.cat)}${p.price != null ? ' <span style=\"color: var(--black-alpha-32);\">·</span> <b>¥' + p.price + '</b>' : ''} <span style=\"color: var(--black-alpha-32);\">·</span> ${p.imgs} 张图</div>\n </div>\n </div>` : '<div class=\"cc-body\">未选择</div>'}\n </div>\n\n <div class=\"confirm-card\">\n <div class=\"cc-h\"><span>// 脚本来源</span><button class=\"cc-edit\" onclick=\"_wiz.jumpTo(2)\">修改</button></div>\n ${s ? `<div class=\"cc-body\">\n <div style=\"font-weight:600; font-size:13px;\">${esc(s.name)}</div>\n <div class=\"ln\">${s.id === 'ai' ? 'LLM 全权 · 走向由 Step 3 决定'\n : s.id === 'theme' ? '主题:<b style=\"margin-left:4px;\">' + esc(state.themeText || '(未填)') + '</b>'\n : '<b>' + state.manualScript.length + '</b> 字 · 自动切镜'}</div>\n </div>` : '<div class=\"cc-body\">未选择</div>'}\n </div>\n\n <div class=\"confirm-card\">\n <div class=\"cc-h\"><span>// 项目配置</span><button class=\"cc-edit\" onclick=\"_wiz.jumpTo(3)\">修改</button></div>\n <div class=\"cc-body\">\n <div style=\"font-weight:600; font-size:13px;\">${esc(state.projectName)}</div>\n <div class=\"ln\"><b>${esc(styleObj.name)}</b> · ${esc(personaObj.name)} · ${esc(personaObj.sub)}</div>\n <div class=\"ln\" style=\"font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);\">卖点:${esc(pointsList)}</div>\n </div>\n </div>\n\n <div class=\"confirm-card\">\n <div class=\"cc-h\"><span>// 输出参数</span></div>\n <div class=\"cc-body\">\n <div class=\"ln\"><b>${esc(durObj.label)}</b> · <b>${durObj.shots[0]}-${durObj.shots[1]} 镜</b> · 9:16</div>\n <div class=\"ln\">预估完播 <b>${durObj.completion}%</b> · 预估转化 <b>${durObj.conversion}%</b></div>\n <div class=\"ln\" style=\"font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);\">// 数据来源:抖音同品类 TOP 均值</div>\n </div>\n </div>\n </div>\n\n <div class=\"section-sub\">计费明细 · 按量计费</div>\n <div class=\"bill-list\">\n <div class=\"bill-row\"><div class=\"l\">脚本生成 <span class=\"l-sub\">LLM · 1 稿</span></div><div class=\"qty\">× 1</div><div class=\"amt\">¥${c.script}</div></div>\n <div class=\"bill-row\"><div class=\"l\">故事板生成 <span class=\"l-sub\">含分镜画面描述</span></div><div class=\"qty\">× 1</div><div class=\"amt\">¥${c.sb}</div></div>\n <div class=\"bill-row\"><div class=\"l\">资产生成 <span class=\"l-sub\">主图 → 镜头素材</span></div><div class=\"qty\">× ${p ? p.imgs : 0} 张</div><div class=\"amt\">¥${c.assets}</div></div>\n <div class=\"bill-row\"><div class=\"l\">视频渲染 <span class=\"l-sub\">合成 · 配乐 · 字幕</span></div><div class=\"qty\">× ${getShots()} 镜</div><div class=\"amt\">¥${c.render}</div></div>\n <div class=\"bill-row subtotal\"><div class=\"l\">小计</div><div class=\"qty\"></div><div class=\"amt\">¥${c.subtotal}</div></div>\n <div class=\"bill-row subtotal\"><div class=\"l\">平台服务费 <span class=\"l-sub\">5%</span></div><div class=\"qty\"></div><div class=\"amt\">¥${c.fee}</div></div>\n <div class=\"bill-row total\"><div class=\"l\">合计</div><div class=\"qty\"></div><div class=\"amt\">¥${Math.floor(c.total)}<small>.${c.total.toFixed(2).split('.')[1]}</small></div></div>\n </div>\n\n <div class=\"balance-row${low ? ' low' : ''}\">\n <div class=\"bl\">\n ${ICONS.wallet}\n <span class=\"lbl\">账户余额</span>\n <span class=\"val\">¥${ACCOUNT_BALANCE.toFixed(2)}</span>\n <span class=\"arrow\">→</span>\n <span class=\"lbl\">扣款后</span>\n <span class=\"val after\">¥${ba.toFixed(2)}</span>\n </div>\n ${low\n ? `<span class=\"pill err\"><span class=\"dot\"></span>余额不足 · <a>去充值</a></span>`\n : `<span class=\"pill ok\"><span class=\"dot\"></span>余额充足</span>`}\n </div>\n\n <div class=\"section-sub\">预估耗时 · 通知</div>\n <div class=\"eta-block\">\n <div class=\"eta-tile\">\n <div class=\"lbl\">预估出片</div>\n <div class=\"v\">~ ${eta}<small>分钟</small></div>\n <div class=\"desc\">// pipeline 5 阶段累计 · 不含人工审核</div>\n </div>\n <div class=\"eta-tile\">\n <div class=\"lbl\">完成后通知</div>\n <div class=\"check-row${state.notifyEmail ? ' on' : ''}\" onclick=\"_wiz.toggleEmail()\" style=\"padding:4px 0;\">\n <span class=\"check-box\"></span>\n <span class=\"lab\">邮件 <span class=\"mono\">${esc(USER_EMAIL)}</span></span>\n </div>\n <div class=\"check-row${state.notifyWeChat ? ' on' : ''}\" onclick=\"_wiz.toggleWeChat()\" style=\"padding:4px 0;\">\n <span class=\"check-box\"></span>\n <span class=\"lab\">微信 <span class=\"mono\">未绑定 · 去绑定</span></span>\n </div>\n </div>\n </div>\n\n <div class=\"tos-row${state.agreed ? ' on' : ''}\" onclick=\"_wiz.toggleTos()\">\n <span class=\"check-box\"></span>\n <span class=\"lab\">我已阅读并同意 <a>《按量计费协议》</a> 与 <a>《商品素材使用授权》</a></span>\n </div>\n </div>`;\n }\n\n function renderCollapsedStep(n) {\n const p = getProduct(), s = getSource();\n let title = '', body = '';\n if (n === 1) {\n title = '第 1 步 · 选择商品';\n body = p\n ? `<div style=\"display:flex; gap:12px; align-items:flex-start;\">\n <div class=\"placeholder\" style=\"width:44px; height:56px; flex-shrink:0;\"><span class=\"ph-frame\">9:16</span></div>\n <div style=\"min-width:0;\">\n <div style=\"font-weight:600; font-size:13.5px;\">${esc(p.name)}</div>\n <div class=\"mono\" style=\"font-size:11.5px; color: var(--black-alpha-48); margin-top:3px; letter-spacing:.02em;\">${esc(p.cat)}${p.price != null ? ' · ¥' + p.price : ''} · ${p.imgs} 张图 · ${p.points.length} 个卖点</div>\n </div>\n </div>`\n : '<span class=\"mono\" style=\"color: var(--black-alpha-48); font-size: 11.5px;\">未选择</span>';\n } else if (n === 2) {\n title = '第 2 步 · 脚本来源';\n if (s) {\n let extra = '';\n if (s.id === 'theme' && state.themeText) {\n extra = `<span class=\"muted\" style=\"color: var(--black-alpha-56);\">主题:</span><span style=\"font-size: 13px;\">${esc(state.themeText)}</span>`;\n } else if (s.id === 'manual') {\n extra = `<span class=\"muted\" style=\"color: var(--black-alpha-56);\">脚本:</span><span style=\"font-size: 13px;\">${state.manualScript.length} 字</span>`;\n } else {\n extra = `<span class=\"mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em;\">// 走向由 Step 3 决定</span>`;\n }\n body = `<div style=\"display:flex; gap:8px; align-items:center; flex-wrap:wrap;\">\n <span class=\"pill info\" style=\"display:inline-flex; align-items:center; gap:6px; padding: 4px 10px; border-radius: 999px; font-size: 11.5px; background: var(--heat-12); color: var(--heat); border: 1px solid var(--heat-20); font-weight: 500;\"><span class=\"dot\" style=\"width:6px;height:6px;border-radius:50%;background:currentColor;\"></span>${esc(s.name)}</span>\n ${extra}\n </div>`;\n } else {\n body = '<span class=\"mono\" style=\"color: var(--black-alpha-48); font-size: 11.5px;\">未选择</span>';\n }\n }\n return `<div class=\"wiz-pane collapsed\">\n <div class=\"wiz-pane-h\">\n <h3>${esc(title)}</h3>\n <span style=\"flex:1\"></span>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"_wiz.jumpTo(${n})\">修改</button>\n </div>\n ${body}\n </div>`;\n }\n\n function renderFoot() {\n const cfg = railConfig();\n const last = state.currentStep === 4;\n const passOk = state.currentStep === 1 ? canPass1()\n : state.currentStep === 2 ? canPass2()\n : state.currentStep === 3 ? canPass3()\n : canFinish();\n const nextLabel = last ? '开始生成 →' : '下一步 →';\n const hint = last\n ? `// 扣款 ¥${getCost().total.toFixed(2)} · 进入 pipeline`\n : `// 下一步:${cfg[state.currentStep].label}`;\n const action = last ? '_wiz.startGenerate()' : '_wiz.goNext()';\n return `<div class=\"wiz-foot\">\n <button class=\"btn btn-ghost\"${state.currentStep === 1 ? ' disabled' : ''} onclick=\"_wiz.goPrev()\">← 上一步</button>\n <div style=\"display:flex; align-items:center; gap:12px;\">\n <span class=\"mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em;\">${esc(hint)}</span>\n <button class=\"btn btn-primary btn-lg${!passOk ? ' disabled' : ''}\" onclick=\"${action}\">${nextLabel}</button>\n </div>\n </div>`;\n }\n\n function renderPreview() {\n // 实时预估面板已移除 (改为底部「开始」CTA)\n const previewEl = $('#preview');\n if (previewEl) previewEl.innerHTML = '';\n return;\n // legacy code below kept for reference but unreachable\n const p = getProduct(), personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle(), c = getCost();\n const shots = getShots();\n const pointsOn = Object.entries(state.points).filter(([, v]) => v).map(([k]) => k);\n const title = state.projectName || (p ? p.name + ' · 待命名' : '未命名项目');\n\n const arrows = '<span class=\"arrow\">' + ICONS.arrow + '</span>';\n\n const ready = canPass1() && canPass2() && canPass3();\n const balOk = balanceAfter() >= 5;\n const canGo = ready && balOk && state.agreed;\n const footState = !ready ? '填写中' : !balOk ? '余额不足' : !state.agreed ? '待确认协议' : '就绪';\n const stepProgress = [canPass1(), canPass2(), canPass3()].filter(Boolean).length;\n\n const configReady = !!(state.persona && state.duration && state.scriptStyle);\n const summaryBlock = (p || configReady) ? `\n <div class=\"pv-section\">\n <div class=\"lbl\">// 已选</div>\n <ul class=\"pv-list\">\n ${p ? `<li>${esc(p.name)}</li>` : ''}\n ${configReady ? `<li>${esc(personaObj.name)} · ${esc(styleObj.name)} · ${esc(durObj.label)}</li>` : ''}\n </ul>\n </div>` : `\n <div class=\"pv-section\">\n <div class=\"lbl\">// 待选择</div>\n <ul class=\"pv-list\" style=\"opacity: .6;\">\n <li style=\"color: var(--black-alpha-48);\">先选商品 · 预估会自动填充</li>\n </ul>\n </div>`;\n\n // 计费 + 余额(精简版,不展开 5 阶段明细)\n const billingHTML = `\n <div class=\"pv-bill-summary\">\n <div class=\"row\"><span class=\"k\">预估合计</span><span class=\"v\"><b>¥${c.total.toFixed(2)}</b></span></div>\n <div class=\"row\"><span class=\"k\">扣款后余额</span><span class=\"v ${balOk ? '' : 'low'}\">¥${balanceAfter().toFixed(2)}</span></div>\n </div>\n <div class=\"pv-agree-row\">\n <label>\n <input type=\"checkbox\" id=\"pv-agree\" ${state.agreed ? 'checked' : ''} onchange=\"_wiz.toggleAgree(this.checked)\">\n <span>同意 <a href=\"#\" onclick=\"event.preventDefault();Shell.toast('用户协议')\">协议</a> 与 <a href=\"#\" onclick=\"event.preventDefault();Shell.toast('计费规则')\">计费规则</a>(失败不扣费)</span>\n </label>\n </div>\n `;\n\n $('#preview').innerHTML = `\n <div class=\"pv-h\"><span>实时预估</span><span class=\"live\">LIVE</span></div>\n <div class=\"pv-title\">${esc(title)}</div>\n <div class=\"pv-metrics\">\n <div class=\"pv-metric\"><div class=\"l\">镜头</div><div class=\"v\">${shots}<small>镜</small></div></div>\n <div class=\"pv-metric accent\"><div class=\"l\">预估完播</div><div class=\"v\">${durObj.completion}<small>%</small></div></div>\n <div class=\"pv-metric\"><div class=\"l\">预估转化</div><div class=\"v\">${durObj.conversion}<small>%</small></div></div>\n <div class=\"pv-metric\"><div class=\"l\">预估成本</div><div class=\"v\">¥${c.total.toFixed(2)}</div></div>\n </div>\n ${summaryBlock}\n ${billingHTML}\n <button class=\"btn btn-primary btn-lg\" style=\"width:100%;margin-top:14px;justify-content:center;${canGo ? '' : 'opacity:.5;pointer-events:none;cursor:not-allowed;'}\" onclick=\"_wiz.startGenerate()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 3l14 9-14 9V3z\"/></svg>\n 开始生成\n </button>\n <div class=\"pv-foot\">\n <span>${stepProgress}/3 步已完成</span>\n <strong>${footState}</strong>\n </div>\n `;\n }\n\n /* ---------- partial updates (to keep inputs from losing focus) ---------- */\n\n function renderStep1Only() {\n // when user types in search or clicks cat chip — only re-render Step 1 main area\n if (state.currentStep !== 1) return;\n const body = $('#wiz-body');\n const active = body.querySelector('.wiz-pane.active');\n if (active) {\n const tmp = document.createElement('div');\n tmp.innerHTML = renderStep1();\n active.replaceWith(tmp.firstElementChild);\n }\n // refocus search input (旧 .search-input → 新 .pp-toolbar .search-inline)\n const inp = body.querySelector('.pp-toolbar .search-inline input')\n || body.querySelector('.search-input input');\n if (inp && state.pickSearch && document.activeElement !== inp) {\n inp.focus();\n const v = inp.value;\n try { inp.setSelectionRange(v.length, v.length); } catch (e) {}\n }\n }\n\n function updatePreviewLive() { renderPreview(); }\n function updateFootOnly() {\n const body = $('#wiz-body');\n const foot = body.querySelector('.wiz-foot');\n if (foot) {\n const tmp = document.createElement('div');\n tmp.innerHTML = renderFoot();\n foot.replaceWith(tmp.firstElementChild);\n }\n }\n function updateRailOnly() { renderRail(); }\n\n /* ---------- main render ---------- */\n\n function render() {\n renderRail();\n const body = $('#wiz-body');\n // 单页式: 商品 (step1) + 项目配置 (原 step3, 现 step2),底部「开始」CTA\n const p = getProduct(), du = getDuration(), st = getStyle();\n const canStart = !!(p && du && st && state.projectName.trim().length >= 2);\n let html = '';\n html += '<section id=\"step-pane-1\" class=\"step-pane-wrap\">' + renderStep1() + '</section>';\n html += '<section id=\"step-pane-2\" class=\"step-pane-wrap\">' + renderStep3() + '</section>';\n html += `<div class=\"wiz-start-bar\">\n <button class=\"btn-start${canStart ? '' : ' disabled'}\" type=\"button\" onclick=\"_wiz.startGenerate()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 3l14 9-14 9V3z\"/></svg>\n <span>开始</span>\n </button>\n </div>`;\n body.innerHTML = html;\n }\n\n // 商品库弹窗事件绑定 (DOM 静态元素)\n document.getElementById('pl-close-btn').addEventListener('click', closeProdLib);\n document.getElementById('pl-cancel-btn').addEventListener('click', closeProdLib);\n document.getElementById('pl-search-input').addEventListener('input', e => {\n _plQuery = e.target.value.trim();\n renderProdLib();\n });\n document.getElementById('pl-new-btn').addEventListener('click', () => {\n // 商品库保持 open(drawer z-index 1101 > pl-modal-bg 998 会覆盖之上)\n openNewProduct();\n });\n\n // 全局点击 → 关闭 picker chip 菜单\n document.addEventListener('click', e => {\n if (!e.target.closest('.pp-chip-wrap')) {\n document.querySelectorAll('.pp-chip-wrap.open').forEach(w => w.classList.remove('open'));\n }\n });\n\n // initial render\n render();\n})();\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"projects": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"projects.html\">\n<meta charset=\"utf-8\">\n<title>视频项目 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── List view ─── */\n .proj-name-cell { display: flex; align-items: center; gap: 12px; }\n .proj-thumb { width: 40px; height: 52px; flex-shrink: 0; border-radius: var(--r-md); }\n .proj-name { font-weight: 600; color: var(--accent-black); font-size: 13.5px; }\n .proj-sub { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }\n .row-action { display: flex; gap: 4px; visibility: hidden; }\n table.t tbody tr:hover .row-action { visibility: visible; }\n .row-action a { width: 28px; height: 28px; display: grid; place-items: center; color: var(--black-alpha-56); border-radius: var(--r-md); }\n .row-action a:hover { background: var(--surface); color: var(--heat); border: 1px solid var(--border-faint); }\n\n /* ─── View toggle ─── */\n .view-toggle { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }\n .view-toggle button { padding: 0 14px; background: var(--surface); color: var(--black-alpha-56); font-size: 13px; border-right: 1px solid var(--border-faint); border-radius: 0; height: 36px; cursor: pointer; font-family: inherit; display: flex; align-items: center; gap: 6px; transition: background var(--t-base), color var(--t-base); }\n .view-toggle button:last-child { border-right: 0; }\n .view-toggle button:hover { background: var(--background-lighter); color: var(--accent-black); }\n .view-toggle button.active { background: var(--heat-12); color: var(--heat); font-weight: 600; }\n .view-toggle button svg { width: 13px; height: 13px; }\n\n /* ─── Grid view ─── */\n .proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }\n .proj-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; display: flex; flex-direction: column; position: relative; }\n .proj-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }\n .proj-card:hover .card-del-btn { opacity: 1; }\n .proj-card .card-thumb { aspect-ratio: 9/16; max-height: 280px; border-radius: var(--r-md) var(--r-md) 0 0; }\n /* 编辑模式 checkbox */\n .proj-card .card-check {\n position: absolute; top: 10px; left: 10px;\n width: 22px; height: 22px; border-radius: 50%;\n background: var(--surface); border: 2px solid var(--black-alpha-32);\n display: none; place-items: center;\n color: var(--accent-white); z-index: 5;\n pointer-events: none;\n }\n .proj-card .card-check svg { width: 11px; height: 11px; opacity: 0; }\n body.edit-mode .proj-card .card-check { display: grid; }\n body.edit-mode .proj-card.selected .card-check {\n background: var(--heat); border-color: var(--heat);\n }\n body.edit-mode .proj-card.selected .card-check svg { opacity: 1; }\n body.edit-mode .proj-card.selected { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; }\n body.edit-mode .proj-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; }\n /* 列表行末 ⋯ 删除气泡 */\n .row-more {\n position: relative; display: inline-flex;\n cursor: pointer; align-items: center;\n color: var(--black-alpha-56);\n padding: 4px;\n }\n .row-more:hover { color: var(--accent-black); }\n .row-more-tip {\n position: absolute; top: calc(100% + 6px); right: 0;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n box-shadow: 0 4px 16px rgba(0,0,0,.08);\n padding: 4px; min-width: 110px;\n opacity: 0; pointer-events: none;\n transform: translateY(-2px);\n transition: opacity .15s, transform .15s;\n z-index: 12;\n }\n .row-more-tip::before {\n content: ''; position: absolute;\n top: -8px; left: 0; right: 0; height: 8px;\n }\n .row-more:hover .row-more-tip,\n .row-more-tip:hover { opacity: 1; pointer-events: auto; transform: translateY(0); }\n .row-more-tip .mi {\n display: flex; align-items: center; gap: 6px;\n width: 100%; padding: 6px 10px;\n background: transparent; border: 0;\n border-radius: var(--r-sm); cursor: pointer;\n font-size: 12.5px; color: var(--accent-black);\n font-family: inherit; text-align: left;\n transition: background var(--t-base), color var(--t-base);\n }\n .row-more-tip .mi:hover {\n background: var(--crimson-bg, #fdebea);\n color: var(--accent-crimson, #c43d3d);\n }\n .row-more-tip .mi svg { width: 13px; height: 13px; }\n .btn.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }\n /* bulk-bar */\n .bulk-bar {\n position: fixed; bottom: 24px; left: 50%;\n transform: translateX(-50%);\n background: var(--accent-black); color: var(--accent-white);\n border-radius: var(--r-md);\n padding: 10px 14px 10px 18px;\n display: none; align-items: center; gap: 16px;\n box-shadow: 0 8px 24px rgba(0,0,0,.18);\n z-index: 100; font-size: 13px;\n }\n body.edit-mode .bulk-bar { display: inline-flex; }\n .bulk-bar .ct { font-family: var(--font-mono); letter-spacing: .02em; }\n .bulk-bar .ct b { color: var(--heat); font-weight: 700; padding: 0 3px; }\n .bulk-bar .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); }\n .bulk-bar button {\n height: 30px; padding: 0 12px;\n background: transparent; border: 1px solid rgba(255,255,255,.24);\n border-radius: var(--r-sm); color: var(--accent-white);\n font-size: 12.5px; font-family: inherit; cursor: pointer;\n display: inline-flex; align-items: center; gap: 5px;\n }\n .bulk-bar button:hover { background: rgba(255,255,255,.08); }\n .bulk-bar button.danger { background: var(--accent-crimson); border-color: var(--accent-crimson); }\n .bulk-bar button svg { width: 12px; height: 12px; }\n .bulk-bar .clear-sel { color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer; background: none; border: 0; padding: 4px 6px; }\n .proj-card .card-body { padding: 14px; display: flex; flex-direction: column; gap: 10px; flex: 1; }\n .proj-card .card-name { font-size: 13.5px; font-weight: 600; color: var(--accent-black); line-height: 1.4; }\n .proj-card .card-sub { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }\n .proj-card .card-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 10px; border-top: 1px solid var(--border-faint); margin-top: auto; }\n .proj-card .card-time { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>视频项目</h1>\n <div class=\"sub\"><span class=\"mono\">// <span id=\"sub-total\">0</span> 个 · <span id=\"sub-wip\">0</span> 进行中 · <span id=\"sub-done\">0</span> 完成 · <span id=\"sub-fail\">0</span> 失败</span></div>\n </div>\n <div class=\"actions\">\n <button class=\"btn\" type=\"button\" id=\"proj-manage-btn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m3 7 2 2 4-4\"/><path d=\"m3 17 2 2 4-4\"/><path d=\"M13 6h8\"/><path d=\"M13 12h8\"/><path d=\"M13 18h8\"/></svg>\n <span class=\"proj-manage-label\">管理项目</span>\n </button>\n <a class=\"btn btn-primary btn-lg btn-create\" href=\"projects-new.html\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m12.3 3.5 3 4\"/><path d=\"M20.2 6 3 11l-.9-2.4a2 2 0 0 1 1.3-2.5l13.5-4a2 2 0 0 1 2.5 1.3Z\"/><path d=\"m6.2 5.3 3.1 3.9\"/><path d=\"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z\"/></svg>\n 新建项目\n </a>\n </div>\n</div>\n\n<div class=\"tabs\" id=\"status-tabs\">\n <div class=\"tab active\" data-filter=\"all\">全部 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-filter=\"wip\">进行中 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-filter=\"done\">已完成 <span class=\"count\">0</span></div>\n <div class=\"tab\" data-filter=\"fail\">失败 <span class=\"count\">0</span></div>\n</div>\n\n<div class=\"toolbar\">\n <div class=\"search-inline\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n <input class=\"input\" id=\"search-input\" placeholder=\"搜索项目名称、商品\">\n </div>\n <div class=\"chip-wrap\" data-key=\"product\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">商品品类</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <div class=\"chip-wrap\" data-key=\"source\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">脚本来源</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <div class=\"chip-wrap\" data-key=\"time\">\n <button class=\"chip\" type=\"button\"><span class=\"chip-label\">创建时间</span> <svg class=\"caret\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 6l4 4 4-4\"/></svg></button>\n <div class=\"chip-menu\"></div>\n </div>\n <button class=\"clear-filters\" id=\"clear-filters\" type=\"button\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>\n 清空筛选\n </button>\n <span class=\"spacer\"></span>\n <div class=\"view-toggle\">\n <button id=\"view-grid\" data-view=\"grid\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"2\" width=\"5\" height=\"5\"/><rect x=\"9\" y=\"2\" width=\"5\" height=\"5\"/><rect x=\"2\" y=\"9\" width=\"5\" height=\"5\"/><rect x=\"9\" y=\"9\" width=\"5\" height=\"5\"/></svg>\n 网格\n </button>\n <button id=\"view-list\" class=\"active\" data-view=\"list\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg>\n 列表\n </button>\n </div>\n</div>\n\n<div class=\"result-meta\" id=\"result-meta\">// 显示 <span class=\"count\">12</span> / 12 个项目</div>\n\n<!-- ============= LIST VIEW ============= -->\n<div id=\"list-view\">\n <table class=\"t\">\n <thead>\n <tr>\n <th style=\"width:32%\">项目</th>\n <th>商品</th>\n <th>脚本来源</th>\n <th style=\"width:200px\">进度</th>\n <th>状态</th>\n <th style=\"width:120px\">更新于</th>\n <th style=\"width:60px\"></th>\n </tr>\n </thead>\n <tbody id=\"list-tbody\">\n <tr data-status=\"wip\" data-name=\"补水面膜 痛点种草\" onclick=\"location.href='pipeline.html#stage-3'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">补水面膜 · 痛点种草 · v3</div><div class=\"proj-sub\">6 镜 · 0-15s</div></div>\n </div>\n </td>\n <td>透真补水面膜</td>\n <td><span class=\"muted\">AI 全生</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"cur\"></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">3/5</span>\n </div>\n </td>\n <td><span class=\"pill info\"><span class=\"dot\"></span>故事板生成中</span></td>\n <td class=\"muted-2\">12 分钟前</td>\n <td>\n <div class=\"row-action\">\n <a href=\"pipeline.html#stage-3\" onclick=\"event.stopPropagation()\" title=\"继续\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><path d=\"M5 4l6 4-6 4z\" fill=\"currentColor\"/></svg></a>\n <span class=\"row-more\" onclick=\"event.stopPropagation()\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><circle cx=\"3\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"8\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"13\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/></svg><div class=\"row-more-tip\"><button class=\"mi mi-del-row\" type=\"button\" onclick=\"event.stopPropagation();\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>删除项目</button></div></span>\n </div>\n </td>\n </tr>\n <tr data-status=\"wip\" data-name=\"速食牛肉面 加班治愈\" onclick=\"location.href='pipeline.html#stage-2'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">速食牛肉面 · 加班治愈</div><div class=\"proj-sub\">4 镜 · 0-12s</div></div>\n </div>\n </td>\n <td>滋啦速食 · 6 桶装</td>\n <td><span class=\"muted\">一句话主题</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"cur\"></span><span></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">2/5</span>\n </div>\n </td>\n <td><span class=\"pill info\"><span class=\"dot\"></span>资产生成中</span></td>\n <td class=\"muted-2\">37 分钟前</td>\n <td></td>\n </tr>\n <tr data-status=\"wip\" data-name=\"透真防晒 通勤对比\" onclick=\"location.href='pipeline.html#stage-4'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">透真防晒 · 通勤对比</div><div class=\"proj-sub\">6 镜 · 0-18s</div></div>\n </div>\n </td>\n <td>透真清透防晒霜</td>\n <td><span class=\"muted\">AI 全生</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"cur\"></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">4/5</span>\n </div>\n </td>\n <td><span class=\"pill info\"><span class=\"dot\"></span>视频生成 4/6</span></td>\n <td class=\"muted-2\">2 小时前</td>\n <td></td>\n </tr>\n <tr data-status=\"fail\" data-name=\"咖啡冻干 早八剧情\" onclick=\"location.href='pipeline.html#stage-3'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">咖啡冻干 · 早八剧情</div><div class=\"proj-sub\">5 镜 · 0-15s</div></div>\n </div>\n </td>\n <td>三顿半同款冻干</td>\n <td><span class=\"muted\">一句话主题</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"fail\"></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">3/5</span>\n </div>\n </td>\n <td><span class=\"pill err\"><span class=\"dot\"></span>故事板生成失败</span></td>\n <td class=\"muted-2\">昨天 18:42</td>\n <td></td>\n </tr>\n <tr data-status=\"done\" data-name=\"蓝牙耳机 开箱测评\" onclick=\"location.href='pipeline.html#stage-5'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">蓝牙耳机 · 开箱测评</div><div class=\"proj-sub\">5 镜 · 0-15s</div></div>\n </div>\n </td>\n <td>南卡 Lite Pro</td>\n <td><span class=\"muted\">自带脚本</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">5/5</span>\n </div>\n </td>\n <td><span class=\"pill ok\"><span class=\"dot\"></span>已完成</span></td>\n <td class=\"muted-2\">5 月 7 日</td>\n <td></td>\n </tr>\n <tr data-status=\"done\" data-name=\"瑜伽裤 通勤穿搭\" onclick=\"location.href='pipeline.html#stage-5'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">瑜伽裤 · 通勤穿搭</div><div class=\"proj-sub\">5 镜 · 0-15s</div></div>\n </div>\n </td>\n <td>露露同款瑜伽裤</td>\n <td><span class=\"muted\">AI 全生</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">5/5</span>\n </div>\n </td>\n <td><span class=\"pill ok\"><span class=\"dot\"></span>已完成</span></td>\n <td class=\"muted-2\">5 月 6 日</td>\n <td></td>\n </tr>\n <tr data-status=\"done\" data-name=\"空气炸锅 小户型\" onclick=\"location.href='pipeline.html#stage-5'\">\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">空气炸锅 · 小户型</div><div class=\"proj-sub\">4 镜 · 0-12s</div></div>\n </div>\n </td>\n <td>小熊 4L 空气炸锅</td>\n <td><span class=\"muted\">一句话主题</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">5/5</span>\n </div>\n </td>\n <td><span class=\"pill ok\"><span class=\"dot\"></span>已完成</span></td>\n <td class=\"muted-2\">5 月 4 日</td>\n <td></td>\n </tr>\n </tbody>\n </table>\n</div>\n\n<!-- ============= GRID VIEW ============= -->\n<div id=\"grid-view\" style=\"display:none;\">\n <div class=\"proj-grid\" id=\"grid-body\">\n <div class=\"proj-card\" data-status=\"wip\" data-name=\"补水面膜 痛点种草\" onclick=\"location.href='pipeline.html#stage-3'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 镜 3/6</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">补水面膜 · 痛点种草 · v3</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">透真补水面膜 · 6 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"cur\"></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">3/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill info\"><span class=\"dot\"></span>故事板生成中</span>\n <span class=\"card-time\">12 分钟前</span>\n </div>\n </div>\n </div>\n\n <div class=\"proj-card\" data-status=\"wip\" data-name=\"速食牛肉面 加班治愈\" onclick=\"location.href='pipeline.html#stage-2'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 镜 2/4</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">速食牛肉面 · 加班治愈</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">滋啦速食 · 4 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"cur\"></span><span></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">2/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill info\"><span class=\"dot\"></span>资产生成中</span>\n <span class=\"card-time\">37 分钟前</span>\n </div>\n </div>\n </div>\n\n <div class=\"proj-card\" data-status=\"wip\" data-name=\"透真防晒 通勤对比\" onclick=\"location.href='pipeline.html#stage-4'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 镜 4/6</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">透真防晒 · 通勤对比</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">透真清透防晒霜 · 6 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"cur\"></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">4/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill info\"><span class=\"dot\"></span>视频 4/6</span>\n <span class=\"card-time\">2 小时前</span>\n </div>\n </div>\n </div>\n\n <div class=\"proj-card\" data-status=\"fail\" data-name=\"咖啡冻干 早八剧情\" onclick=\"location.href='pipeline.html#stage-3'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 镜 3/5</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">咖啡冻干 · 早八剧情</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">三顿半同款 · 5 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"fail\"></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">3/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill err\"><span class=\"dot\"></span>故事板失败</span>\n <span class=\"card-time\">昨天 18:42</span>\n </div>\n </div>\n </div>\n\n <div class=\"proj-card\" data-status=\"done\" data-name=\"蓝牙耳机 开箱测评\" onclick=\"location.href='pipeline.html#stage-5'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 5/5 ✓</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">蓝牙耳机 · 开箱测评</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">南卡 Lite Pro · 5 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">5/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n <span class=\"card-time\">5 月 7 日</span>\n </div>\n </div>\n </div>\n\n <div class=\"proj-card\" data-status=\"done\" data-name=\"瑜伽裤 通勤穿搭\" onclick=\"location.href='pipeline.html#stage-5'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 5/5 ✓</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">瑜伽裤 · 通勤穿搭</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">露露同款 · 5 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">5/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n <span class=\"card-time\">5 月 6 日</span>\n </div>\n </div>\n </div>\n\n <div class=\"proj-card\" data-status=\"done\" data-name=\"空气炸锅 小户型\" onclick=\"location.href='pipeline.html#stage-5'\">\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 5/5 ✓</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">空气炸锅 · 小户型</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">小熊 4L · 4 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span><span class=\"done\"></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">5/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill ok\"><span class=\"dot\"></span>已完成</span>\n <span class=\"card-time\">5 月 4 日</span>\n </div>\n </div>\n </div>\n\n </div>\n</div>\n\n<!-- Empty state -->\n<div class=\"empty-state\" id=\"empty\">\n <div class=\"ic-empty\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/></svg>\n </div>\n <h3>没有匹配的项目</h3>\n <p>// 试试切换 tab 或修改搜索词</p>\n</div>\n\n<!-- ===== 删除确认 modal (必须在 <script> 之前 · 否则 getElementById 取 null 导致脚本崩) ===== -->\n<div class=\"modal-bg\" id=\"del-confirm-bg\">\n <div class=\"modal\" role=\"dialog\">\n <span class=\"corner-tr\" aria-hidden></span>\n <span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n </div>\n <div class=\"ti\">确认删除项目<span>// CONFIRM DELETE</span></div>\n </div>\n <div class=\"modal-b\" id=\"del-confirm-body\">即将删除项目。</div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" id=\"del-confirm-cancel\">取消</button>\n <button class=\"btn\" type=\"button\" id=\"del-confirm-ok\" style=\"background:var(--accent-crimson);color:var(--accent-white);border-color:var(--accent-crimson)\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/></svg>\n 确认删除\n </button>\n </div>\n </div>\n</div>\n\n<!-- ===== bulk-bar (必须在 <script> 之前) ===== -->\n<div class=\"bulk-bar\" id=\"bulk-bar\">\n <span class=\"ct\">已选 <b id=\"bulk-count\">0</b> 项</span>\n <button class=\"clear-sel\" type=\"button\" id=\"bulk-clear\">清空</button>\n <span class=\"sep\"></span>\n <button class=\"danger\" type=\"button\" id=\"bulk-del\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>\n 删除所选\n </button>\n <button type=\"button\" id=\"bulk-exit\">完成</button>\n</div>\n\n</div>\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目' }] });\n\n// ============== 注入用户新建的项目 (来自 projects-new.html 向导, 写入 localStorage) ==============\n// 必须在计数 / tagItem / buildMenu 之前执行, 让后续逻辑把它们当作普通项目处理\n(function injectExtraProjects() {\n let pending;\n try {\n pending = JSON.parse(localStorage.getItem('fs-extra-projects') || '[]');\n } catch (e) { return; }\n if (!Array.isArray(pending) || !pending.length) return;\n\n const tbody = document.getElementById('list-tbody');\n const gridBody = document.getElementById('grid-body');\n if (!tbody || !gridBody) return;\n const esc = s => String(s == null ? '' : s).replace(/[&<>\"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n\n // 按 createdAt 升序 → 倒序 insert,最新的排最上\n pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));\n pending.forEach(p => {\n // 链接带 ?product= 让 pipeline 显示正确商品名(贯穿 5 stage)\n const productQ = p.product ? `?product=${encodeURIComponent(p.product)}` : '';\n const href = `pipeline.html${productQ}#stage-${p.stage || 1}`;\n const status = p.status || 'wip';\n const shots = p.shots || 5;\n const durLabel = p.durationLabel || '0-15s';\n const pill = p.pillText || '脚本生成中';\n\n // List row\n const tr = document.createElement('tr');\n tr.dataset.status = status;\n tr.dataset.name = p.name;\n tr.dataset.extraId = p.id;\n tr.setAttribute('onclick', `location.href='${href}'`);\n tr.innerHTML = `\n <td>\n <div class=\"proj-name-cell\">\n <div class=\"placeholder proj-thumb\"><span class=\"ph-frame\">9:16</span></div>\n <div><div class=\"proj-name\">${esc(p.name)}</div><div class=\"proj-sub\">${shots} 镜 · ${esc(durLabel)}</div></div>\n </div>\n </td>\n <td>${esc(p.product)}</td>\n <td><span class=\"muted\">${esc(p.source)}</span></td>\n <td>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"cur\"></span><span></span><span></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:11px;\">1/5</span>\n </div>\n </td>\n <td><span class=\"pill info\"><span class=\"dot\"></span>${esc(pill)}</span></td>\n <td class=\"muted-2\">刚刚</td>\n <td>\n <div class=\"row-action\">\n <a href=\"${href}\" onclick=\"event.stopPropagation()\" title=\"继续\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><path d=\"M5 4l6 4-6 4z\" fill=\"currentColor\"/></svg></a>\n <span class=\"row-more\" onclick=\"event.stopPropagation()\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><circle cx=\"3\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"8\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"13\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/></svg><div class=\"row-more-tip\"><button class=\"mi mi-del-row\" type=\"button\" onclick=\"event.stopPropagation();\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>删除项目</button></div></span>\n </div>\n </td>\n `;\n tbody.insertBefore(tr, tbody.firstElementChild);\n\n // Grid card\n const card = document.createElement('div');\n card.className = 'proj-card';\n card.dataset.status = status;\n card.dataset.name = p.name;\n card.dataset.extraId = p.id;\n card.setAttribute('onclick', `location.href='${href}'`);\n card.innerHTML = `\n <span class=\"card-check\" aria-hidden=\"true\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg></span>\n <button class=\"card-del-btn\" type=\"button\" title=\"删除项目\" onclick=\"event.stopPropagation();\" data-action=\"delete-project\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg></button>\n <div class=\"placeholder card-thumb\"><span class=\"ph-frame\">9:16 · 刚刚</span></div>\n <div class=\"card-body\">\n <div>\n <div class=\"card-name\">${esc(p.name)}</div>\n <div class=\"card-sub\" style=\"margin-top:4px;\">${esc(p.product)} · ${shots} 镜</div>\n </div>\n <div class=\"hstack\">\n <div class=\"prog\"><span class=\"cur\"></span><span></span><span></span><span></span><span></span></div>\n <span class=\"muted-2 mono\" style=\"font-size:10.5px;\">1/5</span>\n </div>\n <div class=\"card-foot\">\n <span class=\"pill info\"><span class=\"dot\"></span>${esc(pill)}</span>\n <span class=\"card-time\">刚刚</span>\n </div>\n </div>\n `;\n gridBody.insertBefore(card, gridBody.firstElementChild);\n });\n})();\n\n// ============== 从 DOM 实算项目计数,同步副标题 + tab 角标 ==============\nconst allRows = document.querySelectorAll('#list-tbody tr');\nconst counts = { total: allRows.length, wip: 0, done: 0, fail: 0 };\nallRows.forEach(r => { const s = r.dataset.status; if (counts[s] !== undefined) counts[s]++; });\n\ndocument.getElementById('sub-total').textContent = counts.total;\ndocument.getElementById('sub-wip').textContent = counts.wip;\ndocument.getElementById('sub-done').textContent = counts.done;\ndocument.getElementById('sub-fail').textContent = counts.fail;\n\ndocument.querySelectorAll('#status-tabs .tab').forEach(t => {\n const f = t.dataset.filter;\n const n = f === 'all' ? counts.total : (counts[f] || 0);\n t.querySelector('.count').textContent = n;\n});\n\n// ============== Multi-filter state ==============\nconst state = { filter: 'all', view: 'list', search: '', product: 'all', source: 'all', time: 'all' };\n\nconst CHIP_LABELS = { product: '商品品类', source: '脚本来源', time: '创建时间' };\n\n// 抖音爆款品类 · TOP 8\nconst CATEGORIES = ['美妆个护', '服饰内衣', '食品饮料', '家居家电', '数码 3C', '个护清洁', '运动户外', '母婴亲子'];\n\n// 商品名 → 品类(关键字推断,demo 数据覆盖)\nfunction inferCategory(product) {\n const p = product || '';\n if (/面膜|防晒|精华|护肤|彩妆|口红|粉底/.test(p)) return '美妆个护';\n if (/速食|冻干|咖啡|零食|饮料|酒|茶/.test(p)) return '食品饮料';\n if (/耳机|手机|数码|充电|蓝牙|3C/.test(p)) return '数码 3C';\n if (/瑜伽|健身|户外|露营|运动/.test(p)) return '运动户外';\n if (/服装|内衣|裤|衣|鞋|包/.test(p)) return '服饰内衣';\n if (/炸锅|家电|香薰|收纳|床|家居/.test(p)) return '家居家电';\n if (/洗|牙膏|清洁/.test(p)) return '个护清洁';\n if (/婴|童|奶粉|辅食|玩具/.test(p)) return '母婴亲子';\n return '';\n}\n\n// 时间字符串 → 时间桶 (today / week / month / earlier)\nfunction parseTimeBucket(txt) {\n if (!txt) return 'earlier';\n if (txt.includes('分钟前') || txt.includes('小时前')) return 'today';\n if (txt.includes('昨天')) return 'week';\n // \"5 月 7 日\" 等当月日期 → month; 其他月份 → earlier\n const m = txt.match(/(\\d+)\\s*月/);\n if (m) {\n const month = +m[1];\n const now = new Date();\n const curMonth = now.getMonth() + 1;\n if (month === curMonth) return 'month';\n if (month === curMonth - 1) return 'earlier';\n }\n return 'earlier';\n}\nconst TIME_LABEL = { today: '今天', week: '本周', month: '本月', earlier: '更早' };\n\n// 给行/卡片打数据标签 (从已有的列文字推断)\nfunction tagItem(el, productCellText, sourceCellText, timeCellText) {\n const product = (productCellText || '').trim();\n el.dataset.product = product;\n el.dataset.category = inferCategory(product);\n el.dataset.source = (sourceCellText || '').trim();\n el.dataset.time = parseTimeBucket(timeCellText || '');\n}\n\n// List view: 行内列顺序是 项目/商品/脚本来源/进度/状态/更新于\ndocument.querySelectorAll('#list-tbody tr').forEach(tr => {\n const tds = tr.querySelectorAll('td');\n if (tds.length < 6) return;\n tagItem(tr, tds[1].innerText, tds[2].innerText, tds[5].innerText);\n});\n\n// Grid view: 从 card-sub (商品 · N 镜) 抽出商品名,从 card-time 抽时间,无脚本来源信息 → 沿用同名 list 项的 source\nconst listMap = {};\ndocument.querySelectorAll('#list-tbody tr').forEach(tr => {\n listMap[tr.dataset.name] = { source: tr.dataset.source, product: tr.dataset.product };\n});\ndocument.querySelectorAll('.proj-card').forEach(card => {\n const sub = card.querySelector('.card-sub')?.innerText || '';\n const product = sub.split('·')[0].trim();\n const time = card.querySelector('.card-time')?.innerText || '';\n const name = card.dataset.name;\n const listed = listMap[name] || {};\n card.dataset.product = listed.product || product;\n card.dataset.category = inferCategory(card.dataset.product);\n card.dataset.source = listed.source || '';\n card.dataset.time = parseTimeBucket(time);\n});\n\n// 计算唯一值列表用于填充下拉\nfunction uniqueValues(key) {\n const set = new Set();\n document.querySelectorAll('#list-tbody tr').forEach(tr => {\n const v = tr.dataset[key];\n if (v) set.add(v);\n });\n return [...set];\n}\n\n// 填充菜单\nfunction buildMenu(key, options) {\n const wrap = document.querySelector(`.chip-wrap[data-key=\"${key}\"]`);\n const menu = wrap.querySelector('.chip-menu');\n const checkSvg = '<svg class=\"mi-check\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\"><polyline points=\"3 8 7 12 13 4\"/></svg>';\n const all = `<div class=\"mi selected\" data-value=\"all\">${checkSvg}<span>全部${CHIP_LABELS[key]}</span></div><div class=\"mi-sep\"></div>`;\n const items = options.map(o => `<div class=\"mi\" data-value=\"${o.value}\">${checkSvg}<span>${o.label}</span></div>`).join('');\n menu.innerHTML = all + items;\n}\n\nbuildMenu('product', CATEGORIES.map(v => ({ value: v, label: v })));\nbuildMenu('source', uniqueValues('source').filter(Boolean).map(v => ({ value: v, label: v })));\nbuildMenu('time', [\n { value: 'today', label: '今天' },\n { value: 'week', label: '本周' },\n { value: 'month', label: '本月' },\n { value: 'earlier', label: '更早' },\n]);\n\nconst TOTAL = document.querySelectorAll('#list-tbody tr').length;\n\nfunction applyFilter() {\n const isList = state.view === 'list';\n document.getElementById('list-view').style.display = isList ? '' : 'none';\n document.getElementById('grid-view').style.display = isList ? 'none' : '';\n\n const items = document.querySelectorAll(isList ? '#list-tbody tr' : '.proj-card');\n let visible = 0;\n items.forEach(el => {\n const status = el.dataset.status || '';\n const name = (el.dataset.name || '').toLowerCase();\n const matchFilter = state.filter === 'all' || status.split(' ').includes(state.filter);\n const matchSearch = !state.search\n || name.includes(state.search.toLowerCase())\n || (el.dataset.product || '').toLowerCase().includes(state.search.toLowerCase());\n const matchProduct = state.product === 'all' || el.dataset.category === state.product;\n const matchSource = state.source === 'all' || el.dataset.source === state.source;\n const matchTime = state.time === 'all' || el.dataset.time === state.time;\n const show = matchFilter && matchSearch && matchProduct && matchSource && matchTime;\n el.style.display = show ? '' : 'none';\n if (show) visible++;\n });\n\n const empty = document.getElementById('empty');\n if (visible === 0) {\n empty.classList.add('show');\n document.getElementById('list-view').style.display = 'none';\n document.getElementById('grid-view').style.display = 'none';\n } else {\n empty.classList.remove('show');\n }\n\n document.getElementById('result-meta').innerHTML = `// 显示 <span class=\"count\">${visible}</span> / ${TOTAL} 个项目`;\n\n // 是否有任意筛选生效 → 显示\"清空筛选\"\n const hasFilter = state.filter !== 'all' || state.search || state.product !== 'all' || state.source !== 'all' || state.time !== 'all';\n document.getElementById('clear-filters').hidden = !hasFilter;\n}\n\n// 清空所有筛选\ndocument.getElementById('clear-filters').addEventListener('click', () => {\n state.filter = 'all'; state.search = ''; state.product = 'all'; state.source = 'all'; state.time = 'all';\n // tab 回到\"全部\"\n document.querySelectorAll('#status-tabs .tab').forEach(t => t.classList.toggle('active', t.dataset.filter === 'all'));\n // 搜索框清空\n document.getElementById('search-input').value = '';\n // 三个 chip 同步\n ['product', 'source', 'time'].forEach(syncChipUI);\n applyFilter();\n Shell.toast('已清空筛选');\n});\n\n// chip label + active 状态同步\nfunction syncChipUI(key) {\n const wrap = document.querySelector(`.chip-wrap[data-key=\"${key}\"]`);\n const label = wrap.querySelector('.chip-label');\n const chip = wrap.querySelector('.chip');\n const v = state[key];\n if (v === 'all') {\n label.textContent = CHIP_LABELS[key];\n chip.classList.remove('active');\n } else {\n label.textContent = key === 'time' ? TIME_LABEL[v] : v;\n chip.classList.add('active');\n }\n wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === v));\n}\n\n// Chip wrap → 点击 chip 打开/关闭,点击菜单项设置 state\ndocument.querySelectorAll('.chip-wrap').forEach(wrap => {\n const key = wrap.dataset.key;\n wrap.querySelector('.chip').addEventListener('click', e => {\n e.stopPropagation();\n const isOpen = wrap.classList.contains('open');\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n if (!isOpen) wrap.classList.add('open');\n });\n wrap.querySelectorAll('.mi').forEach(mi => {\n mi.addEventListener('click', e => {\n e.stopPropagation();\n state[key] = mi.dataset.value;\n wrap.classList.remove('open');\n syncChipUI(key);\n applyFilter();\n });\n });\n});\n\n// 点击页面其他地方关闭打开的菜单\ndocument.addEventListener('click', () => {\n document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));\n});\n\n// Tab clicks\ndocument.querySelectorAll('#status-tabs .tab').forEach(t => {\n t.addEventListener('click', () => {\n document.querySelectorAll('#status-tabs .tab').forEach(x => x.classList.remove('active'));\n t.classList.add('active');\n state.filter = t.dataset.filter;\n applyFilter();\n });\n});\n\n// View toggle\ndocument.querySelectorAll('.view-toggle button').forEach(b => {\n b.addEventListener('click', () => {\n document.querySelectorAll('.view-toggle button').forEach(x => x.classList.remove('active'));\n b.classList.add('active');\n state.view = b.dataset.view;\n applyFilter();\n });\n});\n\n// Search\ndocument.getElementById('search-input').addEventListener('input', e => {\n state.search = e.target.value.trim();\n applyFilter();\n});\n\n// 从 URL ?filter=wip|done|fail|all 接收外部跳转 (例如工作台 stat 卡)\n(function applyUrlFilter() {\n try {\n const q = new URLSearchParams(location.search);\n const f = q.get('filter');\n if (!f) return;\n const valid = ['all', 'wip', 'done', 'fail'];\n if (!valid.includes(f)) return;\n state.filter = f;\n document.querySelectorAll('#status-tabs .tab').forEach(t =>\n t.classList.toggle('active', t.dataset.filter === f)\n );\n } catch (e) {}\n})();\n\napplyFilter();\n\n// ============================================================\n// 列表行 + 网格卡 删除按钮 + 管理项目 模式\n// ============================================================\nconst PROJECT_NEVER_DELETE = []; // 未来可挂载\"未完成不可删\"逻辑\nfunction getProjectRefs(_card) { return []; } // 项目无引用检查 (PRD 没说项目反向)\n\n// 给所有 list 行末 td 注入 row-more (含删除气泡)\nconst moreHTML = '<span class=\"row-more\" onclick=\"event.stopPropagation()\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\"><circle cx=\"3\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"8\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/><circle cx=\"13\" cy=\"8\" r=\"1.2\" fill=\"currentColor\"/></svg><div class=\"row-more-tip\"><button class=\"mi mi-del-row\" type=\"button\" onclick=\"event.stopPropagation();\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2\"/><path d=\"M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6\"/></svg>删除项目</button></div></span>';\ndocument.querySelectorAll('#list-tbody tr').forEach(tr => {\n const lastTd = tr.querySelector('td:last-child');\n if (!lastTd) return;\n if (!lastTd.querySelector('.row-more')) {\n // 已经有 row-action 的合并, 否则直接放\n if (lastTd.querySelector('.row-action')) {\n lastTd.querySelector('.row-action').insertAdjacentHTML('beforeend', moreHTML);\n } else {\n lastTd.innerHTML = lastTd.innerHTML + moreHTML;\n }\n }\n});\n\n// 删除确认 modal\nconst delBg = document.getElementById('del-confirm-bg');\nconst delBody = document.getElementById('del-confirm-body');\nconst delCancel = document.getElementById('del-confirm-cancel');\nconst delOk = document.getElementById('del-confirm-ok');\nlet _delQueue = [];\nfunction openDelConfirm(targets) {\n _delQueue = targets;\n if (targets.length === 1) {\n const name = targets[0].dataset.name || '该项目';\n delBody.innerHTML = '即将删除项目 <span class=\"mono-acc\">' + name + '</span>,已生成的视频和中间素材将清理,被引用的共享资产不受影响。';\n } else {\n delBody.innerHTML = '即将删除 <span class=\"mono-acc\">' + targets.length + ' 个项目</span>,这些项目的视频和中间素材都将清理。';\n }\n delBg.classList.add('show');\n}\nfunction closeDelConfirm() { delBg.classList.remove('show'); _delQueue = []; }\ndelCancel.addEventListener('click', closeDelConfirm);\ndelBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });\ndelOk.addEventListener('click', () => {\n const n = _delQueue.length;\n\n // 同步 localStorage (注入的项目带 data-extra-id) + 把同名的另一视图元素一起删\n const KEY = 'fs-extra-projects';\n let extraList = [];\n try { extraList = JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) {}\n let extraDirty = false;\n const toRemove = new Set();\n\n _delQueue.forEach(el => {\n toRemove.add(el);\n // 配对: 同 data-name 的另一视图元素一起删\n const name = el.dataset.name;\n if (name) {\n document.querySelectorAll(`[data-name=\"${CSS.escape(name)}\"]`).forEach(other => {\n if (other.matches('.proj-card, #list-tbody tr')) toRemove.add(other);\n });\n }\n // 注入的项目 → 从 localStorage 移除\n const eid = el.dataset.extraId;\n if (eid) {\n const idx = extraList.findIndex(p => p.id === eid);\n if (idx >= 0) { extraList.splice(idx, 1); extraDirty = true; }\n }\n });\n\n toRemove.forEach(el => el.remove());\n if (extraDirty) {\n try { localStorage.setItem(KEY, JSON.stringify(extraList)); } catch (e) {}\n }\n\n closeDelConfirm();\n Shell.toast('已删除', n === 1 ? '项目已移除' : '已删除 ' + n + ' 个项目');\n updateBulkBar();\n});\n\n// 网格卡 card-del-btn 绑定\ndocument.querySelectorAll('.proj-card .card-del-btn').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const card = btn.closest('.proj-card');\n if (card) openDelConfirm([card]);\n });\n});\n// 列表行 row-more 删除项目按钮绑定\ndocument.querySelectorAll('.mi-del-row').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const tr = btn.closest('tr');\n if (tr) openDelConfirm([tr]);\n });\n});\n\n// 管理项目模式\nconst projManageBtn = document.getElementById('proj-manage-btn');\nconst projManageLabel = projManageBtn.querySelector('.proj-manage-label');\nconst bulkBar = document.getElementById('bulk-bar');\nconst bulkCount = document.getElementById('bulk-count');\nconst bulkClear = document.getElementById('bulk-clear');\nconst bulkDel = document.getElementById('bulk-del');\nconst bulkExit = document.getElementById('bulk-exit');\n\nfunction getSelectedProjects() {\n return [\n ...document.querySelectorAll('.proj-card.selected'),\n ...document.querySelectorAll('#list-tbody tr.selected'),\n ];\n}\nfunction updateBulkBar() {\n const sel = getSelectedProjects();\n bulkCount.textContent = sel.length;\n bulkDel.disabled = sel.length === 0;\n bulkDel.style.opacity = sel.length === 0 ? '.4' : '1';\n}\nfunction enterEditMode() {\n document.body.classList.add('edit-mode');\n projManageBtn.classList.add('active');\n projManageLabel.textContent = '完成';\n updateBulkBar();\n}\nfunction exitEditMode() {\n document.body.classList.remove('edit-mode');\n projManageBtn.classList.remove('active');\n projManageLabel.textContent = '管理项目';\n document.querySelectorAll('.proj-card.selected, #list-tbody tr.selected').forEach(c => c.classList.remove('selected'));\n}\nprojManageBtn.addEventListener('click', () => {\n if (document.body.classList.contains('edit-mode')) exitEditMode();\n else enterEditMode();\n});\nbulkExit.addEventListener('click', exitEditMode);\nbulkClear.addEventListener('click', () => {\n document.querySelectorAll('.proj-card.selected, #list-tbody tr.selected').forEach(c => c.classList.remove('selected'));\n updateBulkBar();\n});\nbulkDel.addEventListener('click', () => {\n const sel = getSelectedProjects();\n if (sel.length) openDelConfirm(sel);\n});\n\n// 编辑模式下,卡片/列表行 点击切换 selected\ndocument.querySelectorAll('.proj-card').forEach(card => {\n card.addEventListener('click', e => {\n if (!document.body.classList.contains('edit-mode')) return;\n e.stopImmediatePropagation(); e.preventDefault();\n card.classList.toggle('selected');\n updateBulkBar();\n }, true);\n});\ndocument.querySelectorAll('#list-tbody tr').forEach(tr => {\n tr.addEventListener('click', e => {\n if (!document.body.classList.contains('edit-mode')) return;\n e.stopImmediatePropagation(); e.preventDefault();\n tr.classList.toggle('selected');\n updateBulkBar();\n }, true);\n});\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"register": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"register.html\">\n<meta charset=\"utf-8\">\n<title>注册团队 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n body { margin: 0; min-height: 100vh; background: var(--background-base); display: grid; place-items: center; padding: 32px 24px; }\n\n .auth-wrap { width: 100%; max-width: 980px; display: grid; grid-template-columns: minmax(0, 1fr) 460px; gap: 0; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; overflow: hidden; min-height: 620px; }\n\n /* 4 装订线 */\n .auth-wrap::before, .auth-wrap::after,\n .auth-wrap > .corner-tr, .auth-wrap > .corner-bl {\n content: ''; position: absolute; width: 14px; height: 14px;\n 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;\n background-size: contain; pointer-events: none; z-index: 2;\n }\n .auth-wrap::before { top: -7px; left: -7px; }\n .auth-wrap::after { bottom: -7px; right: -7px; }\n .auth-wrap > .corner-tr { top: -7px; right: -7px; }\n .auth-wrap > .corner-bl { bottom: -7px; left: -7px; }\n\n /* 左:品牌 + 价值点 */\n .auth-brand { background: var(--accent-black); color: var(--accent-white); padding: 40px 44px; display: flex; flex-direction: column; position: relative; overflow: hidden; }\n .auth-brand .logo { display: flex; align-items: center; height: 56px; }\n .auth-brand .logo-img { display: block; width: 196px; height: auto; margin: -14px 0 -10px -14px; object-fit: contain; }\n .auth-brand .tag { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.5); letter-spacing: .08em; text-transform: uppercase; margin-top: 4px; }\n\n .auth-brand .hero { margin-top: 28px; }\n .auth-brand .hero h1 { font-size: 28px; font-weight: 700; letter-spacing: -.02em; line-height: 1.25; margin: 0 0 14px; }\n .auth-brand .hero h1 .h { color: var(--heat); }\n .auth-brand .hero p { font-size: 13.5px; color: rgba(255,255,255,.62); line-height: 1.7; max-width: 320px; margin: 0; }\n\n /* 价值点列表 */\n .val-list { margin: 30px 0 0; display: flex; flex-direction: column; gap: 14px; }\n .val-item { display: grid; grid-template-columns: 24px minmax(0,1fr); gap: 12px; align-items: start; }\n .val-item .ic-v { width: 22px; height: 22px; border: 1px solid rgba(255,255,255,.18); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--heat); }\n .val-item .ic-v svg { width: 12px; height: 12px; }\n .val-item .txt-v { font-size: 12.5px; color: rgba(255,255,255,.78); line-height: 1.5; }\n .val-item .txt-v b { color: var(--accent-white); font-weight: 600; }\n\n .auth-brand .foot { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.32); letter-spacing: .04em; margin-top: auto; padding-top: 24px; display: flex; gap: 14px; }\n .auth-brand .foot a { color: rgba(255,255,255,.5); text-decoration: none; }\n .auth-brand .foot a:hover { color: var(--heat); }\n\n /* 右:表单 */\n .auth-form { padding: 40px 44px 32px; display: flex; flex-direction: column; }\n .auth-form .h-row { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }\n .auth-form h2 { font-size: 22px; font-weight: 600; letter-spacing: -.012em; margin: 0; }\n .auth-form .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; }\n .auth-form .lead { font-size: 13px; color: var(--black-alpha-56); margin: 0 0 22px; }\n\n .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }\n .field-label { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; }\n .field-label .req { color: var(--accent-crimson); }\n .field-label .hint { margin-left: auto; font-size: 10.5px; color: var(--black-alpha-32); }\n .field-input-wrap { position: relative; }\n .field-input-wrap .ic-l { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--black-alpha-32); width: 14px; height: 14px; pointer-events: none; }\n .field input { width: 100%; box-sizing: border-box; padding: 11px 12px 11px 36px; font-size: 13.5px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-family: inherit; color: var(--accent-black); transition: border-color var(--t-base), box-shadow var(--t-base); }\n .field input:focus { outline: none; border-color: var(--heat); box-shadow: 0 0 0 3px var(--heat-12); }\n .field input::placeholder { color: var(--black-alpha-32); }\n .field .toggle-pwd { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--black-alpha-48); cursor: pointer; background: none; border: 0; padding: 0; display: grid; place-items: center; }\n .field .toggle-pwd:hover { color: var(--accent-black); }\n .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }\n .field-row .field { margin-bottom: 0; }\n\n .agree { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; color: var(--black-alpha-56); line-height: 1.6; margin: 8px 0 18px; cursor: pointer; user-select: none; }\n .agree input { margin-top: 3px; width: 13px; height: 13px; accent-color: var(--heat); flex-shrink: 0; }\n .agree a { color: var(--heat); text-decoration: none; }\n .agree a:hover { text-decoration: underline; }\n\n .btn-cta { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); padding: 12px 14px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: box-shadow var(--t-base), transform var(--t-base); display: inline-flex; align-items: center; justify-content: center; gap: 8px; }\n .btn-cta:hover { box-shadow: 0 4px 14px rgba(250,93,25,.28); }\n .btn-cta:active { transform: translateY(1px); }\n .btn-cta:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }\n .btn-cta svg { width: 15px; height: 15px; }\n\n .switch-row { margin-top: 22px; text-align: center; font-size: 12.5px; color: var(--black-alpha-56); }\n .switch-row a { color: var(--heat); text-decoration: none; font-weight: 500; }\n .switch-row a:hover { text-decoration: underline; }\n\n @media (max-width: 820px) {\n .auth-wrap { grid-template-columns: 1fr; min-height: 0; }\n .auth-brand { padding: 32px 28px; }\n .auth-brand .val-list { display: none; }\n .auth-form { padding: 32px 28px; }\n }\n\n .top-back { position: fixed; top: 20px; left: 24px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); text-decoration: none; letter-spacing: .04em; }\n .top-back:hover { color: var(--heat); }\n</style>\n</head>\n<body>\n<a class=\"top-back\" href=\"login.html\">← 返回登录</a>\n\n<div class=\"auth-wrap\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n\n <aside class=\"auth-brand\">\n <div class=\"logo\">\n <img class=\"logo-img\" src=\"/exact/assets/logo-dark.png\" alt=\"Airshelf\">\n </div>\n <div class=\"tag\">// SHORT-VIDEO COMMERCE PLATFORM</div>\n\n <div class=\"hero\">\n <h1>开通团队,<br>开始 <span class=\"h\">AI 带货</span> 第一条短剧</h1>\n <p>个人 / 企业团队 1-2 小时出成片,失败不扣费,确认后扣。</p>\n </div>\n\n <div class=\"val-list\">\n <div class=\"val-item\">\n <span class=\"ic-v\"><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></span>\n <span class=\"txt-v\"><b>5 阶段流水线</b> · 脚本 → 资产 → 故事板 → 视频片段 → 拼接导出</span>\n </div>\n <div class=\"val-item\">\n <span class=\"ic-v\"><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></span>\n <span class=\"txt-v\"><b>失败不扣费</b> · 任务仅在用户通过时扣费,失败 / 超时 / 重跑一律不扣</span>\n </div>\n <div class=\"val-item\">\n <span class=\"ic-v\"><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></span>\n <span class=\"txt-v\"><b>团队 + 角色 + 四层额度</b> · 超管 / 团管 / 成员,日 / 月 + 团队 / 总四层防超支</span>\n </div>\n <div class=\"val-item\">\n <span class=\"ic-v\"><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></span>\n <span class=\"txt-v\"><b>跨项目资产库</b> · 主播 / 场景 / 商品图沉淀复用,不重复生成</span>\n </div>\n </div>\n\n <div class=\"foot\">\n <a href=\"#\">关于</a>\n <a href=\"#\">定价</a>\n <a href=\"#\">联系</a>\n <a href=\"#\">隐私</a>\n </div>\n </aside>\n\n <main class=\"auth-form\">\n <div class=\"h-row\">\n <h2>注册团队</h2>\n <span class=\"sub\">// /auth/register</span>\n </div>\n <p class=\"lead\">填写团队信息开通账户,默认成为团队超管。</p>\n\n <form id=\"register-form\" autocomplete=\"off\" onsubmit=\"event.preventDefault(); doRegister();\">\n <div class=\"field\">\n <label class=\"field-label\" for=\"reg-team\">团队名 <span class=\"req\">*</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/><path d=\"M9 22V12h6v10\"/></svg>\n <input type=\"text\" id=\"reg-team\" placeholder=\"例: 小李的店 / XX 文化传媒\" required>\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\" for=\"reg-email\">超管邮箱 <span class=\"req\">*</span><span class=\"hint\">用于成员邀请 + 找回密码</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z\"/><path d=\"m22 6-10 7L2 6\"/></svg>\n <input type=\"email\" id=\"reg-email\" placeholder=\"name@company.com\" required>\n </div>\n </div>\n\n <div class=\"field-row\">\n <div class=\"field\">\n <label class=\"field-label\" for=\"reg-pwd\">密码 <span class=\"req\">*</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></svg>\n <input type=\"password\" id=\"reg-pwd\" placeholder=\"至少 8 位\" required>\n <button type=\"button\" class=\"toggle-pwd\" onclick=\"togglePwd('reg-pwd')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field-label\" for=\"reg-pwd2\">确认密码 <span class=\"req\">*</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></svg>\n <input type=\"password\" id=\"reg-pwd2\" placeholder=\"再输一次\" required>\n <button type=\"button\" class=\"toggle-pwd\" onclick=\"togglePwd('reg-pwd2')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"field\">\n <label class=\"field-label\" for=\"reg-invite\">邀请码 <span class=\"hint\">可选 · 团队邀请才需要</span></label>\n <div class=\"field-input-wrap\">\n <svg class=\"ic-l\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"/></svg>\n <input type=\"text\" id=\"reg-invite\" placeholder=\"例: TEAM-XXXX-XXXX\">\n </div>\n </div>\n\n <label class=\"agree\">\n <input type=\"checkbox\" id=\"reg-agree\" checked>\n <span>我已阅读并同意 <a href=\"#\" onclick=\"event.preventDefault();_regToast('用户协议','含数据处理 / 内容生成版权 / 计费规则三章 · 完整文本将在正式版接入');\">用户协议</a> 与 <a href=\"#\" onclick=\"event.preventDefault();_regToast('隐私政策','遵循《个人信息保护法》· 团队数据存中国境内 · 默认不用于模型训练');\">隐私政策</a>,知悉「失败不扣费 · 确认后扣」的扣费规则。</span>\n </label>\n\n <button class=\"btn-cta\" type=\"submit\" id=\"reg-submit\">\n 创建团队 · 开始使用\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </button>\n\n <div class=\"switch-row\">\n 已有账号? <a href=\"login.html\">登录 →</a>\n </div>\n </form>\n </main>\n</div>\n\n<script>\n function togglePwd(id) {\n const inp = document.getElementById(id);\n inp.type = inp.type === 'password' ? 'text' : 'password';\n }\n function doRegister() {\n const team = document.getElementById('reg-team').value.trim();\n const email = document.getElementById('reg-email').value.trim();\n const p1 = document.getElementById('reg-pwd').value;\n const p2 = document.getElementById('reg-pwd2').value;\n const agree = document.getElementById('reg-agree').checked;\n if (!team || !email) return alert('请补全团队名 + 邮箱');\n if (p1.length < 8) return alert('密码至少 8 位');\n if (p1 !== p2) return alert('两次密码不一致');\n if (!agree) return alert('请同意用户协议');\n const btn = document.getElementById('reg-submit');\n btn.innerHTML = '<span style=\"font-family:var(--font-mono);font-size:12px;letter-spacing:.04em;\">// 创建团队中...</span>';\n btn.disabled = true;\n setTimeout(() => { location.href = 'index.html'; }, 900);\n }\n // 轻量 toast(不依赖 shell.js)\n function _regToast(t, sub) {\n let el = document.getElementById('__reg-toast');\n if (!el) {\n el = document.createElement('div');\n el.id = '__reg-toast';\n el.style.cssText = 'position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(20px);background:#fff;border:1px solid #e0e0e0;border-radius:8px;padding:12px 18px;box-shadow:0 8px 24px rgba(0,0,0,.12);display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .2s,transform .2s;z-index:9999;font-family:inherit;max-width:380px;';\n document.body.appendChild(el);\n }\n el.innerHTML = '<div style=\"font-size:13.5px;font-weight:600;color:#262626;\">' + t + '</div>' + (sub ? '<div style=\"font-size:11.5px;color:rgba(0,0,0,.56);font-family:\\'Inter\\',system-ui,sans-serif;letter-spacing:.02em;\">// ' + sub + '</div>' : '');\n requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateX(-50%) translateY(0)'; });\n clearTimeout(el._t);\n el._t = setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(-50%) translateY(20px)'; }, 3000);\n }\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"settings": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"settings.html\">\n<meta charset=\"utf-8\">\n<title>设置 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── 设置布局:左 nav + 右 panel ─── */\n .settings-grid { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 24px; align-items: start; }\n\n .settings-nav { position: sticky; top: 16px; }\n .settings-nav .nav-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; padding: 0 12px 8px; }\n .settings-nav :where(a, button) { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; font: inherit; font-size: 13px; color: var(--accent-black); border-radius: var(--r-md); border: 1px solid transparent; background: transparent; cursor: pointer; text-decoration: none; text-align: left; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); position: relative; }\n .settings-nav :where(a, button):hover { background: var(--background-lighter); }\n .settings-nav :where(a, button):focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }\n .settings-nav a.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }\n .settings-nav :where(a, button) svg { width: 16px; height: 16px; stroke-width: 1.5; flex: 0 0 auto; }\n .settings-nav a .nav-badge { margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); padding: 1px 6px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-pill); letter-spacing: .02em; line-height: 14px; }\n .settings-nav a.active .nav-badge { color: var(--heat); background: var(--accent-white); border-color: var(--heat-20); }\n .settings-nav a .nav-dot { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); width: 6px; height: 6px; border-radius: 50%; background: var(--heat); display: none; }\n .settings-nav a.has-changes .nav-dot { display: block; }\n .settings-nav a.active .nav-dot { right: -4px; }\n .settings-nav .logout-pill {\n width: calc(100% - 24px);\n height: 38px;\n margin: 4px 12px 0;\n justify-content: center;\n border-radius: var(--r-pill);\n background: var(--accent-black);\n border-color: var(--accent-black);\n color: var(--accent-white);\n font-weight: 500;\n }\n .settings-nav .logout-pill:hover,\n .settings-nav .logout-pill:focus-visible {\n background: var(--black-alpha-88);\n border-color: var(--black-alpha-88);\n color: var(--accent-white);\n }\n\n /* ─── pane ─── */\n .pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 24px; margin-bottom: 16px; }\n .pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; }\n .pane .pane-desc { font-size: 12px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; margin-bottom: 18px; }\n\n /* ─── form row ─── */\n .form-row { display: grid; grid-template-columns: 160px minmax(0, 1fr); gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--border-faint); align-items: center; }\n .form-row:last-child { border-bottom: 0; }\n .form-row .lbl { font-size: 12.5px; color: var(--black-alpha-56); }\n .form-row .lbl .req { color: var(--accent-crimson); margin-left: 2px; }\n .form-row .lbl-sub { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }\n .form-row .val { display: flex; align-items: center; gap: 10px; min-width: 0; }\n .form-row .val .input, .form-row .val .select { width: 100%; max-width: 380px; }\n .form-row .val .static { font-size: 13px; color: var(--accent-black); font-variant-numeric: tabular-nums; }\n .form-row .val .static.mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--black-alpha-56); }\n .form-row .val .role-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; background: var(--heat-12); color: var(--heat); }\n .form-row .val .role-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }\n\n /* ─── 头像上传 ─── */\n .avatar-edit { display: flex; align-items: center; gap: 16px; }\n .avatar-edit .av-big { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 24px; font-weight: 600; color: var(--accent-black); }\n .avatar-edit .av-actions { display: flex; gap: 8px; }\n\n /* ─── toggle switch ─── */\n .switch { position: relative; width: 36px; height: 20px; flex: 0 0 36px; }\n .switch input { opacity: 0; width: 0; height: 0; }\n .switch .slider { position: absolute; inset: 0; background: var(--black-alpha-24); border-radius: 20px; cursor: pointer; transition: background var(--t-base); }\n .switch .slider::before { content: ''; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; background: var(--accent-white); border-radius: 50%; transition: transform var(--t-base); }\n .switch input:checked + .slider { background: var(--heat); }\n .switch input:checked + .slider::before { transform: translateX(16px); }\n\n /* ─── 偏好选项卡 ─── */\n .pref-choices { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; max-width: 540px; }\n .pref-choice { padding: 10px 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }\n .pref-choice:hover { background: var(--background-lighter); }\n .pref-choice.selected { border-color: var(--heat); background: var(--heat-12); }\n .pref-choice .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }\n .pref-choice .d { font-size: 11px; color: var(--black-alpha-48); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }\n .pref-choice.selected .t { color: var(--heat); }\n\n /* ─── 时长档 ─── */\n .duration-row { display: flex; gap: 8px; }\n .dur-chip { padding: 6px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 13px; cursor: pointer; font-family: var(--font-mono); font-variant-numeric: tabular-nums; transition: all var(--t-base); background: var(--surface); }\n .dur-chip:hover { background: var(--background-lighter); }\n .dur-chip.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }\n\n /* ─── 设备列表 ─── */\n .device-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-faint); }\n .device-row:last-child { border-bottom: 0; }\n .device-row .ic { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--background-lighter); display: grid; place-items: center; color: var(--black-alpha-56); }\n .device-row .ic svg { width: 18px; height: 18px; }\n .device-row .nm { font-size: 13px; font-weight: 500; }\n .device-row .meta { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); margin-top: 2px; letter-spacing: .02em; }\n .device-row .tag-cur { font-family: var(--font-mono); font-size: 10.5px; padding: 1px 6px; background: var(--accent-forest); color: var(--accent-white); border-radius: var(--r-sm); margin-left: 8px; letter-spacing: .04em; font-weight: 600; }\n .device-row .spacer { margin-left: auto; }\n\n /* ─── 头像上传 modal · V2.1 Restraint ─── */\n .av-up-modal { width: min(440px, 92vw); max-width: min(440px, 92vw); }\n\n /* 预览区:左圆头像 + 右 mono 元数据 · 装订线点划线分隔 */\n .av-up-preview-row { display: flex; align-items: center; gap: 14px; padding-bottom: 14px; margin-bottom: 14px; position: relative; }\n .av-up-preview-row::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: repeating-linear-gradient(to right, var(--border-faint) 0, var(--border-faint) 4px, transparent 4px, transparent 8px); }\n .av-up-preview { width: 64px; height: 64px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 22px; font-weight: 600; color: var(--accent-black); overflow: hidden; flex: 0 0 64px; }\n .av-up-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }\n .av-up-preview-meta { min-width: 0; }\n .av-up-preview-meta .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); margin-bottom: 3px; letter-spacing: .01em; }\n .av-up-preview-meta .d { font-size: 11px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; line-height: 1.55; }\n\n /* 规则文本 · 纯 mono, 装订线分隔 */\n .av-up-rules { margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--border-faint); font-size: 11px; color: var(--black-alpha-56); font-family: var(--font-mono); letter-spacing: .02em; line-height: 1.7; }\n .av-up-rules .li { display: flex; gap: 8px; }\n .av-up-rules .li::before { content: '//'; color: var(--black-alpha-32); flex: 0 0 auto; }\n .logout-confirm-modal { width: min(440px, 92vw); max-width: min(440px, 92vw); }\n .logout-confirm-copy { margin: 0 0 12px; color: var(--black-alpha-72); }\n .logout-confirm-points {\n display: grid;\n gap: 8px;\n padding: 12px;\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n }\n .logout-confirm-points .li {\n display: flex;\n gap: 8px;\n font-size: 12.5px;\n line-height: 1.55;\n color: var(--black-alpha-64);\n }\n .logout-confirm-points .li::before {\n content: '//';\n flex: 0 0 auto;\n font-family: var(--font-mono);\n color: var(--black-alpha-32);\n }\n .logout-unsaved-note {\n margin-top: 12px;\n padding: 9px 11px;\n border: 1px solid var(--heat-20);\n border-radius: var(--r-md);\n background: var(--heat-12);\n color: var(--heat);\n font-size: 12.5px;\n line-height: 1.6;\n }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>设置</h1>\n <div class=\"sub\"><span class=\"mono\">// 个人信息 · 偏好 · 通知 · 安全</span></div>\n </div>\n <div class=\"actions\">\n <button class=\"btn\" id=\"save-cancel\" disabled style=\"opacity:.5; cursor:not-allowed;\">取消</button>\n <button class=\"btn btn-primary\" id=\"save-btn\" disabled style=\"opacity:.5; cursor:not-allowed;\">\n <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 12l5 5L20 6\"/></svg>\n 保存所有变更\n </button>\n </div>\n</div>\n\n<div class=\"settings-grid\">\n <!-- 左侧 nav -->\n <aside class=\"settings-nav\" role=\"tablist\" aria-label=\"设置分区\">\n <div class=\"nav-h\">个人</div>\n <a href=\"#sec-profile\" class=\"active\" data-jump=\"sec-profile\" role=\"tab\" aria-controls=\"sec-profile\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"7.5\" r=\"3.5\"/><path d=\"M5 21a7 7 0 0 1 14 0\"/></svg>\n <span>个人信息</span>\n <span class=\"nav-dot\" aria-hidden=\"true\"></span>\n </a>\n <a href=\"#sec-security\" data-jump=\"sec-security\" role=\"tab\" aria-controls=\"sec-security\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z\"/><path d=\"m9 12 2 2 4-4\"/></svg>\n <span>安全</span>\n <span class=\"nav-badge\" data-count-source=\"sec-security\">3</span>\n <span class=\"nav-dot\" aria-hidden=\"true\"></span>\n </a>\n <a href=\"#sec-notify\" data-jump=\"sec-notify\" role=\"tab\" aria-controls=\"sec-notify\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\"/><path d=\"M10 21a2 2 0 0 0 4 0\"/></svg>\n <span>通知</span>\n <span class=\"nav-badge\" data-count-source=\"sec-notify\">5</span>\n <span class=\"nav-dot\" aria-hidden=\"true\"></span>\n </a>\n <div class=\"nav-h\" style=\"margin-top: 16px;\">偏好</div>\n <a href=\"#sec-pref\" data-jump=\"sec-pref\" role=\"tab\" aria-controls=\"sec-pref\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M21 4h-7\"/><path d=\"M10 4H3\"/><path d=\"M21 12h-9\"/><path d=\"M8 12H3\"/><path d=\"M21 20h-5\"/><path d=\"M12 20H3\"/><path d=\"M14 2v4\"/><path d=\"M8 10v4\"/><path d=\"M16 18v4\"/></svg>\n <span>创作默认</span>\n <span class=\"nav-dot\" aria-hidden=\"true\"></span>\n </a>\n <a href=\"#sec-display\" data-jump=\"sec-display\" role=\"tab\" aria-controls=\"sec-display\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"13\" rx=\"2\"/><path d=\"M8 21h8\"/><path d=\"M12 17v4\"/></svg>\n <span>显示</span>\n <span class=\"nav-dot\" aria-hidden=\"true\"></span>\n </a>\n <div class=\"nav-h\" style=\"margin-top: 16px;\">账号</div>\n <button class=\"logout-pill\" type=\"button\" id=\"settings-logout-btn\" onclick=\"logoutCurrentDevice()\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><path d=\"m16 17 5-5-5-5\"/><path d=\"M21 12H9\"/></svg>\n <span>退出登录</span>\n </button>\n </aside>\n\n <!-- 右侧内容 -->\n <main>\n <!-- ─── 个人信息 ─── -->\n <section class=\"pane\" id=\"sec-profile\">\n <h3>个人信息</h3>\n <div class=\"pane-desc\">// 头像、姓名、联系方式 · 邮箱用于接收通知</div>\n\n <div class=\"form-row\">\n <div class=\"lbl\">头像</div>\n <div class=\"val\">\n <div class=\"avatar-edit\">\n <div class=\"av-big\" id=\"prof-avatar-preview\">李</div>\n <div class=\"av-actions\">\n <button class=\"btn btn-sm\" type=\"button\" onclick=\"Shell.openModal('avatar-up-bg')\">上传新头像</button>\n <button class=\"btn btn-ghost btn-sm\" type=\"button\" id=\"prof-avatar-reset\">恢复默认</button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">显示名称<span class=\"req\">*</span></div>\n <div class=\"val\"><input class=\"input\" id=\"prof-name\" value=\"小李\" data-track></div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">登录邮箱</div>\n <div class=\"val\">\n <input class=\"input\" id=\"prof-email\" value=\"li@shop.com\" data-track>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"Shell.toast('已发送验证邮件', 'li@shop.com')\">验证</button>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">手机号</div>\n <div class=\"val\">\n <input class=\"input\" id=\"prof-phone\" value=\"138****8000\" data-track>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"Shell.toast('已发送短信验证码', '+86 138****8000')\">更换</button>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">所属团队<div class=\"lbl-sub\">// 一人一团队</div></div>\n <div class=\"val\">\n <span class=\"static\">小李的店</span>\n <span class=\"role-tag\"><span class=\"dot\"></span>超管 · 创建者</span>\n <a href=\"team.html\" style=\"font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;\">管理团队 →</a>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">用户 ID<div class=\"lbl-sub\">// 不可改</div></div>\n <div class=\"val\"><span class=\"static mono\">USR-2026-A8F2-001</span></div>\n </div>\n </section>\n\n <!-- ─── 安全 ─── -->\n <section class=\"pane\" id=\"sec-security\">\n <h3>安全</h3>\n <div class=\"pane-desc\">// 登录密码、双因素、在用设备</div>\n\n <div class=\"form-row\">\n <div class=\"lbl\">登录密码</div>\n <div class=\"val\">\n <span class=\"static mono\">●●●●●●●●●●</span>\n <span class=\"muted-2 mono\" style=\"font-size: 11px; margin-left: auto;\">上次修改 2026-04-12</span>\n <button class=\"btn btn-sm\" onclick=\"Shell.toast('修改密码', '/settings/password')\" style=\"margin-left: 10px;\">修改</button>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">两步验证<div class=\"lbl-sub\">// 推荐开启</div></div>\n <div class=\"val\">\n <label class=\"switch\"><input type=\"checkbox\" id=\"opt-2fa\"><span class=\"slider\"></span></label>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48);\">短信 + Authenticator</span>\n </div>\n </div>\n\n <h3 style=\"margin-top: 24px;\">在用设备</h3>\n <div class=\"pane-desc\">// 不在此列表上的设备登录会触发短信告警</div>\n <div>\n <div class=\"device-row\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M2 20h20\"/></svg></div>\n <div>\n <div class=\"nm\">MacBook Pro · Chrome<span class=\"tag-cur\">CURRENT</span></div>\n <div class=\"meta\">// 上海 · 2026-05-21 14:08 · IP 116.xxx.xxx.42</div>\n </div>\n <div class=\"spacer\"></div>\n <span class=\"muted-2 mono\" style=\"font-size: 11px;\">当前会话</span>\n </div>\n <div class=\"device-row\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"6\" y=\"2\" width=\"12\" height=\"20\" rx=\"2\"/><path d=\"M11 18h2\"/></svg></div>\n <div>\n <div class=\"nm\">iPhone 15 · Safari</div>\n <div class=\"meta\">// 上海 · 2026-05-20 21:43</div>\n </div>\n <div class=\"spacer\"></div>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"Shell.toast('已下线', 'iPhone 15')\">下线</button>\n </div>\n <div class=\"device-row\">\n <div class=\"ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M2 20h20\"/></svg></div>\n <div>\n <div class=\"nm\">Windows · Edge</div>\n <div class=\"meta\">// 杭州 · 2026-05-18 09:12</div>\n </div>\n <div class=\"spacer\"></div>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"Shell.toast('已下线', 'Windows Edge')\">下线</button>\n </div>\n </div>\n <div style=\"margin-top: 14px;\">\n <button class=\"btn\" onclick=\"if(confirm('下线所有其他设备?')) Shell.toast('已下线其他设备', '2 个')\">下线所有其他设备</button>\n </div>\n </section>\n\n <!-- ─── 通知 ─── -->\n <section class=\"pane\" id=\"sec-notify\">\n <h3>通知</h3>\n <div class=\"pane-desc\">// 邮件、短信、站内提示开关</div>\n\n <div class=\"form-row\">\n <div class=\"lbl\">项目完成通知<div class=\"lbl-sub\">// 视频导出后</div></div>\n <div class=\"val\">\n <label class=\"switch\"><input type=\"checkbox\" id=\"n-export\" checked><span class=\"slider\"></span></label>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48);\">站内 · 邮件 · 短信</span>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">任务失败告警</div>\n <div class=\"val\">\n <label class=\"switch\"><input type=\"checkbox\" id=\"n-fail\" checked><span class=\"slider\"></span></label>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48);\">站内 · 邮件</span>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">额度不足提醒<div class=\"lbl-sub\">// 团队或个人剩余 < 20%</div></div>\n <div class=\"val\">\n <label class=\"switch\"><input type=\"checkbox\" id=\"n-quota\" checked><span class=\"slider\"></span></label>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48);\">站内 · 短信</span>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">异地登录告警</div>\n <div class=\"val\">\n <label class=\"switch\"><input type=\"checkbox\" id=\"n-login\" checked><span class=\"slider\"></span></label>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48);\">短信</span>\n </div>\n </div>\n </section>\n\n <!-- ─── 创作默认 ─── -->\n <section class=\"pane\" id=\"sec-pref\">\n <h3>创作默认</h3>\n <div class=\"pane-desc\">// 新建项目时的预填值,可在向导中改</div>\n\n <div class=\"form-row\" style=\"grid-template-columns: 160px 1fr; align-items: flex-start;\">\n <div class=\"lbl\" style=\"padding-top: 4px;\">默认模板</div>\n <div class=\"val\" style=\"display: block;\">\n <div class=\"pref-choices\" id=\"pref-template\">\n <div class=\"pref-choice selected\" data-v=\"pain\"><div class=\"t\">痛点种草</div><div class=\"d\">// 30s 默认档</div></div>\n <div class=\"pref-choice\" data-v=\"unbox\"><div class=\"t\">开箱测评</div><div class=\"d\">// 45s 默认档</div></div>\n <div class=\"pref-choice\" data-v=\"compare\"><div class=\"t\">对比展示</div><div class=\"d\">// 45s 默认档</div></div>\n <div class=\"pref-choice\" data-v=\"howto\"><div class=\"t\">教程演示</div><div class=\"d\">// 60s 默认档</div></div>\n <div class=\"pref-choice\" data-v=\"drama\"><div class=\"t\">剧情带货</div><div class=\"d\">// 60s 默认档</div></div>\n </div>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">默认时长档</div>\n <div class=\"val\">\n <div class=\"duration-row\" id=\"pref-duration\">\n <span class=\"dur-chip\" data-v=\"30\">30s</span>\n <span class=\"dur-chip\" data-v=\"45\">45s</span>\n <span class=\"dur-chip selected\" data-v=\"60\">60s</span>\n </div>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48); margin-left: 10px;\">// 60s = 4 段 × 15s</span>\n </div>\n </div>\n <div class=\"form-row\" style=\"grid-template-columns: 160px 1fr; align-items: flex-start;\">\n <div class=\"lbl\" style=\"padding-top: 4px;\">默认字幕样式</div>\n <div class=\"val\" style=\"display: block;\">\n <div class=\"pref-choices\" id=\"pref-subtitle\">\n <div class=\"pref-choice selected\" data-v=\"big-variety\"><div class=\"t\">大字综艺</div><div class=\"d\">// 抖音热门</div></div>\n <div class=\"pref-choice\" data-v=\"clean-ec\"><div class=\"t\">简洁电商</div><div class=\"d\">// 信息清晰</div></div>\n <div class=\"pref-choice\" data-v=\"premium\"><div class=\"t\">高级排版</div><div class=\"d\">// 居中衬线</div></div>\n <div class=\"pref-choice\" data-v=\"bullet\"><div class=\"t\">弹幕轻量</div><div class=\"d\">// 滚动出现</div></div>\n <div class=\"pref-choice\" data-v=\"emphasis\"><div class=\"t\">强调爆款</div><div class=\"d\">// 高对比</div></div>\n </div>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">默认 BGM 库</div>\n <div class=\"val\">\n <select class=\"select\" id=\"pref-bgm\" data-track>\n <option value=\"kapian\">抖音 Top10 卡点曲库</option>\n <option value=\"emotion\">情绪向 · 治愈/悬念</option>\n <option value=\"urban\">都市电子 · 通勤场景</option>\n <option value=\"none\">无 BGM</option>\n </select>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">默认转场</div>\n <div class=\"val\">\n <select class=\"select\" id=\"pref-transition\" data-track>\n <option>无转场</option>\n <option selected>淡入淡出 · 0.3s</option>\n <option>滑动 · 0.3s</option>\n <option>缩放 · 0.3s</option>\n </select>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">导出水印<div class=\"lbl-sub\">// VIP 可关闭</div></div>\n <div class=\"val\">\n <label class=\"switch\"><input type=\"checkbox\" id=\"opt-watermark\" checked disabled><span class=\"slider\"></span></label>\n <span class=\"static mono\" style=\"font-size: 11.5px; color: var(--black-alpha-48);\">右下角 · Airshelf</span>\n <a href=\"account.html\" style=\"font-size: 12px; color: var(--heat); text-decoration: none; margin-left: auto;\">升级 VIP →</a>\n </div>\n </div>\n </section>\n\n <!-- ─── 显示 ─── -->\n <section class=\"pane\" id=\"sec-display\">\n <h3>显示</h3>\n <div class=\"pane-desc\">// 界面外观与语言</div>\n\n <div class=\"form-row\">\n <div class=\"lbl\">外观</div>\n <div class=\"val\">\n <select class=\"select\" id=\"pref-theme\" data-track>\n <option selected>跟随系统</option>\n <option>浅色</option>\n <option disabled>深色(V2)</option>\n </select>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">语言</div>\n <div class=\"val\">\n <select class=\"select\" id=\"pref-lang\" data-track>\n <option selected>简体中文</option>\n <option disabled>English(V2)</option>\n </select>\n </div>\n </div>\n <div class=\"form-row\">\n <div class=\"lbl\">表格密度</div>\n <div class=\"val\">\n <select class=\"select\" id=\"pref-density\" data-track>\n <option>紧凑</option>\n <option selected>标准</option>\n <option>宽松</option>\n </select>\n </div>\n </div>\n </section>\n\n <div style=\"text-align: center; padding: 24px 0 8px; color: var(--black-alpha-32); font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;\">\n // Airshelf · v2.1 · build 20260521\n </div>\n </main>\n</div>\n\n<!-- ─── 退出登录确认 modal ─── -->\n<div class=\"modal-bg\" id=\"logout-confirm-bg\" onclick=\"if(event.target===this)Shell.closeModal('logout-confirm-bg')\">\n <div class=\"modal logout-confirm-modal\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"logout-confirm-title\" aria-describedby=\"logout-confirm-desc\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><path d=\"m16 17 5-5-5-5\"/><path d=\"M21 12H9\"/></svg>\n </div>\n <div class=\"ti\" id=\"logout-confirm-title\">退出当前账号<span>// LOG OUT CURRENT SESSION</span></div>\n </div>\n <div class=\"modal-b\" id=\"logout-confirm-desc\">\n <p class=\"logout-confirm-copy\">确认后将退出当前设备上的 Airshelf,再次使用需要重新登录。</p>\n <div class=\"logout-confirm-points\">\n <div class=\"li\">项目、资产、团队成员与余额数据都会保留</div>\n <div class=\"li\">仅影响当前浏览器会话,不会下线其他设备</div>\n </div>\n <div class=\"logout-unsaved-note\" id=\"logout-unsaved-note\" hidden>当前有未保存的设置变更,退出后这些变更不会保存。</div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" id=\"logout-confirm-cancel\" onclick=\"Shell.closeModal('logout-confirm-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"logout-confirm-ok\">\n <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=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\"/><path d=\"m16 17 5-5-5-5\"/><path d=\"M21 12H9\"/></svg>\n 确认退出\n </button>\n </div>\n </div>\n</div>\n\n<!-- ─── 上传头像 modal ─── -->\n<div class=\"modal-bg\" id=\"avatar-up-bg\" onclick=\"if(event.target===this)Shell.closeModal('avatar-up-bg')\">\n <div class=\"modal av-up-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8\"/></svg>\n </div>\n <div class=\"ti\">上传头像<span>// 用于个人主页、评论与团队展示</span></div>\n </div>\n <div class=\"modal-b np-body\">\n\n <div class=\"av-up-preview-row\">\n <div class=\"av-up-preview\" id=\"av-up-preview\">李</div>\n <div class=\"av-up-preview-meta\">\n <div class=\"t\" id=\"av-up-preview-name\">当前头像 · 默认</div>\n <div class=\"d\" id=\"av-up-preview-info\">// 系统生成 · 取姓氏首字</div>\n </div>\n </div>\n\n <div class=\"upload-zone\" id=\"av-up-zone\" tabindex=\"0\" role=\"button\" aria-label=\"点击或拖入图片上传\">\n <span class=\"uz-ic\">\n <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-4\"/><polyline points=\"17 8 12 3 7 8\"/><line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"/></svg>\n </span>\n <div><strong>点击选择</strong> · 或拖入图片</div>\n <span class=\"uz-hint\">JPG / PNG / WebP · ≤ 2 MB · 推荐 256 × 256</span>\n <input type=\"file\" id=\"av-up-file\" accept=\"image/jpeg,image/png,image/webp\" hidden>\n </div>\n\n <div class=\"av-up-rules\">\n <div class=\"li\">最大 2 MB · 长宽比建议 1:1 · 系统会自动裁切为圆形</div>\n <div class=\"li\">不要上传含他人肖像的图片,违规可能导致账号封停</div>\n </div>\n\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('avatar-up-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"av-up-confirm\">\n <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 12l5 5L20 6\"/></svg>\n 确认使用\n </button>\n </div>\n </div>\n</div>\n\n</div>\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({\n active: 'settings',\n crumbs: [{ label: '工作台', href: 'index.html' }, { label: '设置' }]\n});\n\n/* ─── 配置 ─── */\nconst SECTIONS = ['sec-profile', 'sec-security', 'sec-notify', 'sec-pref', 'sec-display'];\nconst navLinks = document.querySelectorAll('.settings-nav a[data-jump]');\nlet suppressLeavePrompt = false;\n\nfunction logoutCurrentDevice() {\n const note = document.getElementById('logout-unsaved-note');\n if (note) note.hidden = !(typeof dirtyFields !== 'undefined' && dirtyFields.size > 0);\n Shell.openModal('logout-confirm-bg');\n requestAnimationFrame(() => document.getElementById('logout-confirm-cancel')?.focus());\n}\n\nfunction confirmLogout() {\n suppressLeavePrompt = true;\n Shell.closeModal('logout-confirm-bg');\n Shell.toast('已退出', '正在跳转登录页');\n setTimeout(() => location.href = 'login.html', 600);\n}\n\ndocument.getElementById('logout-confirm-ok')?.addEventListener('click', confirmLogout);\n\n/* ─── 1. 点击 nav → 只显示对应 section,其余隐藏 + 同步 URL hash ─── */\nfunction showSection(id) {\n if (!SECTIONS.includes(id)) id = SECTIONS[0];\n SECTIONS.forEach(sid => {\n const el = document.getElementById(sid);\n if (el) el.style.display = (sid === id) ? '' : 'none';\n });\n navLinks.forEach(a => a.classList.toggle('active', a.dataset.jump === id));\n if (location.hash.slice(1) !== id) {\n try { window.location.hash = id; } catch (e) {}\n }\n // 切换面板后回到顶端,避免长面板留下的滚动位置错乱\n window.scrollTo({ top: 0, behavior: 'instant' });\n}\n\nnavLinks.forEach(a => {\n a.addEventListener('click', e => {\n e.preventDefault();\n showSection(a.dataset.jump);\n a.focus();\n });\n a.addEventListener('keydown', e => {\n const idx = [...navLinks].indexOf(a);\n if (e.key === 'ArrowDown') { e.preventDefault(); navLinks[(idx + 1) % navLinks.length].focus(); }\n else if (e.key === 'ArrowUp') { e.preventDefault(); navLinks[(idx - 1 + navLinks.length) % navLinks.length].focus(); }\n else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); a.click(); }\n });\n});\n\n/* ─── 2. 初始加载:读 URL hash 显示对应 section(无 hash 时默认 sec-profile) ─── */\n(function initSection() {\n const hash = location.hash.slice(1);\n showSection(SECTIONS.includes(hash) ? hash : 'sec-profile');\n})();\n\n/* hash 外部变化(如浏览器前进后退)也跟着切 */\nwindow.addEventListener('hashchange', () => {\n const hash = location.hash.slice(1);\n if (SECTIONS.includes(hash)) showSection(hash);\n});\n\n/* ─── 4. 偏好 chip 选择 ─── */\nfunction bindChoice(containerId) {\n const ct = document.getElementById(containerId);\n if (!ct) return;\n ct.querySelectorAll('.pref-choice').forEach(c => {\n c.addEventListener('click', () => {\n ct.querySelectorAll('.pref-choice').forEach(x => x.classList.remove('selected'));\n c.classList.add('selected');\n markDirty(c);\n });\n });\n}\nbindChoice('pref-template');\nbindChoice('pref-subtitle');\n\ndocument.querySelectorAll('#pref-duration .dur-chip').forEach(c => {\n c.addEventListener('click', () => {\n document.querySelectorAll('#pref-duration .dur-chip').forEach(x => x.classList.remove('selected'));\n c.classList.add('selected');\n markDirty(c);\n });\n});\n\n/* ─── 5. dirty state · 追踪改了哪几节 + save btn 显示变更条数 ─── */\nconst dirtyFields = new Set(); // 改过的字段 id 集合\nconst dirtySections = new Set(); // 涉及的 section id 集合\nconst saveBtn = document.getElementById('save-btn');\nconst cancelBtn = document.getElementById('save-cancel');\nconst saveBtnDefaultLabel = saveBtn.innerHTML;\n\nfunction sectionOf(el) {\n const sec = el.closest('section[id]');\n return sec ? sec.id : null;\n}\n\nfunction syncSaveBtn() {\n const n = dirtyFields.size;\n if (n === 0) {\n saveBtn.disabled = true;\n cancelBtn.disabled = true;\n saveBtn.style.opacity = '.5';\n saveBtn.style.cursor = 'not-allowed';\n cancelBtn.style.opacity = '.5';\n cancelBtn.style.cursor = 'not-allowed';\n saveBtn.innerHTML = saveBtnDefaultLabel;\n } else {\n saveBtn.disabled = false;\n cancelBtn.disabled = false;\n saveBtn.style.opacity = '';\n saveBtn.style.cursor = '';\n cancelBtn.style.opacity = '';\n cancelBtn.style.cursor = '';\n saveBtn.innerHTML = `<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 12l5 5L20 6\"/></svg> 保存所有变更 <span style=\"opacity:.75;font-family:var(--font-mono);font-size:11px;margin-left:4px;\">· ${n} 项</span>`;\n }\n // nav 上加 dirty dot\n navLinks.forEach(a => a.classList.toggle('has-changes', dirtySections.has(a.dataset.jump)));\n}\n\nfunction markDirty(el) {\n const id = el.id || (el.dataset && el.dataset.v) || ('anon-' + Math.random().toString(36).slice(2, 8));\n if (dirtyFields.has(id)) return;\n dirtyFields.add(id);\n const sec = sectionOf(el);\n if (sec) dirtySections.add(sec);\n syncSaveBtn();\n}\nfunction clearDirty() {\n dirtyFields.clear();\n dirtySections.clear();\n syncSaveBtn();\n}\n\ndocument.querySelectorAll('[data-track], input[type=\"checkbox\"], select').forEach(el => {\n el.addEventListener('change', () => markDirty(el));\n if (el.tagName === 'INPUT' && el.type !== 'checkbox') el.addEventListener('input', () => markDirty(el));\n});\n\n/* ─── 6. 表单校验:必填 + 邮箱格式 ─── */\nconst VALIDATORS = {\n 'prof-name': v => v.trim().length === 0 ? '显示名称不能为空' : (v.length > 20 ? '显示名称最多 20 字' : null),\n 'prof-email': v => !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v.trim()) ? '邮箱格式不正确' : null,\n 'prof-phone': v => !/^\\d{3}\\*{4}\\d{4}$|^1\\d{10}$/.test(v.trim()) ? '手机号格式不正确' : null,\n};\nfunction validate() {\n const errors = [];\n Object.entries(VALIDATORS).forEach(([id, fn]) => {\n const el = document.getElementById(id);\n if (!el) return;\n const err = fn(el.value);\n el.classList.toggle('invalid', !!err);\n if (err) errors.push(`${id}: ${err}`);\n });\n return errors;\n}\n\n/* ─── 7. 保存 / 取消 ─── */\nsaveBtn.addEventListener('click', () => {\n if (dirtyFields.size === 0) return;\n const errors = validate();\n if (errors.length) {\n Shell.toast('校验未通过', errors[0].split(': ')[1] + ' · 共 ' + errors.length + ' 项');\n return;\n }\n Shell.toast('设置已保存', `${dirtyFields.size} 项变更已生效 · 涉及 ${dirtySections.size} 个分区`);\n clearDirty();\n});\ncancelBtn.addEventListener('click', () => {\n if (dirtyFields.size === 0) return;\n if (confirm(`放弃 ${dirtyFields.size} 项未保存的变更?`)) location.reload();\n});\n\n/* ─── 8. 离开页面前提醒(有未保存变更时)─── */\nwindow.addEventListener('beforeunload', e => {\n if (!suppressLeavePrompt && dirtyFields.size > 0) {\n e.preventDefault();\n e.returnValue = '';\n }\n});\n\n/* ─── 9. invalid input 视觉反馈样式 ─── */\nconst _invalidStyle = document.createElement('style');\n_invalidStyle.textContent = `\n .input.invalid { border-color: var(--accent-crimson); box-shadow: 0 0 0 3px rgba(235,52,36,.12); }\n .input.invalid:focus { border-color: var(--accent-crimson); box-shadow: 0 0 0 3px rgba(235,52,36,.18); }\n`;\ndocument.head.appendChild(_invalidStyle);\n\n/* ─── 10. nav badge 计数自动同步(从 section 内 switch 开启数 / device 数等) ─── */\nfunction syncBadges() {\n const secNotify = document.getElementById('sec-notify');\n if (secNotify) {\n const onCount = secNotify.querySelectorAll('input[type=\"checkbox\"]:checked').length;\n const totalCount = secNotify.querySelectorAll('input[type=\"checkbox\"]').length;\n const badge = document.querySelector('.settings-nav a[data-jump=\"sec-notify\"] .nav-badge');\n if (badge) badge.textContent = `${onCount}/${totalCount}`;\n }\n const secSecurity = document.getElementById('sec-security');\n if (secSecurity) {\n const devCount = secSecurity.querySelectorAll('.device-row').length;\n const badge = document.querySelector('.settings-nav a[data-jump=\"sec-security\"] .nav-badge');\n if (badge) badge.textContent = `${devCount} 设备`;\n }\n}\nsyncBadges();\ndocument.querySelectorAll('#sec-notify input[type=\"checkbox\"]').forEach(cb => cb.addEventListener('change', syncBadges));\n\n/* ─── 11. 头像上传 modal ─── */\nconst DEFAULT_AVATAR = { kind: 'default', label: '李', name: '默认', info: '// 系统生成 · 取姓氏首字' };\nlet _draftAvatar = { ...DEFAULT_AVATAR };\nlet _currentAvatar = { ...DEFAULT_AVATAR };\n\nconst avPreview = document.getElementById('av-up-preview');\nconst avPreviewName = document.getElementById('av-up-preview-name');\nconst avPreviewInfo = document.getElementById('av-up-preview-info');\nconst avProfPreview = document.getElementById('prof-avatar-preview');\nconst avZone = document.getElementById('av-up-zone');\nconst avFile = document.getElementById('av-up-file');\nconst avConfirm = document.getElementById('av-up-confirm');\nconst avResetBtn = document.getElementById('prof-avatar-reset');\n\nfunction paintPreview() {\n if (_draftAvatar.kind === 'image') {\n avPreview.innerHTML = `<img src=\"${_draftAvatar.src}\" alt=\"头像预览\">`;\n } else {\n avPreview.innerHTML = '';\n avPreview.textContent = _draftAvatar.label || '李';\n }\n avPreviewName.textContent = `预览 · ${_draftAvatar.name}`;\n avPreviewInfo.textContent = _draftAvatar.info;\n}\n\nfunction applyAvatarTo(el, avatar) {\n if (avatar.kind === 'image') {\n el.innerHTML = `<img src=\"${avatar.src}\" alt=\"头像\" style=\"width:100%;height:100%;object-fit:cover;border-radius:50%;display:block;\">`;\n } else {\n el.innerHTML = '';\n el.textContent = avatar.label || '李';\n }\n}\n\navZone.addEventListener('click', () => avFile.click());\navZone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); avFile.click(); } });\navZone.addEventListener('dragover', e => { e.preventDefault(); avZone.classList.add('dragover'); });\navZone.addEventListener('dragleave', () => avZone.classList.remove('dragover'));\navZone.addEventListener('drop', e => {\n e.preventDefault();\n avZone.classList.remove('dragover');\n const f = e.dataTransfer.files && e.dataTransfer.files[0];\n if (f) handleAvatarFile(f);\n});\navFile.addEventListener('change', () => { if (avFile.files[0]) handleAvatarFile(avFile.files[0]); });\n\nfunction handleAvatarFile(f) {\n if (!/^image\\/(jpeg|png|webp)$/.test(f.type)) {\n Shell.toast('格式不支持', '仅支持 JPG / PNG / WebP');\n return;\n }\n if (f.size > 2 * 1024 * 1024) {\n Shell.toast('文件过大', `${(f.size / 1024 / 1024).toFixed(2)} MB · 限 2 MB`);\n return;\n }\n const url = URL.createObjectURL(f);\n _draftAvatar = { kind: 'image', src: url, name: f.name, info: `// ${(f.size / 1024).toFixed(0)} KB · ${f.type.split('/')[1].toUpperCase()}` };\n paintPreview();\n}\n\navConfirm.addEventListener('click', () => {\n _currentAvatar = { ..._draftAvatar };\n applyAvatarTo(avProfPreview, _currentAvatar);\n Shell.closeModal('avatar-up-bg');\n Shell.toast('头像已更新', _currentAvatar.name);\n markDirty(avProfPreview);\n});\n\navResetBtn.addEventListener('click', () => {\n _currentAvatar = { ...DEFAULT_AVATAR };\n applyAvatarTo(avProfPreview, _currentAvatar);\n _draftAvatar = { ...DEFAULT_AVATAR };\n paintPreview();\n Shell.toast('已恢复默认头像', _currentAvatar.name);\n markDirty(avProfPreview);\n});\n\n// 打开 modal 时同步当前到 draft + preview\ndocument.querySelector('.av-actions button.btn-sm').addEventListener('click', () => {\n _draftAvatar = { ..._currentAvatar };\n paintPreview();\n});\n\npaintPreview();\n\n/* ─── 12. 实时邮箱 / 名称 input 校验反馈(blur 时显示错) ─── */\n['prof-name', 'prof-email', 'prof-phone'].forEach(id => {\n const el = document.getElementById(id);\n if (!el || !VALIDATORS[id]) return;\n el.addEventListener('blur', () => {\n const err = VALIDATORS[id](el.value);\n el.classList.toggle('invalid', !!err);\n });\n el.addEventListener('input', () => {\n if (el.classList.contains('invalid')) {\n const err = VALIDATORS[id](el.value);\n if (!err) el.classList.remove('invalid');\n }\n });\n});\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n",
"team": "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta name=\"x-airshelf-exact-source\" content=\"team.html\">\n<meta charset=\"utf-8\">\n<title>团队 · Airshelf</title>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<link rel=\"stylesheet\" href=\"/exact/assets/restraint.css?v=2026060101\">\n<style>\n /* ─── 团队信息卡(深色 banner · 上标题行 + 下统计行)─── */\n /* 顶部行:banner(左)+ 团队动态(右),与下方 team-grid 同列宽对齐 */\n .team-top { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; margin-bottom: 24px; align-items: stretch; }\n\n .team-banner {\n background: var(--accent-black);\n color: var(--accent-white);\n padding: 22px 28px 24px;\n position: relative;\n border: 1px solid var(--accent-black);\n border-radius: var(--r-md);\n }\n\n /* ─── 团队动态卡(贴 banner 右边)─── */\n .team-feed { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 16px 18px; display: flex; flex-direction: column; min-width: 0; }\n .team-feed .h { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }\n .team-feed .h h3 { font-size: 13.5px; font-weight: 600; margin: 0; }\n .team-feed .h .ct { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }\n .team-feed .h .more { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--heat); text-decoration: none; cursor: pointer; }\n .team-feed .feed-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; max-height: 240px; padding-right: 4px; scrollbar-width: thin; }\n .team-feed .feed-list::-webkit-scrollbar { width: 4px; }\n .team-feed .feed-list::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }\n .team-feed .feed-item { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 10px; align-items: start; }\n .team-feed .feed-item .av { width: 24px; height: 24px; border-radius: 50%; background: var(--background-lighter); border: 1px solid var(--border-faint); display: grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--accent-black); }\n .team-feed .feed-item .txt { font-size: 12.5px; line-height: 1.45; color: var(--accent-black); min-width: 0; }\n .team-feed .feed-item .txt .who { font-weight: 600; }\n .team-feed .feed-item .txt .act { color: var(--black-alpha-56); margin: 0 3px; }\n .team-feed .feed-item .txt .obj { color: var(--heat); }\n .team-feed .feed-item .txt .obj-money { color: var(--accent-forest); font-variant-numeric: tabular-nums; font-family: var(--font-mono); }\n .team-feed .feed-item .ts { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }\n /* 4 个装订线小十字(2 个 pseudo + 2 个 span)*/\n .team-banner::before, .team-banner::after,\n .team-banner > .corner-tr, .team-banner > .corner-bl {\n content: ''; position: absolute; width: 14px; height: 14px;\n 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;\n background-size: contain; pointer-events: none;\n }\n .team-banner::before { top: -7px; left: -7px; }\n .team-banner::after { bottom: -7px; right: -7px; }\n .team-banner > .corner-tr { top: -7px; right: -7px; }\n .team-banner > .corner-bl { bottom: -7px; left: -7px; }\n\n /* 第 1 行:标题 + 主操作 */\n .banner-head { display: flex; align-items: flex-start; gap: 20px; }\n .banner-id { flex: 1; min-width: 0; }\n .banner-id .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }\n .banner-id .nm { font-size: 22px; font-weight: 700; letter-spacing: -.012em; margin-top: 4px; display: flex; align-items: baseline; gap: 10px; }\n .banner-id .nm .tag { font-size: 10.5px; font-family: var(--font-mono); padding: 2px 8px; background: rgba(255,255,255,.12); border-radius: var(--r-pill); letter-spacing: .04em; font-weight: 500; }\n .banner-id .meta { font-size: 12px; color: rgba(255,255,255,.5); margin-top: 6px; font-family: var(--font-mono); letter-spacing: .02em; }\n .banner-actions { display: flex; gap: 8px; flex-shrink: 0; }\n .banner-actions .btn { background: var(--accent-white); color: var(--accent-black); border-color: var(--accent-white); }\n .banner-actions .btn:hover { background: var(--background-base); }\n .banner-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); }\n .banner-actions .btn-ghost:hover { background: rgba(255,255,255,.08); color: var(--accent-white); }\n\n /* 分隔线 */\n .banner-divider { height: 1px; background: rgba(255,255,255,.1); margin: 20px 0 18px; }\n\n /* 第 2 行:4 列统计 */\n .banner-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; }\n .banner-stats .stat { min-width: 0; }\n .banner-stats .stat .lbl { font-family: var(--font-mono); font-size: 10.5px; color: rgba(255,255,255,.55); letter-spacing: .06em; text-transform: uppercase; }\n .banner-stats .stat .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.012em; margin-top: 6px; }\n /* color 必须显式写,否则会被 restraint.css 全局 .stat .v 的 color: var(--accent-black) 覆盖成黑字 */\n .banner-stats .stat .v { color: var(--accent-white); }\n .banner-stats .stat .v.warn { color: #FFB870; }\n .banner-stats .stat .sub { font-size: 11px; color: rgba(255,255,255,.5); margin-top: 4px; font-family: var(--font-mono); letter-spacing: .02em; }\n\n /* ─── 主体两栏 ─── */\n .team-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 24px; align-items: start; }\n\n .pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }\n .pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }\n .pane h3 .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); font-weight: 400; }\n .pane h3 .spacer { margin-left: auto; }\n\n /* ─── 成员表 ─── */\n .members-table .av { width: 32px; height: 32px; border-radius: 50%; background: var(--background-lighter); display: inline-grid; place-items: center; font-weight: 600; font-size: 13px; color: var(--accent-black); border: 1px solid var(--border-faint); }\n .members-table .who { display: flex; align-items: center; gap: 10px; }\n .members-table .nm { font-weight: 500; font-size: 13.5px; line-height: 1.2; }\n .members-table .em { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); }\n .members-table .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 11px; font-weight: 500; }\n .members-table .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; }\n .members-table .role-super { background: var(--heat-12); color: var(--heat); }\n .members-table .role-super .dot { background: var(--heat); }\n .members-table .role-admin { background: rgba(30,64,175,.1); color: #1E40AF; }\n .members-table .role-admin .dot { background: #1E40AF; }\n .members-table .role-member { background: var(--background-lighter); color: var(--black-alpha-56); }\n .members-table .role-member .dot { background: var(--black-alpha-56); }\n .members-table .quota-cell { font-variant-numeric: tabular-nums; font-family: var(--font-mono); font-size: 12px; }\n .members-table .quota-cell .lbl { color: var(--black-alpha-48); }\n .members-table .quota-cell .v { color: var(--accent-black); font-weight: 600; }\n .members-table .used-bar { width: 80px; height: 4px; background: var(--background-lighter); border-radius: 2px; overflow: hidden; margin-top: 4px; }\n .members-table .used-bar > span { display: block; height: 100%; background: var(--heat); }\n .members-table .used-bar > span.ok { background: var(--accent-forest); }\n .members-table .used-bar > span.warn { background: #B45309; }\n .members-table .acts { display: flex; gap: 4px; justify-content: flex-end; }\n .members-table .icon-btn-sm { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); cursor: pointer; color: var(--black-alpha-56); transition: all var(--t-base); }\n .members-table .icon-btn-sm:hover { color: var(--heat); border-color: var(--heat-20); }\n .members-table .icon-btn-sm svg { width: 14px; height: 14px; }\n .members-table .icon-btn-sm.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }\n .members-table tr.pending td { opacity: .65; }\n .members-table tr.pending .nm::after { content: '· 待激活'; font-size: 11px; color: var(--black-alpha-48); margin-left: 6px; font-weight: 400; font-family: var(--font-mono); }\n\n /* ─── 角色权限矩阵 ─── */\n .perm-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface); border: 1px solid var(--border-muted); border-radius: var(--r-md); overflow: hidden; font-size: 12.5px; }\n .perm-table th, .perm-table td { padding: 8px 10px; border-bottom: 0; }\n .perm-table th { border-bottom: 1px solid var(--border-muted); background: var(--background-lighter); font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; text-align: left; }\n .perm-table th:not(:first-child), .perm-table td:not(:first-child) { text-align: center; }\n .perm-table tbody td:first-child { color: var(--accent-black); }\n .perm-table .yes { color: var(--accent-forest); font-weight: 600; }\n .perm-table .no { color: var(--black-alpha-32); }\n\n /* ─── 额度检查规则 ─── */\n .quota-rules { font-size: 12.5px; color: var(--black-alpha-56); line-height: 1.8; }\n .quota-rules .step { display: flex; gap: 10px; padding: 6px 0; align-items: flex-start; }\n .quota-rules .num { width: 18px; height: 18px; border-radius: 50%; background: var(--heat-12); color: var(--heat); font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; flex: 0 0 18px; margin-top: 1px; }\n .quota-rules .v { color: var(--accent-black); font-weight: 500; }\n .quota-rules .formula { font-family: var(--font-mono); font-size: 11px; color: var(--heat); background: var(--heat-12); padding: 1px 6px; }\n\n /* ─── 邀请 modal ─── */\n .invite-modal { width: min(480px, 92vw); }\n .invite-modal .field { margin-bottom: 14px; }\n .invite-modal label.field-label { display: block; font-size: 12px; color: var(--black-alpha-56); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }\n .invite-modal label.field-label .req { color: var(--accent-crimson); margin-left: 2px; }\n .role-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n .role-choice { padding: 12px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: all var(--t-base); }\n .role-choice:hover { background: var(--background-lighter); }\n .role-choice.selected { border-color: var(--heat); background: var(--heat-12); }\n .role-choice .title { font-size: 13px; font-weight: 600; color: var(--accent-black); }\n .role-choice .desc { font-size: 11px; color: var(--black-alpha-56); margin-top: 2px; font-family: var(--font-mono); letter-spacing: .02em; }\n .quota-input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n .quota-input-row .input { font-variant-numeric: tabular-nums; }\n\n /* ─── 重置密码 modal ─── */\n .reset-pwd-modal { width: min(480px, 92vw); }\n .reset-pwd-modal .reset-pwd-warn {\n display: flex; align-items: flex-start; gap: 10px;\n padding: 10px 12px;\n background: var(--heat-12);\n border: 1px solid var(--heat-20);\n border-radius: var(--r-md);\n margin-bottom: 14px;\n font-size: 12.5px; color: var(--heat);\n line-height: 1.5;\n }\n .reset-pwd-modal .reset-pwd-warn svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 2px; }\n .reset-pwd-modal .btn-sm { height: 38px; padding: 0 12px; }\n\n /* ─── 全部团队动态 modal ─── */\n .feed-all-modal { width: min(640px, 92vw); max-width: min(640px, 92vw); position: relative; }\n .feed-all-modal .md-x {\n position: absolute; top: 14px; right: 16px;\n width: 28px; height: 28px;\n background: transparent; border: 0; border-radius: var(--r-sm);\n color: var(--black-alpha-56); cursor: pointer;\n display: grid; place-items: center;\n }\n .feed-all-modal .md-x:hover { background: var(--background-lighter); color: var(--accent-black); }\n .feed-all-modal .md-x svg { width: 14px; height: 14px; }\n .feed-all-filter {\n display: flex; align-items: center; gap: 6px;\n padding: 12px 20px;\n border-bottom: 1px solid var(--border-faint);\n background: var(--background-lighter);\n }\n .feed-all-filter .spacer { flex: 1; }\n .feed-all-filter .fa-meta { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .feed-all-filter .fa-chip {\n height: 26px; padding: 0 10px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n font-size: 11.5px; color: var(--black-alpha-72);\n font-family: inherit; cursor: pointer;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .feed-all-filter .fa-chip:hover { border-color: var(--heat-20); color: var(--heat); }\n .feed-all-filter .fa-chip.selected { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }\n .feed-all-body { max-height: 56vh; overflow-y: auto; padding: 8px 20px 20px; }\n .feed-all-list { display: flex; flex-direction: column; gap: 10px; }\n .feed-all-list .feed-item {\n display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 12px;\n align-items: start;\n padding: 10px 0;\n border-bottom: 1px solid var(--border-faint);\n }\n .feed-all-list .feed-item:last-child { border-bottom: 0; }\n .feed-all-list .feed-item .av {\n width: 28px; height: 28px;\n background: var(--background-lighter); border: 1px solid var(--border-faint);\n border-radius: 50%; display: grid; place-items: center;\n font-size: 12px; font-weight: 600; color: var(--accent-black);\n }\n .feed-all-list .feed-item .txt { font-size: 13px; line-height: 1.5; color: var(--accent-black); }\n .feed-all-list .feed-item .txt .who { font-weight: 600; }\n .feed-all-list .feed-item .txt .act { color: var(--black-alpha-56); margin: 0 4px; }\n .feed-all-list .feed-item .txt .obj { color: var(--heat); }\n .feed-all-list .feed-item .ts { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; white-space: nowrap; padding-top: 2px; }\n .feed-all-list .feed-empty { padding: 40px 0; text-align: center; font-family: var(--font-mono); font-size: 12px; color: var(--black-alpha-48); letter-spacing: .02em; }\n\n /* ─── 月限额 modal ─── */\n .limit-modal .limit-presets { display: flex; flex-wrap: wrap; gap: 6px; }\n .limit-modal .limit-presets .lp {\n height: 32px; padding: 0 12px;\n background: var(--surface);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-pill);\n font-size: 12px; font-family: var(--font-mono);\n color: var(--accent-black);\n cursor: pointer;\n font-variant-numeric: tabular-nums;\n transition: border-color var(--t-base), background var(--t-base), color var(--t-base);\n }\n .limit-modal .limit-presets .lp:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }\n .limit-modal .limit-presets .lp.selected { border-color: var(--heat); background: var(--heat-12); color: var(--heat); font-weight: 600; }\n .limit-modal .limit-info {\n margin-top: 14px;\n padding: 10px 12px;\n background: var(--background-lighter);\n border-radius: var(--r-md);\n font-size: 11.5px;\n display: flex; flex-direction: column; gap: 4px;\n }\n .limit-modal .limit-info .li-row { display: flex; align-items: baseline; gap: 8px; }\n .limit-modal .limit-info .lk { font-family: var(--font-mono); color: var(--black-alpha-48); letter-spacing: .02em; }\n .limit-modal .limit-info .lv { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--accent-black); font-weight: 600; }\n .limit-modal .limit-info .lv.neg { color: var(--accent-crimson); }\n .limit-modal label.field-label .lbl-note { color: var(--black-alpha-48); font-weight: 400; margin-left: 2px; font-family: var(--font-mono); }\n .limit-modal input[type=number]::-webkit-outer-spin-button,\n .limit-modal input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n .limit-modal input[type=number] { -moz-appearance: textfield; }\n\n /* ─── 创建账户 modal ─── */\n .create-acct-modal label.field-label .lbl-note { color: var(--black-alpha-48); font-weight: 400; margin-left: 2px; font-family: var(--font-mono); }\n .create-acct-modal .btn-sm { height: 38px; padding: 0 12px; }\n .create-acct-modal .quota-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }\n .create-acct-modal .quota-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n .create-acct-modal .quota-cell .qk { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }\n .create-acct-modal .quota-cell .input { font-variant-numeric: tabular-nums; height: 34px; padding: 0 10px; }\n .create-acct-modal input[type=number]::-webkit-outer-spin-button,\n .create-acct-modal input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n .create-acct-modal input[type=number] { -moz-appearance: textfield; }\n\n /* ─── 分享凭据 modal ─── */\n .share-acct-modal { width: min(480px, 92vw); }\n .share-acct-modal .cred-card {\n background: var(--background-lighter);\n border: 1px solid var(--border-faint);\n border-radius: var(--r-md);\n overflow: hidden;\n }\n .share-acct-modal .cred-row {\n display: grid;\n grid-template-columns: 80px minmax(0, 1fr) 28px;\n gap: 12px;\n align-items: center;\n padding: 12px 14px;\n border-bottom: 1px solid var(--border-faint);\n }\n .share-acct-modal .cred-row:last-child { border-bottom: 0; }\n .share-acct-modal .cred-row .ck { font-size: 11.5px; color: var(--black-alpha-48); font-family: var(--font-mono); letter-spacing: .02em; }\n .share-acct-modal .cred-row .cv {\n font-size: 13px; color: var(--accent-black); font-weight: 500;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .share-acct-modal .cred-row .cv.mono { font-family: var(--font-mono); letter-spacing: .04em; }\n .share-acct-modal .cred-copy {\n width: 28px; height: 28px;\n background: transparent; border: 1px solid var(--border-faint);\n border-radius: var(--r-sm); color: var(--black-alpha-56);\n display: grid; place-items: center; cursor: pointer;\n transition: border-color var(--t-base), color var(--t-base);\n }\n .share-acct-modal .cred-copy:hover { border-color: var(--heat-20); color: var(--heat); }\n .share-acct-modal .cred-copy svg { width: 13px; height: 13px; }\n .share-acct-modal .cred-rules {\n margin-top: 14px;\n font-family: var(--font-mono); font-size: 11px;\n color: var(--black-alpha-56);\n letter-spacing: .02em; line-height: 1.7;\n }\n .share-acct-modal .cred-rules .li { display: flex; gap: 8px; }\n .share-acct-modal .cred-rules .li::before { content: '//'; color: var(--black-alpha-32); flex: 0 0 auto; }\n\n /* ─── 编辑成员 modal ─── */\n .edit-member-modal .ti .em-sep { color: var(--black-alpha-32); font-weight: 400; margin: 0 2px; }\n .edit-member-modal .ti #edit-username { color: var(--accent-black); font-weight: 500; }\n .edit-member-modal label.field-label .lbl-note { color: var(--black-alpha-48); font-weight: 400; margin-left: 2px; font-family: var(--font-mono); }\n .edit-member-modal #edit-name-readonly { background: var(--background-lighter); color: var(--black-alpha-56); cursor: not-allowed; }\n .edit-member-modal .readonly-text { font-size: 13px; color: var(--accent-black); padding: 8px 0; line-height: 1.4; }\n .edit-member-modal .readonly-text .role-pill { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--r-pill); font-size: 12px; font-weight: 500; background: var(--heat-12); color: var(--heat); }\n .edit-member-modal .readonly-text .role-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--heat); }\n .edit-member-modal input[type=number]::-webkit-outer-spin-button,\n .edit-member-modal input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n .edit-member-modal input[type=number] { -moz-appearance: textfield; font-variant-numeric: tabular-nums; }\n</style>\n</head>\n<body>\n<div id=\"page\">\n\n<div class=\"page-head\">\n <div>\n <h1>团队管理</h1>\n <div class=\"sub\"><span class=\"mono\">// 成员 · 角色 · 额度 · 共享资产库</span></div>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-primary\" id=\"open-invite\">\n <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=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n 创建账户\n </button>\n </div>\n</div>\n\n<!-- 顶部行:团队 banner(左)+ 团队动态(右) -->\n<div class=\"team-top\">\n\n<!-- 团队信息 banner -->\n<div class=\"team-banner\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n\n <div class=\"banner-head\">\n <div class=\"banner-id\">\n <div class=\"lbl\">[ TEAM ]</div>\n <div class=\"nm\">小李的店 <span class=\"tag\">企业</span></div>\n <div class=\"meta\">// 团队 ID: T-2026-A8F2 · 创建于 2026-04-12 · 5 名成员</div>\n </div>\n <div class=\"banner-actions\">\n <button class=\"btn btn-sm\" onclick=\"location.href='account.html'\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4\"/></svg>\n 充值\n </button>\n <button class=\"btn btn-ghost btn-sm\" id=\"open-limit\">设置月限额</button>\n </div>\n </div>\n\n <div class=\"banner-divider\"></div>\n\n <div class=\"banner-stats\">\n <div class=\"stat\">\n <div class=\"lbl\">[ 充值余额 ]</div>\n <div class=\"v\">¥327.40</div>\n <div class=\"sub\">// 团队总池</div>\n </div>\n <div class=\"stat\">\n <div class=\"lbl\">[ 月限额 ]</div>\n <div class=\"v\" id=\"stat-limit\">¥3,000</div>\n <div class=\"sub\">// 自然月重置</div>\n </div>\n <div class=\"stat\">\n <div class=\"lbl\">[ 当月已用 ]</div>\n <div class=\"v\" id=\"stat-used\">¥162.60</div>\n <div class=\"sub\" id=\"stat-used-sub\">// 占月限 5.4%</div>\n </div>\n <div class=\"stat\">\n <div class=\"lbl\">[ 当月剩余 ]</div>\n <div class=\"v warn\" id=\"stat-left\">¥2,837.40</div>\n <div class=\"sub\" id=\"stat-left-sub\">// 还可生成约 280 个项目</div>\n </div>\n </div>\n</div>\n\n<!-- 团队动态(banner 右栏)-->\n<div class=\"team-feed\">\n <div class=\"h\">\n <h3>团队动态</h3>\n <span class=\"ct\">// 最近 12 条</span>\n <a class=\"more\" id=\"open-feed-all\" role=\"button\" tabindex=\"0\">全部 →</a>\n </div>\n <div class=\"feed-list\">\n <div class=\"feed-item\">\n <div class=\"av\">张</div>\n <div>\n <div class=\"txt\"><span class=\"who\">张运营</span><span class=\"act\">完成视频</span><span class=\"obj\">补水面膜 · v3</span></div>\n <div class=\"ts\">10 分钟前</div>\n </div>\n </div>\n <div class=\"feed-item\">\n <div class=\"av\">王</div>\n <div>\n <div class=\"txt\"><span class=\"who\">王小姐</span><span class=\"act\">上传到资产库</span><span class=\"obj\">林夕 · 主播图</span></div>\n <div class=\"ts\">28 分钟前</div>\n </div>\n </div>\n <div class=\"feed-item\">\n <div class=\"av\">李</div>\n <div>\n <div class=\"txt\"><span class=\"who\">小李</span><span class=\"act\">邀请新成员</span><span class=\"obj\">林新人</span></div>\n <div class=\"ts\">2 小时前</div>\n </div>\n </div>\n <div class=\"feed-item\">\n <div class=\"av\">陈</div>\n <div>\n <div class=\"txt\"><span class=\"who\">陈策划</span><span class=\"act\">创建项目</span><span class=\"obj\">蓝牙耳机 · 开箱测评</span></div>\n <div class=\"ts\">4 小时前</div>\n </div>\n </div>\n <div class=\"feed-item\">\n <div class=\"av\">张</div>\n <div>\n <div class=\"txt\"><span class=\"who\">张运营</span><span class=\"act\">采用故事板</span><span class=\"obj\">补水面膜 · 场 3 · v2</span></div>\n <div class=\"ts\">昨天 18:32</div>\n </div>\n </div>\n <div class=\"feed-item\">\n <div class=\"av\">李</div>\n <div>\n <div class=\"txt\"><span class=\"who\">小李</span><span class=\"act\">团队充值</span><span class=\"obj-money\">+¥500.00</span></div>\n <div class=\"ts\">昨天 11:02</div>\n </div>\n </div>\n <div class=\"feed-item\">\n <div class=\"av\">王</div>\n <div>\n <div class=\"txt\"><span class=\"who\">王小姐</span><span class=\"act\">删除资产</span><span class=\"obj\">透真防晒 · 旧版主图</span></div>\n <div class=\"ts\">2 天前</div>\n </div>\n </div>\n </div>\n</div>\n\n</div><!-- /.team-top -->\n\n<div class=\"team-grid\">\n <!-- 左:成员表 -->\n <div>\n <div class=\"pane\" style=\"padding: 0;\">\n <div style=\"display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border-faint);\">\n <h3 style=\"margin: 0;\">成员列表 <span class=\"ct\">// 5 人 · 1 超管 / 1 团管 / 3 成员</span></h3>\n <span class=\"spacer\"></span>\n <input class=\"input\" id=\"member-search\" placeholder=\"搜索姓名 / 手机号\" style=\"height: 32px; font-size: 12px; width: 220px;\">\n </div>\n <table class=\"t members-table\" style=\"border: 0; border-radius: 0;\">\n <thead>\n <tr>\n <th>成员</th>\n <th>角色</th>\n <th>每日额度</th>\n <th>月度额度</th>\n <th style=\"width: 140px;\">当月已用</th>\n <th style=\"text-align: right; width: 88px;\">操作</th>\n </tr>\n </thead>\n <tbody id=\"members-tbody\">\n <!-- JS 注入 -->\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- 右:权限矩阵 + 额度规则 -->\n <div>\n <div class=\"pane\">\n <h3>角色权限</h3>\n <div style=\"font-size: 12px; color: var(--black-alpha-48); margin-top: -10px; margin-bottom: 12px; font-family: var(--font-mono); letter-spacing: .02em;\">// PRD §10.2 权限矩阵节选</div>\n <table class=\"perm-table\">\n <thead>\n <tr><th>能力</th><th>超管</th><th>团管</th><th>成员</th></tr>\n </thead>\n <tbody>\n <tr><td>邀请 / 移除成员</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td><td class=\"no\">—</td></tr>\n <tr><td>设置成员额度</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td><td class=\"no\">—</td></tr>\n <tr><td>团队充值</td><td class=\"yes\">✓</td><td class=\"no\">—</td><td class=\"no\">—</td></tr>\n <tr><td>设置月限额</td><td class=\"yes\">✓</td><td class=\"no\">—</td><td class=\"no\">—</td></tr>\n <tr><td>编辑别人项目</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td><td class=\"no\">—</td></tr>\n <tr><td>团队共享资产库管理</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td><td class=\"no\">仅自传</td></tr>\n <tr><td>查看团队消费明细</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td><td class=\"no\">仅自己</td></tr>\n <tr style=\"border-bottom: 0;\"><td>创建项目 / 用 AI 流程</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td><td class=\"yes\">✓</td></tr>\n </tbody>\n </table>\n </div>\n\n </div>\n</div>\n\n<!-- 设置月限额 modal -->\n<div class=\"modal-bg\" id=\"limit-bg\" onclick=\"if(event.target===this)Shell.closeModal('limit-bg')\">\n <div class=\"modal invite-modal limit-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4\"/></svg>\n </div>\n <div class=\"ti\">设置月限额<span>// 自然月重置 · 仅超管可改</span></div>\n </div>\n <div class=\"modal-b\">\n <div class=\"field\">\n <label class=\"field-label\">月限额 ¥ <span class=\"lbl-note\">(-1 为不限)</span></label>\n <input class=\"input\" id=\"limit-input\" type=\"number\" inputmode=\"numeric\" placeholder=\"例: 3000\" style=\"font-variant-numeric: tabular-nums;\">\n </div>\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">快捷选择</label>\n <div class=\"limit-presets\">\n <button type=\"button\" class=\"lp\" data-v=\"1000\">¥1,000</button>\n <button type=\"button\" class=\"lp\" data-v=\"3000\">¥3,000</button>\n <button type=\"button\" class=\"lp\" data-v=\"5000\">¥5,000</button>\n <button type=\"button\" class=\"lp\" data-v=\"10000\">¥10,000</button>\n <button type=\"button\" class=\"lp\" data-v=\"-1\">不限</button>\n </div>\n </div>\n <div class=\"limit-info\">\n <div class=\"li-row\"><span class=\"lk\">当月已用</span><span class=\"lv mono\" id=\"limit-info-used\">¥162.60</span></div>\n <div class=\"li-row\"><span class=\"lk\">本次调整后剩余</span><span class=\"lv mono\" id=\"limit-info-left\">¥2,837.40</span></div>\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('limit-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"limit-save\">\n <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 12l5 5L20 6\"/></svg>\n 保存\n </button>\n </div>\n </div>\n</div>\n\n<!-- 重置密码 modal -->\n<div class=\"modal-bg\" id=\"reset-pwd-bg\" onclick=\"if(event.target===this)Shell.closeModal('reset-pwd-bg')\">\n <div class=\"modal invite-modal reset-pwd-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"color: var(--heat); background: var(--heat-12);\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></svg>\n </div>\n <div class=\"ti\">重置登录密码 <span class=\"em-sep\" style=\"color: var(--black-alpha-32); margin: 0 2px;\">—</span> <span id=\"reset-pwd-name\" style=\"color: var(--accent-black); font-weight: 500;\">—</span><span>// 该成员当前会话会被强制下线</span></div>\n </div>\n <div class=\"modal-b\">\n <div class=\"reset-pwd-warn\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 8v4M12 16h.01\"/></svg>\n <span>旧密码即刻失效,该成员需用新密码重新登录</span>\n </div>\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">新密码</label>\n <div style=\"display:flex; gap:8px;\">\n <input class=\"input\" id=\"reset-pwd-input\" autocomplete=\"off\" placeholder=\"≥ 8 位 · 含字母与数字\" style=\"flex:1; font-family:var(--font-mono); letter-spacing:.04em;\">\n <button class=\"btn btn-sm\" type=\"button\" id=\"reset-pwd-gen\" title=\"生成随机密码\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>\n </button>\n </div>\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('reset-pwd-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"reset-pwd-confirm\">\n <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 12l5 5L20 6\"/></svg>\n 确认重置\n </button>\n </div>\n </div>\n</div>\n\n<!-- 全部团队动态 modal -->\n<div class=\"modal-bg\" id=\"feed-all-bg\" onclick=\"if(event.target===this)Shell.closeModal('feed-all-bg')\">\n <div class=\"modal feed-all-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12h4l3-9 4 18 3-9h4\"/></svg>\n </div>\n <div class=\"ti\">团队动态<span id=\"feed-all-count\">// 共 12 条</span></div>\n <button class=\"md-x\" type=\"button\" aria-label=\"关闭\" onclick=\"Shell.closeModal('feed-all-bg')\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\"><path d=\"M18 6L6 18M6 6l12 12\"/></svg>\n </button>\n </div>\n <div class=\"feed-all-filter\">\n <button type=\"button\" class=\"fa-chip selected\" data-filter=\"all\">全部</button>\n <button type=\"button\" class=\"fa-chip\" data-filter=\"project\">项目</button>\n <button type=\"button\" class=\"fa-chip\" data-filter=\"asset\">资产</button>\n <button type=\"button\" class=\"fa-chip\" data-filter=\"team\">团队</button>\n <span class=\"spacer\"></span>\n <span class=\"fa-meta mono\" id=\"feed-all-meta\">// 共 12 条</span>\n </div>\n <div class=\"modal-b feed-all-body\">\n <div class=\"feed-all-list\" id=\"feed-all-list\"><!-- JS 注入 --></div>\n </div>\n </div>\n</div>\n\n<!-- 创建账户 modal -->\n<div class=\"modal-bg\" id=\"invite-bg\" onclick=\"if(event.target===this)Shell.closeModal('invite-bg')\">\n <div class=\"modal invite-modal create-acct-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n </div>\n <div class=\"ti\">创建账户<span>// 直接生成账号 · 分享给成员登录</span></div>\n </div>\n <div class=\"modal-b\">\n <div class=\"field\">\n <label class=\"field-label\">用户名 <span class=\"req\">*</span></label>\n <div style=\"display:flex; gap:8px;\">\n <input class=\"input\" id=\"inv-username\" autocomplete=\"off\" placeholder=\"例: zhang.yunying\" style=\"flex:1;\">\n <button class=\"btn btn-sm\" type=\"button\" id=\"inv-suggest-name\" title=\"生成随机用户名\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field-label\">备注姓名(可选)</label>\n <input class=\"input\" id=\"inv-name\" placeholder=\"例: 张某\">\n </div>\n <div class=\"field\">\n <label class=\"field-label\">登录密码 <span class=\"req\">*</span></label>\n <div style=\"display:flex; gap:8px;\">\n <input class=\"input\" id=\"inv-password\" autocomplete=\"off\" placeholder=\"≥ 8 位 · 含字母与数字\" style=\"flex:1; font-family:var(--font-mono); letter-spacing:.04em;\">\n <button class=\"btn btn-sm\" type=\"button\" id=\"inv-gen-pwd\" title=\"生成随机密码\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field-label\">分配角色 <span class=\"req\">*</span></label>\n <div class=\"role-choices\">\n <div class=\"role-choice selected\" data-role=\"member\">\n <div class=\"title\">成员</div>\n <div class=\"desc\">// 创建项目 + 用资产</div>\n </div>\n <div class=\"role-choice\" data-role=\"admin\">\n <div class=\"title\">团管</div>\n <div class=\"desc\">// 管成员 + 改额度</div>\n </div>\n </div>\n </div>\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">额度配置 <span class=\"lbl-note\">(¥ · -1 为不限)</span></label>\n <div class=\"quota-grid\">\n <label class=\"quota-cell\">\n <span class=\"qk\">每日额度 ¥</span>\n <input class=\"input\" id=\"inv-daily\" type=\"number\" inputmode=\"numeric\" value=\"100\">\n </label>\n <label class=\"quota-cell\">\n <span class=\"qk\">每月额度 ¥</span>\n <input class=\"input\" id=\"inv-monthly\" type=\"number\" inputmode=\"numeric\" value=\"2000\">\n </label>\n <label class=\"quota-cell\">\n <span class=\"qk\">总额度 ¥</span>\n <input class=\"input\" id=\"inv-total\" type=\"number\" inputmode=\"numeric\" value=\"-1\">\n </label>\n </div>\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('invite-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"inv-send\">\n <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=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/><path d=\"M19 8v6M22 11h-6\"/></svg>\n 创建账户\n </button>\n </div>\n </div>\n</div>\n\n<!-- 账户创建成功 · 分享凭据弹窗 -->\n<div class=\"modal-bg\" id=\"invite-share-bg\" onclick=\"if(event.target===this)Shell.closeModal('invite-share-bg')\">\n <div class=\"modal invite-modal share-acct-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\" style=\"color: var(--accent-forest); background: rgba(34, 110, 51, .08);\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 12l5 5L20 6\"/></svg>\n </div>\n <div class=\"ti\">账户已创建<span>// 把下方凭据分享给成员 · 凭据仅展示一次</span></div>\n </div>\n <div class=\"modal-b\">\n <div class=\"cred-card\">\n <div class=\"cred-row\">\n <span class=\"ck\">登录地址</span>\n <span class=\"cv mono\" id=\"share-url\">https://airshelf.com/login</span>\n <button class=\"cred-copy\" type=\"button\" data-copy=\"share-url\" title=\"复制\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"/><path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/></svg>\n </button>\n </div>\n <div class=\"cred-row\">\n <span class=\"ck\">用户名</span>\n <span class=\"cv mono\" id=\"share-username\">—</span>\n <button class=\"cred-copy\" type=\"button\" data-copy=\"share-username\" title=\"复制\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"/><path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/></svg>\n </button>\n </div>\n <div class=\"cred-row\">\n <span class=\"ck\">初始密码</span>\n <span class=\"cv mono\" id=\"share-password\">—</span>\n <button class=\"cred-copy\" type=\"button\" data-copy=\"share-password\" title=\"复制\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"/><path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/></svg>\n </button>\n </div>\n </div>\n <div class=\"cred-rules\">\n <div class=\"li\">建议成员首次登录后立即修改密码</div>\n <div class=\"li\">凭据请通过加密渠道(企微 / 飞书私聊)分享,不要发到公共群</div>\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" id=\"share-copy-all\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\"/><path d=\"M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1\"/></svg>\n 一键复制全部\n </button>\n <button class=\"btn btn-primary\" type=\"button\" onclick=\"Shell.closeModal('invite-share-bg')\">完成</button>\n </div>\n </div>\n</div>\n\n<!-- 编辑成员 modal -->\n<div class=\"modal-bg\" id=\"edit-member-bg\" onclick=\"if(event.target===this)Shell.closeModal('edit-member-bg')\">\n <div class=\"modal invite-modal edit-member-modal\">\n <span class=\"corner-tr\" aria-hidden></span><span class=\"corner-bl\" aria-hidden></span>\n <div class=\"modal-h\">\n <div class=\"ic-m\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/><path d=\"M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z\"/></svg>\n </div>\n <div class=\"ti\" id=\"edit-title\">编辑成员 <span class=\"em-sep\">—</span> <span id=\"edit-username\">—</span></div>\n </div>\n <div class=\"modal-b\">\n <div class=\"field\">\n <label class=\"field-label\">用户名 <span class=\"lbl-note\">(无权修改)</span></label>\n <input class=\"input\" id=\"edit-name-readonly\" readonly tabindex=\"-1\">\n </div>\n <div class=\"field\">\n <label class=\"field-label\">角色 <span class=\"lbl-note\" id=\"edit-role-note\"></span></label>\n <div class=\"readonly-text\" id=\"edit-role-text\" hidden>—</div>\n <div class=\"role-choices\" id=\"edit-role-choices\" hidden>\n <div class=\"role-choice\" data-edit-role=\"member\">\n <div class=\"title\">成员</div>\n <div class=\"desc\">// 创建项目 + 用资产</div>\n </div>\n <div class=\"role-choice\" data-edit-role=\"admin\">\n <div class=\"title\">团管</div>\n <div class=\"desc\">// 管成员 + 改额度</div>\n </div>\n </div>\n </div>\n <div class=\"field\">\n <label class=\"field-label\">每日额度 ¥ <span class=\"lbl-note\">(-1 为不限)</span></label>\n <input class=\"input\" id=\"edit-daily\" type=\"number\" inputmode=\"numeric\" placeholder=\"例: 100\">\n </div>\n <div class=\"field\">\n <label class=\"field-label\">每月额度 ¥ <span class=\"lbl-note\">(-1 为不限)</span></label>\n <input class=\"input\" id=\"edit-monthly\" type=\"number\" inputmode=\"numeric\" placeholder=\"例: 2000\">\n </div>\n <div class=\"field\" style=\"margin-bottom: 0;\">\n <label class=\"field-label\">总额度 ¥ <span class=\"lbl-note\">(-1 为不限)</span></label>\n <input class=\"input\" id=\"edit-total\" type=\"number\" inputmode=\"numeric\" placeholder=\"例: -1\">\n </div>\n </div>\n <div class=\"modal-f\">\n <button class=\"btn\" type=\"button\" onclick=\"Shell.closeModal('edit-member-bg')\">取消</button>\n <button class=\"btn btn-primary\" type=\"button\" id=\"edit-save\">保存</button>\n </div>\n </div>\n</div>\n\n</div>\n<script src=\"/exact/assets/icons.js?v=2026052608\"></script>\n<script src=\"/exact/assets/shell.js?v=2026060101\"></script>\n<script>\nShell.render({\n active: 'team',\n crumbs: [{ label: '工作台', href: 'index.html' }, { label: '团队' }]\n});\n\n/* ─── 团队成员 mock + 渲染 ─── */\nconst ROLE_META = {\n super: { cls: 'role-super', label: '超管' },\n admin: { cls: 'role-admin', label: '团管' },\n member: { cls: 'role-member', label: '成员' },\n};\n\n/* PRD §10.2 权限矩阵 · 4 条精简到弹窗速览(grant + deny 各 2 条) */\nconst ROLE_PERMS = {\n super: [\n { ok: true, txt: '团队设置 · 月限额 / 名称' },\n { ok: true, txt: '充值 · 划拨团队总额度' },\n { ok: true, txt: '任命团管 · 调整任意成员额度' },\n { ok: true, txt: '查看团队全部消费明细 + 财务' },\n ],\n admin: [\n { ok: true, txt: '邀请 / 移除成员 + 设置额度' },\n { ok: true, txt: '管理团队共享资产库' },\n { ok: true, txt: '查看团队全部消费明细' },\n { ok: false, txt: '团队设置 / 充值 · 仅超管' },\n ],\n member: [\n { ok: true, txt: '创建项目 + 流水线操作' },\n { ok: true, txt: '上传 / 引用团队共享资产' },\n { ok: false, txt: '邀请 / 移除成员 · 仅超管 / 团管' },\n { ok: false, txt: '消费明细 · 仅本人可见' },\n ],\n};\n\nconst MEMBERS = [\n { id: 'u1', av: '李', name: '小李', email: 'li@shop.com', role: 'super', daily: 500, monthly: 10000, used: 162.60, usedToday: 0.45, lastActive: '15 分钟前', pending: false, creator: true,\n joinDate: '2026-04-12', inviter: '—', projectsActive: 2, projectsDone: 14, assetsUploaded: 32,\n activity: [\n { ts: '昨天 11:02', act: '团队充值', obj: '+¥500.00' },\n { ts: '2 天前', act: '邀请新成员', obj: '林新人' },\n { ts: '3 天前', act: '创建项目', obj: '春日新品 · 立体口红' },\n ],\n byStage: { video: 98.40, storyboard: 36.00, asset: 21.00, script: 7.20 },\n transactions: [\n { ts: '05.21 14:32', proj: '补水面膜 · v3', type: '视频片段 · 1 镜', amount: -0.45 },\n { ts: '05.20 18:21', proj: '透真防晒 · 通勤对比', type: '视频片段 · 6 镜', amount: -1.20 },\n { ts: '05.20 11:02', proj: '充值', type: '微信支付', amount: 500.00 },\n { ts: '05.19 16:08', proj: '补水面膜 · v3', type: '故事板 image-2', amount: -0.45 },\n { ts: '05.19 14:02', proj: '补水面膜 · v3', type: '脚本 LLM · 2.4k', amount: -0.04 },\n { ts: '05.19 13:38', proj: '补水面膜 · v3', type: '基础资产 · 5 张', amount: -1.05 },\n { ts: '05.18 15:42', proj: '咖啡冻干 · 早八', type: '故事板生成失败', amount: 0 },\n { ts: '05.17 10:30', proj: '瑜伽裤 · 通勤穿搭', type: '项目导出', amount: -3.20 },\n ] },\n { id: 'u2', av: '张', name: '张运营', email: 'zhang@shop.com', role: 'admin', daily: 300, monthly: 6000, used: 98.40, usedToday: 0.45, lastActive: '10 分钟前', pending: false,\n joinDate: '2026-04-18', inviter: '小李', projectsActive: 3, projectsDone: 8, assetsUploaded: 18,\n activity: [\n { ts: '10 分钟前', act: '完成视频', obj: '补水面膜 · v3' },\n { ts: '昨天 18:32', act: '采用故事板', obj: '场 3 · v2' },\n { ts: '2 天前', act: '创建项目', obj: '蓝牙耳机 · 开箱测评' },\n ],\n byStage: { video: 60.00, storyboard: 22.00, asset: 12.00, script: 4.40 },\n transactions: [\n { ts: '05.21 14:08', proj: '补水面膜 · v3', type: '视频片段 · 1 镜', amount: -0.45 },\n { ts: '05.20 21:42', proj: '蓝牙耳机 · 开箱测评', type: '视频片段 · 6 镜', amount: -1.20 },\n { ts: '05.20 16:00', proj: '蓝牙耳机 · 开箱测评', type: '故事板 image-2', amount: -0.45 },\n { ts: '05.19 11:18', proj: '补水面膜 · v3', type: '故事板 image-2', amount: -0.45 },\n { ts: '05.18 09:42', proj: '蓝牙耳机 · 开箱测评', type: '基础资产 · 4 张', amount: -0.84 },\n { ts: '05.17 14:38', proj: '蓝牙耳机 · 开箱测评', type: '脚本 LLM · 1.8k', amount: -0.03 },\n ] },\n { id: 'u3', av: '王', name: '王小姐', email: 'wang@shop.com', role: 'member', daily: 100, monthly: 2000, used: 45.20, usedToday: 0, lastActive: '28 分钟前', pending: false,\n joinDate: '2026-04-22', inviter: '小李', projectsActive: 1, projectsDone: 4, assetsUploaded: 12,\n activity: [\n { ts: '28 分钟前', act: '上传到资产库', obj: '林夕 · 主播图' },\n { ts: '2 天前', act: '删除资产', obj: '透真防晒 · 旧版主图' },\n { ts: '5 天前', act: '完成视频', obj: '透真防晒 · 通勤对比' },\n ],\n byStage: { video: 28.00, storyboard: 10.00, asset: 5.00, script: 2.20 },\n transactions: [\n { ts: '05.16 19:38', proj: '透真防晒 · 通勤对比', type: '视频片段 · 4 镜', amount: -0.80 },\n { ts: '05.16 11:42', proj: '透真防晒 · 通勤对比', type: '故事板 image-2', amount: -0.45 },\n { ts: '05.15 16:08', proj: '透真防晒 · 通勤对比', type: '基础资产 · 3 张', amount: -0.63 },\n { ts: '05.15 09:30', proj: '透真防晒 · 通勤对比', type: '脚本 LLM · 1.5k', amount: -0.02 },\n ] },\n { id: 'u4', av: '陈', name: '陈策划', email: 'chen@shop.com', role: 'member', daily: 100, monthly: 2000, used: 12.80, usedToday: 0, lastActive: '4 小时前', pending: false,\n joinDate: '2026-05-02', inviter: '张运营', projectsActive: 1, projectsDone: 1, assetsUploaded: 5,\n activity: [\n { ts: '4 小时前', act: '创建项目', obj: '蓝牙耳机 · 开箱测评' },\n { ts: '昨天', act: '完成视频', obj: '速食面 · 加班场景' },\n ],\n byStage: { video: 8.00, storyboard: 3.00, asset: 1.20, script: 0.60 },\n transactions: [\n { ts: '05.20 14:32', proj: '速食面 · 加班场景', type: '视频片段 · 3 镜', amount: -0.60 },\n { ts: '05.19 18:08', proj: '速食面 · 加班场景', type: '故事板 image-2', amount: -0.30 },\n { ts: '05.19 11:12', proj: '速食面 · 加班场景', type: '基础资产 · 2 张', amount: -0.42 },\n ] },\n { id: 'u5', av: '林', name: '林新人', email: '186****1102', role: 'member', daily: 100, monthly: 2000, used: 0, usedToday: 0, lastActive: '尚未激活', pending: true,\n joinDate: '2026-05-19', inviter: '小李', projectsActive: 0, projectsDone: 0, assetsUploaded: 0,\n activity: [],\n byStage: { video: 0, storyboard: 0, asset: 0, script: 0 },\n transactions: [] },\n];\n\nfunction fmtMoney(n) { return '¥' + n.toFixed(2).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ','); }\nfunction usedClass(pct) { if (pct < 0.5) return 'ok'; if (pct < 0.85) return ''; return 'warn'; }\n\n/* ─── 当前操作者角色 + PRD §10.2 权限矩阵 · 必须在 renderMembers 之前声明 ─── */\nconst CURRENT_ROLE = 'super'; // 'super' | 'admin' | 'member'\nfunction canEditRole(target) {\n // 只有超管可以改成员角色,创建者(初始超管)不可降级,其它超管暂不支持改\n if (CURRENT_ROLE !== 'super') return false;\n if (target.creator) return false;\n return target.role !== 'super';\n}\nfunction canEditMember(target) {\n if (target.creator) return false;\n if (CURRENT_ROLE === 'super') return true;\n if (CURRENT_ROLE === 'admin') return target.role === 'member';\n return false;\n}\nfunction canResetPassword(target) {\n if (target.creator) return false;\n if (CURRENT_ROLE === 'super') return target.role !== 'super';\n if (CURRENT_ROLE === 'admin') return target.role === 'member';\n return false;\n}\nfunction canRemoveMember(target) {\n return canEditMember(target);\n}\n\nfunction renderMembers(filter = '') {\n const tb = document.getElementById('members-tbody');\n const list = MEMBERS.filter(m => {\n if (!filter) return true;\n const q = filter.toLowerCase();\n return m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q);\n });\n tb.innerHTML = list.map(m => {\n const r = ROLE_META[m.role];\n const pct = m.monthly > 0 ? m.used / m.monthly : 0;\n return `\n <tr data-id=\"${m.id}\" class=\"${m.pending ? 'pending' : ''}${m.creator ? ' creator-row' : ''}\">\n <td>\n <div class=\"who\">\n <div class=\"av\">${m.av}</div>\n <div>\n <div class=\"nm\">${m.name}${m.creator ? ' <span style=\"font-family:var(--font-mono);font-size:10px;color:var(--black-alpha-48);\">· 创建者</span>' : ''}</div>\n <div class=\"em\">${m.email}</div>\n </div>\n </div>\n </td>\n <td><span class=\"role-pill ${r.cls}\"><span class=\"dot\"></span>${r.label}</span></td>\n <td><span class=\"quota-cell\"><span class=\"v\">${fmtMoney(m.daily)}</span></span></td>\n <td><span class=\"quota-cell\"><span class=\"v\">${fmtMoney(m.monthly)}</span></span></td>\n <td>\n <div class=\"quota-cell\"><span class=\"v\">${fmtMoney(m.used)}</span> <span class=\"lbl\">/ ${(pct * 100).toFixed(0)}%</span></div>\n <div class=\"used-bar\"><span class=\"${usedClass(pct)}\" style=\"width: ${Math.min(100, pct * 100).toFixed(1)}%\"></span></div>\n </td>\n <td>\n <div class=\"acts\">\n ${m.creator\n ? '<span style=\"font-family:var(--font-mono);font-size:10.5px;color:var(--black-alpha-32);align-self:center;\">不可编辑</span>'\n : [\n canEditMember(m) ? `<button class=\"icon-btn-sm\" data-act=\"edit\" data-id=\"${m.id}\" title=\"编辑\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/><path d=\"M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z\"/></svg>\n </button>` : '',\n canResetPassword(m) ? `<button class=\"icon-btn-sm\" data-act=\"reset-pwd\" data-id=\"${m.id}\" title=\"重置密码\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></svg>\n </button>` : '',\n canRemoveMember(m) ? `<button class=\"icon-btn-sm danger\" data-act=\"remove\" data-id=\"${m.id}\" title=\"移出\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"/></svg>\n </button>` : '',\n ].join('')\n }\n </div>\n </td>\n </tr>\n `;\n }).join('');\n\n tb.querySelectorAll('[data-act=\"edit\"]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n openEdit(btn.dataset.id);\n });\n });\n tb.querySelectorAll('[data-act=\"reset-pwd\"]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n openResetPwd(btn.dataset.id);\n });\n });\n tb.querySelectorAll('[data-act=\"remove\"]').forEach(btn => {\n btn.addEventListener('click', e => {\n e.stopPropagation();\n const m = MEMBERS.find(x => x.id === btn.dataset.id);\n if (!m) return;\n if (confirm('确定将「' + m.name + '」移出团队?')) {\n const i = MEMBERS.findIndex(x => x.id === m.id);\n MEMBERS.splice(i, 1);\n Shell.toast('已移除成员', m.name);\n renderMembers(document.getElementById('member-search').value);\n }\n });\n });\n}\nrenderMembers();\n\ndocument.getElementById('member-search').addEventListener('input', e => {\n renderMembers(e.target.value);\n});\n\n/* ─── 创建账户 modal ─── */\nconst $username = document.getElementById('inv-username');\nconst $name = document.getElementById('inv-name');\nconst $password = document.getElementById('inv-password');\nconst $daily = document.getElementById('inv-daily');\nconst $monthly = document.getElementById('inv-monthly');\nconst $total = document.getElementById('inv-total');\n\nfunction genUsername() {\n const prefixes = ['user', 'team', 'shop', 'creator', 'editor'];\n const p = prefixes[Math.floor(Math.random() * prefixes.length)];\n const n = Math.floor(1000 + Math.random() * 9000);\n return `${p}.${n}`;\n}\nfunction genPassword() {\n // 12 位 · 字母 + 数字混合 · 排除易混淆字符(0/O/1/l/I)\n const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';\n let s = '';\n for (let i = 0; i < 12; i++) s += chars[Math.floor(Math.random() * chars.length)];\n return s;\n}\nfunction parseQuota(v, fallback) {\n if (v === '' || v == null) return fallback;\n const n = Number(v);\n return Number.isFinite(n) ? n : fallback;\n}\n\ndocument.getElementById('open-invite').addEventListener('click', () => {\n // 打开时填充建议用户名 + 默认密码,用户可改\n if (!$username.value.trim()) $username.value = genUsername();\n if (!$password.value.trim()) $password.value = genPassword();\n Shell.openModal('invite-bg');\n});\ndocument.querySelectorAll('#invite-bg .role-choice').forEach(c => {\n c.addEventListener('click', () => {\n document.querySelectorAll('#invite-bg .role-choice').forEach(x => x.classList.remove('selected'));\n c.classList.add('selected');\n });\n});\ndocument.getElementById('inv-suggest-name').addEventListener('click', () => {\n $username.value = genUsername();\n $username.focus();\n});\ndocument.getElementById('inv-gen-pwd').addEventListener('click', () => {\n $password.value = genPassword();\n});\n\ndocument.getElementById('inv-send').addEventListener('click', () => {\n const username = ($username.value || '').trim();\n const password = ($password.value || '').trim();\n const name = ($name.value || '').trim() || username;\n if (!username) { Shell.toast('请填用户名', '账户未创建'); $username.focus(); return; }\n if (!/^[A-Za-z0-9._-]{3,32}$/.test(username)) {\n Shell.toast('用户名格式不正确', '332 位 · 字母/数字/. _ -');\n $username.focus(); return;\n }\n if (!password) { Shell.toast('请填密码', '账户未创建'); $password.focus(); return; }\n if (password.length < 8 || !/[A-Za-z]/.test(password) || !/\\d/.test(password)) {\n Shell.toast('密码强度不够', '至少 8 位 · 含字母与数字');\n $password.focus(); return;\n }\n const role = document.querySelector('#invite-bg .role-choice.selected')?.dataset.role || 'member';\n const daily = parseQuota($daily.value, 100);\n const monthly = parseQuota($monthly.value, 2000);\n const totalQuota = parseQuota($total.value, -1);\n\n MEMBERS.push({\n id: 'u' + Date.now(),\n av: name[0] || username[0]?.toUpperCase() || 'U',\n name, username,\n email: username + '@airshelf.local',\n role,\n daily, monthly, totalQuota,\n used: 0, usedToday: 0,\n lastActive: '尚未激活',\n pending: true,\n });\n renderMembers();\n Shell.closeModal('invite-bg');\n Shell.toast('账户已创建', username + ' · 共享凭据已生成');\n\n // 填充并打开分享凭据弹窗\n document.getElementById('share-username').textContent = username;\n document.getElementById('share-password').textContent = password;\n Shell.openModal('invite-share-bg');\n\n // 重置表单\n $username.value = '';\n $name.value = '';\n $password.value = '';\n});\n\n/* ─── 分享凭据 · 复制 ─── */\nasync function copyText(text) {\n try {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n await navigator.clipboard.writeText(text);\n return true;\n }\n } catch (e) { /* fallthrough to execCommand */ }\n // 兜底:textarea + execCommand\n const ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.select();\n let ok = false;\n try { ok = document.execCommand('copy'); } catch (e) { ok = false; }\n document.body.removeChild(ta);\n return ok;\n}\ndocument.querySelectorAll('#invite-share-bg .cred-copy').forEach(btn => {\n btn.addEventListener('click', async () => {\n const target = btn.dataset.copy;\n const el = document.getElementById(target);\n if (!el) return;\n const ok = await copyText(el.textContent.trim());\n const lbl = target === 'share-url' ? '登录地址' : (target === 'share-username' ? '用户名' : '密码');\n Shell.toast(ok ? '已复制' : '复制失败', ok ? lbl : '请手动选中复制');\n });\n});\ndocument.getElementById('share-copy-all').addEventListener('click', async () => {\n const url = document.getElementById('share-url').textContent.trim();\n const u = document.getElementById('share-username').textContent.trim();\n const p = document.getElementById('share-password').textContent.trim();\n const text = `登录地址: ${url}\\n用户名: ${u}\\n初始密码: ${p}`;\n const ok = await copyText(text);\n Shell.toast(ok ? '已复制全部' : '复制失败', ok ? '可以粘贴到企微/飞书私聊' : '请手动复制');\n});\n\n/* ─── 编辑 modal ─── */\nconst ROLE_LABEL = { super: '超级管理员 · 创建者', admin: '副管理员', member: '成员' };\nlet editingId = null;\nfunction openEdit(id) {\n const m = MEMBERS.find(x => x.id === id);\n if (!m) return;\n editingId = id;\n document.getElementById('edit-username').textContent = m.name;\n document.getElementById('edit-name-readonly').value = m.name;\n\n // 角色:可编辑 → 显示 role-choices,只读 → 显示 pill\n const roleNote = document.getElementById('edit-role-note');\n const roleText = document.getElementById('edit-role-text');\n const roleChoices = document.getElementById('edit-role-choices');\n if (canEditRole(m)) {\n roleNote.textContent = '(可改)';\n roleText.hidden = true;\n roleChoices.hidden = false;\n roleChoices.querySelectorAll('.role-choice').forEach(c => {\n c.classList.toggle('selected', c.dataset.editRole === m.role);\n });\n } else {\n roleNote.textContent = m.creator ? '(创建者不可改)' : (CURRENT_ROLE === 'super' ? '' : '(仅超管可改)');\n roleText.hidden = false;\n roleChoices.hidden = true;\n const roleLabel = ROLE_LABEL[m.role] || '成员';\n roleText.innerHTML = '<span class=\"role-pill\"><span class=\"dot\"></span>' + roleLabel + '</span>';\n }\n\n document.getElementById('edit-daily').value = m.daily != null ? m.daily : -1;\n document.getElementById('edit-monthly').value = m.monthly != null ? m.monthly : -1;\n document.getElementById('edit-total').value = m.totalQuota != null ? m.totalQuota : -1;\n Shell.openModal('edit-member-bg');\n}\n// role chip 切换\ndocument.querySelectorAll('#edit-role-choices .role-choice').forEach(c => {\n c.addEventListener('click', () => {\n document.querySelectorAll('#edit-role-choices .role-choice').forEach(x => x.classList.remove('selected'));\n c.classList.add('selected');\n });\n});\n/* ─── 重置密码 modal ─── */\nlet _resetPwdTarget = null;\nfunction openResetPwd(id) {\n const m = MEMBERS.find(x => x.id === id);\n if (!m) return;\n if (!canResetPassword(m)) {\n Shell.toast('无权重置该账户密码', '当前角色权限不足');\n return;\n }\n _resetPwdTarget = m;\n document.getElementById('reset-pwd-name').textContent = m.name;\n document.getElementById('reset-pwd-input').value = genPassword(); // 复用创建账户的生成器\n Shell.openModal('reset-pwd-bg');\n}\ndocument.getElementById('reset-pwd-gen').addEventListener('click', () => {\n document.getElementById('reset-pwd-input').value = genPassword();\n});\ndocument.getElementById('reset-pwd-confirm').addEventListener('click', () => {\n if (!_resetPwdTarget) return;\n const pwd = document.getElementById('reset-pwd-input').value.trim();\n if (!pwd) { Shell.toast('请填写新密码', '或点刷新生成'); return; }\n if (pwd.length < 8 || !/[A-Za-z]/.test(pwd) || !/\\d/.test(pwd)) {\n Shell.toast('密码强度不够', '至少 8 位 · 含字母与数字');\n return;\n }\n const m = _resetPwdTarget;\n Shell.closeModal('reset-pwd-bg');\n // 复用「分享凭据」弹窗展示新密码,让管理员可一键复制给成员\n document.getElementById('share-username').textContent = m.username || m.email || m.name;\n document.getElementById('share-password').textContent = pwd;\n // 标题语境从「账户已创建」临时改为「密码已重置」\n const shareTi = document.querySelector('#invite-share-bg .ti');\n if (shareTi) shareTi.innerHTML = '密码已重置<span>// 把新凭据分享给 ' + m.name + ' · 旧密码已失效</span>';\n Shell.openModal('invite-share-bg');\n Shell.toast('密码已重置', m.name + ' · 请把新密码同步给成员');\n _resetPwdTarget = null;\n});\n\n// 分享凭据弹窗关闭后,把标题复位到「账户已创建」的默认文案,\n// 这样下次「创建账户」流程打开时不会残留「密码已重置」的语境\n(function () {\n const bg = document.getElementById('invite-share-bg');\n const DEFAULT_TI = '账户已创建<span>// 把下方凭据分享给成员 · 凭据仅展示一次</span>';\n const obs = new MutationObserver(() => {\n if (!bg.classList.contains('show')) {\n const ti = bg.querySelector('.ti');\n if (ti) ti.innerHTML = DEFAULT_TI;\n }\n });\n obs.observe(bg, { attributes: true, attributeFilter: ['class'] });\n})();\n\ndocument.getElementById('edit-save').addEventListener('click', () => {\n const m = MEMBERS.find(x => x.id === editingId);\n if (!m) return;\n const parseQuota = (v, fallback) => {\n if (v === '' || v == null) return fallback;\n const n = Number(v);\n return Number.isFinite(n) ? n : fallback;\n };\n // 如果允许改角色,读取新值\n if (canEditRole(m)) {\n const newRole = document.querySelector('#edit-role-choices .role-choice.selected')?.dataset.editRole;\n if (newRole && newRole !== m.role) {\n m.role = newRole;\n }\n }\n m.daily = parseQuota(document.getElementById('edit-daily').value, m.daily);\n m.monthly = parseQuota(document.getElementById('edit-monthly').value, m.monthly);\n m.totalQuota = parseQuota(document.getElementById('edit-total').value, m.totalQuota != null ? m.totalQuota : -1);\n Shell.closeModal('edit-member-bg');\n Shell.toast('已保存', m.name + ' · 已更新');\n renderMembers(document.getElementById('member-search').value);\n});\n\n/* ─── 设置月限额 modal ─── */\nconst TEAM_BUDGET = { limit: 3000, used: 162.60 };\nfunction fmtMoneyComma(n) {\n if (n < 0) return '不限';\n return '¥' + n.toFixed(2).replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n}\nfunction refreshLimitInfo() {\n const inputEl = document.getElementById('limit-input');\n const raw = inputEl.value.trim();\n const v = raw === '' ? TEAM_BUDGET.limit : Number(raw);\n const left = v < 0 ? null : v - TEAM_BUDGET.used;\n document.getElementById('limit-info-used').textContent = fmtMoneyComma(TEAM_BUDGET.used);\n const leftEl = document.getElementById('limit-info-left');\n if (left == null) {\n leftEl.textContent = '不限';\n leftEl.classList.remove('neg');\n } else {\n leftEl.textContent = fmtMoneyComma(left);\n leftEl.classList.toggle('neg', left < 0);\n }\n document.querySelectorAll('#limit-bg .limit-presets .lp').forEach(b => {\n b.classList.toggle('selected', Number(b.dataset.v) === v);\n });\n}\nfunction applyBudgetToBanner() {\n document.getElementById('stat-limit').textContent = TEAM_BUDGET.limit < 0 ? '不限' : fmtMoneyComma(TEAM_BUDGET.limit);\n document.getElementById('stat-used').textContent = fmtMoneyComma(TEAM_BUDGET.used);\n const left = TEAM_BUDGET.limit < 0 ? null : TEAM_BUDGET.limit - TEAM_BUDGET.used;\n const leftEl = document.getElementById('stat-left');\n if (left == null) {\n leftEl.textContent = '不限';\n leftEl.classList.remove('warn');\n } else {\n leftEl.textContent = fmtMoneyComma(left);\n leftEl.classList.toggle('warn', left / TEAM_BUDGET.limit < 0.2);\n }\n // 占比 + 推算项目数\n const pct = TEAM_BUDGET.limit > 0 ? (TEAM_BUDGET.used / TEAM_BUDGET.limit * 100).toFixed(1) : 0;\n document.getElementById('stat-used-sub').textContent = TEAM_BUDGET.limit < 0 ? '// 不限' : `// 占月限 ${pct}%`;\n document.getElementById('stat-left-sub').textContent = TEAM_BUDGET.limit < 0\n ? '// 月限额不限'\n : (left != null ? `// 还可生成约 ${Math.max(0, Math.round(left / 10))} 个项目` : '');\n}\ndocument.getElementById('open-limit').addEventListener('click', () => {\n document.getElementById('limit-input').value = TEAM_BUDGET.limit;\n refreshLimitInfo();\n Shell.openModal('limit-bg');\n});\ndocument.getElementById('limit-input').addEventListener('input', refreshLimitInfo);\ndocument.querySelectorAll('#limit-bg .limit-presets .lp').forEach(b => {\n b.addEventListener('click', () => {\n document.getElementById('limit-input').value = b.dataset.v;\n refreshLimitInfo();\n });\n});\ndocument.getElementById('limit-save').addEventListener('click', () => {\n const raw = document.getElementById('limit-input').value.trim();\n if (raw === '') { Shell.toast('请填写月限额', '或选择「不限」'); return; }\n const v = Number(raw);\n if (!Number.isFinite(v)) { Shell.toast('数值不合法', '请输入数字'); return; }\n if (v >= 0 && v < TEAM_BUDGET.used) {\n if (!confirm(`新月限额 ${fmtMoneyComma(v)} 小于当月已用 ${fmtMoneyComma(TEAM_BUDGET.used)},仍要保存?`)) return;\n }\n TEAM_BUDGET.limit = v;\n applyBudgetToBanner();\n Shell.closeModal('limit-bg');\n Shell.toast('月限额已更新', v < 0 ? '不限' : fmtMoneyComma(v));\n});\n\n/* ─── 团队动态 · 全部 → modal ─── */\nconst FEED_ALL = [\n { who: '张运营', av: '张', cat: 'project', act: '完成视频', obj: '补水面膜 · v3', ts: '10 分钟前' },\n { who: '王小姐', av: '王', cat: 'asset', act: '上传到资产库', obj: '林夕 · 主播图', ts: '28 分钟前' },\n { who: '小李', av: '李', cat: 'team', act: '邀请新成员', obj: '林新人', ts: '2 小时前' },\n { who: '陈策划', av: '陈', cat: 'project', act: '创建项目', obj: '蓝牙耳机 · 开箱测评', ts: '4 小时前' },\n { who: '张运营', av: '张', cat: 'project', act: '采用故事板', obj: '补水面膜 · 场 3 · v2', ts: '昨天 18:32' },\n { who: '小李', av: '李', cat: 'team', act: '充值', obj: '¥500.00', ts: '昨天 16:08' },\n { who: '陈策划', av: '陈', cat: 'asset', act: '加入资产库', obj: '阿强 · 健身男', ts: '昨天 14:12' },\n { who: '王小姐', av: '王', cat: 'project', act: '完成视频', obj: '瑜伽裤 · 通勤穿搭', ts: '昨天 11:38' },\n { who: '张运营', av: '张', cat: 'asset', act: '上传商品图', obj: '南卡 Lite Pro 蓝牙耳机', ts: '2 天前' },\n { who: '小李', av: '李', cat: 'team', act: '调整月限额', obj: '¥3,000', ts: '2 天前' },\n { who: '陈策划', av: '陈', cat: 'project', act: '取消项目', obj: '速食面 · 加班场景 · v1', ts: '2 天前' },\n { who: '王小姐', av: '王', cat: 'asset', act: '删除资产', obj: '透真防晒 · 旧版主图', ts: '2 天前' },\n];\nlet _feedFilter = 'all';\nfunction renderFeedAll() {\n const list = _feedFilter === 'all' ? FEED_ALL : FEED_ALL.filter(f => f.cat === _feedFilter);\n const root = document.getElementById('feed-all-list');\n root.innerHTML = list.length === 0\n ? '<div class=\"feed-empty\">// 没有匹配的动态</div>'\n : list.map(f => `\n <div class=\"feed-item\">\n <div class=\"av\">${f.av}</div>\n <div class=\"txt\"><span class=\"who\">${f.who}</span><span class=\"act\">${f.act}</span><span class=\"obj\">${f.obj}</span></div>\n <div class=\"ts\">${f.ts}</div>\n </div>\n `).join('');\n document.getElementById('feed-all-meta').textContent = `// 共 ${list.length} 条`;\n document.getElementById('feed-all-count').textContent = `// 共 ${list.length} 条`;\n}\ndocument.getElementById('open-feed-all').addEventListener('click', () => {\n _feedFilter = 'all';\n document.querySelectorAll('#feed-all-bg .fa-chip').forEach(c => c.classList.toggle('selected', c.dataset.filter === 'all'));\n renderFeedAll();\n Shell.openModal('feed-all-bg');\n});\ndocument.querySelectorAll('#feed-all-bg .fa-chip').forEach(chip => {\n chip.addEventListener('click', () => {\n _feedFilter = chip.dataset.filter;\n document.querySelectorAll('#feed-all-bg .fa-chip').forEach(c => c.classList.toggle('selected', c === chip));\n renderFeedAll();\n });\n});\n\n/* ─── 成员行点击 → 打开编辑(创建者除外)─── */\ndocument.getElementById('members-tbody').addEventListener('click', e => {\n // 行内按钮已 stopPropagation,只剩单元格 / 头像 / 文本会冒泡上来\n if (e.target.closest('button')) return;\n const tr = e.target.closest('tr[data-id]');\n if (!tr) return;\n const m = MEMBERS.find(x => x.id === tr.dataset.id);\n if (!m || m.creator) return;\n openEdit(tr.dataset.id);\n});\n// 行 hover 提示可点击\nconst styleHint = document.createElement('style');\nstyleHint.textContent = `\n #members-tbody tr[data-id]:not(.creator-row) { cursor: pointer; }\n #members-tbody tr[data-id]:not(.creator-row):hover { background: var(--background-lighter); }\n`;\ndocument.head.appendChild(styleHint);\n\n</script>\n<script src=\"/exact/assets/api-bridge.js?v=2026060114\"></script>\n</body>\n</html>\n"
};