feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索 - model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框 - model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除) - model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批) - model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃 - model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段 - image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通 - image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64 - asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列 - pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
This commit is contained in:
parent
f420af2069
commit
04335f3269
99
CLAUDE.md
Normal file
99
CLAUDE.md
Normal file
@ -0,0 +1,99 @@
|
||||
# 流·Studio · 电商 AI 平台 · Claude Code 工程约定
|
||||
|
||||
> **本文件由 Claude Code 启动时自动加载。所有 AI 协作必须遵循以下规则。**
|
||||
|
||||
---
|
||||
|
||||
## 项目简介
|
||||
|
||||
**流·Studio** · AI 短视频带货生成平台 · 5 阶段流水线(商品 → 故事板 → 镜头 → 生成 → 投放)
|
||||
|
||||
- **设计代号:** Restraint · V2.1 · Firecrawl-aligned
|
||||
- **主要工作目录:** [电商AI平台/](电商AI平台/)
|
||||
- **Next.js 工程(独立):** [app/](app/)
|
||||
- **V1 历史归档:** [v1/](v1/)
|
||||
- **V2.1 归档(原 v2.1/):** [v2/](v2/)
|
||||
|
||||
---
|
||||
|
||||
## ★ 设计规范铁律(每次涉及页面 / CSS / UI 必读)
|
||||
|
||||
### 触发条件
|
||||
**只要任务涉及以下任一种,必须先 Read [电商AI平台/design.md](电商AI平台/design.md):**
|
||||
- 修改 `.html` 文件
|
||||
- 修改 `assets/restraint.css` 或任何 `.css`
|
||||
- 修改 inline `<style>` 块
|
||||
- 添加新页面 / 新组件
|
||||
- 调整布局 / 间距 / 颜色 / 字号
|
||||
- 用户提到"页面" "样式" "视觉" "组件" "色" "字" "圆角" "间距" 等关键词
|
||||
|
||||
### 必读章节
|
||||
- [design.md §0 AI 协作铁律](电商AI平台/design.md#0--ai-协作铁律每次启动必读) — 必读
|
||||
- [design.md §1 设计哲学](电商AI平台/design.md#1--设计哲学) — 价值观
|
||||
- [design.md §3 组件清单](电商AI平台/design.md#4--组件清单restraintcss-已实现--不要重发明) — 用现成组件
|
||||
- [design.md §8 Don't List](电商AI平台/design.md#8--dont-list绝对禁止--每次自检) — 自检
|
||||
|
||||
### 7 条铁律
|
||||
|
||||
1. **任何页面 / CSS 调整前必须 Read [电商AI平台/design.md](电商AI平台/design.md)** — 不读不动手
|
||||
2. **检查 [电商AI平台/assets/restraint.css](电商AI平台/assets/restraint.css) 已有组件** — `Grep ".btn|.pill|.input"` 等
|
||||
3. **禁止在页面 inline `<style>` 重写共享类**(`.btn` `.pill` `.input` `.modal` `.drawer` `.toast` `.field` `.tabs` `.chip` `.stats` `.list-row` 等)— 要变体回 restraint.css 加
|
||||
4. **禁止创建新色值** — 必须用 design.md §2.1 的 token,不写裸 hex
|
||||
5. **禁止改动基础 token**(`--heat` `--background-base` `--border-faint` 等)— 改了破坏全站
|
||||
6. **完成后对照 [design.md §8 Don't List](电商AI平台/design.md#8--dont-list绝对禁止--每次自检) 逐条自检**
|
||||
7. **不确定就问用户**,不要凭感觉发挥 — 用户原话:"我都希望你能遵循我们的设计规范,而不是乱做"
|
||||
|
||||
---
|
||||
|
||||
## 设计核心速记(详见 design.md)
|
||||
|
||||
- **冷灰底** `#f9f9f9` · 主橙 `#fa5d19` · 主前景 `#262626`
|
||||
- **全场 8 px 圆角**(Pill / dot 999 例外)· `>12 px` 直接判错
|
||||
- **inside-border** 而非真 `border`(hover 不抖动)
|
||||
- **单橙锚点** · 全场只有一个 accent · hover 用 alpha 不用换 hue
|
||||
- **Mono 装饰必有** · `[ 200 OK ]` `// 05.14` `[ /v2 ]`(品牌签名)
|
||||
- **主 CTA 唯一允许阴影** · 4 层橙色发光 · 其他场景禁阴影
|
||||
- **Inter(英)+ Alibaba PuHuiTi(中)+ JetBrains Mono(装饰)** · 字符级 fallthrough
|
||||
- **字重仅 3 档** · 400 / 500 / 600 · 700 仅给 Ctrl K 徽标
|
||||
|
||||
---
|
||||
|
||||
## Git 工作流
|
||||
|
||||
- **当前开发分支:** `dev`
|
||||
- **主分支:** `main` (生产)
|
||||
- **严禁直推 master/main** — 走 dev 分支 → PR → 合并触发 CI/CD
|
||||
- **严禁 `--no-verify` 跳过 hook**
|
||||
- **Push 规则:** 默认不 push,改完即停 · 用户明确说"push / 推一下"才执行
|
||||
- **commit 前不要 amend** — 创建新 commit,避免破坏历史
|
||||
|
||||
## 文件操作
|
||||
|
||||
- **三视图 = 单张 16:9 图** · 不要拆成 3 张缩略 · 用 `aspect-ratio: 16/9` 单容器
|
||||
- **设计稿优先** · 写代码前必须先读 [电商AI平台/_design_src/](电商AI平台/_design_src/) 设计稿(如果有)
|
||||
- **`.pen` 文件加密** · 只能用 pencil MCP 工具,不能 Read/Grep
|
||||
|
||||
---
|
||||
|
||||
## 用户偏好
|
||||
|
||||
- **角色:** UI 设计师 · 不读代码报错,只看最终视觉结果
|
||||
- **不需要的:** 终端报错截图、深奥的代码解释、过度的实施细节
|
||||
- **需要的:** 简短状态更新、视觉结果对照、清晰的"对/错"反馈
|
||||
|
||||
---
|
||||
|
||||
## 关键路径速查
|
||||
|
||||
| 资产 | 路径 |
|
||||
| ---- | ---- |
|
||||
| **设计规范(SSoT)** | [电商AI平台/design.md](电商AI平台/design.md) |
|
||||
| **共享 CSS** | [电商AI平台/assets/restraint.css](电商AI平台/assets/restraint.css) |
|
||||
| **Shell 注入** | [电商AI平台/assets/shell.js](电商AI平台/assets/shell.js) |
|
||||
| **视觉样板间(归档)** | [电商AI平台/_archive/design-system.html](电商AI平台/_archive/design-system.html) |
|
||||
| **规范理论(归档)** | [电商AI平台/_archive/DESIGN_SPEC_V2.md](电商AI平台/_archive/DESIGN_SPEC_V2.md) |
|
||||
| **设计稿源** | [电商AI平台/_design_src/](电商AI平台/_design_src/) |
|
||||
|
||||
---
|
||||
|
||||
**违反任何规范规则,用户有权要求重做,无需解释。**
|
||||
BIN
ui参考/063fc572-d234-4416-823d-d878583a9374.png
Normal file
BIN
ui参考/063fc572-d234-4416-823d-d878583a9374.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 993 KiB |
1730
电商AI平台/_archive/design-system.html
Normal file
1730
电商AI平台/_archive/design-system.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -52,6 +52,34 @@
|
||||
.balance-actions .btn-ghost { background: transparent; color: var(--accent-white); border: 1px solid rgba(255,255,255,.25); flex: 1; }
|
||||
.balance-actions .btn-ghost:hover { background: rgba(255,255,255,.1); color: var(--accent-white); }
|
||||
|
||||
/* 导出菜单 */
|
||||
.balance-actions .export-menu {
|
||||
position: absolute; right: 0; top: calc(100% + 6px); z-index: 30;
|
||||
min-width: 220px;
|
||||
background: var(--surface); color: var(--accent-black);
|
||||
border: 1px solid var(--border-faint); border-radius: var(--r-md);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.16);
|
||||
padding: 6px;
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
}
|
||||
.balance-actions .export-menu[hidden] { display: none; }
|
||||
.balance-actions .export-menu button {
|
||||
background: transparent; border: 0; padding: 8px 10px;
|
||||
display: grid; grid-template-columns: 22px 1fr; gap: 8px;
|
||||
align-items: center; cursor: pointer;
|
||||
border-radius: var(--r-sm);
|
||||
font-family: inherit; font-size: 13px; color: var(--accent-black);
|
||||
text-align: left;
|
||||
}
|
||||
.balance-actions .export-menu button:hover { background: var(--background-lighter); }
|
||||
.balance-actions .export-menu button .ic {
|
||||
width: 22px; height: 22px; display: grid; place-items: center;
|
||||
font-family: var(--font-mono); color: var(--heat); font-size: 14px; font-weight: 700;
|
||||
background: var(--heat-12); border-radius: var(--r-sm);
|
||||
}
|
||||
.balance-actions .export-menu button .t { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
|
||||
.balance-actions .export-menu button .t .d { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 2px; }
|
||||
|
||||
/* ─── 快速充值 pane(右栏)─── */
|
||||
.pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 20px; margin-bottom: 16px; }
|
||||
.pane h3 { font-size: 14px; font-weight: 600; margin-bottom: 6px; }
|
||||
@ -85,10 +113,10 @@
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
/* ─── 总览 · 趋势 + 阶段分布 ─── */
|
||||
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: start; }
|
||||
/* ─── 总览 · 趋势 + 阶段分布 (两栏等高) ─── */
|
||||
.overview-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 16px; align-items: stretch; }
|
||||
|
||||
.trend-pane { padding: 18px 20px 14px; }
|
||||
.trend-pane { padding: 18px 20px 14px; display: flex; flex-direction: column; }
|
||||
.trend-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 14px; }
|
||||
.trend-head h3 { margin-bottom: 0; }
|
||||
.trend-head .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
@ -96,7 +124,7 @@
|
||||
.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; }
|
||||
.trend-head .chip.active { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
|
||||
|
||||
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; height: 170px; padding: 6px 4px 2px; position: relative; }
|
||||
.trend-chart { display: grid; grid-template-rows: 1fr auto; gap: 6px; min-height: 170px; flex: 1; padding: 6px 4px 2px; position: relative; }
|
||||
.trend-chart .bars { display: grid; grid-template-columns: repeat(14, 1fr); gap: 5px; align-items: end; height: 100%; }
|
||||
.trend-chart .bar { background: var(--background-lighter); border-radius: 2px 2px 0 0; position: relative; transition: background var(--t-base); cursor: pointer; }
|
||||
.trend-chart .bar > span { display: block; width: 100%; background: var(--heat); border-radius: 2px 2px 0 0; }
|
||||
@ -218,7 +246,17 @@
|
||||
</div>
|
||||
<div class="balance-actions">
|
||||
<button class="btn btn-lg" onclick="openTopup()">充值</button>
|
||||
<button class="btn btn-ghost btn-lg" onclick="Shell.toast('提取明细', 'CSV · /billing/export')">导出账单</button>
|
||||
<div class="export-wrap" style="flex:1; position:relative;">
|
||||
<button class="btn btn-ghost btn-lg" id="export-trigger" type="button" style="width:100%;">
|
||||
导出账单
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px; vertical-align:-1px;"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="export-menu" id="export-menu" hidden>
|
||||
<button type="button" data-fmt="csv"><span class="ic">⌥</span><span class="t">CSV<span class="d">// 全部明细 · Excel 兼容</span></span></button>
|
||||
<button type="button" data-fmt="xlsx"><span class="ic">∑</span><span class="t">XLSX<span class="d">// 含格式 + 公式</span></span></button>
|
||||
<button type="button" data-fmt="pdf"><span class="ic">▤</span><span class="t">PDF<span class="d">// 含发票抬头</span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -256,11 +294,11 @@
|
||||
<div class="pane trend-pane">
|
||||
<div class="trend-head">
|
||||
<h3>消费趋势</h3>
|
||||
<span class="sub">// 近 14 天 · 单位 ¥</span>
|
||||
<span class="sub" id="trend-sub">// 近 14 天 · 单位 ¥</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="chip active">日</button>
|
||||
<button class="chip">周</button>
|
||||
<button class="chip">月</button>
|
||||
<button class="chip active" data-grain="day">日</button>
|
||||
<button class="chip" data-grain="week">周</button>
|
||||
<button class="chip" data-grain="month">月</button>
|
||||
</div>
|
||||
<div class="trend-chart">
|
||||
<div class="bars" id="trend-bars">
|
||||
@ -314,10 +352,20 @@
|
||||
<!-- ===== Tab 2: 按项目 ===== -->
|
||||
<div class="tab-panel" id="panel-by-project">
|
||||
<div class="filter-bar">
|
||||
<select><option>全部状态</option><option>进行中</option><option>已完成</option><option>已归档</option></select>
|
||||
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
|
||||
<select id="proj-f-status">
|
||||
<option value="all">全部状态</option>
|
||||
<option value="wip">进行中</option>
|
||||
<option value="ok">已完成</option>
|
||||
<option value="fail">失败 · 待重跑</option>
|
||||
</select>
|
||||
<select id="proj-f-range">
|
||||
<option value="month">本月</option>
|
||||
<option value="30d">近 30 天</option>
|
||||
<option value="90d">近 90 天</option>
|
||||
</select>
|
||||
<button class="chip" id="proj-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="ct">共 <b style="color:var(--accent-black);">8</b> 个项目 · 当月消耗 ¥162.60</span>
|
||||
<span class="ct" id="proj-count">共 <b style="color:var(--accent-black);">0</b> 个项目 · 消耗 ¥0.00</span>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
@ -327,22 +375,35 @@
|
||||
<th>所属成员</th>
|
||||
<th>当前阶段</th>
|
||||
<th>状态</th>
|
||||
<th style="text-align:right;">当月消耗</th>
|
||||
<th style="text-align:right;">消耗</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="proj-body">
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
// 当前筛选条件下没有项目 · 试试调整状态 / 时间范围
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 3: 按成员 ===== -->
|
||||
<div class="tab-panel" id="panel-by-member">
|
||||
<div class="filter-bar">
|
||||
<select><option>全部角色</option><option>超管</option><option>团管</option><option>成员</option></select>
|
||||
<select><option>本月</option><option>近 30 天</option><option>近 90 天</option></select>
|
||||
<select id="mem-f-role">
|
||||
<option value="all">全部角色</option>
|
||||
<option value="super">超管</option>
|
||||
<option value="admin">团管</option>
|
||||
<option value="member">成员</option>
|
||||
</select>
|
||||
<select id="mem-f-range">
|
||||
<option value="month">本月</option>
|
||||
<option value="30d">近 30 天</option>
|
||||
<option value="90d">近 90 天</option>
|
||||
</select>
|
||||
<button class="chip" id="mem-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="ct">共 <b style="color:var(--accent-black);">5</b> 人 · 当月合计 ¥319.00</span>
|
||||
<span class="ct" id="mem-count">共 <b style="color:var(--accent-black);">0</b> 人 · 合计 ¥0.00</span>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
@ -350,7 +411,7 @@
|
||||
<th>成员</th>
|
||||
<th>角色</th>
|
||||
<th>已完成项目</th>
|
||||
<th>当月已用 / 月度额度</th>
|
||||
<th>已用 / 月度额度</th>
|
||||
<th>最近活跃</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -358,16 +419,39 @@
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
// 当前筛选条件下没有成员 · 试试调整角色 / 时间范围
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Tab 4: 账单流水 ===== -->
|
||||
<div class="tab-panel" id="panel-bills">
|
||||
<div class="filter-bar">
|
||||
<select><option>全部阶段</option><option>视频片段</option><option>故事板</option><option>基础资产</option><option>脚本 LLM</option><option>充值</option><option>导出</option></select>
|
||||
<select><option>全部成员</option><option>小李</option><option>张运营</option><option>王小姐</option><option>陈策划</option></select>
|
||||
<select><option>近 30 天</option><option>近 7 天</option><option>本月</option><option>全部</option></select>
|
||||
<select id="bills-f-stage">
|
||||
<option value="all">全部阶段</option>
|
||||
<option value="视频片段">视频片段</option>
|
||||
<option value="故事板">故事板</option>
|
||||
<option value="基础资产">基础资产</option>
|
||||
<option value="脚本 LLM">脚本 LLM</option>
|
||||
<option value="充值">充值</option>
|
||||
<option value="导出">导出</option>
|
||||
</select>
|
||||
<select id="bills-f-member">
|
||||
<option value="all">全部成员</option>
|
||||
<option value="李">小李</option>
|
||||
<option value="张">张运营</option>
|
||||
<option value="王">王小姐</option>
|
||||
<option value="陈">陈策划</option>
|
||||
</select>
|
||||
<select id="bills-f-range">
|
||||
<option value="30d">近 30 天</option>
|
||||
<option value="7d">近 7 天</option>
|
||||
<option value="month">本月</option>
|
||||
<option value="all">全部</option>
|
||||
</select>
|
||||
<button class="chip" id="bills-f-reset" type="button" style="height:30px;font-size:11px;display:none;">清除筛选</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="ct">共 <b style="color:var(--accent-black);">28</b> 条 · <button class="chip" style="height:24px;font-size:11px;margin-left:4px;" onclick="Shell.toast('导出 CSV', '/billing/export?range=30d')">导出 CSV</button></span>
|
||||
<span class="ct">共 <b id="bills-count" style="color:var(--accent-black);">0</b> 条</span>
|
||||
</div>
|
||||
<table class="billing-table">
|
||||
<thead>
|
||||
@ -384,6 +468,9 @@
|
||||
<!-- JS 注入 -->
|
||||
</tbody>
|
||||
</table>
|
||||
<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;">
|
||||
// 当前筛选条件下没有账单 · 试试调整阶段 / 成员 / 时间范围
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 充值 modal -->
|
||||
@ -427,6 +514,24 @@ const TREND_DAYS = [
|
||||
{ d: '05.20', v: 19.40 }, { d: '05.21', v: 7.80 },
|
||||
];
|
||||
|
||||
// 8 周(以「W18~W21 + 前 4 周」做演示),按 7 天合计估算
|
||||
const TREND_WEEKS = [
|
||||
{ d: 'W14', v: 64.20 }, { d: 'W15', v: 92.80 }, { d: 'W16', v: 118.40 }, { d: 'W17', v: 78.60 },
|
||||
{ d: 'W18', v: 102.30 }, { d: 'W19', v: 138.20 }, { d: 'W20', v: 86.40 }, { d: 'W21', v: 27.20 },
|
||||
];
|
||||
// 6 个月 demo,自然月聚合估算 + 当月真实合计 162.60
|
||||
const TREND_MONTHS = [
|
||||
{ d: '2025-12', v: 384.20 }, { d: '2026-01', v: 528.40 }, { d: '2026-02', v: 296.80 },
|
||||
{ d: '2026-03', v: 412.00 }, { d: '2026-04', v: 348.60 }, { d: '2026-05', v: 162.60 },
|
||||
];
|
||||
const TREND_DATA = { day: TREND_DAYS, week: TREND_WEEKS, month: TREND_MONTHS };
|
||||
const TREND_LABEL = {
|
||||
day: { sub: '// 近 14 天 · 单位 ¥', sumLbl: '14 天合计', avgLbl: '日均' },
|
||||
week: { sub: '// 近 8 周 · 单位 ¥', sumLbl: '8 周合计', avgLbl: '周均' },
|
||||
month: { sub: '// 近 6 月 · 单位 ¥', sumLbl: '6 月合计', avgLbl: '月均' },
|
||||
};
|
||||
let _trendGrain = 'day';
|
||||
|
||||
const PROJECTS_BILL = [
|
||||
{ name: '补水面膜 · v3', product: '透真补水面膜', owner: '李', role: 'super', stage: 'Stage 3 故事板', stagePct: 60, status: 'wip', statusLabel: '进行中', amount: 48.20 },
|
||||
{ name: '透真防晒 · 通勤对比', product: '透真防晒', owner: '李', role: 'super', stage: 'Stage 5 导出', stagePct: 100, status: 'ok', statusLabel: '已完成', amount: 32.60 },
|
||||
@ -484,26 +589,61 @@ function amtCls(n) { return n > 0 ? 'pos' : (n === 0 ? 'zero' : 'neg'); }
|
||||
|
||||
/* ─── 趋势柱 ─── */
|
||||
function renderTrend() {
|
||||
const data = TREND_DATA[_trendGrain];
|
||||
const meta = TREND_LABEL[_trendGrain];
|
||||
const bars = document.getElementById('trend-bars');
|
||||
const xax = document.getElementById('trend-xaxis');
|
||||
const max = Math.max(...TREND_DAYS.map(d => d.v));
|
||||
bars.innerHTML = TREND_DAYS.map(d => {
|
||||
const max = Math.max(...data.map(d => d.v));
|
||||
// bars 的 grid-template-columns 默认是 repeat(14, 1fr),需要按数据长度动态调整
|
||||
bars.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;
|
||||
xax.style.gridTemplateColumns = `repeat(${data.length}, 1fr)`;
|
||||
bars.innerHTML = data.map(d => {
|
||||
const h = max > 0 ? (d.v / max * 100) : 0;
|
||||
const isPeak = d.v === max;
|
||||
return `<div class="bar${isPeak ? ' peak' : ''}" title="${d.d} · ${fmtMoney(d.v)}"><span style="height: ${h.toFixed(1)}%"></span></div>`;
|
||||
}).join('');
|
||||
xax.innerHTML = TREND_DAYS.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
|
||||
const sum = TREND_DAYS.reduce((s, d) => s + d.v, 0);
|
||||
// x 轴标签:日 = 隔列显示 MM·DD 的 DD 部分;周/月 = 全显示
|
||||
if (_trendGrain === 'day') {
|
||||
xax.innerHTML = data.map((d, i) => i % 2 === 0 ? `<span>${d.d.slice(3)}</span>` : '<span></span>').join('');
|
||||
} else if (_trendGrain === 'week') {
|
||||
xax.innerHTML = data.map(d => `<span>${d.d}</span>`).join('');
|
||||
} else {
|
||||
xax.innerHTML = data.map(d => `<span>${d.d.slice(5)}</span>`).join('');
|
||||
}
|
||||
const sum = data.reduce((s, d) => s + d.v, 0);
|
||||
document.getElementById('trend-sub').textContent = meta.sub;
|
||||
document.getElementById('trend-sum').textContent = fmtMoney(sum);
|
||||
document.getElementById('trend-avg').textContent = fmtMoney(sum / TREND_DAYS.length);
|
||||
document.getElementById('trend-avg').textContent = fmtMoney(sum / data.length);
|
||||
document.getElementById('trend-peak').textContent = fmtMoney(max);
|
||||
// 旁标 label
|
||||
document.querySelectorAll('.trend-foot .item').forEach((it, idx) => {
|
||||
const k = it.querySelector('.k');
|
||||
if (idx === 0 && k) k.textContent = meta.sumLbl;
|
||||
else if (idx === 1 && k) k.textContent = meta.avgLbl;
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── 按项目 表格 ─── */
|
||||
/* ─── 趋势 日/周/月 切换 ─── */
|
||||
document.querySelectorAll('.trend-head .chip[data-grain]').forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
document.querySelectorAll('.trend-head .chip[data-grain]').forEach(c => c.classList.remove('active'));
|
||||
chip.classList.add('active');
|
||||
_trendGrain = chip.dataset.grain;
|
||||
renderTrend();
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── 按项目 表格 + 筛选 ─── */
|
||||
const PROJ_FILTER = { status: 'all', range: 'month' };
|
||||
const RANGE_MULT = { 'month': 1, '30d': 1, '90d': 2.6 }; // 演示用,30/90 天放大倍率(月 = 当月真实)
|
||||
const RANGE_LBL = { 'month': '当月', '30d': '近 30 天', '90d': '近 90 天' };
|
||||
function renderProjects() {
|
||||
const tb = document.getElementById('proj-body');
|
||||
tb.innerHTML = PROJECTS_BILL.map(p => {
|
||||
const mult = RANGE_MULT[PROJ_FILTER.range] || 1;
|
||||
const list = PROJECTS_BILL.filter(p => PROJ_FILTER.status === 'all' || p.status === PROJ_FILTER.status);
|
||||
tb.innerHTML = list.map(p => {
|
||||
const r = ROLE_META[p.role];
|
||||
const amt = p.amount * mult;
|
||||
return `
|
||||
<tr>
|
||||
<td><a href="pipeline.html?product=${encodeURIComponent(p.product)}" style="color:var(--accent-black);text-decoration:none;font-weight:500;">${p.name}</a></td>
|
||||
@ -511,25 +651,49 @@ function renderProjects() {
|
||||
<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>
|
||||
<td><span class="muted">${p.stage}</span><span class="progress-mini"><span style="width:${p.stagePct}%"></span></span></td>
|
||||
<td><span class="status-tag ${p.status}">${p.statusLabel}</span></td>
|
||||
<td class="${amtCls(-p.amount)}">${p.amount === 0 ? '¥0.00' : '-' + fmtMoney(p.amount)}</td>
|
||||
<td class="${amtCls(-amt)}">${amt === 0 ? '¥0.00' : '-' + fmtMoney(amt)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
const sum = list.reduce((s, p) => s + p.amount * mult, 0);
|
||||
document.getElementById('proj-count').innerHTML =
|
||||
`共 <b style="color:var(--accent-black);">${list.length}</b> 个项目 · ${RANGE_LBL[PROJ_FILTER.range]}消耗 ${fmtMoney(sum)}`;
|
||||
const empty = document.getElementById('proj-empty');
|
||||
const tbl = document.querySelector('#panel-by-project .billing-table');
|
||||
empty.style.display = list.length === 0 ? '' : 'none';
|
||||
tbl.style.display = list.length === 0 ? 'none' : '';
|
||||
const isDef = PROJ_FILTER.status === 'all' && PROJ_FILTER.range === 'month';
|
||||
document.getElementById('proj-f-reset').style.display = isDef ? 'none' : '';
|
||||
}
|
||||
|
||||
/* ─── 按成员 表格 ─── */
|
||||
['status', 'range'].forEach(key => {
|
||||
const sel = document.getElementById('proj-f-' + key);
|
||||
sel.addEventListener('change', () => { PROJ_FILTER[key] = sel.value; renderProjects(); });
|
||||
});
|
||||
document.getElementById('proj-f-reset').addEventListener('click', () => {
|
||||
PROJ_FILTER.status = 'all'; PROJ_FILTER.range = 'month';
|
||||
document.getElementById('proj-f-status').value = 'all';
|
||||
document.getElementById('proj-f-range').value = 'month';
|
||||
renderProjects();
|
||||
});
|
||||
|
||||
/* ─── 按成员 表格 + 筛选 ─── */
|
||||
const MEM_FILTER = { role: 'all', range: 'month' };
|
||||
function renderMembers() {
|
||||
const tb = document.getElementById('member-body');
|
||||
tb.innerHTML = MEMBERS_BILL.map(m => {
|
||||
const mult = RANGE_MULT[MEM_FILTER.range] || 1;
|
||||
const list = MEMBERS_BILL.filter(m => MEM_FILTER.role === 'all' || m.role === MEM_FILTER.role);
|
||||
tb.innerHTML = list.map(m => {
|
||||
const r = ROLE_META[m.role];
|
||||
const pct = m.monthly > 0 ? (m.used / m.monthly * 100) : 0;
|
||||
const used = m.used * mult;
|
||||
const pct = m.monthly > 0 ? (used / m.monthly * 100) : 0;
|
||||
return `
|
||||
<tr style="cursor:pointer;" onclick="location.href='team.html'">
|
||||
<td><span class="who"><span class="av">${m.av}</span><strong style="font-weight:500;">${m.name}</strong></span></td>
|
||||
<td><span class="role-pill ${r.cls}"><span class="dot"></span>${r.label}</span></td>
|
||||
<td>${m.projectsDone}</td>
|
||||
<td>
|
||||
<strong style="font-variant-numeric:tabular-nums;font-weight:600;">${fmtMoney(m.used)}</strong>
|
||||
<strong style="font-variant-numeric:tabular-nums;font-weight:600;">${fmtMoney(used)}</strong>
|
||||
<span class="muted"> / ${fmtMoney(m.monthly)} · ${pct.toFixed(1)}%</span>
|
||||
<span class="progress-mini"><span style="width:${Math.min(100, pct)}%; background:${pct >= 85 ? '#B45309' : 'var(--heat)'};"></span></span>
|
||||
</td>
|
||||
@ -537,12 +701,62 @@ function renderMembers() {
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
const sum = list.reduce((s, m) => s + m.used * mult, 0);
|
||||
document.getElementById('mem-count').innerHTML =
|
||||
`共 <b style="color:var(--accent-black);">${list.length}</b> 人 · ${RANGE_LBL[MEM_FILTER.range]}合计 ${fmtMoney(sum)}`;
|
||||
const empty = document.getElementById('mem-empty');
|
||||
const tbl = document.querySelector('#panel-by-member .billing-table');
|
||||
empty.style.display = list.length === 0 ? '' : 'none';
|
||||
tbl.style.display = list.length === 0 ? 'none' : '';
|
||||
const isDef = MEM_FILTER.role === 'all' && MEM_FILTER.range === 'month';
|
||||
document.getElementById('mem-f-reset').style.display = isDef ? 'none' : '';
|
||||
}
|
||||
|
||||
['role', 'range'].forEach(key => {
|
||||
const sel = document.getElementById('mem-f-' + key);
|
||||
sel.addEventListener('change', () => { MEM_FILTER[key] = sel.value; renderMembers(); });
|
||||
});
|
||||
document.getElementById('mem-f-reset').addEventListener('click', () => {
|
||||
MEM_FILTER.role = 'all'; MEM_FILTER.range = 'month';
|
||||
document.getElementById('mem-f-role').value = 'all';
|
||||
document.getElementById('mem-f-range').value = 'month';
|
||||
renderMembers();
|
||||
});
|
||||
|
||||
/* ─── 账单流水:筛选 + 表格渲染 ─── */
|
||||
const BILLS_FILTER = { stage: 'all', member: 'all', range: '30d' };
|
||||
// demo "今天" = 05.21,所有 ts 都是 MM.DD 格式,这里基于这一假定算时间区间
|
||||
const TODAY_MD = '05.21';
|
||||
function mdToDay(md) {
|
||||
// 把 "MM.DD" 当成 2026 年的日子换算成 1970 epoch ms,只用来比较先后
|
||||
const [m, d] = md.split('.').map(Number);
|
||||
return Date.UTC(2026, (m || 1) - 1, d || 1);
|
||||
}
|
||||
const TODAY_MS = mdToDay(TODAY_MD);
|
||||
|
||||
function passRange(ts, range) {
|
||||
if (range === 'all') return true;
|
||||
const md = ts.slice(0, 5); // "05.21"
|
||||
if (range === 'month') return md.startsWith(TODAY_MD.slice(0, 3));
|
||||
const diff = TODAY_MS - mdToDay(md);
|
||||
if (range === '7d') return diff <= 6 * 86400000;
|
||||
if (range === '30d') return diff <= 29 * 86400000;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getFilteredBills() {
|
||||
return BILLS.filter(b => {
|
||||
if (BILLS_FILTER.stage !== 'all' && b.type !== BILLS_FILTER.stage) return false;
|
||||
if (BILLS_FILTER.member !== 'all' && b.who !== BILLS_FILTER.member) return false;
|
||||
if (!passRange(b.ts, BILLS_FILTER.range)) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── 账单流水 表格 ─── */
|
||||
function renderBills() {
|
||||
const tb = document.getElementById('bills-body');
|
||||
tb.innerHTML = BILLS.map(b => {
|
||||
const list = getFilteredBills();
|
||||
tb.innerHTML = list.map(b => {
|
||||
const r = ROLE_META[b.role];
|
||||
return `
|
||||
<tr>
|
||||
@ -555,7 +769,36 @@ function renderBills() {
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
document.getElementById('bills-count').textContent = list.length;
|
||||
const empty = document.getElementById('bills-empty');
|
||||
const table = document.querySelector('#panel-bills .billing-table');
|
||||
if (list.length === 0) {
|
||||
empty.style.display = '';
|
||||
table.style.display = 'none';
|
||||
} else {
|
||||
empty.style.display = 'none';
|
||||
table.style.display = '';
|
||||
}
|
||||
// 「清除筛选」按钮:任何一个非默认就显示
|
||||
const isDefault = BILLS_FILTER.stage === 'all' && BILLS_FILTER.member === 'all' && BILLS_FILTER.range === '30d';
|
||||
document.getElementById('bills-f-reset').style.display = isDefault ? 'none' : '';
|
||||
}
|
||||
|
||||
/* ─── 筛选绑定 ─── */
|
||||
['stage', 'member', 'range'].forEach(key => {
|
||||
const sel = document.getElementById('bills-f-' + key);
|
||||
sel.addEventListener('change', () => {
|
||||
BILLS_FILTER[key] = sel.value;
|
||||
renderBills();
|
||||
});
|
||||
});
|
||||
document.getElementById('bills-f-reset').addEventListener('click', () => {
|
||||
BILLS_FILTER.stage = 'all'; BILLS_FILTER.member = 'all'; BILLS_FILTER.range = '30d';
|
||||
document.getElementById('bills-f-stage').value = 'all';
|
||||
document.getElementById('bills-f-member').value = 'all';
|
||||
document.getElementById('bills-f-range').value = '30d';
|
||||
renderBills();
|
||||
});
|
||||
|
||||
/* ─── Tab 切换 ─── */
|
||||
document.querySelectorAll('.billing-tabs .tab').forEach(tab => {
|
||||
@ -602,6 +845,29 @@ function topupDone() {
|
||||
Shell.toast('充值成功', '余额已更新 · 可开发票');
|
||||
}
|
||||
|
||||
/* ─── 导出菜单 ─── */
|
||||
(function bindExport() {
|
||||
const trigger = document.getElementById('export-trigger');
|
||||
const menu = document.getElementById('export-menu');
|
||||
if (!trigger || !menu) return;
|
||||
const FMT_LABEL = { csv: 'CSV', xlsx: 'XLSX', pdf: 'PDF' };
|
||||
trigger.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
menu.hidden = !menu.hidden;
|
||||
});
|
||||
menu.querySelectorAll('button[data-fmt]').forEach(b => {
|
||||
b.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const fmt = b.dataset.fmt;
|
||||
menu.hidden = true;
|
||||
Shell.toast('正在生成 ' + FMT_LABEL[fmt], '完成后会发送到注册邮箱');
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', () => { if (!menu.hidden) menu.hidden = true; });
|
||||
// Esc 关闭
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !menu.hidden) menu.hidden = true; });
|
||||
})();
|
||||
|
||||
/* ─── 初始化 ─── */
|
||||
renderTrend();
|
||||
renderProjects();
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<style>
|
||||
#page-content { padding: 24px 28px 60px; }
|
||||
|
||||
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片优化 · 等比)─── */
|
||||
/* ─── 三 Hero 卡片网格(模特上身图 / 平台套图 / 图片创作 · 等比)─── */
|
||||
.factory-hero {
|
||||
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px; margin-bottom: 56px;
|
||||
@ -118,9 +118,6 @@
|
||||
.model-visual .stack .placeholder { aspect-ratio: 3 / 4; }
|
||||
.kit-visual { grid-template-columns: repeat(4, 1fr); }
|
||||
.kit-visual .placeholder { aspect-ratio: 1 / 1; }
|
||||
.tri-visual { display: block; }
|
||||
.tri-visual .placeholder { aspect-ratio: 16 / 9; }
|
||||
.tri-visual .placeholder + .placeholder { display: none; }
|
||||
|
||||
/* ─── 任务中心 · section header ─── */
|
||||
.section-h { display: flex; align-items: center; gap: 12px; margin-top: 24px; margin-bottom: 14px; }
|
||||
@ -365,14 +362,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="platform-row">
|
||||
<div class="platform-chip"><span class="code">DY</span><span class="nm">抖音</span></div>
|
||||
<div class="platform-chip"><span class="code">TB</span><span class="nm">淘宝</span></div>
|
||||
<div class="platform-chip"><span class="code">XHS</span><span class="nm">小红书</span></div>
|
||||
<div class="platform-chip"><span class="code">PDD</span><span class="nm">拼多多</span></div>
|
||||
<div class="platform-chip"><span class="code">AMZ</span><span class="nm">亚马逊</span></div>
|
||||
</div>
|
||||
|
||||
<div class="factory-cta">
|
||||
<a class="btn btn-primary btn-lg" href="platform-cover.html">
|
||||
开始生成
|
||||
@ -391,16 +380,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片 C · 图片优化(生成人物 / 商品三视图)-->
|
||||
<!-- 卡片 C · 图片创作(自由创作 AI 图片工作台)-->
|
||||
<div class="factory-card with-corners">
|
||||
<span class="corner-tr" aria-hidden></span>
|
||||
<span class="corner-bl" aria-hidden></span>
|
||||
|
||||
<div class="factory-body">
|
||||
<div class="factory-text">
|
||||
<span class="factory-tag">[ TRI-VIEW · OPTIMIZE ]</span>
|
||||
<div class="factory-title">图片优化</div>
|
||||
<div class="factory-desc">为人物 / 商品生成三视图,保证多镜头一致性</div>
|
||||
<span class="factory-tag">[ IMAGE · STUDIO ]</span>
|
||||
<div class="factory-title">图片创作</div>
|
||||
<div class="factory-desc">自由创作 AI 图片,适用于详情图 / 海报 / 灵感速写</div>
|
||||
|
||||
<ul class="factory-features">
|
||||
<li>
|
||||
@ -424,7 +413,7 @@
|
||||
</ul>
|
||||
|
||||
<div class="factory-cta">
|
||||
<a class="btn btn-primary btn-lg" href="model-photo.html?mode=tri">
|
||||
<a class="btn btn-primary btn-lg" href="image-optimize.html">
|
||||
开始生成
|
||||
<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>
|
||||
</a>
|
||||
@ -460,6 +449,16 @@
|
||||
<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>
|
||||
<input class="input" id="tc-search" placeholder="搜索任务名">
|
||||
</div>
|
||||
<div class="chip-wrap" data-key="time">
|
||||
<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>
|
||||
<div class="chip-menu">
|
||||
<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>
|
||||
<div class="mi-sep"></div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chip-wrap" data-key="type">
|
||||
<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>
|
||||
<div class="chip-menu"></div>
|
||||
@ -488,9 +487,8 @@
|
||||
<table class="t">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32%">任务</th>
|
||||
<th>类型</th>
|
||||
<th style="width:140px">进度</th>
|
||||
<th style="width:42%">任务</th>
|
||||
<th style="width:160px">进度</th>
|
||||
<th>状态</th>
|
||||
<th style="width:120px">创建于</th>
|
||||
<th style="width:48px"></th>
|
||||
@ -547,10 +545,19 @@ Shell.render({
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图' };
|
||||
const TYPE_LABEL = { model: '模特上身图', platform: '平台套图', image: '图片创作' };
|
||||
const STATUS_LABEL = { ok: '已完成', gen: '生成中', err: '失败' };
|
||||
const STATUS_PILL = { ok: 'ok', gen: 'info', err: 'err' };
|
||||
const KEY_BY_TYPE = { model: 'fs-image-tasks-model', platform: 'fs-image-tasks-platform' };
|
||||
const KEY_BY_TYPE = {
|
||||
model: 'fs-image-tasks-model',
|
||||
platform: 'fs-image-tasks-platform',
|
||||
image: 'fs-image-tasks-image',
|
||||
};
|
||||
const URL_BY_TYPE = {
|
||||
model: 'model-photo.html',
|
||||
platform: 'platform-cover.html',
|
||||
image: 'image-optimize.html',
|
||||
};
|
||||
|
||||
const taskGrid = document.getElementById('task-grid');
|
||||
const listTbody = document.getElementById('task-list-tbody');
|
||||
@ -559,7 +566,21 @@ Shell.render({
|
||||
|
||||
let cards = []; // 动态生成的 .task-card 元素数组(顺序与任务时间倒序一致)
|
||||
|
||||
const state = { filter: 'all', type: 'all', search: '', view: 'list' };
|
||||
const state = { filter: 'all', type: 'all', time: 'all', search: '', view: 'list' };
|
||||
|
||||
function _timeMatch(createdAt, key) {
|
||||
if (key === 'all' || !createdAt) return true;
|
||||
const now = Date.now();
|
||||
const diff = now - Number(createdAt);
|
||||
if (key === '10min') return diff <= 10 * 60 * 1000;
|
||||
if (key === '1h') return diff <= 60 * 60 * 1000;
|
||||
if (key === 'today') {
|
||||
const a = new Date(now); const b = new Date(Number(createdAt));
|
||||
return a.toDateString() === b.toDateString();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const TIME_LABEL = { all: '时间', today: '今天', '1h': '1 小时内', '10min': '10 分钟内' };
|
||||
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
|
||||
@ -593,9 +614,8 @@ Shell.render({
|
||||
|
||||
/* ---------- 点击行 / 卡片 → 跳转工作台(携带 taskId) ---------- */
|
||||
function goToWorkbench(t) {
|
||||
const url = (t.type === 'model' ? 'model-photo.html' : 'platform-cover.html')
|
||||
+ '?taskId=' + encodeURIComponent(t.id);
|
||||
location.href = url;
|
||||
const base = URL_BY_TYPE[t.type] || URL_BY_TYPE.model;
|
||||
location.href = base + '?taskId=' + encodeURIComponent(t.id);
|
||||
}
|
||||
|
||||
/* ---------- 1. 从 task 数据生成卡片 + list 行 ---------- */
|
||||
@ -606,6 +626,7 @@ Shell.render({
|
||||
card.dataset.type = t.type;
|
||||
card.dataset.name = t.name;
|
||||
card.dataset.taskId = t.id;
|
||||
card.dataset.createdAt = String(t.createdAt || 0);
|
||||
card.style.cursor = 'pointer';
|
||||
card.innerHTML = `
|
||||
<button class="card-del-btn" type="button" title="删除任务">
|
||||
@ -642,7 +663,6 @@ Shell.render({
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="muted">${esc(TYPE_LABEL[t.type] || t.type)}</span></td>
|
||||
<td>${t.status === 'gen'
|
||||
? `<div class="task-list-prog"><div class="bar"><span style="width:60%"></span></div><span class="pct">60%</span></div>`
|
||||
: (t.status === 'ok' ? '<span class="muted-2 mono" style="font-size:11px;">已完成</span>' : '<span class="muted-2 mono" style="font-size:11px;">—</span>')}</td>
|
||||
@ -719,6 +739,21 @@ Shell.render({
|
||||
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.type));
|
||||
}
|
||||
|
||||
function syncTimeChip() {
|
||||
const wrap = document.querySelector('.chip-wrap[data-key="time"]');
|
||||
if (!wrap) return;
|
||||
const label = wrap.querySelector('.chip-label');
|
||||
const chip = wrap.querySelector('.chip');
|
||||
if (state.time === 'all') {
|
||||
label.textContent = '时间';
|
||||
chip.classList.remove('active');
|
||||
} else {
|
||||
label.textContent = TIME_LABEL[state.time] || state.time;
|
||||
chip.classList.add('active');
|
||||
}
|
||||
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.time));
|
||||
}
|
||||
|
||||
/* ---------- 3. applyFilter ---------- */
|
||||
function applyFilter() {
|
||||
const q = state.search.toLowerCase();
|
||||
@ -726,8 +761,9 @@ Shell.render({
|
||||
cards.forEach(card => {
|
||||
const okStatus = state.filter === 'all' || card.dataset.status === state.filter;
|
||||
const okType = state.type === 'all' || card.dataset.type === state.type;
|
||||
const okTime = _timeMatch(card.dataset.createdAt, state.time);
|
||||
const okSearch = !q || (card.dataset.name || '').toLowerCase().includes(q);
|
||||
const show = okStatus && okType && okSearch;
|
||||
const show = okStatus && okType && okTime && okSearch;
|
||||
card.style.display = show ? '' : 'none';
|
||||
if (card._listRow) card._listRow.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
@ -746,7 +782,7 @@ Shell.render({
|
||||
document.getElementById('tc-sub-err').textContent = counts.err;
|
||||
|
||||
document.getElementById('tc-result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${cards.length} 个任务`;
|
||||
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all');
|
||||
document.getElementById('tc-clear').hidden = !(state.search || state.type !== 'all' || state.time !== 'all');
|
||||
|
||||
// 空态
|
||||
const emptyEl = document.getElementById('tc-empty');
|
||||
@ -799,6 +835,24 @@ Shell.render({
|
||||
syncTypeChip();
|
||||
applyFilter();
|
||||
});
|
||||
// time chip
|
||||
const timeWrap = document.querySelector('.chip-wrap[data-key="time"]');
|
||||
const timeMenu = timeWrap.querySelector('.chip-menu');
|
||||
timeWrap.querySelector('.chip').addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const isOpen = timeWrap.classList.contains('open');
|
||||
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
|
||||
if (!isOpen) timeWrap.classList.add('open');
|
||||
});
|
||||
timeMenu.addEventListener('click', e => {
|
||||
const mi = e.target.closest('.mi');
|
||||
if (!mi) return;
|
||||
e.stopPropagation();
|
||||
state.time = mi.dataset.value;
|
||||
timeWrap.classList.remove('open');
|
||||
syncTimeChip();
|
||||
applyFilter();
|
||||
});
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
|
||||
});
|
||||
@ -806,8 +860,10 @@ Shell.render({
|
||||
document.getElementById('tc-clear').addEventListener('click', () => {
|
||||
state.search = '';
|
||||
state.type = 'all';
|
||||
state.time = 'all';
|
||||
document.getElementById('tc-search').value = '';
|
||||
syncTypeChip();
|
||||
syncTimeChip();
|
||||
applyFilter();
|
||||
Shell.toast('已清空筛选');
|
||||
});
|
||||
|
||||
@ -250,6 +250,58 @@
|
||||
@media (max-width: 900px) {
|
||||
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* 放弃填写 · 确认弹窗 · 覆盖在 drawer 上 */
|
||||
#npd-confirm-bg {
|
||||
position: fixed; inset: 0; z-index: 1200;
|
||||
background: rgba(21, 20, 15, .42);
|
||||
display: grid; place-items: center;
|
||||
padding: 16px;
|
||||
}
|
||||
#npd-confirm-bg[hidden] { display: none; }
|
||||
.npd-confirm-modal {
|
||||
width: min(420px, 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;
|
||||
}
|
||||
.npd-confirm-modal .dh {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 18px 20px 14px;
|
||||
}
|
||||
.npd-confirm-modal .dh .ic {
|
||||
width: 36px; height: 36px; flex-shrink: 0;
|
||||
border-radius: var(--r-md);
|
||||
background: rgba(180, 30, 30, .08); color: var(--accent-crimson);
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.npd-confirm-modal .dh .ic svg { width: 18px; height: 18px; }
|
||||
.npd-confirm-modal .dh .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||
.npd-confirm-modal .dh .ti strong { font-size: 14.5px; color: var(--accent-black); font-weight: 600; }
|
||||
.npd-confirm-modal .dh .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.5; }
|
||||
.npd-confirm-modal .df {
|
||||
display: flex; gap: 8px;
|
||||
padding: 0 20px 18px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.npd-confirm-modal .df button {
|
||||
height: 32px; padding: 0 14px;
|
||||
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), background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.npd-confirm-modal .df button:hover { border-color: var(--heat-20); background: var(--heat-12); color: var(--heat); }
|
||||
.npd-confirm-modal .df button.danger { color: var(--accent-crimson); }
|
||||
.npd-confirm-modal .df button.danger:hover {
|
||||
background: rgba(180, 30, 30, .08);
|
||||
border-color: rgba(180, 30, 30, .35);
|
||||
color: var(--accent-crimson);
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML = `
|
||||
@ -341,11 +393,33 @@
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 放弃填写 · 确认弹窗(覆盖在 drawer 之上) -->
|
||||
<div id="npd-confirm-bg" hidden>
|
||||
<div class="npd-confirm-modal" role="dialog" aria-label="放弃填写">
|
||||
<div class="dh">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 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"/><path d="M12 9v4M12 17h.01"/></svg>
|
||||
</div>
|
||||
<div class="ti">
|
||||
<strong>放弃当前填写?</strong>
|
||||
<span class="mono">// 已填写的商品名、卖点、图片等内容将不会保留</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="df">
|
||||
<button type="button" data-act="keep">继续填写</button>
|
||||
<button type="button" class="danger" data-act="discard">放弃退出</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
/* ---------- DOM refs (populated by ensureInjected) ---------- */
|
||||
let injected = false;
|
||||
let bg, drawer, $f, $grid, $bullets, $blInput;
|
||||
let confirmBg;
|
||||
let _forceClose = false; // 保存成功后绕过脏检查
|
||||
let _initialCat = ''; // 打开时类目下拉的初始值,用来判断是否被改过
|
||||
let currentOpts = {};
|
||||
const PF_MAX = 5;
|
||||
let pfFiles = []; // { id, dataUrl, name }
|
||||
@ -370,6 +444,7 @@
|
||||
|
||||
bg = document.getElementById(DRAWER_BG_ID);
|
||||
drawer = document.getElementById(DRAWER_ID);
|
||||
confirmBg = document.getElementById('npd-confirm-bg');
|
||||
$f = {
|
||||
name: drawer.querySelector('[data-f="name"]'),
|
||||
cat: drawer.querySelector('[data-f="cat"]'),
|
||||
@ -549,6 +624,7 @@
|
||||
if (drawer.classList.contains('show')) return; // 已开则不重复锁
|
||||
currentOpts = opts || {};
|
||||
resetForm();
|
||||
_initialCat = $f.cat.value; // 快照「打开时类目」用于脏检查
|
||||
bg.classList.add('show');
|
||||
drawer.classList.add('show');
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
@ -556,9 +632,49 @@
|
||||
setTimeout(() => $f.name.focus(), 280);
|
||||
}
|
||||
|
||||
function close() {
|
||||
/* ---------- 脏检查 + 退出确认 ---------- */
|
||||
function isDirty() {
|
||||
if (!injected) return false;
|
||||
if (($f.name.value || '').trim()) return true;
|
||||
if (($f.target.value || '').trim()) return true;
|
||||
if (($blInput.value || '').trim()) return true;
|
||||
if (pfFiles.length > 0) return true;
|
||||
if ($bullets.querySelectorAll('.bl-item').length > 0) return true;
|
||||
if ($f.cat.value !== _initialCat) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function confirmDiscard() {
|
||||
return new Promise(resolve => {
|
||||
confirmBg.hidden = false;
|
||||
const buttons = confirmBg.querySelectorAll('button[data-act]');
|
||||
function done(choice) {
|
||||
confirmBg.hidden = true;
|
||||
buttons.forEach(b => b.onclick = null);
|
||||
confirmBg.onclick = null;
|
||||
document.removeEventListener('keydown', escHandler, true);
|
||||
resolve(choice === 'discard');
|
||||
}
|
||||
function escHandler(e) {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation();
|
||||
done('keep');
|
||||
}
|
||||
}
|
||||
buttons.forEach(b => b.onclick = () => done(b.dataset.act));
|
||||
confirmBg.onclick = e => { if (e.target === confirmBg) done('keep'); };
|
||||
// 用 capture · 截在外层 Esc 监听器之前
|
||||
document.addEventListener('keydown', escHandler, true);
|
||||
});
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (!injected) return;
|
||||
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
|
||||
if (!_forceClose && isDirty()) {
|
||||
const ok = await confirmDiscard();
|
||||
if (!ok) return;
|
||||
}
|
||||
bg.classList.remove('show');
|
||||
drawer.classList.remove('show');
|
||||
drawer.setAttribute('aria-hidden', 'true');
|
||||
@ -566,7 +682,20 @@
|
||||
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
|
||||
}
|
||||
|
||||
function closeForce() {
|
||||
_forceClose = true;
|
||||
try { close(); } finally { _forceClose = false; }
|
||||
}
|
||||
|
||||
function save() {
|
||||
// 兜底:用户在卖点输入框敲了字但没回车/没点「添加卖点」就直接点创建,
|
||||
// 这里自动提交一次,避免静默丢失最后一条卖点(常见误操作)
|
||||
if ($blInput && ($blInput.value || '').trim()) {
|
||||
blAdd($blInput.value);
|
||||
$blInput.value = '';
|
||||
updateBlAddBtn();
|
||||
}
|
||||
|
||||
const name = ($f.name.value || '').trim();
|
||||
const cat = $f.cat.value;
|
||||
const target = ($f.target.value || '').trim();
|
||||
@ -595,15 +724,14 @@
|
||||
images,
|
||||
imgs: images.length,
|
||||
};
|
||||
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
|
||||
// 自动从 storage 读出并 prepend 到 grid(否则用户在工作台创建后 → 跳详情 →
|
||||
// 回商品库会看不到刚创建的商品)
|
||||
// 持久化到 sessionStorage('fs-extra-products') · 仅当前标签页生命周期内有效
|
||||
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
|
||||
try {
|
||||
const KEY = 'fs-extra-products';
|
||||
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
|
||||
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
|
||||
list.push({
|
||||
id: product.id,
|
||||
name, cat,
|
||||
name, cat, target,
|
||||
tags: '',
|
||||
assets: 0,
|
||||
videos: 0,
|
||||
@ -611,13 +739,19 @@
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
localStorage.setItem(KEY, JSON.stringify(list));
|
||||
sessionStorage.setItem(KEY, JSON.stringify(list));
|
||||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||||
|
||||
// 短期 sessionStorage 缓存完整 product(含图片 dataUrl),供 detail 页 ?id=new
|
||||
// 读出后渲染。读完即清除,避免污染后续操作。
|
||||
try {
|
||||
sessionStorage.setItem('npd-last-created', JSON.stringify(product));
|
||||
} catch (e) { /* dataUrl 可能过大 → 退化为不带图,detail 仍能渲染文本字段 */ }
|
||||
|
||||
if (typeof currentOpts.onSave === 'function') {
|
||||
toast('商品已创建', '+ ' + name);
|
||||
currentOpts.onSave(product);
|
||||
close();
|
||||
closeForce();
|
||||
return;
|
||||
}
|
||||
// 默认行为: 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底
|
||||
|
||||
@ -431,6 +431,15 @@ main { position: relative; background: var(--background-base); min-width: 0; }
|
||||
}
|
||||
.page-head .sub .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||||
.page-head .actions { display: flex; gap: 10px; align-items: center; }
|
||||
/* page-head 右上角主操作按钮 · 统一尺寸(对齐 .btn-lg),覆盖各页里 btn / btn-sm / btn-lg 混用 */
|
||||
.page-head .actions > .btn,
|
||||
.page-head .actions > a.btn,
|
||||
.page-head .actions > button.btn {
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.page-head .actions > .btn svg { width: 14px; height: 14px; }
|
||||
|
||||
.section-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
|
||||
.section-h h2 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; color: var(--accent-black); }
|
||||
@ -1415,11 +1424,22 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
background: var(--background-lighter);
|
||||
}
|
||||
|
||||
/* ─── Scrollbar ─── */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--black-alpha-12); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--black-alpha-24); }
|
||||
/* ─── Scrollbar · 全局隐藏可视滚动条,保留滚动能力
|
||||
用 !important 覆盖各页 inline <style> 里残留的 scrollbar-width:thin / 自定义 webkit 滚动条 ─── */
|
||||
* { scrollbar-width: none !important; -ms-overflow-style: none !important; }
|
||||
*::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-scrollbar-track,
|
||||
*::-webkit-scrollbar-thumb,
|
||||
*::-webkit-scrollbar-thumb:hover,
|
||||
*::-webkit-scrollbar-corner {
|
||||
background: transparent !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ─── Responsive ─── */
|
||||
@media (max-width: 1100px) {
|
||||
@ -1456,10 +1476,29 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
letter-spacing: .03em;
|
||||
border-radius: var(--r-sm);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.12);
|
||||
pointer-events: none;
|
||||
cursor: help;
|
||||
user-select: none;
|
||||
transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.tri-missing-badge::before {
|
||||
.tri-missing-badge[hidden] { display: none; }
|
||||
.tri-missing-badge:hover {
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,.18);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.tri-missing-badge .ico {
|
||||
width: 12px; height: 12px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
font-size: 9px; font-weight: 700;
|
||||
line-height: 1;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.tri-missing-badge .ico::before { content: '!'; }
|
||||
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
|
||||
|
||||
/* 兼容旧 markup(没有 .ico 子元素的也保留 ! 圆点) */
|
||||
.tri-missing-badge:not(:has(.ico))::before {
|
||||
content: '!';
|
||||
width: 12px; height: 12px;
|
||||
border: 1px solid currentColor;
|
||||
@ -1468,4 +1507,85 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
font-size: 9px; font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.tri-missing-badge .lbl-mono { font-family: var(--font-mono); letter-spacing: .04em; }
|
||||
|
||||
/* hover popover · V2.1 dark tooltip with arrow */
|
||||
.tri-missing-pop {
|
||||
position: absolute;
|
||||
top: calc(100% + 9px);
|
||||
left: 0;
|
||||
width: 252px;
|
||||
padding: 11px 13px 12px;
|
||||
background: var(--accent-black);
|
||||
color: var(--accent-white);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.20), 0 2px 6px rgba(0,0,0,.10);
|
||||
opacity: 0; visibility: hidden;
|
||||
transform: translateY(-4px);
|
||||
transition: opacity .15s ease, visibility .15s ease, transform .15s ease;
|
||||
pointer-events: none;
|
||||
z-index: 30;
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
}
|
||||
.tri-missing-pop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px; left: 14px;
|
||||
width: 10px; height: 10px;
|
||||
background: var(--accent-black);
|
||||
transform: rotate(45deg);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.tri-missing-pop .pop-h {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: .04em;
|
||||
color: var(--heat);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.tri-missing-pop .pop-h svg { display: block; }
|
||||
.tri-missing-pop .pop-body {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255,255,255,.88);
|
||||
}
|
||||
.tri-missing-pop .pop-tip {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,.12);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
letter-spacing: .02em;
|
||||
color: rgba(255,255,255,.78);
|
||||
}
|
||||
.tri-missing-pop .pop-tip b {
|
||||
color: var(--accent-white);
|
||||
font-weight: 600;
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
.tri-missing-badge:hover .tri-missing-pop,
|
||||
.tri-missing-badge:focus-within .tri-missing-pop {
|
||||
opacity: 1; visibility: visible; transform: translateY(0);
|
||||
transition-delay: .12s;
|
||||
}
|
||||
|
||||
/* 让 popover 能溢出 thumb / 卡片 容器(否则被 overflow:hidden 裁切) */
|
||||
.placeholder:has(.tri-missing-badge:hover),
|
||||
.placeholder:has(.tri-missing-badge:focus-within) {
|
||||
overflow: visible;
|
||||
z-index: 4;
|
||||
}
|
||||
.product-card:has(.tri-missing-badge:hover),
|
||||
.product-card:has(.tri-missing-badge:focus-within),
|
||||
.asset-card:has(.tri-missing-badge:hover),
|
||||
.asset-card:has(.tri-missing-badge:focus-within) {
|
||||
overflow: visible;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ const NAV = [
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html', badge: '12',
|
||||
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
|
||||
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>'
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
712
电商AI平台/design.md
Normal file
712
电商AI平台/design.md
Normal file
@ -0,0 +1,712 @@
|
||||
# 流·Studio 设计规范 · design.md
|
||||
|
||||
> **唯一权威 source of truth** · 所有页面调整必须遵循本文件。
|
||||
> **代号:** Restraint(克制)· V2.1 · Firecrawl-aligned
|
||||
> **维护日期:** 2026-05-22
|
||||
> **配套实现:** [assets/restraint.css](assets/restraint.css) (token + 100+ 组件类 · 1592 行)
|
||||
> **可视样板间(归档):** [_archive/design-system.html](_archive/design-system.html)
|
||||
> **历史规范(归档):** [_archive/DESIGN_SPEC_V2.md](_archive/DESIGN_SPEC_V2.md)
|
||||
|
||||
---
|
||||
|
||||
## §0 · AI 协作铁律(每次启动必读)
|
||||
|
||||
**Claude / 任何 AI 在做页面或 CSS 调整前,必须执行以下流程:**
|
||||
|
||||
1. **先 Read 本文件** · 至少读 §1 设计哲学 + §3 组件清单 + §8 Don't List
|
||||
2. **检查 restraint.css 是否已有该组件** · 用 `Grep "\.btn|\.pill|\.input" assets/restraint.css` 查现成类名
|
||||
3. **禁止在页面 inline `<style>` 重写 restraint.css 已有的共享类**(`.btn` `.pill` `.input` `.modal` `.drawer` `.toast` `.field` `.tabs` `.chip` 等)— 要变体先回 restraint.css 加
|
||||
4. **禁止创建新色值** · 颜色必须用 §2.1 的 token,不能写裸 hex
|
||||
5. **禁止动 token 数值** · 不要改 `--heat` `--background-base` `--border-faint` 等基础变量,改了破坏全站
|
||||
6. **完成后对照 §8 Don't List 自检**
|
||||
7. **不确定就问用户**,而不是凭感觉发挥
|
||||
|
||||
**违反任何一条,用户有权要求重做。**
|
||||
|
||||
---
|
||||
|
||||
## §1 · 设计哲学
|
||||
|
||||
**一句话:** 一台精密设备的工作面板。
|
||||
|
||||
**三条铁律:**
|
||||
1. **克制大于装饰** · 留白 > 容器 > 内容,大量空气感
|
||||
2. **单色锚点** · 全场只有一个 accent(橙),只用于 CTA / 关键状态 / 强调单词
|
||||
3. **结构清晰可见** · 用 1 px 边框 + 8 px 圆角 + 准星 + 装订线 + mono 标签暴露"图纸感",而非阴影/渐变隐藏结构
|
||||
|
||||
**避免的 "AI 味":** 渐变铺面 / 玻璃拟态 / 彩色阴影 / emoji 图标 / 圆角无差别 / 卡片"贴纸感" / 装饰盖过内容。
|
||||
|
||||
**核心签名(4 个不能丢):**
|
||||
- 主橙 `#fa5d19`(Firecrawl 实测)+ 单 hue alpha 阶梯
|
||||
- 全场 8 px 圆角(Pill 999 例外)
|
||||
- 主容器左右装订线 + 四角准星
|
||||
- Mono 装饰 `[ 200 OK ]` `// 05.14` `[ /v2 ]`(品牌签名 · Firecrawl 没有)
|
||||
|
||||
---
|
||||
|
||||
## §2 · 全局 Token
|
||||
|
||||
### §2.1 色彩 · 你 90% 时间用这 14 个
|
||||
|
||||
| Token | Hex / Value | 用途 |
|
||||
| ----- | ----------- | ---- |
|
||||
| `--background-base` | `#f9f9f9` | 页面底色 · 冷灰无色相 |
|
||||
| `--background-lighter` | `#fbfbfb` | 容器底 / hover 浅底(部分场景) |
|
||||
| `--surface` | `#ffffff` | 卡片 / 主容器表面 |
|
||||
| `--surface-raised` | `#ffffff` | Modal / 浮层 |
|
||||
| `--border-faint` | `#ededed` | **默认 1 px 边框 ★ 80% 场景** |
|
||||
| `--border-muted` | `#e8e8e8` | 略深(主分隔线) |
|
||||
| `--border-loud` | `#e6e6e6` | 最深(强分隔) |
|
||||
| `--accent-black` | `#262626` | **主前景色 · 文字** |
|
||||
| `--heat` | `#fa5d19` | **主橙 100% · CTA / 链接 ★** |
|
||||
| `--heat-12` | `rgba(250,93,25,.12)` | tint 底 · active nav / icon-box |
|
||||
| `--heat-20` | `rgba(250,93,25,.20)` | pill 边框 / selection 底色 |
|
||||
| `--heat-40` | `rgba(250,93,25,.40)` | focus ring / disabled CTA |
|
||||
| `--accent-forest` | `#42c366` | 绿 · success / 已完成 |
|
||||
| `--accent-crimson` | `#eb3424` | 红 · error / 失败 |
|
||||
| `--accent-honey` | `#ecb730` | 黄 · warning |
|
||||
|
||||
### §2.2 Black-Alpha 阶梯(20 档 · 核心工具尺)
|
||||
|
||||
> **规则:** 0–24% 用 `rgba(0,0,0,...)`;32% 起换 `rgba(38,38,38,...)`(避免叠出"灰中带蓝")。
|
||||
|
||||
最常用 8 档:
|
||||
|
||||
| Token | 用途 |
|
||||
| ----- | ---- |
|
||||
| `--black-alpha-4` | **hover 底色 ★** |
|
||||
| `--black-alpha-7` | **active 底色 ★** |
|
||||
| `--black-alpha-12` | **inside-border / disabled fg ★** |
|
||||
| `--black-alpha-24` | input hover 边框 |
|
||||
| `--black-alpha-48` | **占位字色 ★** |
|
||||
| `--black-alpha-56` | **次级文字 / 未选中 Tab ★** |
|
||||
| `--black-alpha-64` | 描述文字 |
|
||||
| `--black-alpha-88` | 近主前景 |
|
||||
|
||||
完整 20 档清单见 [assets/restraint.css](assets/restraint.css) §root。
|
||||
|
||||
### §2.3 状态色配套(底 / 边)
|
||||
|
||||
| 含义 | 主色 | 配套底色 8% | 配套边框 20% |
|
||||
| ---- | ---- | ----------- | ------------ |
|
||||
| 成功 | `--accent-forest` `#42c366` | `--forest-bg` | `--forest-bd` |
|
||||
| 失败 | `--accent-crimson` `#eb3424` | `--crimson-bg` | `--crimson-bd` |
|
||||
| 信息 | `--heat` `#fa5d19` | `--heat-12` | `--heat-20` |
|
||||
| 警告 | `--accent-honey` `#ecb730` | `--honey-bg` | `--honey-bd` |
|
||||
|
||||
### §2.4 Selection · Firecrawl 签名细节
|
||||
|
||||
```css
|
||||
::selection { background: var(--heat-20); color: var(--heat); }
|
||||
```
|
||||
|
||||
任何文字选中时,底色 20% 橙 + 文字 100% 橙。已全局生效 · 不要覆盖。
|
||||
|
||||
### §2.5 字体族 · 中英协作
|
||||
|
||||
| 用途 | 字体声明 | 加载方式 |
|
||||
| ---- | -------- | -------- |
|
||||
| 正文 / UI · `--font-sans` | `'Inter', 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif` | Inter Google Fonts + 普惠体 CDN |
|
||||
| 强制纯英 · `--font-inter` | `'Inter', system-ui, sans-serif` | 用于 Ctrl K 等英文徽标 |
|
||||
| Mono 装饰 · `--font-mono` | `'JetBrains Mono', 'Geist Mono', ui-monospace, 'Alibaba PuHuiTi', 'PingFang SC', 'Microsoft YaHei', monospace` | JetBrains Google Fonts |
|
||||
|
||||
**字符级 fallthrough 原理:** 浏览器对每个字符逐个查找,Inter 不含 CJK → 中文自动跳到普惠体。**不需要 JS,不会字重错位。**
|
||||
|
||||
**字重档位仅 3 档:** `400 / 500 / 600`。700 仅用于 `Ctrl K` 这种纯英文徽标。
|
||||
|
||||
### §2.6 字号 / 字重 / 行高(11 档)
|
||||
|
||||
| 角色 | 字号 | 字重 | 字距 | 行高 | 用途 |
|
||||
| ---- | ---- | ---- | ---- | ---- | ---- |
|
||||
| H1 / Hero | 36 px | 500 | -0.024em | 1.2 | 页面主标题 |
|
||||
| 区块 H2 | 28 px | 500 | -0.02em | 1.25 | section-head |
|
||||
| KPI 数值 | 32 px | 500 | -0.02em | 1.1 | 统计大数(`tabular-nums`) |
|
||||
| 子区 H3 | 16 px | 500 | -0.01em | 1.4 | subsection |
|
||||
| 卡片标题 | 14 px | 500 | normal | 1.4 | 项目名 / 商品名 |
|
||||
| 正文 body | 14 px | 400 | normal | **1.65** | 默认正文 |
|
||||
| 描述次级 | 13 px | 400 | normal | 1.8 | 子区说明 |
|
||||
| Label / Tab | 13 px | 500 | normal | 1.4 | 按钮文字 / Tab |
|
||||
| Pill | 11.5 px | 500 | normal | 1.3 | 状态徽标 |
|
||||
| Inter Bold 徽标 | 11.5 px | 700 | 0.02em | 1 | Ctrl K / ESC(`--font-inter`) |
|
||||
| Mono 标签 | 11 px | 400 | 0.04em | 1.5 | `[ STATUS ]` `// 注释` |
|
||||
| Mono 散点 | 8.5 px | 400 | 0.04em | 1 | 背景 ASCII 装饰 |
|
||||
|
||||
**数值类必加** `font-variant-numeric: tabular-nums`(或加 `.num` 工具类)。
|
||||
**正文中文行高 1.65–1.8**(给中文留呼吸)。
|
||||
|
||||
### §2.7 圆角
|
||||
|
||||
| 元素 | 值 | Token |
|
||||
| ---- | -- | ----- |
|
||||
| 所有结构性容器 / 按钮 / 输入框 / 缩略图 | **8 px** | `--r-md` |
|
||||
| 头像 / 小色块 | 6 px | — |
|
||||
| Mono 标签 / kbd / badge | 4 px | `--r-sm` |
|
||||
| 进度条段位 | 2 px | — |
|
||||
| Pill / dot | 999 px | `--r-pill` |
|
||||
|
||||
**默认是 8 px。** 不确定就选 8。`>12 px` 直接判错。
|
||||
|
||||
### §2.8 间距(全站统一)
|
||||
|
||||
**基础栅格:** 4 px。
|
||||
**阶梯:** `4 / 8 / 12 / 16 / 20 / 24 / 28 / 32 / 40 / 48 / 64 / 72 / 80 / 104`。
|
||||
|
||||
| 场景 | 值 |
|
||||
| ---- | -- |
|
||||
| Sidebar 宽度 | `248 px` |
|
||||
| Topbar 高度 | `64 px` |
|
||||
| Content padding | `48 28 72` |
|
||||
| 卡片内 padding(大) | `28 30` / `24 28` |
|
||||
| 卡片内 padding(列表行) | `14 18` ~ `20 24` |
|
||||
| KPI cell padding | `24 28` |
|
||||
| 主区块间(section margin) | `36–48 px`(子)/ `64–104 px`(主) |
|
||||
| 子区标题底距 | `mb 14`(h2)/ `mb 22-28`(h1) |
|
||||
| 卡片网格 gap | `14`(子区)/ `24`(主区) |
|
||||
| Modal padding | `head 24 28 / body 24 28 / foot 18 28` |
|
||||
|
||||
**最重要的一条:** 别再吝啬空气。**当不确定 padding 是 16 还是 24 时,选 24。** 当不确定 margin 是 48 还是 64 时,选 64。
|
||||
|
||||
### §2.9 阴影(只 3 个允许场景)
|
||||
|
||||
| 场景 | 阴影 |
|
||||
| ---- | ---- |
|
||||
| 主 CTA(`.btn-primary`) | 4 层橙色阴影 `--shadow-cta` |
|
||||
| 主 CTA hover | 阴影抬升 `--shadow-cta-hover` |
|
||||
| Toast / Dropdown 浮层 | 白色 `0 4px 20px rgba(21,20,15,.06)` = `--shadow-floating` |
|
||||
|
||||
**禁:** 灰色阴影 / 文字阴影 / 通用 box-shadow。
|
||||
|
||||
### §2.10 边框策略 · inside-border
|
||||
|
||||
默认边框用 `::before` 伪元素绘制(而非真 `border`),hover 时让 `::before` 透明度 → 0,不触发布局抖动。
|
||||
|
||||
```css
|
||||
.inside-border { position: relative; }
|
||||
.inside-border::before {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
border: 1px solid var(--black-alpha-12);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s, border-color .2s;
|
||||
}
|
||||
.inside-border:hover::before { opacity: 0; }
|
||||
```
|
||||
|
||||
**禁:** 2 px / 3 px 实线 / 真 `border` + hover 边框消失。
|
||||
**例外:** `.tip` 提示框允许 `1 px dashed`。
|
||||
|
||||
---
|
||||
|
||||
## §3 · 页面骨架(全站统一)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ <div class="app"> │
|
||||
│ ┌────────┬──────────────────────────────┐ │
|
||||
│ │ │ <div class="topbar"> │ │
|
||||
│ │ aside │ crumbs · right(chips) │ │
|
||||
│ │ side │ </div> │ │
|
||||
│ │ bar │ ┌────────────────────────┐ │ │
|
||||
│ │ 248px │ │ <div class="content"> │ │ │
|
||||
│ │ │ │ <div class="page-head"> │ │
|
||||
│ │ │ │ <div class="section-h"> │ │
|
||||
│ │ │ │ ... │ │ │
|
||||
│ │ │ │ </div> │ │ │
|
||||
│ │ │ └────────────────────────┘ │ │
|
||||
│ └────────┴──────────────────────────────┘ │
|
||||
│ </div> │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### §3.1 入口约束
|
||||
|
||||
每个新页面 `<head>`:
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css">
|
||||
```
|
||||
|
||||
每个新页面 `<body>` 末尾:
|
||||
```html
|
||||
<script src="assets/shell.js" defer></script>
|
||||
```
|
||||
|
||||
`shell.js` 自动注入 sidebar + topbar,**页面只写 `<main>` 内的 `.content` 内容**。
|
||||
|
||||
### §3.2 标题区(必有)
|
||||
|
||||
```html
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>商品库</h1>
|
||||
<div class="sub">
|
||||
<span class="mono">// 12 个商品</span> · 你的所有商品和卖点
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn">导入</button>
|
||||
<button class="btn btn-primary">+ 新建商品</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**page-head 内主操作按钮强制 40 px 高**(`.page-head .actions > .btn { height: 40px }` 已全局覆盖)。
|
||||
|
||||
### §3.3 子区块(可选)
|
||||
|
||||
```html
|
||||
<div class="section-h">
|
||||
<h2>最近项目</h2>
|
||||
<a class="more">[ ALL · 12 ] →</a>
|
||||
</div>
|
||||
<!-- 内容 -->
|
||||
```
|
||||
|
||||
`.more` mono 11.5 / `--black-alpha-48` / hover 转橙。
|
||||
|
||||
### §3.4 主容器装订线(签名元素 · 主工作台型必有)
|
||||
|
||||
工作台 / 项目列表 / 商品库 / 流水线主区:左右两条 1 px 边线 + 4 角圆弧内凹 SVG 准星。Modal / 编辑器全屏画布不必加。
|
||||
|
||||
```css
|
||||
.workbench-container {
|
||||
max-width: 1480px;
|
||||
margin: 0 auto;
|
||||
border-left: 1px solid var(--border-faint);
|
||||
border-right: 1px solid var(--border-faint);
|
||||
position: relative;
|
||||
}
|
||||
/* 4 角 SVG · 用 .with-corners + .corner-tr/.corner-bl */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §4 · 组件清单(restraint.css 已实现 · 不要重发明)
|
||||
|
||||
### §4.1 按钮族 · 三级体系(按功能重要性区分)
|
||||
|
||||
> **核心原则:** 按钮的视觉强度 = 操作的重要程度。一个区域**只能有一个**一级按钮(像航海图上只能有一个北极星)。视觉等级混乱比按钮不好看更致命。
|
||||
|
||||
#### 三级体系
|
||||
|
||||
| 等级 | 类名 | 视觉 | 语义 | 数量约束 |
|
||||
| ---- | ---- | ---- | ---- | -------- |
|
||||
| **一级 · Primary** | `.btn .btn-primary` | 橙底 + 4 层橙阴影 + 字重 600 | **页面/弹窗的最重要动作** · 用户主动作 | **一个区域只能有一个** ★ |
|
||||
| **二级 · Secondary** | `.btn` 默认 | 白底 + inside-border + 主前景字 | 跟主操作**并列**的重要动作 | 可以有多个 |
|
||||
| **三级 · Tertiary** | `.btn .btn-ghost` | 透明底 + 无框 + alpha-56 字 | **辅助/链接式**的弱化操作 | 不限 |
|
||||
|
||||
#### 选谁?决策表
|
||||
|
||||
| 操作类型 | 等级 | 例子 |
|
||||
| -------- | ---- | ---- |
|
||||
| 提交 / 确认 / 创建 / 下一步 / 保存 / 生成 | **一级** | "创建商品" "确认删除" "开始生成" |
|
||||
| 取消 / 重置 / 导出 / 导入 / 上一步 / 关闭 | **二级** | "取消" "重置" "导出 CSV" |
|
||||
| 跳过 / 了解更多 / 查看详情 / 折叠 / 链接式 | **三级** | "跳过" "了解更多 →" "[查看] [编辑] [删除]" 行内动作 |
|
||||
| 破坏性(删除/危险) | **二级**(在 modal 里)或**一级**(确认按钮) | modal 里:取消[二级] + 确认删除[一级] |
|
||||
|
||||
#### 典型组合
|
||||
|
||||
| 场景 | 组合 |
|
||||
| ---- | ---- |
|
||||
| `.page-head .actions`(页面标题区) | [二级] + [一级] · 例:[导入] [+ 新建商品] |
|
||||
| `.modal-f`(弹窗底栏) | [二级取消] + [一级确认] · 永远主操作在右 |
|
||||
| 卡片行内操作 | [三级] × N · 例:[查看] [编辑] [删除] |
|
||||
| 表单底部 | [三级上一步] + spacer + [二级保存草稿] + [一级提交] |
|
||||
| 空状态 CTA | [一级] · 唯一一个 |
|
||||
|
||||
#### 尺寸阶梯(独立于等级,按上下文密度选)
|
||||
|
||||
| 类名 | 高度 | padding | 字号 | 用法 |
|
||||
| ---- | ---- | ------- | ---- | ---- |
|
||||
| `.btn-sm` | 28 px | `0 12` | 12 | 行内 / 列表行末 / 表格内 / 紧凑工具栏 |
|
||||
| 默认 | 36 px | `0 16` | 13 | **标准 ★** · 通用场景 |
|
||||
| `.btn-lg` | 40 px | `0 20` | 13.5 | hero CTA / page-head 主操作(已全局覆盖) |
|
||||
| `.icon-btn` 方形 | 36×36 | — | — | 纯 icon(无文字时) |
|
||||
|
||||
> **等级 × 尺寸** 可自由组合 — 一级可以是 sm/默认/lg,二级三级同理。
|
||||
|
||||
#### 5 种状态
|
||||
|
||||
| 状态 | 一级表现 | 二级表现 | 三级表现 |
|
||||
| ---- | -------- | -------- | -------- |
|
||||
| Default | 橙底 + 4 层橙阴影 | 白底 + inside-border `--black-alpha-12` | 透明 + 字 `--black-alpha-56` |
|
||||
| Hover | 阴影抬升(第 3 层加亮) | 底 `--black-alpha-4` + 边 `--black-alpha-24` | 底 `--black-alpha-4` + 字转 `--accent-black` |
|
||||
| Active(按下) | `scale(.995)` + 阴影 inset 加深 | 底 `--black-alpha-7` + `scale(.99)` | 底 `--black-alpha-7` |
|
||||
| Focused | 2 px `--heat-40` ring · offset 2 | 同左 | 同左 |
|
||||
| Disabled | 底 `--heat-40` + 字 `#fff` + 阴影消失 + `not-allowed` | 底 `--black-alpha-5` + 字 `--black-alpha-32` + 边 `--black-alpha-12` | 字 `--black-alpha-32` |
|
||||
|
||||
**所有按钮过渡:** `background 200ms, transform 100ms ease`。
|
||||
|
||||
#### 常见错误
|
||||
|
||||
- ❌ **一个区域两个一级按钮** — 用户视线分散 → 改为 [二级] + [一级]
|
||||
- ❌ **危险操作用一级橙色** — 改为放在 modal 里二次确认,modal 内的"确认删除"用一级
|
||||
- ❌ **重要操作用三级 ghost** — 弱化了主流程,改用一级或二级
|
||||
- ❌ **三级按钮加边框** — 三级就是 ghost,加边框就变二级了
|
||||
- ❌ **页面 inline 调高度/字号** — 用 `.btn-sm` / `.btn-lg`,不要 inline 改
|
||||
|
||||
### §4.2 Pill(状态徽标 · 严格 3 级分层)
|
||||
|
||||
> **同级别尺寸必须完全一致。** 不允许混用。
|
||||
|
||||
| 级别 | 类名 | 高度 | padding | 字号 | dot | 用途 |
|
||||
| ---- | ---- | ---- | ------- | ---- | --- | ---- |
|
||||
| L1 大胶囊 | `.pill-l1` | 28 px | `0 12` | 13 | 8 px | 项目状态 / 列表行主标签 |
|
||||
| **L2 中胶囊 ★ 默认** | `.pill-l2`(或不写) | 22 px | `0 10` | 11.5 | 6 px | 卡片内 / 表格内默认 |
|
||||
| L3 小胶囊 | `.pill-l3` | 18 px | `0 8` | 10.5 | 5 px | KPI 角标 / 行内 mono 标签 |
|
||||
|
||||
**4 种色调:**
|
||||
|
||||
| 类名 | 文字色 | 底色 | 边框 | 含义 |
|
||||
| ---- | ------ | ---- | ---- | ---- |
|
||||
| `.pill.info` | `--heat` | `--heat-12` | `--heat-20` | 信息 / 进行中 / 默认强调 |
|
||||
| `.pill.ok` | `--accent-forest` | `--forest-bg` | `--forest-bd` | 成功 / 已完成 |
|
||||
| `.pill.err` | `--accent-crimson` | `--crimson-bg` | `--crimson-bd` | 失败 / 错误 |
|
||||
| `.pill.neutral` | `--black-alpha-56` | `--black-alpha-4` | `--border-faint` | 中性 / 已归档 |
|
||||
|
||||
```html
|
||||
<span class="pill pill-l2 pill-info"><span class="dot"></span>生成中</span>
|
||||
```
|
||||
|
||||
每个 pill 前置 `<span class="dot"></span>`,色继承 `currentColor`。
|
||||
|
||||
### §4.3 输入族
|
||||
|
||||
| 类名 | 高度 | 用法 |
|
||||
| ---- | ---- | ---- |
|
||||
| `.input` | 36 px | 单行文本框 |
|
||||
| `.textarea` | 自动 | 多行 · min 88 px / line-height 1.6 / resize vertical |
|
||||
| `.select` | 36 px | 下拉 · 带 chevron 背景图 |
|
||||
| `.field` | flex col / gap 6 | 包 label + input + hint 的容器 |
|
||||
| `.field-label` | 13 / 500 | 字段标签(必填用 `<span class="req">*</span>`) |
|
||||
| `.field-hint` | 12 / `--black-alpha-48` | 字段提示 |
|
||||
| `.search-box`(sidebar) | 36 px | 顶部全局搜索 + Ctrl K |
|
||||
| `.toolbar .search-inline` | 36 px | 列表页工具栏内嵌搜索(左侧 icon 必加 `z-index: 2`) |
|
||||
|
||||
**5 种状态:**
|
||||
|
||||
| 状态 | 边框 | 底色 |
|
||||
| ---- | ---- | ---- |
|
||||
| Default | `--black-alpha-12` | `--surface` |
|
||||
| Hover | `--black-alpha-24` | `--surface` |
|
||||
| Focus | `--heat-40` + `inset 0 0 0 1px --heat-40` | `--surface` |
|
||||
| Error | `--accent-crimson` | `--crimson-bg` |
|
||||
| Disabled | `--black-alpha-12` | `--black-alpha-5` + 字 `--black-alpha-32` |
|
||||
|
||||
**占位字色:** `--black-alpha-48`,disabled 占位 `--black-alpha-24`。
|
||||
|
||||
**带 Ctrl K 搜索框关键坑:**
|
||||
1. 左侧 icon 必须 `z-index: 2`(否则被 input 白底盖住)
|
||||
2. 用 "Ctrl K" 纯文本,**不要用 `⌘` 字符**(JetBrains Mono 不带该字形,显示成方框)
|
||||
3. 快捷键提示不要 kbd 边框,**纯灰色 mono 平铺**(克制)
|
||||
4. 字体用 `var(--font-inter) / 700`(Inter Bold 紧凑感)
|
||||
|
||||
### §4.4 表单控件(Checkbox / Radio / Switch)
|
||||
|
||||
**通用原则:** 全部用真 SVG indicator(via background-image data URI 或 inline `<svg>`),**禁止用 border-width / transform: rotate(45deg) 凑对勾**。
|
||||
|
||||
| 控件 | 容器尺寸 | Checked 表现 |
|
||||
| ---- | -------- | ------------ |
|
||||
| Checkbox `.ck` | 16×16 / 4 px 圆角 / 边 `--black-alpha-24` | `--heat` 底 + 12×12 白 SVG checkmark |
|
||||
| Indeterminate | 同上 | `--heat` 底 + 12×12 白 SVG 横线 |
|
||||
| Radio `.rd` | 16×16 圆 / 边 `--black-alpha-24` | 8×8 `--heat` 实心圆(`::after` 即可) |
|
||||
| Switch `.sw` | 28×16 / 999 圆角 | On: `--heat` 底,圆球右移 `left: 14px` |
|
||||
|
||||
Disabled: 底 `--black-alpha-5` + 边 `--black-alpha-12` + 半透明。
|
||||
|
||||
### §4.5 KPI 统计行(`.stats`)
|
||||
|
||||
```html
|
||||
<div class="stats with-corners">
|
||||
<div class="stat">
|
||||
<div class="lbl">本月营收 <span class="pill pill-l3 pill-ok"><span class="dot"></span>+33%</span></div>
|
||||
<div class="v">¥327<small>.40 K</small></div>
|
||||
<div class="delta up">↑ 较上月 +33%</div>
|
||||
</div>
|
||||
<!-- ×4 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
- 1 行 4 格 grid,共用一个 inside-border 容器,圆角 8 px,**无 gap**
|
||||
- 列与列之间用 `border-right: 1px solid var(--border-faint)`(最后一列去掉)
|
||||
- 容器四角加 SVG "+" 准星(`.with-corners`)
|
||||
- 每格结构:label + L3 pill → 大数字(32 px) → delta / mini progress
|
||||
|
||||
### §4.6 进度条(5 段流水线)
|
||||
|
||||
```html
|
||||
<div class="prog">
|
||||
<span class="done"></span><span class="done"></span>
|
||||
<span class="cur"></span><!-- 或 .now -->
|
||||
<span></span><span></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
每段 18×5 px / 3 px 间距 / 2 px 圆角。
|
||||
|
||||
| 状态 | 颜色 | 动画 |
|
||||
| ---- | ---- | ---- |
|
||||
| 未开始 | `--black-alpha-8` | 静态 |
|
||||
| 已完成 `.done` | `--accent-forest` | 静态 |
|
||||
| 进行中 `.cur` 或 `.now` | `--heat` | **1.4s 脉动** opacity 1↔0.55 + scaleY 1↔0.7 |
|
||||
| 失败 `.fail` / `.err` | `--accent-crimson` | 静态 |
|
||||
|
||||
**核心:** 动 = 在运行,静 = 完成/失败。脉动只给"进行中"。
|
||||
|
||||
### §4.7 列表行(`.list-row`)
|
||||
|
||||
```
|
||||
[缩略图 56×72] [标题 + meta] [.prog 5 段] [.pill L2] [.btn-sm]
|
||||
```
|
||||
|
||||
- grid: `56px 1fr auto auto auto` / gap 22
|
||||
- padding: `20 24`
|
||||
- 行间 1 px 分隔(`--border-faint`),最后行去
|
||||
- Hover: `--black-alpha-4` 底色
|
||||
|
||||
### §4.8 Tabs(主 / 副)
|
||||
|
||||
**主 Tab(下划线激活):**
|
||||
- 高 36 px / padding `0 14` / 字号 13 / 字重 500
|
||||
- 未选中: `--black-alpha-56`
|
||||
- 选中: `--accent-black` + bottom 2 px `--heat` 横线
|
||||
- Hover(未选中): `--black-alpha-4` 底
|
||||
|
||||
**副 Tab(过滤型):**
|
||||
- 高 28 px / padding `0 10` / 字号 12
|
||||
- 未选中: `--black-alpha-56` + 灰度 icon
|
||||
- 选中: `--accent-black` + 彩色 icon
|
||||
- Tab 之间用 1 px 高 12 的 `--ink-alpha-7` 竖条分隔
|
||||
|
||||
### §4.9 Chip 筛选(下拉)
|
||||
|
||||
```html
|
||||
<div class="chip-wrap">
|
||||
<button class="chip"><span>商品品类</span><svg class="caret"></svg></button>
|
||||
<div class="chip-menu"><!-- .mi 选项 · 多选含 .mi-check --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- `.chip` 高 36 / padding `0 14` / 字号 13 / 圆角 8
|
||||
- Active: 边 `--heat-40` + 字 `--heat` + 底 `--heat-12`
|
||||
- 打开后菜单 `.chip-wrap.open .chip-menu { display: block }`
|
||||
- 选项 `.mi` 高 32 / hover `--black-alpha-4` / selected `--heat-12` + 右侧 `--heat` 勾
|
||||
|
||||
### §4.10 卡片 / 快捷入口
|
||||
|
||||
| 类名 | 描述 |
|
||||
| ---- | ---- |
|
||||
| `.card-hard` | 通用硬卡 · 白底 + inside-border + 8 px 圆角 |
|
||||
| `.card-hard.with-corners` | 加四角 SVG 准星 |
|
||||
| `.shortcut` | 快捷入口 · 左 32×32 橙 icon-box + 右标题 + mono 描述 |
|
||||
| `.tip` | 提示框 · **1 px dashed** 边框 + 加粗标题 + 正文 |
|
||||
| `.placeholder` | 缩略图占位 · 灰底网纹 · 内置 `.ph-frame` 显示 `9:16` |
|
||||
| `.product-card / .proj-card / .asset-card` | 各类卡片(模板见现有页面) |
|
||||
|
||||
`.icon-box`:32×32 / 8 px 圆角 / `--heat-12` 底 / 16 px line icon(`--heat`)。
|
||||
|
||||
### §4.11 浮层 / 反馈
|
||||
|
||||
| 组件 | 类名 | 关键参数 |
|
||||
| ---- | ---- | -------- |
|
||||
| Modal | `.modal-bg > .modal > .modal-h/b/f` | 居中 460-480 px · 4 角 SVG 准星 · `rgba(21,20,15,.42)` 遮罩 + `backdrop-filter: blur(8px)` · 进入 `scale(.96→1)` / 250ms 弹性 · ESC / 点击遮罩关闭 |
|
||||
| Drawer | `.drawer-bg > .drawer > .drawer-h/b/f` | 右侧抽屉 · 默认 540 px(商品 drawer 820 px) · 进入 `translateX(100%→0)` / 250 ms |
|
||||
| Toast | `.toast`(`.show` 触发) | 右下 24 px / 白底 inside-border + `--shadow-floating` / 进入 `translateX(420→0)` 300ms 弹性 / 自动 2400 ms 消失 / 内含 24×24 橙 icon-box + 标题 + mono 副文 |
|
||||
| Empty | `.empty-state.show` | 虚线边框 + 灰 icon + 标题(14/600)+ mono 描述 |
|
||||
| 缺图徽标 | `.tri-missing-badge` + `.tri-missing-pop` | 商品/人物卡缩图左上角橙底告警徽标 + hover 暗色 tooltip |
|
||||
| 卡片删除 | `.card-del-btn` | 卡片右上角 hover 显示 · 32×32 / 红 hover |
|
||||
| 批量栏 | `.bulk-bar` | 选中后吸底浮动操作栏 |
|
||||
| 卡片勾选 | `.card-check` | 卡片左上角 22×22 圆形 checkbox(编辑模式 hover 显示) |
|
||||
| Spinner | `.spinner` | 18×18 旋转 · 2 px 橙边 |
|
||||
|
||||
### §4.12 Icon 系统
|
||||
|
||||
**通用规则:**
|
||||
- 一律 **SVG inline**,禁止 `<img>` 引图标
|
||||
- 库:Lucide 或 Phosphor Regular
|
||||
- `stroke-width: 1.5` / `stroke-linecap: round` / `stroke-linejoin: round` / `fill: none`
|
||||
- 颜色:`stroke="currentColor"` 继承
|
||||
- **禁:** 彩色 emoji / filled icon
|
||||
|
||||
**5 档尺寸:**
|
||||
|
||||
| 档 | 尺寸 | 用途 |
|
||||
| -- | ---- | ---- |
|
||||
| S | 14 px | 内嵌 inline 文字旁 |
|
||||
| **M ★ 默认** | 16 px | 按钮内 / Tab / list 行 |
|
||||
| L | 20 px | 顶栏 / 快捷入口 / dropdown 触发器 |
|
||||
| XL | 24 px | Modal 头 / Toast / 空状态 |
|
||||
| Hero | 36 px | 空状态插画 / 大占位 |
|
||||
|
||||
**颜色:** 默认 `--black-alpha-56` / hover `--accent-black` / active `--heat` / disabled `--black-alpha-12` / 在主 CTA 内 `#fff`。
|
||||
|
||||
---
|
||||
|
||||
## §5 · Mono 装饰元素(品牌签名 · 不能丢)
|
||||
|
||||
| 用法 | 示例 |
|
||||
| ---- | ---- |
|
||||
| 方括号标签 | `[ 200 OK ]` `[ /v2 ]` `[ .MP4 · 9:16 ]` `[ STUDIO ]` `[ ALL · 12 ] →` |
|
||||
| 注释时间戳 | `// 05.14 · 周五 · 14:32` |
|
||||
| 命令路径 | `/sidebar collapse · /toast dismiss` |
|
||||
| 数值后缀 | `¥327`(主体大字)+ `<small>.40</small>`(小字次级) |
|
||||
| 强调单词上色 | `<b style="color: var(--accent-black)">3 个项目</b>` — 加深一档,**不变橙**。橙色只留给 CTA |
|
||||
| ASCII 散点装饰 | 主区域 4 角撒 `· · + +XX+ +XXXX·` / 8.5 px / `--black-alpha-12` |
|
||||
| 边角 Mono 标签 | 主区域 4 角:左上 `[ 200 OK ]` / 右上 `[ /v2 ]` / 左下 `[ .MP4 · 9:16 ]` / 右下 `[ STUDIO ]` |
|
||||
| 链接式"更多" | `[ ALL · 12 ] →` — mono 标签 + 箭头,`--black-alpha-48`,hover 转橙 |
|
||||
| 缩略图占位 | 不放假图,放比例 `9:16` mono 字符 |
|
||||
|
||||
**字体:** `var(--font-mono)` / 11 px / `0.04em` 字距(标签场景)。
|
||||
|
||||
---
|
||||
|
||||
## §6 · 五种页面级布局模式
|
||||
|
||||
页面骨架统一,**内容区布局**按用途分 5 类:
|
||||
|
||||
### A · 看板型(Dashboard)
|
||||
**代表页:** [index.html](index.html) · [projects.html](projects.html)(列表)· [library.html](library.html)
|
||||
**布局:** KPI 行(`.stats.with-corners`)→ 多个 section(`.section-h` + 内容卡)→ 列表行 / shortcut 网格
|
||||
**辅助类:** `.dash-grid`(1.7 fr + 1 fr 主辅)/ `.recent-row` / `.shortcut` / `.tip`(虚线提示)
|
||||
|
||||
### B · 列表 + 筛选型(List + Filter)
|
||||
**代表页:** [products.html](products.html) · [projects.html](projects.html) · [library.html](library.html) · [team.html](team.html)
|
||||
**布局:**
|
||||
```
|
||||
[ toolbar(search-inline + chip-wrap × n + view-toggle) ] ← sticky top
|
||||
[ result-meta · 共 N 条 ]
|
||||
[ ░░ 卡片网格 / 列表 ░░ ]
|
||||
[ pagination ] ← sticky bottom
|
||||
[ bulk-bar(选中后显示) ] ← 吸底浮动
|
||||
```
|
||||
**辅助类:** `.product-card / .proj-card / .asset-card` · `.view-toggle` · `.pagination` · `.bulk-bar` · `.card-check` · `.card-del-btn`
|
||||
|
||||
### C · 编辑器型(Editor / IDE)
|
||||
**代表页:** [pipeline.html](pipeline.html) · [model-photo.html](model-photo.html) · [image-optimize.html](image-optimize.html) · [studio.html](studio.html)
|
||||
**特征:** 锁视口高度 · 多栏内部滚 · 大量页面级独有样式(目前 inline `<style>` 600-1400 行)
|
||||
**布局:** 左栏(资产/导航 sticky)+ 中央(画布/参数)+ 右栏(预览/AI 助手)
|
||||
**⚠️ 警告:** 这一类页面定制最多,改之前先确认 restraint.css 没有现成组件再加 inline。
|
||||
|
||||
### D · 表单 / 向导型(Wizard / Form)
|
||||
**代表页:** [projects-new.html](projects-new.html) · [products.html](products.html) drawer · [account.html](account.html) · [settings.html](settings.html)
|
||||
**布局:** 左侧 sticky 步数条 + 右侧多 pane 同时显示(非 Tab 切换)
|
||||
**辅助类:** `.wizard` · `.steps .step` · `.wiz-pane` · `.opt-card` · `.bullet-list`
|
||||
|
||||
### E · 单卡型(Single Screen)
|
||||
**代表页:** [login.html](login.html) · [register.html](register.html)
|
||||
**特征:** 不渲染 sidebar/topbar · 全屏灰底 + 中心白卡
|
||||
|
||||
---
|
||||
|
||||
## §7 · 待统一清单(V2.1 落地偏离点)
|
||||
|
||||
> 扫完 18 个 HTML 发现的偏离点。按影响视觉一致性的程度排序。**改页面时遇到这些点,顺手改正。**
|
||||
|
||||
### 高优先级(影响视觉对齐)
|
||||
|
||||
| # | 问题 | 现状 | 应改为 |
|
||||
| - | ---- | ---- | ------ |
|
||||
| 1 | **按钮等级混乱**(★ 最严重) | 各页随意选 `.btn` / `.btn-primary` / `.btn-ghost`,有的页面 2 个一级按钮并列,有的把"取消"做成主橙;尺寸也乱(38/32/44 混) | 按 §4.1 三级体系:**一个区域只 1 个一级**(`.btn-primary`,主动作)· 二级 `.btn`(并列动作)· 三级 `.btn-ghost`(辅助)· 尺寸默认 36 / lg 40 / sm 28 |
|
||||
| 2 | 输入框高度 | products 38 / projects search 32 / login 36 | 统一 36 |
|
||||
| 3 | Tab 激活样式 | product-detail 下划线 / library 底色填充 | **统一下划线 + bottom 2 px `--heat`** |
|
||||
| 4 | Hover 底色 | 多数 `--background-lighter` / 部分 `--black-alpha-4` | **统一 `--black-alpha-4`** |
|
||||
| 5 | 卡片标题字号 | projects 13.5 / products 14 | **统一 14 px / 500** |
|
||||
| 6 | 卡片网格列宽 | products 240 固定 / library auto-fill 180 / projects-new 4 列固定 | 商品类 240 · 资产类 180 · 通用 `auto-fill minmax(220px, 1fr)` |
|
||||
|
||||
### 中优先级(语义偏离)
|
||||
|
||||
| # | 问题 | 现状 | 应改为 |
|
||||
| - | ---- | ---- | ------ |
|
||||
| 7 | Pill 变体不全 | restraint.css 只 4 种(info/ok/err/neutral),library 用了 `.pill.archive` 没定义 | 在 restraint.css 补 `.pill.archive`(灰)、`.pill.warn`(honey 黄) |
|
||||
| 8 | X 关闭按钮 | drawer 30 / modal 32 / toast inline 28 | **统一 32×32 / 8 px 圆角** |
|
||||
| 9 | label 字重 | products 500 / product-detail 600 | **统一 500** |
|
||||
| 10 | mono 装饰 | 部分页有 4 角 `[ 200 OK ]`,部分没 | **主工作台型必须有,编辑器型可省** |
|
||||
| 11 | section-h 分隔 | 部分用 `border-bottom`,部分不用 | **统一不用**(留白即分隔) |
|
||||
|
||||
### 低优先级(架构清理)
|
||||
|
||||
| # | 问题 | 现状 | 应改为 |
|
||||
| - | ---- | ---- | ------ |
|
||||
| 12 | 编辑器页 inline `<style>` 过大 | pipeline 795 / model-photo 1393 / platform-cover 1003 行 | 抽 `.stepper / .shot-card / .mp-prod-item / .pl-modal` 等高频组件到 restraint.css 或独立 editor.css |
|
||||
| 13 | 四角准星重复 SVG | restraint.css 已有 `.with-corners` 工具,部分页面又自己定义了 SVG 背景 | 全部改用 `.with-corners` + `<span class="corner-tr/.corner-bl">` |
|
||||
| 14 | 滚动条样式残留 | 部分页 inline `scrollbar-width: thin` | restraint.css 已 `!important` 全局隐藏,清理 inline |
|
||||
| 15 | mono 字体里的中文 | 部分页直接写 `font-family: monospace` 走系统字体 | 全部换 `var(--font-mono)` |
|
||||
|
||||
---
|
||||
|
||||
## §8 · Don't List(绝对禁止 · 每次自检)
|
||||
|
||||
- ❌ **0 px 硬切角的卡片** — 一律 8 px
|
||||
- ❌ **大圆角 `>12 px`** — 直接判错
|
||||
- ❌ **渐变背景 / 玻璃拟态** — 只允许 modal 遮罩 `backdrop-filter: blur`
|
||||
- ❌ **彩色 emoji** — 一律 SVG line icon · stroke 1.5
|
||||
- ❌ **多个 accent 色** — 全场只有橙(其他色仅做语义信号点)
|
||||
- ❌ **hover 时换深 hue 的橙** — 用 alpha,不用更深的橙(`#D04E1F` 这种判错)
|
||||
- ❌ **真 `border` + hover 边框消失** — 用 inside-border `::before`
|
||||
- ❌ **居中对齐大段正文** — 全部左对齐
|
||||
- ❌ **同一行混用直角和圆角** — "不要有些是直角,胶囊又是圆角"
|
||||
- ❌ **荧光状态色** — 避免霓虹绿、电光蓝、霓虹粉
|
||||
- ❌ **页面内 `<style>` 重写共享类**(`.btn` `.pill` `.input` `.modal` 等)— 改 restraint.css
|
||||
- ❌ **新建色 token** — 必须复用现有 token
|
||||
- ❌ **`⌘` Unicode 字符** — JetBrains Mono webfont 不带该字形,显示成方框 → 用 "Ctrl K" 纯文本或 SVG
|
||||
- ❌ **多色 emoji icon / filled icon** — 一律 line icon
|
||||
- ❌ **居中卡片浮在背景上的"贴纸感"** — 用边框 + 装订线,不用阴影
|
||||
- ❌ **彩虹流光 / hover 旋转缩放** — 无意义微动效
|
||||
- ❌ **场记板 / 丝绒 / 霓虹灯** — 装饰盖过内容,判错
|
||||
|
||||
---
|
||||
|
||||
## §9 · 新页面 / 改页面 checklist(每次必过)
|
||||
|
||||
### 写代码前
|
||||
- [ ] 已 Read 本文件 §1 § 3 § 8(至少)
|
||||
- [ ] 用 `Grep` 查 restraint.css 是否已有该组件
|
||||
- [ ] 看 `_archive/design-system.html` 找视觉参考(用浏览器打开 file://)
|
||||
- [ ] 不确定的设计点 — **先问用户**,不要凭感觉
|
||||
|
||||
### 写代码时
|
||||
- [ ] HTML 用 `<div class="app"><aside class="sidebar"></aside><main><div class="topbar"></div><div class="content">…</div></main></div>` 骨架
|
||||
- [ ] head 含 `<link rel="stylesheet" href="assets/restraint.css">` + Inter + JetBrains Mono Google Fonts
|
||||
- [ ] body 末尾 `<script src="assets/shell.js" defer></script>`
|
||||
- [ ] 标题区用 `.page-head > h1 + .sub`
|
||||
- [ ] 主操作按钮放 `.page-head > .actions`(自动 40 px 高)
|
||||
- [ ] 子区块用 `.section-h > h2 + .more`
|
||||
- [ ] 按钮全 `.btn` 系列 · 不要自己写
|
||||
- [ ] 状态徽标全 `.pill.info/.ok/.err/.neutral` · 要新变体先回 restraint.css 加
|
||||
- [ ] 输入框全 `.input / .select / .textarea` · 字段用 `.field`
|
||||
- [ ] 浮层全用现成 Modal / Drawer / Toast
|
||||
- [ ] 图标 SVG line icon · stroke 1.5 / linecap round / `stroke="currentColor"`
|
||||
- [ ] 时间戳 mono 注释 `// 05.22 · 周四`
|
||||
- [ ] 强调单词加深(不变橙) `<b style="color:var(--accent-black)">3 个</b>`
|
||||
- [ ] 数字加 `.num` 或 `tabular-nums`
|
||||
- [ ] 状态色按语义选 `--heat / --accent-forest / --accent-crimson / --accent-honey`
|
||||
|
||||
### 写完自检
|
||||
- [ ] 对照 §8 Don't List 逐条过
|
||||
- [ ] 在浏览器打开页面 · 截图 + 跟 design-system.html 对比
|
||||
- [ ] 测 hover / focus / active / disabled 状态都正确
|
||||
- [ ] 测 dark mode(给 body 加 `.dark` class 看是否破)
|
||||
- [ ] 移动端缩到 1100 px 以下看响应式
|
||||
|
||||
### 提交前
|
||||
- [ ] 不在 master/main 上,在 dev 分支
|
||||
- [ ] 不带 `--no-verify`,不跳过 hook
|
||||
- [ ] commit 信息简洁,说"为什么"不只是"什么"
|
||||
|
||||
---
|
||||
|
||||
## §10 · 参考资料
|
||||
|
||||
- [assets/restraint.css](assets/restraint.css) — 共享 CSS · token 定义 + 100+ 组件类(1592 行)· 实现层
|
||||
- [assets/shell.js](assets/shell.js) — 通用 sidebar / topbar 注入
|
||||
- [_archive/design-system.html](_archive/design-system.html) — 历史交互样板间(浏览器打开看效果)
|
||||
- [_archive/DESIGN_SPEC_V2.md](_archive/DESIGN_SPEC_V2.md) — 历史详细规范(理论依据 / 迁移轨迹)
|
||||
- 视觉灵感:Firecrawl Playground · Linear · Stripe Dashboard
|
||||
- 图纸感来源:印刷套版准星 + 老 Unix 终端
|
||||
|
||||
---
|
||||
|
||||
**任何条款不清晰、想加新组件、或想改 token,先在群里讨论 → 写进本文件 → 改 restraint.css。**
|
||||
**永远不要在某个页面 inline `<style>` 里偷偷扩展规范。**
|
||||
1684
电商AI平台/image-optimize.html
Normal file
1684
电商AI平台/image-optimize.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -57,12 +57,12 @@
|
||||
<div class="v">8</div>
|
||||
<div class="delta up"><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 19V5M5 12l7-7 7 7"/></svg> 本月 +3</div>
|
||||
</a>
|
||||
<a class="stat" href="projects.html">
|
||||
<a class="stat" href="projects.html?filter=wip">
|
||||
<div class="lbl">进行中 <span class="badge">WIP</span></div>
|
||||
<div class="v">3</div>
|
||||
<div class="delta">2 个待审核</div>
|
||||
</a>
|
||||
<a class="stat" href="projects.html">
|
||||
<a class="stat" href="projects.html?filter=done">
|
||||
<div class="lbl">本月成片 <span class="badge">DONE</span></div>
|
||||
<div class="v">3</div>
|
||||
<div class="delta up"><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 19V5M5 12l7-7 7 7"/></svg> 较上月 +33%</div>
|
||||
|
||||
@ -176,6 +176,42 @@
|
||||
.bulk-bar button svg { width: 12px; height: 12px; }
|
||||
.bulk-bar .clear-sel { color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer; background: none; border: 0; padding: 4px 6px; }
|
||||
.bulk-bar .clear-sel:hover { color: var(--accent-white); }
|
||||
/* 移动到 · 弹层菜单 (向上弹) */
|
||||
.bulk-bar .move-wrap { position: relative; display: inline-flex; }
|
||||
.bulk-bar .move-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.18);
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.bulk-bar .move-menu.show { display: block; }
|
||||
.bulk-bar .move-menu .mv-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
width: 100%; height: 32px; padding: 0 10px;
|
||||
background: transparent; border: 0;
|
||||
color: var(--accent-black);
|
||||
font-size: 13px; font-family: inherit;
|
||||
cursor: pointer; border-radius: var(--r-sm);
|
||||
text-align: left;
|
||||
}
|
||||
.bulk-bar .move-menu .mv-item:hover { background: var(--heat-12); color: var(--heat); }
|
||||
.bulk-bar .move-menu .mv-item svg { width: 12px; height: 12px; opacity: .7; }
|
||||
/* tab 作为拖拽目标 hover 态 */
|
||||
.tabs .tab.drag-over {
|
||||
background: var(--heat-12);
|
||||
color: var(--heat);
|
||||
border-radius: var(--r-sm);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
body.edit-mode .asset-card { cursor: grab; }
|
||||
body.edit-mode .asset-card.dragging { opacity: .4; }
|
||||
.asset-thumb { aspect-ratio: 1; }
|
||||
.asset-card.video .asset-thumb { aspect-ratio: 9/16; max-height: 280px; }
|
||||
.asset-body { padding: 12px 14px; }
|
||||
@ -286,6 +322,7 @@
|
||||
<div class="tab" data-tab="products">商品图 <span class="count">0</span></div>
|
||||
<div class="tab" data-tab="finals">成片 <span class="count">0</span></div>
|
||||
<div class="tab" data-tab="uploads">我的上传 <span class="count">0</span></div>
|
||||
<div class="tab" data-tab="unclassified">未分类 <span class="count">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
@ -399,7 +436,18 @@
|
||||
<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>
|
||||
<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.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 class="placeholder asset-thumb">
|
||||
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||||
<span class="ico" aria-hidden="true"></span>
|
||||
<span class="lbl-mono">缺三视图</span>
|
||||
<span class="tri-missing-pop" role="tooltip">
|
||||
<span class="pop-h">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
MISSING TRI-VIEW
|
||||
</span>
|
||||
<span class="pop-body">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>
|
||||
<span class="pop-tip">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ph-frame">妈妈 · 居家</span>
|
||||
</div>
|
||||
<div class="asset-body">
|
||||
@ -456,7 +504,18 @@
|
||||
<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>
|
||||
<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.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 class="placeholder asset-thumb">
|
||||
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||||
<span class="ico" aria-hidden="true"></span>
|
||||
<span class="lbl-mono">缺三视图</span>
|
||||
<span class="tri-missing-pop" role="tooltip">
|
||||
<span class="pop-h">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
MISSING TRI-VIEW
|
||||
</span>
|
||||
<span class="pop-body">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>
|
||||
<span class="pop-tip">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ph-frame">李爷爷 · 居家</span>
|
||||
</div>
|
||||
<div class="asset-body">
|
||||
@ -692,6 +751,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 未分类(由图片优化"加入资产库"持久化进来) ============ -->
|
||||
<div class="asset-grid" data-tab="unclassified" id="grid-unclassified" hidden></div>
|
||||
|
||||
<div class="empty-state" id="empty">
|
||||
<div class="ic-empty">
|
||||
<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>
|
||||
@ -866,6 +928,14 @@
|
||||
<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>
|
||||
删除所选
|
||||
</button>
|
||||
<div class="move-wrap">
|
||||
<button type="button" id="bulk-move">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/><path d="M3 12h12"/></svg>
|
||||
移动到
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" style="margin-left:2px"><path d="M4 10l4-4 4 4"/></svg>
|
||||
</button>
|
||||
<div class="move-menu" id="bulk-move-menu"></div>
|
||||
</div>
|
||||
<button type="button" id="bulk-exit">完成</button>
|
||||
</div>
|
||||
|
||||
@ -897,7 +967,47 @@ Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.ht
|
||||
})();
|
||||
|
||||
// ============== State ==============
|
||||
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads'];
|
||||
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads', 'unclassified'];
|
||||
|
||||
/* ============== 加载图片优化"加入资产库"持久化数据 ==============
|
||||
image-optimize.html 把图保存到 localStorage['fs-library-unclassified']
|
||||
这里读出后注入到 #grid-unclassified ============== */
|
||||
(function loadUnclassified() {
|
||||
let list;
|
||||
try { list = JSON.parse(localStorage.getItem('fs-library-unclassified') || '[]'); } catch (e) { list = []; }
|
||||
if (!Array.isArray(list) || !list.length) return;
|
||||
const grid = document.getElementById('grid-unclassified');
|
||||
if (!grid) return;
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
function fmtDate(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts), z = n => (n < 10 ? '0' + n : '' + n);
|
||||
return d.getFullYear() + z(d.getMonth() + 1) + z(d.getDate());
|
||||
}
|
||||
list.forEach(it => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'asset-card';
|
||||
card.dataset.name = it.name || '未命名';
|
||||
card.dataset.kind = '未分类';
|
||||
card.dataset.source = it.source || '图片优化';
|
||||
card.dataset.used = '0';
|
||||
card.dataset.added = fmtDate(it.addedAt);
|
||||
card.dataset.libId = it.id || '';
|
||||
card.innerHTML = `
|
||||
<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>
|
||||
<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.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 class="placeholder asset-thumb"><span class="ph-frame">${esc(it.name || '未命名')}</span></div>
|
||||
<div class="asset-body">
|
||||
<div class="asset-name">${esc(it.name || '未命名')}</div>
|
||||
<div class="asset-meta">未分类 · ${esc(it.source || '图片优化')} · ${esc(it.ratio || '')}</div>
|
||||
</div>
|
||||
`;
|
||||
card.addEventListener('click', () => {
|
||||
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('查看资产', it.name || '未分类素材');
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
})();
|
||||
const PAGE_SIZES = [12, 24, 48, 96];
|
||||
const state = {
|
||||
tab: 'people',
|
||||
@ -1648,7 +1758,19 @@ delCancel.addEventListener('click', closeDelConfirm);
|
||||
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
|
||||
delOk.addEventListener('click', () => {
|
||||
const n = _delQueue.length;
|
||||
// 收集被删除中、source 是"未分类"的 libId,同步从 localStorage 移除
|
||||
const removedLibIds = _delQueue
|
||||
.filter(c => c.dataset.libId)
|
||||
.map(c => c.dataset.libId);
|
||||
_delQueue.forEach(card => card.remove());
|
||||
if (removedLibIds.length) {
|
||||
try {
|
||||
const LIB_KEY = 'fs-library-unclassified';
|
||||
const list = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
|
||||
const next = (Array.isArray(list) ? list : []).filter(x => !removedLibIds.includes(x.id));
|
||||
localStorage.setItem(LIB_KEY, JSON.stringify(next));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
closeDelConfirm();
|
||||
Shell.toast('已删除', n === 1 ? '资产已移除' : '已删除 ' + n + ' 个资产');
|
||||
updateBulkBar();
|
||||
@ -1684,6 +1806,7 @@ function enterEditMode() {
|
||||
document.body.classList.add('edit-mode');
|
||||
libManageBtn.classList.add('active');
|
||||
libManageLabel.textContent = '完成';
|
||||
applyDraggableToCards(true);
|
||||
updateBulkBar();
|
||||
}
|
||||
function exitEditMode() {
|
||||
@ -1691,6 +1814,8 @@ function exitEditMode() {
|
||||
libManageBtn.classList.remove('active');
|
||||
libManageLabel.textContent = '管理资产';
|
||||
document.querySelectorAll('.asset-card.selected').forEach(c => c.classList.remove('selected'));
|
||||
applyDraggableToCards(false);
|
||||
if (bulkMoveMenu) bulkMoveMenu.classList.remove('show');
|
||||
}
|
||||
libManageBtn.addEventListener('click', () => {
|
||||
if (document.body.classList.contains('edit-mode')) exitEditMode();
|
||||
@ -1707,6 +1832,132 @@ bulkDel.addEventListener('click', () => {
|
||||
openDelConfirm(sel);
|
||||
});
|
||||
|
||||
// ── 移动到 · 菜单 + 拖拽 ──
|
||||
const TAB_NAMES = {
|
||||
people: '人物', scenes: '场景', products: '商品图',
|
||||
finals: '成片', uploads: '我的上传', unclassified: '未分类'
|
||||
};
|
||||
const bulkMove = document.getElementById('bulk-move');
|
||||
const bulkMoveMenu = document.getElementById('bulk-move-menu');
|
||||
|
||||
function getCurrentTab() {
|
||||
const active = document.querySelector('#asset-tabs .tab.active');
|
||||
return active ? active.dataset.tab : TAB_KEYS[0];
|
||||
}
|
||||
function refreshAllTabCounts() {
|
||||
TAB_KEYS.forEach(t => {
|
||||
const tab = document.querySelector(`.tab[data-tab="${t}"] .count`);
|
||||
if (tab && cardsByTab[t]) tab.textContent = cardsByTab[t].length;
|
||||
});
|
||||
const total = TAB_KEYS.reduce((s, t) => s + (cardsByTab[t] ? cardsByTab[t].length : 0), 0);
|
||||
const sidebarBadge = document.querySelector('aside.sidebar a[href="library.html"] .pill-mini');
|
||||
if (sidebarBadge) sidebarBadge.textContent = total;
|
||||
const subMap = { people:'sub-people', scenes:'sub-scenes', products:'sub-products', finals:'sub-finals' };
|
||||
Object.keys(subMap).forEach(k => {
|
||||
const el = document.getElementById(subMap[k]);
|
||||
if (el && cardsByTab[k]) el.textContent = cardsByTab[k].length;
|
||||
});
|
||||
}
|
||||
function moveSelectedTo(targetTab) {
|
||||
const sel = getSelected();
|
||||
if (!sel.length) { Shell.toast('请先选中资产'); return; }
|
||||
const targetGrid = document.getElementById(`grid-${targetTab}`);
|
||||
if (!targetGrid) return;
|
||||
let movedCount = 0;
|
||||
sel.forEach(card => {
|
||||
// 找出 card 当前所在 tab (DOM-based,跨多 tab 的 selected 也能正确移动)
|
||||
const curGrid = card.closest('.asset-grid');
|
||||
const curTab = curGrid ? curGrid.dataset.tab : getCurrentTab();
|
||||
if (curTab === targetTab) return; // 同分类跳过
|
||||
// 更新内存
|
||||
if (cardsByTab[curTab]) {
|
||||
const idx = cardsByTab[curTab].indexOf(card);
|
||||
if (idx >= 0) cardsByTab[curTab].splice(idx, 1);
|
||||
}
|
||||
if (cardsByTab[targetTab]) cardsByTab[targetTab].push(card);
|
||||
// 更新 DOM
|
||||
card.classList.remove('selected');
|
||||
card.dataset.kind = TAB_NAMES[targetTab];
|
||||
targetGrid.appendChild(card);
|
||||
movedCount += 1;
|
||||
});
|
||||
refreshAllTabCounts();
|
||||
updateBulkBar();
|
||||
if (movedCount > 0) {
|
||||
Shell.toast('已移动', `${movedCount} 个资产 → 「${TAB_NAMES[targetTab]}」`);
|
||||
} else {
|
||||
Shell.toast('未移动', '所选资产已在该分类');
|
||||
}
|
||||
}
|
||||
function renderMoveMenu() {
|
||||
const cur = getCurrentTab();
|
||||
bulkMoveMenu.innerHTML = TAB_KEYS
|
||||
.filter(t => t !== cur)
|
||||
.map(t => `<button class="mv-item" type="button" data-target="${t}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
移到「${TAB_NAMES[t]}」
|
||||
</button>`).join('');
|
||||
bulkMoveMenu.querySelectorAll('.mv-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
moveSelectedTo(btn.dataset.target);
|
||||
bulkMoveMenu.classList.remove('show');
|
||||
});
|
||||
});
|
||||
}
|
||||
bulkMove.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
if (!getSelected().length) { Shell.toast('请先选中资产'); return; }
|
||||
renderMoveMenu();
|
||||
bulkMoveMenu.classList.toggle('show');
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
if (!bulkMove.contains(e.target) && !bulkMoveMenu.contains(e.target)) {
|
||||
bulkMoveMenu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// ── 拖拽到 tab 移动 (edit-mode 下生效) ──
|
||||
function applyDraggableToCards(on) {
|
||||
document.querySelectorAll('.asset-card').forEach(c => {
|
||||
if (on) c.setAttribute('draggable', 'true');
|
||||
else c.removeAttribute('draggable');
|
||||
});
|
||||
}
|
||||
document.addEventListener('dragstart', e => {
|
||||
const card = e.target.closest('.asset-card');
|
||||
if (!card || !document.body.classList.contains('edit-mode')) return;
|
||||
// 没选中就当前 card 也算 (允许直接拖单张)
|
||||
if (!card.classList.contains('selected')) {
|
||||
card.classList.add('selected');
|
||||
updateBulkBar();
|
||||
}
|
||||
card.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
try { e.dataTransfer.setData('text/plain', 'move-asset'); } catch (err) {}
|
||||
});
|
||||
document.addEventListener('dragend', e => {
|
||||
const card = e.target.closest('.asset-card');
|
||||
if (card) card.classList.remove('dragging');
|
||||
document.querySelectorAll('#asset-tabs .tab.drag-over').forEach(t => t.classList.remove('drag-over'));
|
||||
});
|
||||
document.querySelectorAll('#asset-tabs .tab').forEach(tab => {
|
||||
tab.addEventListener('dragover', e => {
|
||||
if (!document.body.classList.contains('edit-mode')) return;
|
||||
if (tab.dataset.tab === getCurrentTab()) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
tab.classList.add('drag-over');
|
||||
});
|
||||
tab.addEventListener('dragleave', () => tab.classList.remove('drag-over'));
|
||||
tab.addEventListener('drop', e => {
|
||||
if (!document.body.classList.contains('edit-mode')) return;
|
||||
e.preventDefault();
|
||||
tab.classList.remove('drag-over');
|
||||
if (tab.dataset.tab === getCurrentTab()) return;
|
||||
moveSelectedTo(tab.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
// 编辑模式下,卡片点击切换 selected (不再 toast / 打开)
|
||||
document.querySelectorAll('.asset-card').forEach(card => {
|
||||
card.addEventListener('click', e => {
|
||||
|
||||
664
电商AI平台/model-photo-demo-a.html
Normal file
664
电商AI平台/model-photo-demo-a.html
Normal file
@ -0,0 +1,664 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>方案 A · 商品空间 · 模特上身图 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<style>
|
||||
.app { height: 100vh; overflow: hidden; }
|
||||
main { display: flex; flex-direction: column; min-height: 0; }
|
||||
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
|
||||
|
||||
/* ===== 两栏:左侧栏 / 右侧主区 ===== */
|
||||
.dma {
|
||||
flex: 1; min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
}
|
||||
|
||||
/* ── 左侧栏 · 仅商品空间(搜索 + 列表 + 全部入口) ── */
|
||||
.dma-side {
|
||||
border-right: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.dma-side-h {
|
||||
padding: 14px 14px 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dma-side-h .ti-row {
|
||||
display: flex; align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dma-side-h .ti {
|
||||
font-size: 11px; font-family: var(--font-mono);
|
||||
color: var(--black-alpha-48); letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.dma-side-h .ti-row .add {
|
||||
margin-left: auto;
|
||||
width: 22px; height: 22px;
|
||||
display: grid; place-items: center;
|
||||
background: var(--heat-12); color: var(--heat);
|
||||
border: 0; border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.dma-side-h .add svg { width: 11px; height: 11px; }
|
||||
/* 搜索框 (Q1-A) */
|
||||
.dma-search {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
height: 32px; padding: 0 10px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
transition: border-color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.dma-search:focus-within { border-color: var(--heat-40); background: var(--surface); }
|
||||
.dma-search svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
|
||||
.dma-search input {
|
||||
flex: 1; min-width: 0; height: 100%;
|
||||
border: 0; outline: 0; background: transparent;
|
||||
font-size: 12.5px; color: var(--accent-black); font-family: inherit;
|
||||
}
|
||||
|
||||
/* 商品空间列表(flex:1 占据中部所有剩余空间) */
|
||||
.dma-prod-list {
|
||||
flex: 1; min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 4px 10px 10px;
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.dma-prod {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.dma-prod:hover { background: var(--background-lighter); }
|
||||
.dma-prod.active { background: var(--heat-12); border-color: var(--heat-20); }
|
||||
.dma-prod .thumb {
|
||||
flex-shrink: 0;
|
||||
width: 40px; height: 40px;
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden;
|
||||
background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.04) 4px 5px);
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-mono); font-size: 9px;
|
||||
color: var(--black-alpha-32);
|
||||
}
|
||||
.dma-prod.active .thumb { border-color: var(--heat); }
|
||||
.dma-prod .body { flex: 1; min-width: 0; }
|
||||
.dma-prod .nm {
|
||||
font-size: 12.5px;
|
||||
color: var(--accent-black); font-weight: 500;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.dma-prod.active .nm { color: var(--heat); font-weight: 600; }
|
||||
.dma-prod .sub {
|
||||
margin-top: 2px;
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
|
||||
/* 全部商品入口(Q1-B · 固定贴底) */
|
||||
.dma-all {
|
||||
flex-shrink: 0;
|
||||
margin: 0 10px 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px dashed var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-72);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.dma-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||||
.dma-all .ct {
|
||||
color: var(--black-alpha-48);
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.dma-all svg { width: 12px; height: 12px; }
|
||||
|
||||
/* ── 右侧主区 ── */
|
||||
.dma-main {
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dma-main-h {
|
||||
flex-shrink: 0;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 14px 28px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
}
|
||||
.dma-main-h .crumb {
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .04em;
|
||||
}
|
||||
.dma-main-h h2 {
|
||||
font-size: 20px; font-weight: 600;
|
||||
letter-spacing: -.015em;
|
||||
color: var(--accent-black);
|
||||
}
|
||||
.dma-main-h .stats {
|
||||
margin-left: auto;
|
||||
display: flex; gap: 6px;
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.dma-main-h .stats b { color: var(--accent-black); font-weight: 600; }
|
||||
.dma-main-h .stats .sep { color: var(--black-alpha-24); }
|
||||
|
||||
/* 主区:参数 + 结果横向二栏 */
|
||||
.dma-body {
|
||||
flex: 1; min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* 左侧参数面板 */
|
||||
.dma-form {
|
||||
border-right: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.dma-form-scroll {
|
||||
flex: 1; min-height: 0; overflow-y: auto;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
.dma-field { margin-bottom: 16px; }
|
||||
.dma-field-h {
|
||||
font-size: 12px; font-weight: 600;
|
||||
color: var(--accent-black);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.dma-field-h .opt {
|
||||
font-weight: 400; font-size: 11px;
|
||||
color: var(--black-alpha-48); margin-left: 4px;
|
||||
}
|
||||
.dma-models {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
.dma-model {
|
||||
aspect-ratio: 3 / 4;
|
||||
border: 1px solid var(--border-faint);
|
||||
background: var(--background-lighter);
|
||||
border-radius: var(--r-sm);
|
||||
position: relative; cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
.dma-model:hover { border-color: var(--black-alpha-32); }
|
||||
.dma-model.selected { border-color: var(--heat); border-width: 2px; }
|
||||
.dma-model .ph {
|
||||
position: absolute; inset: 0;
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
color: var(--black-alpha-32);
|
||||
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
|
||||
}
|
||||
.dma-model .nm {
|
||||
position: absolute; bottom: 0; left: 0; right: 0;
|
||||
padding: 4px 6px;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,.5));
|
||||
font-size: 10px; color: #fff; font-weight: 500;
|
||||
}
|
||||
.dma-model.selected::after {
|
||||
content: ''; position: absolute; top: 4px; right: 4px;
|
||||
width: 14px; height: 14px;
|
||||
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;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dma-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.dma-chip {
|
||||
display: inline-flex; align-items: center;
|
||||
height: 28px; padding: 0 11px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12px; color: var(--black-alpha-72);
|
||||
font-family: inherit; cursor: pointer;
|
||||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.dma-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.dma-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
|
||||
|
||||
.dma-form-cta {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
}
|
||||
.dma-cost {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.dma-cost .v { color: var(--accent-black); font-weight: 600; }
|
||||
.dma-gen {
|
||||
width: 100%; height: 42px;
|
||||
background: var(--heat); color: #fff;
|
||||
border: 1px solid var(--heat); border-radius: var(--r-md);
|
||||
font-size: 14px; font-weight: 600; font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
box-shadow: var(--shadow-cta);
|
||||
}
|
||||
.dma-gen svg { width: 15px; height: 15px; }
|
||||
|
||||
/* 右侧结果区 */
|
||||
.dma-result {
|
||||
background: var(--background-base);
|
||||
min-height: 0; overflow-y: auto;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
.dma-result-h {
|
||||
display: flex; align-items: baseline; gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.dma-result-h .ti { font-size: 15px; font-weight: 600; color: var(--accent-black); }
|
||||
.dma-result-h .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
|
||||
/* 批次卡 */
|
||||
.dma-batch {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.dma-batch-h {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
}
|
||||
.dma-batch-h .pic {
|
||||
width: 28px; height: 28px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
display: grid; place-items: center;
|
||||
color: var(--heat);
|
||||
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
||||
}
|
||||
.dma-batch-h .meta { flex: 1; min-width: 0; }
|
||||
.dma-batch-h .nm { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||||
.dma-batch-h .info { margin-top: 2px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.dma-batch-h .info .sep { color: var(--black-alpha-24); }
|
||||
.dma-batch-h .ops { display: flex; gap: 4px; }
|
||||
.dma-batch-h .ops button {
|
||||
width: 28px; height: 28px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56); cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
}
|
||||
.dma-batch-h .ops button:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.dma-batch-h .ops button svg { width: 13px; height: 13px; }
|
||||
|
||||
.dma-batch-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||||
@media (max-width: 1400px) { .dma-batch-grid { grid-template-columns: repeat(3, 1fr); } }
|
||||
.dma-cell {
|
||||
aspect-ratio: 3 / 4;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden; position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dma-cell .ph {
|
||||
position: absolute; inset: 0;
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--black-alpha-32);
|
||||
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
|
||||
}
|
||||
.dma-cell .tag {
|
||||
position: absolute; top: 6px; left: 6px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(0,0,0,.65);
|
||||
color: #fff;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 10px; font-weight: 500;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 空态 */
|
||||
.dma-empty {
|
||||
height: 100%;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--black-alpha-48);
|
||||
text-align: center;
|
||||
}
|
||||
.dma-empty .ic {
|
||||
width: 56px; height: 56px;
|
||||
background: var(--surface); border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
display: grid; place-items: center;
|
||||
color: var(--heat);
|
||||
}
|
||||
.dma-empty .ic svg { width: 22px; height: 22px; }
|
||||
.dma-empty .mono { font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em; }
|
||||
|
||||
/* 顶部"提示条" · 这是 demo */
|
||||
.dma-banner {
|
||||
margin: 12px 28px 0;
|
||||
padding: 8px 12px;
|
||||
background: var(--heat-12);
|
||||
border: 1px dashed var(--heat-20);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12px;
|
||||
color: var(--accent-black);
|
||||
font-family: var(--font-mono); letter-spacing: .02em;
|
||||
}
|
||||
.dma-banner b { color: var(--heat); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="dma-banner">// DEMO · 方案 A · <b>商品 = 项目空间</b>(Q1: A+B)。左栏仅商品空间:🔍 搜索 + 最近 6 条 + <b>全部商品</b>兜底入口;历史任务已挪进主区。主区:模特卡 + 张数 + 比例 + 立即生成,生成结果自动绑定到当前商品。</div>
|
||||
|
||||
<div class="dma">
|
||||
|
||||
<!-- ===== 左侧栏 · 仅商品空间 (搜索 + 列表 + 全部入口) ===== -->
|
||||
<aside class="dma-side">
|
||||
<div class="dma-side-h">
|
||||
<div class="ti-row">
|
||||
<span class="ti">商品空间</span>
|
||||
<button class="add" type="button" title="新建商品">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Q1-A · 搜索框 -->
|
||||
<div class="dma-search">
|
||||
<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>
|
||||
<input type="text" placeholder="搜索商品 / 分类">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 · 最近 6 条 -->
|
||||
<div class="dma-prod-list">
|
||||
<div class="dma-prod active">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">透真补水面膜</div>
|
||||
<div class="sub">// 美妆个护 · 6 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">透真清透防晒霜</div>
|
||||
<div class="sub">// 美妆个护 · 3 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">南卡 Lite Pro 蓝牙耳机</div>
|
||||
<div class="sub">// 数码 3C · 2 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">滋啦速食牛肉面</div>
|
||||
<div class="sub">// 食品饮料 · 1 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">三顿半同款冻干咖啡</div>
|
||||
<div class="sub">// 食品饮料 · 1 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">小熊 4L 可视空气炸锅</div>
|
||||
<div class="sub">// 家居家电 · 0 批</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Q1-B · 全部商品入口(贴底) -->
|
||||
<button class="dma-all" type="button">
|
||||
<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>
|
||||
全部商品
|
||||
<span class="ct">24 个</span>
|
||||
<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>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<!-- ===== 右侧主区 ===== -->
|
||||
<section class="dma-main">
|
||||
|
||||
<!-- 顶部 · 商品名 + 统计 -->
|
||||
<div class="dma-main-h">
|
||||
<div>
|
||||
<div class="crumb">// 商品空间</div>
|
||||
<h2>透真补水面膜</h2>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span>本商品 <b>6</b> 批</span>
|
||||
<span class="sep">·</span>
|
||||
<span>累计 <b>22</b> 张图</span>
|
||||
<span class="sep">·</span>
|
||||
<span>最近 <b>3 分钟前</b></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:参数面板 + 结果 -->
|
||||
<div class="dma-body">
|
||||
|
||||
<!-- 参数面板(单一职责:挑模特 + 张数) -->
|
||||
<div class="dma-form">
|
||||
<div class="dma-form-scroll">
|
||||
|
||||
<div class="dma-field">
|
||||
<div class="dma-field-h">选择模特<span class="opt">(已锁定商品 · 透真补水面膜)</span></div>
|
||||
<div class="dma-models">
|
||||
<div class="dma-model selected">
|
||||
<div class="ph">Ava · 3:4</div>
|
||||
<div class="nm">Ava</div>
|
||||
</div>
|
||||
<div class="dma-model">
|
||||
<div class="ph">Zoe · 3:4</div>
|
||||
<div class="nm">Zoe</div>
|
||||
</div>
|
||||
<div class="dma-model">
|
||||
<div class="ph">Ben · 3:4</div>
|
||||
<div class="nm">Ben</div>
|
||||
</div>
|
||||
<div class="dma-model">
|
||||
<div class="ph">Lin · 3:4</div>
|
||||
<div class="nm">Lin</div>
|
||||
</div>
|
||||
<div class="dma-model">
|
||||
<div class="ph">Mia · 3:4</div>
|
||||
<div class="nm">Mia</div>
|
||||
</div>
|
||||
<div class="dma-model" style="border-style:dashed;display:flex;align-items:center;justify-content:center;color:var(--black-alpha-48);">
|
||||
<span style="font-size:18px">+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dma-field">
|
||||
<div class="dma-field-h">生成张数</div>
|
||||
<div class="dma-chip-row">
|
||||
<button class="dma-chip" type="button">1 张</button>
|
||||
<button class="dma-chip" type="button">2 张</button>
|
||||
<button class="dma-chip active" type="button">4 张</button>
|
||||
<button class="dma-chip" type="button">8 张</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dma-field">
|
||||
<div class="dma-field-h">画面比例</div>
|
||||
<div class="dma-chip-row">
|
||||
<button class="dma-chip" type="button">1:1</button>
|
||||
<button class="dma-chip active" type="button">3:4</button>
|
||||
<button class="dma-chip" type="button">9:16</button>
|
||||
<button class="dma-chip" type="button">16:9</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dma-field">
|
||||
<div class="dma-field-h">补充提示词<span class="opt">(选填)</span></div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dma-form-cta">
|
||||
<div class="dma-cost">
|
||||
<span>预估扣费 <span class="v">≈ ¥1.20</span></span>
|
||||
<span>余额 ¥327.40</span>
|
||||
</div>
|
||||
<button class="dma-gen" type="button">
|
||||
<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>
|
||||
立即生成 · 透真补水面膜 × Ava
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧结果 · 当前商品全部批次 -->
|
||||
<div class="dma-result">
|
||||
<div class="dma-result-h">
|
||||
<span class="ti">最近批次 · Ava × 4 张</span>
|
||||
<span class="sub">// 3 分钟前 · 已完成</span>
|
||||
</div>
|
||||
|
||||
<!-- 批次 1 -->
|
||||
<div class="dma-batch">
|
||||
<div class="dma-batch-h">
|
||||
<div class="pic">4×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Ava × 4 张</div>
|
||||
<div class="info">透真补水面膜 <span class="sep">·</span> 3:4 <span class="sep">·</span> 3 分钟前 <span class="sep">·</span> ¥1.20</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-batch-grid">
|
||||
<div class="dma-cell"><div class="ph">Ava · #1</div><span class="tag">3:4</span></div>
|
||||
<div class="dma-cell"><div class="ph">Ava · #2</div><span class="tag">3:4</span></div>
|
||||
<div class="dma-cell"><div class="ph">Ava · #3</div><span class="tag">3:4</span></div>
|
||||
<div class="dma-cell"><div class="ph">Ava · #4</div><span class="tag">3:4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批次 2 -->
|
||||
<div class="dma-batch">
|
||||
<div class="dma-batch-h">
|
||||
<div class="pic">4×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Zoe × 4 张</div>
|
||||
<div class="info">透真补水面膜 <span class="sep">·</span> 3:4 <span class="sep">·</span> 12 分钟前 <span class="sep">·</span> ¥1.20</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-batch-grid">
|
||||
<div class="dma-cell"><div class="ph">Zoe · #1</div><span class="tag">3:4</span></div>
|
||||
<div class="dma-cell"><div class="ph">Zoe · #2</div><span class="tag">3:4</span></div>
|
||||
<div class="dma-cell"><div class="ph">Zoe · #3</div><span class="tag">3:4</span></div>
|
||||
<div class="dma-cell"><div class="ph">Zoe · #4</div><span class="tag">3:4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批次 3 -->
|
||||
<div class="dma-batch">
|
||||
<div class="dma-batch-h">
|
||||
<div class="pic">2×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Ben × 2 张</div>
|
||||
<div class="info">透真补水面膜 <span class="sep">·</span> 3:4 <span class="sep">·</span> 刚刚 <span class="sep">·</span> 生成中</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dma-batch-grid">
|
||||
<div class="dma-cell"><div class="ph">生成中…</div></div>
|
||||
<div class="dma-cell"><div class="ph">生成中…</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="assets/shell.js?v=202605211643"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'asset-factory',
|
||||
crumbs: [
|
||||
{ label: '工作台', href: 'index.html' },
|
||||
{ label: '图片生成', href: 'asset-factory.html' },
|
||||
{ label: '模特上身图 · 方案 A · Demo' }
|
||||
]
|
||||
});
|
||||
|
||||
// 简单交互:点击商品 / 任务切换激活态
|
||||
document.querySelectorAll('.dma-prod').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dma-prod').forEach(x => x.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.dma-task').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dma-task').forEach(x => x.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.dma-model').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dma-model').forEach(x => x.classList.remove('selected'));
|
||||
el.classList.add('selected');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.dma-chip').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
// 同组互斥
|
||||
el.parentElement.querySelectorAll('.dma-chip').forEach(x => x.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
655
电商AI平台/model-photo-demo-b.html
Normal file
655
电商AI平台/model-photo-demo-b.html
Normal file
@ -0,0 +1,655 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>方案 A · v2 · 模特上身图 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<style>
|
||||
.app { height: 100vh; overflow: hidden; }
|
||||
main { display: flex; flex-direction: column; min-height: 0; }
|
||||
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
|
||||
|
||||
/* ===== 两栏:左栏(纯商品空间) + 主区 ===== */
|
||||
.dmb {
|
||||
flex: 1; min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
}
|
||||
|
||||
/* ── 左栏 · 商品空间 ── */
|
||||
.dmb-side {
|
||||
border-right: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.dmb-side-h {
|
||||
padding: 14px 16px 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dmb-side-h .ti {
|
||||
font-size: 11px; font-family: var(--font-mono);
|
||||
color: var(--black-alpha-48); letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/* 搜索框 */
|
||||
.dmb-search {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
height: 32px; padding: 0 10px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
.dmb-search:focus-within { border-color: var(--heat-40); background: var(--surface); }
|
||||
.dmb-search svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
|
||||
.dmb-search input {
|
||||
flex: 1; min-width: 0; height: 100%;
|
||||
border: 0; outline: 0; background: transparent;
|
||||
font-size: 12.5px; color: var(--accent-black); font-family: inherit;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.dmb-list {
|
||||
flex: 1; min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 8px 10px 8px;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.dmb-prod {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.dmb-prod:hover { background: var(--background-lighter); }
|
||||
.dmb-prod.active { background: var(--heat-12); border-color: var(--heat-20); }
|
||||
.dmb-prod .thumb {
|
||||
flex-shrink: 0;
|
||||
width: 44px; height: 44px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden; position: relative;
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-mono); font-size: 9px;
|
||||
color: var(--black-alpha-32);
|
||||
background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(0,0,0,.04) 4px 5px);
|
||||
}
|
||||
.dmb-prod.active .thumb { border-color: var(--heat); }
|
||||
.dmb-prod .body { flex: 1; min-width: 0; }
|
||||
.dmb-prod .nm {
|
||||
font-size: 12.5px;
|
||||
color: var(--accent-black); font-weight: 500;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.dmb-prod.active .nm { color: var(--heat); font-weight: 600; }
|
||||
.dmb-prod .sub {
|
||||
margin-top: 2px;
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
|
||||
/* 底部 · 全部商品入口 */
|
||||
.dmb-all {
|
||||
flex-shrink: 0;
|
||||
margin: 0 10px 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px dashed var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-72);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.dmb-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||||
.dmb-all .ct { color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 10.5px; margin-left: auto; }
|
||||
.dmb-all svg { width: 12px; height: 12px; }
|
||||
|
||||
/* ── 主区 ── */
|
||||
.dmb-main {
|
||||
display: flex; flex-direction: column;
|
||||
min-height: 0; overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.dmb-main-h {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 28px 12px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
}
|
||||
.dmb-main-h .crumb {
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .04em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.dmb-main-h h2 {
|
||||
font-size: 22px; font-weight: 600;
|
||||
letter-spacing: -.015em;
|
||||
color: var(--accent-black);
|
||||
}
|
||||
.dmb-main-h .row {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.dmb-main-h .stats {
|
||||
font-family: var(--font-mono); font-size: 11px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
display: flex; gap: 4px;
|
||||
}
|
||||
.dmb-main-h .stats b { color: var(--accent-black); font-weight: 600; }
|
||||
.dmb-main-h .stats .sep { color: var(--black-alpha-24); }
|
||||
.dmb-main-h .spacer { flex: 1; }
|
||||
|
||||
/* 主区 toolbar (筛选条 · 仿 image-optimize 顶部) */
|
||||
.dmb-main-tb { display: flex; gap: 8px; align-items: center; }
|
||||
.dmb-main-tb .icbtn {
|
||||
width: 30px; height: 30px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-72);
|
||||
cursor: pointer; display: grid; place-items: center;
|
||||
}
|
||||
.dmb-main-tb .icbtn:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.dmb-main-tb .icbtn svg { width: 13px; height: 13px; }
|
||||
.dmb-main-tb .chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
height: 30px; padding: 0 10px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12px; color: var(--black-alpha-72);
|
||||
font-family: inherit; cursor: pointer;
|
||||
}
|
||||
.dmb-main-tb .chip:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.dmb-main-tb .chip svg { width: 10px; height: 10px; opacity: .6; }
|
||||
|
||||
/* 主区滚动体 · 任务流 */
|
||||
.dmb-stream {
|
||||
flex: 1; min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 22px 28px 200px; /* 底部留出参数面板高度 */
|
||||
background: var(--background-base);
|
||||
}
|
||||
.dmb-day-h {
|
||||
display: flex; align-items: baseline; gap: 8px;
|
||||
margin: 6px 0 10px;
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.dmb-day-h::before {
|
||||
content: ''; width: 14px; height: 1px;
|
||||
background: var(--black-alpha-24);
|
||||
display: inline-block; margin-right: 2px;
|
||||
}
|
||||
.dmb-day-h .ct {
|
||||
color: var(--black-alpha-72); font-weight: 500;
|
||||
margin-left: auto;
|
||||
text-transform: none; letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* 批次卡 */
|
||||
.dmb-batch {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.dmb-batch-h {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dmb-batch-h .pic {
|
||||
flex-shrink: 0;
|
||||
width: 30px; height: 30px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
display: grid; place-items: center;
|
||||
color: var(--heat);
|
||||
font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
||||
}
|
||||
.dmb-batch-h .meta { flex: 1; min-width: 0; }
|
||||
.dmb-batch-h .nm { font-size: 13.5px; font-weight: 600; color: var(--accent-black); }
|
||||
.dmb-batch-h .info {
|
||||
margin-top: 2px;
|
||||
display: flex; flex-wrap: wrap; gap: 4px;
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.dmb-batch-h .info .sep { color: var(--black-alpha-24); }
|
||||
.dmb-batch-h .ops { display: flex; gap: 4px; }
|
||||
.dmb-batch-h .ops button {
|
||||
width: 28px; height: 28px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56); cursor: pointer;
|
||||
display: grid; place-items: center;
|
||||
transition: border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.dmb-batch-h .ops button:hover { border-color: var(--heat-20); color: var(--heat); }
|
||||
.dmb-batch-h .ops button svg { width: 13px; height: 13px; }
|
||||
|
||||
/* 状态 pill 紧贴标题右侧 */
|
||||
.dmb-batch-h .stat-pill {
|
||||
margin-left: 8px;
|
||||
padding: 2px 7px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.dmb-batch-h .stat-pill .dot { width: 4px; height: 4px; }
|
||||
|
||||
.dmb-batch-grid {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
|
||||
}
|
||||
@media (max-width: 1400px) { .dmb-batch-grid { grid-template-columns: repeat(3, 1fr); } }
|
||||
.dmb-cell {
|
||||
aspect-ratio: 3 / 4;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
overflow: hidden; position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dmb-cell .ph {
|
||||
position: absolute; inset: 0;
|
||||
display: grid; place-items: center;
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-32);
|
||||
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
|
||||
}
|
||||
.dmb-cell.gen .ph { animation: dmb-pulse 1.4s ease-in-out infinite; }
|
||||
@keyframes dmb-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
|
||||
.dmb-cell.err { border-color: var(--accent-crimson, #c43d3d); }
|
||||
.dmb-cell.err .ph { color: var(--accent-crimson, #c43d3d); background: rgba(196,61,61,.05); }
|
||||
.dmb-cell .tag {
|
||||
position: absolute; top: 6px; left: 6px;
|
||||
padding: 2px 6px;
|
||||
background: rgba(0,0,0,.65); color: #fff;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 10px; font-weight: 500; backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* ── 底部 fixed 参数面板 ── */
|
||||
.dmb-param-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;
|
||||
}
|
||||
.dmb-param {
|
||||
max-width: 1180px; margin: 0 auto;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: 14px;
|
||||
padding: 10px 14px;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
box-shadow: 0 6px 24px rgba(0,0,0,.06);
|
||||
}
|
||||
.dmb-param .label-mono {
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .04em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dmb-param .pchip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
height: 30px; padding: 0 12px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-pill);
|
||||
font-size: 12px; color: var(--black-alpha-72);
|
||||
cursor: pointer; font-family: inherit;
|
||||
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.dmb-param .pchip:hover { background: var(--surface); border-color: var(--border-faint); }
|
||||
.dmb-param .pchip.active { background: var(--heat-12); color: var(--heat); }
|
||||
.dmb-param .pchip svg { width: 10px; height: 10px; opacity: .6; }
|
||||
.dmb-param .pchip .lbl-mono {
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.dmb-param .pchip.active .lbl-mono { color: var(--heat); }
|
||||
.dmb-param .spacer { flex: 1; }
|
||||
.dmb-param .meta-right {
|
||||
font-family: var(--font-mono); font-size: 10.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
margin-right: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
.dmb-param .meta-right .v { color: var(--accent-black); font-weight: 600; }
|
||||
.dmb-param .gen-btn {
|
||||
height: 38px; padding: 0 20px;
|
||||
background: var(--heat); color: #fff;
|
||||
border: 0; border-radius: var(--r-md);
|
||||
font-size: 13.5px; font-weight: 600; font-family: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
box-shadow: var(--shadow-cta);
|
||||
}
|
||||
.dmb-param .gen-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 顶部 demo 提示条 */
|
||||
.dmb-banner {
|
||||
margin: 12px 28px 0;
|
||||
padding: 8px 12px;
|
||||
background: var(--heat-12);
|
||||
border: 1px dashed var(--heat-20);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12px; color: var(--accent-black);
|
||||
font-family: var(--font-mono); letter-spacing: .02em;
|
||||
}
|
||||
.dmb-banner b { color: var(--heat); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="dmb-banner">// DEMO v2 · 方案 A · <b>商品空间(A+B) + 任务流主区</b>。左栏只保留商品空间(搜索+最近6条+全部入口),任务列表搬到主区,筛选放主区顶部 toolbar,参数面板底部 fixed 化(类 image-optimize)。</div>
|
||||
|
||||
<div class="dmb">
|
||||
|
||||
<!-- ===== 左栏 · 商品空间 ===== -->
|
||||
<aside class="dmb-side">
|
||||
<div class="dmb-side-h">
|
||||
<div class="ti">商品空间</div>
|
||||
<div class="dmb-search">
|
||||
<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>
|
||||
<input type="text" placeholder="搜索商品 / 分类">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dmb-list">
|
||||
<div class="dmb-prod active">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">透真补水面膜</div>
|
||||
<div class="sub">美妆个护 · 6 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">透真清透防晒霜</div>
|
||||
<div class="sub">美妆个护 · 3 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">南卡 Lite Pro 蓝牙耳机</div>
|
||||
<div class="sub">数码 3C · 2 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">滋啦速食牛肉面</div>
|
||||
<div class="sub">食品饮料 · 1 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">三顿半同款冻干咖啡</div>
|
||||
<div class="sub">食品饮料 · 1 批</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-prod">
|
||||
<div class="thumb">主图</div>
|
||||
<div class="body">
|
||||
<div class="nm">小熊 4L 可视空气炸锅</div>
|
||||
<div class="sub">家居家电 · 0 批</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="dmb-all" type="button">
|
||||
<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>
|
||||
全部商品
|
||||
<span class="ct">24 个</span>
|
||||
<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>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<!-- ===== 主区 ===== -->
|
||||
<section class="dmb-main">
|
||||
|
||||
<!-- 顶部 标题 + stats + toolbar -->
|
||||
<div class="dmb-main-h">
|
||||
<div class="crumb">// 商品空间 · 模特上身图</div>
|
||||
<h2>透真补水面膜</h2>
|
||||
<div class="row">
|
||||
<div class="stats">
|
||||
<span>美妆个护</span><span class="sep">·</span>
|
||||
<span>本商品 <b>6</b> 批</span><span class="sep">·</span>
|
||||
<span>累计 <b>22</b> 张图</span><span class="sep">·</span>
|
||||
<span>最近 <b>3 分钟前</b></span>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="dmb-main-tb">
|
||||
<button class="icbtn" type="button" title="搜索批次">
|
||||
<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>
|
||||
</button>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务流 -->
|
||||
<div class="dmb-stream">
|
||||
|
||||
<div class="dmb-day-h">
|
||||
<span>今天</span>
|
||||
<span class="ct">3 批 · 10 张</span>
|
||||
</div>
|
||||
|
||||
<!-- 批次 1 -->
|
||||
<div class="dmb-batch">
|
||||
<div class="dmb-batch-h">
|
||||
<div class="pic">4×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Ava × 4 张 <span class="pill ok stat-pill"><span class="dot"></span>已完成</span></div>
|
||||
<div class="info">
|
||||
<span>3:4</span><span class="sep">·</span>
|
||||
<span>3 分钟前</span><span class="sep">·</span>
|
||||
<span>¥1.20</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-batch-grid">
|
||||
<div class="dmb-cell"><div class="ph">Ava · #1</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Ava · #2</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Ava · #3</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Ava · #4</div><span class="tag">3:4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批次 2 -->
|
||||
<div class="dmb-batch">
|
||||
<div class="dmb-batch-h">
|
||||
<div class="pic">4×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Zoe × 4 张 <span class="pill ok stat-pill"><span class="dot"></span>已完成</span></div>
|
||||
<div class="info">
|
||||
<span>3:4</span><span class="sep">·</span>
|
||||
<span>12 分钟前</span><span class="sep">·</span>
|
||||
<span>¥1.20</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-batch-grid">
|
||||
<div class="dmb-cell"><div class="ph">Zoe · #1</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Zoe · #2</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Zoe · #3</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Zoe · #4</div><span class="tag">3:4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批次 3 · 生成中 -->
|
||||
<div class="dmb-batch">
|
||||
<div class="dmb-batch-h">
|
||||
<div class="pic">2×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Ben × 2 张 <span class="pill info stat-pill"><span class="dot"></span>生成中</span></div>
|
||||
<div class="info">
|
||||
<span>3:4</span><span class="sep">·</span>
|
||||
<span>刚刚</span><span class="sep">·</span>
|
||||
<span>¥0.60</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-batch-grid">
|
||||
<div class="dmb-cell gen"><div class="ph">生成中…</div></div>
|
||||
<div class="dmb-cell gen"><div class="ph">生成中…</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 昨天 -->
|
||||
<div class="dmb-day-h">
|
||||
<span>昨天</span>
|
||||
<span class="ct">2 批 · 8 张</span>
|
||||
</div>
|
||||
|
||||
<div class="dmb-batch">
|
||||
<div class="dmb-batch-h">
|
||||
<div class="pic">4×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Lin × 4 张 <span class="pill ok stat-pill"><span class="dot"></span>已完成</span></div>
|
||||
<div class="info">
|
||||
<span>3:4</span><span class="sep">·</span>
|
||||
<span>昨天 18:24</span><span class="sep">·</span>
|
||||
<span>¥1.20</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-batch-grid">
|
||||
<div class="dmb-cell"><div class="ph">Lin · #1</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Lin · #2</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Lin · #3</div><span class="tag">3:4</span></div>
|
||||
<div class="dmb-cell"><div class="ph">Lin · #4</div><span class="tag">3:4</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更早 -->
|
||||
<div class="dmb-day-h">
|
||||
<span>更早</span>
|
||||
<span class="ct">1 批 · 2 张 · 含 1 失败</span>
|
||||
</div>
|
||||
|
||||
<div class="dmb-batch">
|
||||
<div class="dmb-batch-h">
|
||||
<div class="pic">2×</div>
|
||||
<div class="meta">
|
||||
<div class="nm">Ava × 2 张 <span class="pill err stat-pill"><span class="dot"></span>失败</span></div>
|
||||
<div class="info">
|
||||
<span>3:4</span><span class="sep">·</span>
|
||||
<span>2 天前</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dmb-batch-grid">
|
||||
<div class="dmb-cell err"><div class="ph">失败 · 点重跑</div></div>
|
||||
<div class="dmb-cell err"><div class="ph">失败 · 点重跑</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 底部 fixed 参数面板 -->
|
||||
<div class="dmb-param-wrap">
|
||||
<div class="dmb-param">
|
||||
<button class="pchip active" type="button">
|
||||
<span class="lbl-mono">模特</span>
|
||||
<span>Ava</span>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<button class="pchip" type="button">
|
||||
<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>
|
||||
</button>
|
||||
<button class="pchip" type="button">
|
||||
<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>
|
||||
</button>
|
||||
<button class="pchip" type="button">
|
||||
<span class="lbl-mono">补充提示词</span>
|
||||
<span style="color:var(--black-alpha-48)">+ 添加</span>
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="meta-right">预估 <span class="v">¥1.20</span> · 余额 <span class="v">¥327.40</span></span>
|
||||
<button class="gen-btn" type="button">
|
||||
<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>
|
||||
生成 · 透真补水面膜 × Ava
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="assets/shell.js?v=202605211643"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'asset-factory',
|
||||
crumbs: [
|
||||
{ label: '工作台', href: 'index.html' },
|
||||
{ label: '图片生成', href: 'asset-factory.html' },
|
||||
{ label: '模特上身图 · 方案 A · v2' }
|
||||
]
|
||||
});
|
||||
|
||||
// 商品 / chip 切换
|
||||
document.querySelectorAll('.dmb-prod').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dmb-prod').forEach(x => x.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.dmb-param .pchip').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
// demo:只切自己 active(不互斥,每个 chip 都可独立切下拉)
|
||||
el.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -62,7 +62,49 @@
|
||||
.ai-avatar { width: 26px; height: 26px; background: var(--heat); color: var(--accent-white); display: grid; place-items: center; font-size: 11px; font-weight: 700; border: 1px solid var(--heat); border-radius: 50%; }
|
||||
.del { text-decoration: line-through; color: var(--black-alpha-48); }
|
||||
.ins { background: var(--forest-bg); color: var(--accent-forest); padding: 0 3px; }
|
||||
.chat-input { padding: 14px 18px; border-top: 1px solid var(--border-faint); }
|
||||
.chat-input { padding: 14px 18px 18px; border-top: 1px solid var(--border-faint); }
|
||||
.chat-input-card {
|
||||
background: var(--background-base);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 12px 14px 10px;
|
||||
transition: border-color var(--t-base), box-shadow var(--t-base);
|
||||
}
|
||||
.chat-input-card:focus-within { border-color: var(--accent-black); box-shadow: 0 0 0 3px rgba(0,0,0,.04); }
|
||||
.chat-input-area {
|
||||
width: 100%; border: none; outline: none; background: transparent;
|
||||
font-family: var(--font-sans); font-size: 13px; color: var(--accent-black);
|
||||
line-height: 1.55; resize: none; padding: 0; min-height: 42px;
|
||||
}
|
||||
.chat-input-area::placeholder { color: var(--black-alpha-40); }
|
||||
.chat-input-foot { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
||||
.chat-input-foot .hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-40); letter-spacing: .02em; }
|
||||
.chat-input-foot .spacer { flex: 1; }
|
||||
.chat-icon-btn {
|
||||
width: 28px; height: 28px; display: grid; place-items: center;
|
||||
background: transparent; border: 1px solid var(--border-faint);
|
||||
border-radius: 50%; color: var(--black-alpha-56); cursor: pointer;
|
||||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.chat-icon-btn:hover { border-color: var(--accent-black); color: var(--accent-black); }
|
||||
.chat-send-btn {
|
||||
width: 32px; height: 32px; display: grid; place-items: center;
|
||||
background: var(--accent-black); border: 1px solid var(--accent-black);
|
||||
border-radius: 50%; color: var(--accent-white); cursor: pointer;
|
||||
transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.chat-send-btn:hover { background: var(--heat); border-color: var(--heat); }
|
||||
.chat-send-btn:active { transform: scale(.95); }
|
||||
.chat-send-btn:disabled { background: var(--black-alpha-12); border-color: var(--black-alpha-12); color: var(--black-alpha-40); cursor: not-allowed; transform: none; }
|
||||
.chat-attach-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||
.chat-attach-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 3px 6px 3px 8px; background: var(--surface);
|
||||
border: 1px solid var(--border-faint); border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono); font-size: 11px; color: var(--accent-black);
|
||||
}
|
||||
.chat-attach-chip .x { width: 14px; height: 14px; display: grid; place-items: center; background: transparent; border: none; color: var(--black-alpha-48); cursor: pointer; border-radius: 50%; }
|
||||
.chat-attach-chip .x:hover { background: var(--black-alpha-08); color: var(--accent-black); }
|
||||
|
||||
.shot-list { display: flex; flex-direction: column; }
|
||||
.shots-body { padding: 12px 16px; flex: 1; overflow-y: auto; max-height: 540px; display: flex; flex-direction: column; gap: 0; }
|
||||
@ -193,20 +235,195 @@
|
||||
display: grid; place-items: center; overflow: hidden;
|
||||
}
|
||||
.prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }
|
||||
.prod-preview-history .h-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
|
||||
/* 已采用版本:主橙描边 + 「已采用」徽标 */
|
||||
.prod-preview-history .h-thumb.adopted { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
|
||||
/* 仅预览(未采用):黑色描边,无徽标 */
|
||||
.prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }
|
||||
.prod-preview-history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
|
||||
.prod-preview-history .h-thumb.active .v { color: var(--heat); font-weight: 600; }
|
||||
.prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }
|
||||
.prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }
|
||||
.prod-preview-history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
|
||||
.prod-preview-history .h-thumb.active .badge { display: block; }
|
||||
.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); }
|
||||
.prod-preview-history .h-thumb.adopted .badge { display: block; }
|
||||
|
||||
/* 「已采用」状态 · 浅橙 + 主橙文字,与已采用徽标视觉呼应 */
|
||||
#prod-preview-adopt:disabled,
|
||||
#prod-preview-adopt:disabled:hover {
|
||||
color: var(--heat);
|
||||
border-color: var(--heat-40);
|
||||
background: var(--heat-12);
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主图可点击放大 */
|
||||
.prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }
|
||||
.prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }
|
||||
.prod-preview-img.is-zoomable::after {
|
||||
content: '';
|
||||
position: absolute; top: 8px; right: 8px;
|
||||
width: 22px; height: 22px;
|
||||
background: rgba(21,20,15,.72) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='M21 21l-4.3-4.3M8 11h6M11 8v6'/></svg>") center/14px no-repeat;
|
||||
border-radius: var(--r-sm);
|
||||
opacity: 0; transition: opacity var(--t-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
.prod-preview-img.is-zoomable:hover::after { opacity: 1; }
|
||||
|
||||
/* 三视图放大查看 lightbox */
|
||||
#tri-lightbox-bg { z-index: 80; }
|
||||
#tri-lightbox-bg .tri-lightbox {
|
||||
position: relative;
|
||||
width: min(1100px, 92vw);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 18px 20px 20px;
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,.24);
|
||||
}
|
||||
.tri-lightbox-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
|
||||
color: var(--black-alpha-56);
|
||||
padding-right: 32px;
|
||||
}
|
||||
.tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }
|
||||
.tri-lightbox-head .lb-tag {
|
||||
margin-left: 6px;
|
||||
padding: 2px 6px;
|
||||
background: var(--heat-12); color: var(--heat);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.tri-lightbox-close {
|
||||
position: absolute;
|
||||
top: 12px; right: 12px;
|
||||
width: 28px; height: 28px;
|
||||
display: grid; place-items: center;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
z-index: 2;
|
||||
}
|
||||
.tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }
|
||||
.tri-lightbox-close svg { width: 14px; height: 14px; }
|
||||
.tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }
|
||||
.tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
|
||||
.tri-lightbox-foot .spc { flex: 1; }
|
||||
.tri-lightbox-foot kbd {
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--black-alpha-72);
|
||||
}
|
||||
.asset-card-2 { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); overflow: hidden; display: flex; flex-direction: column; }
|
||||
.asset-card-2:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
|
||||
.asset-card-2 .thumb-2 { aspect-ratio: 1; }
|
||||
.asset-card-2 .body-2 { padding: 12px 14px; }
|
||||
.asset-card-2 .body-2 .btn-apply { background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); }
|
||||
.asset-card-2 .body-2 .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
|
||||
|
||||
/* stage2 商品卡 · 与商品库 .product-card 视觉一致 */
|
||||
.asset-card-2.prod-lib-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
|
||||
.asset-card-2.prod-lib-card .prod-thumb { aspect-ratio: 1.4 / 1; position: relative; }
|
||||
.asset-card-2.prod-lib-card .prod-body { padding: 14px 14px 12px; flex: 1; }
|
||||
.asset-card-2.prod-lib-card .prod-name {
|
||||
font-size: 14px; font-weight: 600;
|
||||
color: var(--accent-black);
|
||||
line-height: 1.3;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-cat {
|
||||
display: inline-flex; align-items: center;
|
||||
margin-top: 8px;
|
||||
padding: 2px 8px;
|
||||
background: var(--background-lighter);
|
||||
color: var(--black-alpha-72);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--black-alpha-48);
|
||||
margin-top: 10px;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
font-size: 11.5px;
|
||||
color: var(--black-alpha-56);
|
||||
background: var(--background-base);
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-footer .stat {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: .02em;
|
||||
white-space: nowrap;
|
||||
justify-self: center;
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-footer .stat svg { width: 13px; height: 13px; color: var(--black-alpha-48); flex-shrink: 0; }
|
||||
.asset-card-2.prod-lib-card .prod-footer .stat b { color: var(--accent-black); font-weight: 600; }
|
||||
.asset-card-2.prod-lib-card .prod-footer .sep { color: var(--black-alpha-24); font-family: var(--font-mono); flex-shrink: 0; }
|
||||
.asset-card-2.prod-lib-card .prod-action {
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
background: var(--surface);
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-action[hidden] { display: none; }
|
||||
.asset-card-2.prod-lib-card .prod-action .btn-aigen {
|
||||
width: 100%;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
height: 34px; padding: 0 14px;
|
||||
background: var(--heat);
|
||||
color: var(--accent-white);
|
||||
border: 1px solid var(--heat);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 13px; font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
box-shadow:
|
||||
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
|
||||
0 1px 1px rgba(250, 93, 25, 0.12),
|
||||
0 2px 4px rgba(250, 93, 25, 0.10);
|
||||
transition: background var(--t-base), box-shadow var(--t-base), transform var(--t-base);
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-action .btn-aigen:hover {
|
||||
background: #FB6E2E;
|
||||
box-shadow:
|
||||
inset 0 -2px 4px rgba(250, 93, 25, 0.24),
|
||||
0 2px 4px rgba(250, 93, 25, 0.20),
|
||||
0 4px 12px rgba(250, 93, 25, 0.18);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-action .btn-aigen:active { transform: translateY(0); }
|
||||
.asset-card-2.prod-lib-card .prod-action .btn-aigen:disabled {
|
||||
opacity: .65; cursor: not-allowed; transform: none;
|
||||
box-shadow: inset 0 -2px 4px rgba(250, 93, 25, 0.20);
|
||||
}
|
||||
.asset-card-2.prod-lib-card .prod-action .btn-aigen .ai-spark {
|
||||
width: 14px; height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 通用资产详情 modal · 参考布局 v2 */
|
||||
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
|
||||
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1010; display: none; align-items: center; justify-content: center; padding: 40px; }
|
||||
.asset-modal-bg.show { display: flex; }
|
||||
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
|
||||
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
|
||||
@ -393,16 +610,57 @@
|
||||
.ml-toolbar .chip:hover { color: var(--accent-black); }
|
||||
.ml-toolbar .chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); font-weight: 600; }
|
||||
.ml-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }
|
||||
.ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
|
||||
.ml-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px; cursor: pointer; transition: border-color var(--t-base), box-shadow var(--t-base); display: flex; flex-direction: column; gap: 8px; }
|
||||
.ml-card:hover { border-color: var(--heat-40); box-shadow: 0 1px 3px rgba(0,0,0,.04); }
|
||||
.ml-card .placeholder { aspect-ratio: 3/4; }
|
||||
.ml-card .ml-card-nm { font-size: 13px; font-weight: 500; color: var(--accent-black); }
|
||||
.ml-card .ml-card-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); }
|
||||
.ml-card .ml-card-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; }
|
||||
.ml-card .ml-card-foot .pill { font-size: 10.5px; padding: 1px 7px; }
|
||||
.ml-card .ml-card-foot .btn-apply { margin-left: auto; height: 26px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; }
|
||||
.ml-card .ml-card-foot .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
|
||||
/* 卡片 · 视觉对齐 model-photo .model-card (padding 8 / gap 6 / 无 foot 行) */
|
||||
.ml-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; }
|
||||
.ml-card {
|
||||
position: relative;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
transition: background var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.ml-card:hover { background: var(--surface); }
|
||||
.ml-card .placeholder { aspect-ratio: 3/4; border-radius: var(--r-sm); }
|
||||
.ml-card .ml-card-nm { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }
|
||||
.ml-card .ml-card-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
|
||||
/* 「添加演员/场景」入口卡 · 与 model-photo 模特库视觉一致 */
|
||||
.ml-card.ml-upload-card { border: 1.5px dashed var(--black-alpha-24); background: var(--surface); display: flex; flex-direction: column; gap: 8px; transition: border-color var(--t-base), background var(--t-base); }
|
||||
.ml-card.ml-upload-card:hover { border-color: var(--heat); background: var(--heat-12); box-shadow: none; }
|
||||
.ml-card.ml-upload-card:focus-visible { outline: 2px solid var(--heat); outline-offset: 2px; }
|
||||
.ml-card.ml-upload-card .up-thumb { aspect-ratio: 3/4; border-radius: var(--r-sm); background: transparent; display: grid; place-items: center; }
|
||||
.ml-card.ml-upload-card .up-plus { width: 44px; height: 44px; border-radius: 50%; background: var(--surface); border: 1px solid var(--black-alpha-12); color: var(--black-alpha-56); display: grid; place-items: center; transition: background var(--t-base), color var(--t-base), border-color var(--t-base), transform var(--t-base); }
|
||||
.ml-card.ml-upload-card:hover .up-plus { background: var(--heat); border-color: var(--heat); color: var(--accent-white); transform: scale(1.06); }
|
||||
.ml-card.ml-upload-card .up-plus svg { width: 22px; height: 22px; }
|
||||
.ml-card.ml-upload-card .ml-card-nm { color: var(--accent-black); }
|
||||
.ml-card.ml-upload-card:hover .ml-card-nm { color: var(--heat); }
|
||||
|
||||
/* ─── 添加来源 · 选择 modal (AI 生成 / 本地上传) ─── */
|
||||
.ml-up-choice-bg { position: fixed; inset: 0; z-index: 1200; background: rgba(21, 20, 15, .42); display: none; place-items: center; padding: 16px; }
|
||||
.ml-up-choice-bg.show { display: grid; }
|
||||
.ml-up-choice { width: min(560px, 92vw); background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); box-shadow: 0 16px 48px rgba(21, 20, 15, .18); overflow: hidden; position: relative; }
|
||||
.ml-up-choice .uc-h { display: flex; align-items: center; gap: 12px; padding: 18px 22px 14px; border-bottom: 1px solid var(--border-faint); }
|
||||
.ml-up-choice .uc-h .ic-m { width: 36px; height: 36px; border-radius: var(--r-md); background: var(--heat-12); color: var(--heat); display: grid; place-items: center; flex-shrink: 0; }
|
||||
.ml-up-choice .uc-h .ic-m svg { width: 18px; height: 18px; }
|
||||
.ml-up-choice .uc-h .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||
.ml-up-choice .uc-h .ti strong { font-size: 15px; color: var(--accent-black); font-weight: 600; }
|
||||
.ml-up-choice .uc-h .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||||
.ml-up-choice .uc-h .uc-x { margin-left: auto; width: 28px; height: 28px; background: transparent; border: 0; border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; display: grid; place-items: center; }
|
||||
.ml-up-choice .uc-h .uc-x:hover { background: var(--background-lighter); color: var(--accent-black); }
|
||||
.ml-up-choice .uc-h .uc-x svg { width: 14px; height: 14px; }
|
||||
.ml-up-choice .uc-body { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 20px 22px 22px; }
|
||||
.ml-up-choice .uc-option { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px 16px; text-align: left; cursor: pointer; font-family: inherit; display: flex; flex-direction: column; gap: 10px; transition: border-color var(--t-base), background var(--t-base); }
|
||||
.ml-up-choice .uc-option:hover { border-color: var(--heat); background: var(--heat-12); }
|
||||
.ml-up-choice .uc-option .opt-ic { width: 40px; height: 40px; border-radius: var(--r-md); background: var(--background-lighter); color: var(--heat); border: 1px solid var(--heat-20); display: grid; place-items: center; transition: background var(--t-base), color var(--t-base); }
|
||||
.ml-up-choice .uc-option:hover .opt-ic { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
|
||||
.ml-up-choice .uc-option .opt-ic svg { width: 18px; height: 18px; }
|
||||
.ml-up-choice .uc-option .opt-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }
|
||||
.ml-up-choice .uc-option .opt-d { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-56); letter-spacing: .02em; line-height: 1.55; }
|
||||
.ml-up-choice .uc-option .opt-tag { margin-top: auto; align-self: flex-start; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); color: var(--black-alpha-72); letter-spacing: .04em; }
|
||||
.ml-up-choice .uc-option:hover .opt-tag { background: var(--surface); color: var(--heat); }
|
||||
|
||||
/* 新增人物 modal · 立绘 + 三视图 上传区 */
|
||||
.upload-zone { aspect-ratio: 3/4; background: var(--background-lighter); border: 1px dashed var(--border-faint); border-radius: var(--r-md); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; cursor: pointer; transition: border-color var(--t-base), background var(--t-base); padding: 16px; text-align: center; color: var(--black-alpha-56); font-size: 12px; }
|
||||
@ -586,6 +844,8 @@
|
||||
<div class="pane-h">
|
||||
<strong>镜头脚本</strong>
|
||||
<span class="muted-2 mono" id="shots-meta" style="font-size:11px;">· 空 · 待生成</span>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn btn-ghost btn-sm" id="chat-regen-btn">↻ 整体重写</button>
|
||||
</div>
|
||||
<div class="shots-body" id="shots-body">
|
||||
<!-- JS 注入空态/镜头卡片 -->
|
||||
@ -604,13 +864,21 @@
|
||||
<!-- JS 注入空态/对话内容 -->
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<textarea class="textarea" id="chat-textarea" placeholder="对脚本的修改诉求 · 比如:让第 3 场更夸张一点、整体加一场结尾……" rows="2"></textarea>
|
||||
<div class="hstack" style="margin-top:8px;">
|
||||
<button class="btn btn-ghost btn-sm" id="chat-regen-btn">↻ 整体重写</button>
|
||||
<div class="chat-input-card">
|
||||
<div class="chat-attach-row" id="chat-attach-row" hidden></div>
|
||||
<textarea class="chat-input-area" id="chat-textarea" placeholder='聊聊你的脚本想法,或输入 "@" 引用镜头……' rows="2"></textarea>
|
||||
<div class="chat-input-foot">
|
||||
<button class="chat-icon-btn" id="chat-upload-btn" title="上传脚本附件" aria-label="上传脚本附件">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn btn-primary" id="chat-send-btn">发送 ⌘↵</button>
|
||||
<button class="chat-send-btn" id="chat-send-btn" title="发送" aria-label="发送">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="chat-upload-input" hidden accept=".txt,.md,.docx,.doc,.pdf,.srt,.json">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -647,14 +915,35 @@
|
||||
<span class="spacer"></span>
|
||||
</div>
|
||||
<div class="prod-row">
|
||||
<div class="asset-card-2" data-asset-kind="product" data-asset-id="prod-main" id="asset-prod-card">
|
||||
<div class="placeholder thumb-2"><span class="ph-frame" id="asset-prod-thumb-label">透真补水面膜 · 主图</span></div>
|
||||
<div class="body-2">
|
||||
<div class="hstack"><strong style="font-size:13.5px;" id="asset-prod-card-name">透真补水面膜</strong><span class="spacer"></span><span class="pill info" id="asset-prod-pill"><span class="dot"></span>缺三视图</span></div>
|
||||
<div class="hstack" style="margin-top:10px;">
|
||||
<span class="spacer"></span>
|
||||
<button class="btn btn-sm btn-apply" data-stop id="asset-prod-aigen-btn" style="background: var(--heat); color: var(--accent-white); border-color: var(--heat);">AI 生成三视图</button>
|
||||
<div class="asset-card-2 prod-lib-card" data-asset-kind="product" data-asset-id="prod-main" id="asset-prod-card">
|
||||
<div class="placeholder prod-thumb">
|
||||
<span class="tri-missing-badge" id="asset-prod-tri-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||||
<span class="ico" aria-hidden="true"></span>
|
||||
<span class="lbl-mono">缺三视图</span>
|
||||
<span class="tri-missing-pop" role="tooltip">
|
||||
<span class="pop-h">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
MISSING TRI-VIEW
|
||||
</span>
|
||||
<span class="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
|
||||
<span class="pop-tip">建议:点右下 <b>AI 生成三视图</b> 先补齐三视图,再发起后续生成。</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ph-frame" id="asset-prod-thumb-label">透真补水面膜 · 主图</span>
|
||||
</div>
|
||||
<div class="prod-body">
|
||||
<div class="prod-name" id="asset-prod-card-name">透真补水面膜</div>
|
||||
<div class="prod-cat">美妆个护</div>
|
||||
<div class="prod-date">2026-05-15 创建</div>
|
||||
</div>
|
||||
<div class="prod-action" id="asset-prod-action">
|
||||
<button class="btn-aigen" type="button" data-stop id="asset-prod-aigen-btn">
|
||||
<svg class="ai-spark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3z"/>
|
||||
<path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7L19 14z"/>
|
||||
</svg>
|
||||
AI 生成三视图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prod-preview" id="asset-prod-preview">
|
||||
@ -1191,6 +1480,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 添加演员 / 场景 · 选择来源 modal ===== -->
|
||||
<div class="ml-up-choice-bg" id="ml-up-choice-bg">
|
||||
<div class="ml-up-choice" role="dialog" aria-label="添加来源">
|
||||
<div class="uc-h">
|
||||
<div class="ic-m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/><path d="M19 8v6M22 11h-6"/></svg>
|
||||
</div>
|
||||
<div class="ti">
|
||||
<strong id="ml-up-title">添加</strong>
|
||||
<span class="mono">// 选择来源 · AI 生成或本地上传</span>
|
||||
</div>
|
||||
<button class="uc-x" type="button" id="ml-up-x" aria-label="关闭">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="uc-body">
|
||||
<button type="button" class="uc-option" id="ml-up-ai">
|
||||
<span class="opt-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>
|
||||
</span>
|
||||
<div class="opt-t">AI 生成</div>
|
||||
<div class="opt-d" id="ml-up-ai-desc">描述外形 + 风格,AI 自动生成新形象与三视图</div>
|
||||
<span class="opt-tag">[ AI · STUDIO ]</span>
|
||||
</button>
|
||||
<button type="button" class="uc-option" id="ml-up-local">
|
||||
<span class="opt-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||
</span>
|
||||
<div class="opt-t">本地上传</div>
|
||||
<div class="opt-d" id="ml-up-local-desc">上传真人 / 既有素材,后续可生成三视图统一镜头</div>
|
||||
<span class="opt-tag">[ UPLOAD ]</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="ml-up-file" accept="image/*" multiple hidden>
|
||||
|
||||
<!-- ===== 额度预检 modal · PRD §10.3 四层预检 ===== -->
|
||||
<div class="modal-bg" id="quota-bg" onclick="if(event.target===this)Shell.closeModal('quota-bg')">
|
||||
<div class="modal" id="quota-modal" style="width: min(440px, 92vw);">
|
||||
@ -1466,12 +1792,50 @@ const Stage1 = (function () {
|
||||
});
|
||||
const sendBtn = document.getElementById('chat-send-btn');
|
||||
const ta = document.getElementById('chat-textarea');
|
||||
const attachRow = document.getElementById('chat-attach-row');
|
||||
let attachments = [];
|
||||
const renderAttach = () => {
|
||||
if (!attachRow) return;
|
||||
if (!attachments.length) { attachRow.hidden = true; attachRow.innerHTML = ''; return; }
|
||||
attachRow.hidden = false;
|
||||
attachRow.innerHTML = attachments.map((f, i) => `
|
||||
<span class="chat-attach-chip">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
|
||||
${f.name.replace(/</g, '<')}
|
||||
<button class="x" data-rm="${i}" aria-label="移除">
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</span>`).join('');
|
||||
attachRow.querySelectorAll('button[data-rm]').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
attachments.splice(+b.dataset.rm, 1);
|
||||
renderAttach();
|
||||
});
|
||||
});
|
||||
};
|
||||
const upBtn = document.getElementById('chat-upload-btn');
|
||||
const upInput = document.getElementById('chat-upload-input');
|
||||
if (upBtn && upInput) {
|
||||
upBtn.addEventListener('click', () => upInput.click());
|
||||
upInput.addEventListener('change', (e) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (!files.length) return;
|
||||
attachments.push(...files);
|
||||
renderAttach();
|
||||
Shell.toast('已附加脚本文件', files.map(f => f.name).join('、'));
|
||||
upInput.value = '';
|
||||
});
|
||||
}
|
||||
if (sendBtn && ta) {
|
||||
const send = () => {
|
||||
const v = ta.value.trim();
|
||||
if (!v) return;
|
||||
pushMsg('user', v.replace(/</g, '<'));
|
||||
if (!v && !attachments.length) return;
|
||||
const fileTags = attachments.length
|
||||
? `<div class="hstack" style="gap:6px; flex-wrap:wrap; margin-bottom:6px;">${attachments.map(f => `<span class="pill" style="font-family:var(--font-mono); font-size:10.5px;">📎 ${f.name.replace(/</g, '<')}</span>`).join('')}</div>`
|
||||
: '';
|
||||
pushMsg('user', fileTags + (v ? v.replace(/</g, '<') : '<span class="muted-2">(已附加文件)</span>'));
|
||||
ta.value = '';
|
||||
attachments = []; renderAttach();
|
||||
renderChat();
|
||||
setTimeout(() => {
|
||||
pushMsg('ai', '收到。我会按这个方向调整脚本(静态演示;实际接 LLM API)。');
|
||||
@ -1688,30 +2052,164 @@ const Stage2 = (function () {
|
||||
applyLabel: '应用到当前项目',
|
||||
});
|
||||
}
|
||||
// 用户上传的演员 / 场景(分别累积,source='own')
|
||||
const MODEL_OWN = [];
|
||||
const SCENE_OWN = [];
|
||||
let _curLibKind = 'model';
|
||||
let _curLibSource = 'all';
|
||||
function _libItemsForSource(kind, src) {
|
||||
const isModel = kind === 'model';
|
||||
const presets = isModel ? MODEL_LIB : SCENE_LIB;
|
||||
const owns = isModel ? MODEL_OWN : SCENE_OWN;
|
||||
if (src === 'preset') return presets;
|
||||
if (src === 'own') return owns;
|
||||
return [...owns, ...presets];
|
||||
}
|
||||
function _renderLibGrid() {
|
||||
const isModel = _curLibKind === 'model';
|
||||
const items = _libItemsForSource(_curLibKind, _curLibSource);
|
||||
const grid = document.getElementById('ml-grid');
|
||||
|
||||
// 「添加演员 / 添加场景」入口卡 · 平台预设是只读素材,不展示入口
|
||||
const uploadCardHTML = (_curLibSource === 'preset') ? '' : `
|
||||
<div class="ml-card ml-upload-card" id="ml-upload-card" role="button" tabindex="0" aria-label="${isModel ? '添加演员' : '添加场景'}">
|
||||
<div class="up-thumb">
|
||||
<div class="up-plus">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-card-nm">${isModel ? '添加演员' : '添加场景'}</div>
|
||||
<div class="ml-card-sub">// AI 生成 / 本地上传</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
grid.innerHTML = uploadCardHTML + items.map(it => `
|
||||
<div class="ml-card" data-name="${it.name}" data-sub="${it.sub}">
|
||||
<div class="placeholder"><span class="ph-frame">${it.name}</span></div>
|
||||
<div class="ml-card-nm">${it.name}</div>
|
||||
<div class="ml-card-sub">${it.sub}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const upCard = grid.querySelector('#ml-upload-card');
|
||||
if (upCard) {
|
||||
upCard.addEventListener('click', () => _openLibUploadChoice());
|
||||
upCard.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _openLibUploadChoice(); }
|
||||
});
|
||||
}
|
||||
|
||||
// 普通卡片 click → 应用 / 详情
|
||||
grid.querySelectorAll('.ml-card:not(.ml-upload-card)').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
const name = card.dataset.name;
|
||||
const sub = card.dataset.sub;
|
||||
if (e.target.closest('[data-apply]')) {
|
||||
e.stopPropagation();
|
||||
Shell.toast('已应用「' + name + '」', isModel ? '演员库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设') : '场景库 · 来自' + (_curLibSource === 'own' ? '我的上传' : '预设'));
|
||||
document.getElementById('ml-modal-bg').classList.remove('show');
|
||||
return;
|
||||
}
|
||||
openStripDetail(name, sub, _curLibKind);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ─── 添加演员/场景 · 选择来源 modal ─── */
|
||||
function _openLibUploadChoice() {
|
||||
const isModel = _curLibKind === 'model';
|
||||
document.getElementById('ml-up-title').textContent = isModel ? '添加演员' : '添加场景';
|
||||
document.getElementById('ml-up-ai-desc').textContent = isModel
|
||||
? '描述外形 + 风格,AI 自动生成新演员形象与三视图'
|
||||
: '描述类型 + 氛围,AI 自动生成新场景图与三视图';
|
||||
document.getElementById('ml-up-local-desc').textContent = isModel
|
||||
? '上传真人 / 既有演员素材,后续可生成三视图统一镜头'
|
||||
: '上传商家自有场景图,后续可生成三视图统一镜头';
|
||||
document.getElementById('ml-up-choice-bg').classList.add('show');
|
||||
}
|
||||
function _closeLibUploadChoice() {
|
||||
document.getElementById('ml-up-choice-bg').classList.remove('show');
|
||||
}
|
||||
(function _bindLibUploadChoice() {
|
||||
const bg = document.getElementById('ml-up-choice-bg');
|
||||
if (!bg) return;
|
||||
document.getElementById('ml-up-x').addEventListener('click', _closeLibUploadChoice);
|
||||
bg.addEventListener('click', e => { if (e.target === bg) _closeLibUploadChoice(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && bg.classList.contains('show')) _closeLibUploadChoice();
|
||||
});
|
||||
|
||||
// AI 生成 → 待新增独立工作台,先 toast 不跳转
|
||||
document.getElementById('ml-up-ai').addEventListener('click', () => {
|
||||
const isModel = _curLibKind === 'model';
|
||||
_closeLibUploadChoice();
|
||||
Shell.toast(isModel ? 'AI 演员工作台' : 'AI 场景工作台', '页面待新增 · 暂未跳转');
|
||||
});
|
||||
|
||||
// 本地上传 → 触发文件选择
|
||||
const fileInput = document.getElementById('ml-up-file');
|
||||
document.getElementById('ml-up-local').addEventListener('click', () => {
|
||||
_closeLibUploadChoice();
|
||||
fileInput.click();
|
||||
});
|
||||
fileInput.addEventListener('change', e => {
|
||||
const files = [...(e.target.files || [])].filter(f => /^image\//.test(f.type));
|
||||
if (!files.length) return;
|
||||
const isModel = _curLibKind === 'model';
|
||||
const ownsArr = isModel ? MODEL_OWN : SCENE_OWN;
|
||||
files.forEach((f, i) => {
|
||||
const baseName = (f.name || (isModel ? '我的演员' : '我的场景')).replace(/\.[^.]+$/, '').slice(0, 12);
|
||||
ownsArr.unshift({
|
||||
id: 'up-' + Date.now().toString(36) + i,
|
||||
name: baseName,
|
||||
sub: isModel ? '我的上传 · 待生成三视图' : '我的上传 · 待生成三视图',
|
||||
source: 'own',
|
||||
});
|
||||
});
|
||||
e.target.value = '';
|
||||
// 切到「我的上传」让用户立即看到
|
||||
_curLibSource = 'own';
|
||||
const side = document.getElementById('ml-side');
|
||||
side.querySelectorAll('.ml-side-item').forEach(x => x.classList.toggle('active', x.dataset.source === 'own'));
|
||||
// 更新左侧计数
|
||||
const ownCt = side.querySelector('.ml-side-item[data-source="own"] .ct');
|
||||
if (ownCt) ownCt.textContent = ownsArr.length;
|
||||
const allCt = side.querySelector('.ml-side-item[data-source="all"] .ct');
|
||||
if (allCt) allCt.textContent = ownsArr.length + (isModel ? MODEL_LIB.length : SCENE_LIB.length);
|
||||
_renderLibGrid();
|
||||
Shell.toast('已上传', `+ ${files.length} 张 · 来源 我的上传`);
|
||||
});
|
||||
})();
|
||||
|
||||
function openLib(kind) {
|
||||
_curLibKind = kind;
|
||||
_curLibSource = 'all';
|
||||
const isModel = kind === 'model';
|
||||
const title = isModel ? '演员库' : '场景库';
|
||||
const items = isModel ? MODEL_LIB : SCENE_LIB;
|
||||
const presets = isModel ? MODEL_LIB : SCENE_LIB;
|
||||
const owns = isModel ? MODEL_OWN : SCENE_OWN;
|
||||
|
||||
document.getElementById('ml-modal-title').textContent = title;
|
||||
document.getElementById('ml-modal-ct').textContent = '// 共 ' + items.length + ' 个预设';
|
||||
document.getElementById('ml-modal-ct').textContent = '// 共 ' + (presets.length + owns.length) + ' 个';
|
||||
|
||||
// 侧栏 · 来源
|
||||
const side = document.getElementById('ml-side');
|
||||
side.innerHTML = `
|
||||
<div class="ml-side-h">来源</div>
|
||||
<div class="ml-side-item active" data-source="all">全部 <span class="ct">${items.length}</span></div>
|
||||
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">${items.length}</span></div>
|
||||
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">0</span></div>
|
||||
<div class="ml-side-item active" data-source="all">全部 <span class="ct">${presets.length + owns.length}</span></div>
|
||||
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">${presets.length}</span></div>
|
||||
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">${owns.length}</span></div>
|
||||
`;
|
||||
side.querySelectorAll('.ml-side-item').forEach(it => {
|
||||
it.addEventListener('click', () => {
|
||||
side.querySelectorAll('.ml-side-item').forEach(x => x.classList.remove('active'));
|
||||
it.classList.add('active');
|
||||
_curLibSource = it.dataset.source;
|
||||
_renderLibGrid();
|
||||
});
|
||||
});
|
||||
|
||||
// toolbar · chip groups + 上传按钮
|
||||
// toolbar · chip groups (去掉了 btn-up 上传按钮,改用网格内入口卡)
|
||||
const toolbar = document.getElementById('ml-toolbar');
|
||||
if (isModel) {
|
||||
toolbar.innerHTML = `
|
||||
@ -1727,10 +2225,6 @@ const Stage2 = (function () {
|
||||
<button class="chip" type="button">青年</button>
|
||||
<button class="chip" type="button">中年</button>
|
||||
</div>
|
||||
<button class="btn-up" type="button" style="margin-left:auto">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||||
上传我的演员
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
toolbar.innerHTML = `
|
||||
@ -1746,10 +2240,6 @@ const Stage2 = (function () {
|
||||
<button class="chip" type="button">日</button>
|
||||
<button class="chip" type="button">夜</button>
|
||||
</div>
|
||||
<button class="btn-up" type="button" style="margin-left:auto">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||||
上传我的场景
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
toolbar.querySelectorAll('.chip-group').forEach(group => {
|
||||
@ -1760,36 +2250,9 @@ const Stage2 = (function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
toolbar.querySelector('.btn-up')?.addEventListener('click', () => {
|
||||
Shell.toast('上传我的' + (isModel ? '演员' : '场景'), '占位 · 选择本地文件');
|
||||
});
|
||||
|
||||
// 卡片网格
|
||||
const grid = document.getElementById('ml-grid');
|
||||
grid.innerHTML = items.map(it => `
|
||||
<div class="ml-card" data-name="${it.name}" data-sub="${it.sub}">
|
||||
<div class="placeholder"><span class="ph-frame">${it.name}</span></div>
|
||||
<div class="ml-card-nm">${it.name}</div>
|
||||
<div class="ml-card-sub">${it.sub}</div>
|
||||
<div class="ml-card-foot">
|
||||
<span class="pill info"><span class="dot"></span>预设</span>
|
||||
<button class="btn-apply" type="button" data-apply>应用</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
grid.querySelectorAll('.ml-card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
const name = card.dataset.name;
|
||||
const sub = card.dataset.sub;
|
||||
if (e.target.closest('[data-apply]')) {
|
||||
e.stopPropagation();
|
||||
Shell.toast('已应用「' + name + '」', isModel ? '演员库 · 来自预设' : '场景库 · 来自预设');
|
||||
document.getElementById('ml-modal-bg').classList.remove('show');
|
||||
return;
|
||||
}
|
||||
openStripDetail(name, sub, kind);
|
||||
});
|
||||
});
|
||||
// 卡片网格(含 + 入口 + apply 绑定都在 _renderLibGrid 内完成)
|
||||
_renderLibGrid();
|
||||
|
||||
document.getElementById('ml-modal-bg').classList.add('show');
|
||||
}
|
||||
@ -1860,11 +2323,12 @@ const Stage2 = (function () {
|
||||
el.addEventListener('click', () => bg.classList.remove('show'));
|
||||
});
|
||||
});
|
||||
// 详情 modal · 应用
|
||||
// 详情 modal · 应用 → 关详情 + 关演员/场景库 → 回到项目页
|
||||
document.getElementById('asset-detail-apply-btn')?.addEventListener('click', () => {
|
||||
const name = document.getElementById('asset-detail-title').textContent;
|
||||
Shell.toast('已应用「' + name + '」', '已加入当前项目');
|
||||
document.getElementById('asset-detail-modal').classList.remove('show');
|
||||
document.getElementById('ml-modal-bg')?.classList.remove('show');
|
||||
Shell.toast('已应用「' + name + '」', '已加入当前项目');
|
||||
});
|
||||
// 详情 modal · AI 生成三视图
|
||||
document.querySelectorAll('.ai-gen-btn').forEach(btn => {
|
||||
@ -1892,21 +2356,24 @@ const Stage2 = (function () {
|
||||
});
|
||||
const thumbLbl = document.getElementById('asset-prod-thumb-label');
|
||||
if (thumbLbl) thumbLbl.textContent = CURRENT_PRODUCT_NAME + ' · 主图';
|
||||
// 商品卡 · AI 生成三视图 → 右侧 prod-preview 显示单张 16:9 三视图 + 历史版本
|
||||
// 商品卡 · AI 生成三视图 → 右侧 prod-preview · 预览/采用 双状态 + 点击主图放大
|
||||
(function setupProdPreview() {
|
||||
const aigenBtn = document.getElementById('asset-prod-aigen-btn');
|
||||
const pane = document.getElementById('asset-prod-preview');
|
||||
const img = document.getElementById('prod-preview-img');
|
||||
const statusEl = document.getElementById('prod-preview-status');
|
||||
const foot = document.getElementById('prod-preview-foot');
|
||||
const pill = document.getElementById('asset-prod-pill');
|
||||
const triBadge = document.getElementById('asset-prod-tri-badge');
|
||||
const prodAction = document.getElementById('asset-prod-action');
|
||||
const history = document.getElementById('prod-preview-history');
|
||||
const historyRow = document.getElementById('prod-preview-history-row');
|
||||
const historyCount = document.getElementById('prod-preview-history-count');
|
||||
if (!aigenBtn || !pane || !img || !statusEl || !foot || !history || !historyRow) return;
|
||||
|
||||
const versions = []; // [{ ts, label }]
|
||||
let currentIdx = -1; // 当前选中的版本:主图显示 + 缩略图高亮 + 商品资产即应用此版
|
||||
let previewIdx = -1; // 主图正在「预览」哪一版(浏览态,不动采用状态)
|
||||
let adoptedIdx = -1; // 真正被「采用」的那一版,决定商品资产生效版本
|
||||
let generating = false;
|
||||
|
||||
function prodName() {
|
||||
return CURRENT_PRODUCT_NAME || (document.getElementById('asset-prod-card-name')?.textContent ?? '商品');
|
||||
@ -1919,43 +2386,73 @@ const Stage2 = (function () {
|
||||
}
|
||||
history.classList.add('show');
|
||||
historyCount.textContent = versions.length;
|
||||
historyRow.innerHTML = versions.map((ver, i) => `
|
||||
<div class="h-thumb${i === currentIdx ? ' active' : ''}" data-idx="${i}" title="${ver.label} · ${ver.ts}${i === currentIdx ? ' · 当前应用' : ''}">
|
||||
<span class="badge">当前</span>
|
||||
historyRow.innerHTML = versions.map((ver, i) => {
|
||||
const isAdopted = i === adoptedIdx;
|
||||
const isPreview = i === previewIdx;
|
||||
const cls = [
|
||||
isAdopted ? 'adopted' : '',
|
||||
isPreview && !isAdopted ? 'previewing' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
const titleParts = [ver.label, ver.ts];
|
||||
if (isAdopted) titleParts.push('已采用');
|
||||
else if (isPreview) titleParts.push('预览中');
|
||||
return `
|
||||
<div class="h-thumb ${cls}" data-idx="${i}" title="${titleParts.join(' · ')}">
|
||||
<span class="badge">已采用</span>
|
||||
<span class="v">${ver.label}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
historyRow.querySelectorAll('.h-thumb').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = Number(el.dataset.idx);
|
||||
if (idx === currentIdx) return;
|
||||
setCurrent(idx, /* fromClick */ true);
|
||||
if (idx === previewIdx) return;
|
||||
setPreview(idx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderMain() {
|
||||
if (currentIdx < 0) return;
|
||||
const ver = versions[currentIdx];
|
||||
if (previewIdx < 0) return;
|
||||
const ver = versions[previewIdx];
|
||||
const isAdopted = previewIdx === adoptedIdx;
|
||||
img.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
|
||||
statusEl.textContent = `${ver.label} · 已应用,不满意可重跑`;
|
||||
img.classList.add('is-zoomable');
|
||||
img.title = '点击放大查看';
|
||||
statusEl.textContent = isAdopted
|
||||
? `${ver.label} · 已采用,不满意可重跑`
|
||||
: `${ver.label} · 预览中(未采用)`;
|
||||
foot.innerHTML = `
|
||||
<button class="btn btn-ghost btn-sm" id="prod-preview-rerun">↻ 重跑</button>
|
||||
<button class="btn btn-sm ${isAdopted ? '' : 'btn-primary'}" id="prod-preview-adopt" ${isAdopted ? 'disabled title="此版本已采用"' : 'title="将此版本设为商品采用版本,其余转为不通过"'} style="display:inline-flex; align-items:center; gap:4px;">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
${isAdopted ? '已采用' : '采用此版本'}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<span class="muted-2 mono" style="font-size:11px;">~¥0.30 / 次</span>
|
||||
`;
|
||||
document.getElementById('prod-preview-rerun')?.addEventListener('click', start);
|
||||
document.getElementById('prod-preview-adopt')?.addEventListener('click', adoptPreview);
|
||||
}
|
||||
|
||||
function setCurrent(idx, fromClick) {
|
||||
currentIdx = idx;
|
||||
const ver = versions[idx];
|
||||
// 商品状态徽标:选中任意版本即视为已三视图
|
||||
if (pill) {
|
||||
pill.className = 'pill ok';
|
||||
pill.innerHTML = '<span class="dot"></span>已三视图';
|
||||
// 仅切预览主图,不动采用/不动商品资产
|
||||
function setPreview(idx) {
|
||||
previewIdx = idx;
|
||||
renderHistory();
|
||||
renderMain();
|
||||
}
|
||||
// 同步到商品详情 modal 的数据源 ASSET_DETAILS['prod-main']
|
||||
|
||||
// 显式「采用」当前预览版本 · 同步商品资产 + 隐藏缺三视图徽标
|
||||
function adoptPreview() {
|
||||
if (previewIdx < 0) return;
|
||||
if (previewIdx === adoptedIdx) return;
|
||||
adoptedIdx = previewIdx;
|
||||
applyAdoption(/* fromClick */ true);
|
||||
}
|
||||
|
||||
function applyAdoption(fromClick) {
|
||||
const ver = versions[adoptedIdx];
|
||||
if (triBadge) triBadge.hidden = true;
|
||||
const detail = ASSET_DETAILS['prod-main'];
|
||||
if (detail) {
|
||||
detail.hasTri = true;
|
||||
@ -1963,34 +2460,57 @@ const Stage2 = (function () {
|
||||
detail.info = [
|
||||
['类别', '商品 · 当前项目'],
|
||||
['名称', prodName()],
|
||||
['三视图', '已生成 · ' + ver.label],
|
||||
['三视图', '已采用 · ' + ver.label],
|
||||
['状态', '已三视图'],
|
||||
];
|
||||
}
|
||||
renderHistory();
|
||||
renderMain();
|
||||
if (fromClick) Shell.toast('已切换 ' + ver.label, prodName() + ' · 商品资产已更新');
|
||||
if (fromClick) Shell.toast('已采用 ' + ver.label, prodName() + ' · 商品资产已更新为该版本');
|
||||
}
|
||||
|
||||
function renderLoading() {
|
||||
img.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px;align-items:center;"><div class="spinner"></div><span class="ph-frame" style="font-size:10.5px;">生成中</span></div>`;
|
||||
img.classList.remove('is-zoomable');
|
||||
img.removeAttribute('title');
|
||||
statusEl.textContent = '生成中 · 约 12s';
|
||||
foot.innerHTML = '<span class="muted-2 mono" style="font-size:11px;">// POST /assets/tri-view</span>';
|
||||
aigenBtn.disabled = true;
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (generating) return;
|
||||
generating = true;
|
||||
pane.classList.add('show');
|
||||
renderLoading();
|
||||
setTimeout(() => {
|
||||
generating = false;
|
||||
aigenBtn.disabled = false;
|
||||
const now = new Date();
|
||||
const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
||||
versions.push({ ts, label: 'v' + (versions.length + 1) });
|
||||
setCurrent(versions.length - 1, /* fromClick */ false);
|
||||
const newVer = { ts, label: 'v' + (versions.length + 1) };
|
||||
versions.push(newVer);
|
||||
const newIdx = versions.length - 1;
|
||||
previewIdx = newIdx;
|
||||
if (adoptedIdx === -1) {
|
||||
adoptedIdx = newIdx;
|
||||
applyAdoption(/* fromClick */ false);
|
||||
} else {
|
||||
renderHistory();
|
||||
renderMain();
|
||||
Shell.toast('三视图已生成 ' + newVer.label, prodName() + ' · 预览中,满意请点「采用此版本」');
|
||||
}
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
// 主图点击 → 放大查看
|
||||
img.addEventListener('click', (e) => {
|
||||
if (!img.classList.contains('is-zoomable')) return;
|
||||
if (previewIdx < 0) return;
|
||||
e.stopPropagation();
|
||||
openTriLightbox(versions[previewIdx], previewIdx === adoptedIdx, prodName());
|
||||
});
|
||||
|
||||
aigenBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
start();
|
||||
@ -2224,6 +2744,49 @@ window.Quota = (function () {
|
||||
|
||||
return { preflight };
|
||||
})();
|
||||
|
||||
/* ============================================================
|
||||
三视图 · 放大查看 lightbox · setupProdPreview 共用
|
||||
============================================================ */
|
||||
function openTriLightbox(ver, isAdopted, prodName) {
|
||||
let bg = document.getElementById('tri-lightbox-bg');
|
||||
if (!bg) {
|
||||
bg = document.createElement('div');
|
||||
bg.id = 'tri-lightbox-bg';
|
||||
bg.className = 'modal-bg';
|
||||
bg.innerHTML = `
|
||||
<div class="tri-lightbox" role="dialog" aria-label="三视图放大查看">
|
||||
<button class="tri-lightbox-close" type="button" aria-label="关闭">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<div class="tri-lightbox-head">
|
||||
// 三视图(正/侧/背) · <span class="lb-ver" id="tri-lightbox-label">v1</span>
|
||||
<span class="lb-tag" id="tri-lightbox-tag" hidden>已采用</span>
|
||||
</div>
|
||||
<div class="placeholder tri-lightbox-img" id="tri-lightbox-img"></div>
|
||||
<div class="tri-lightbox-foot">
|
||||
<span id="tri-lightbox-meta">// 生成于 --:--</span>
|
||||
<span class="spc"></span>
|
||||
<span><kbd>Esc</kbd> 关闭</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(bg);
|
||||
bg.addEventListener('click', (e) => {
|
||||
if (e.target === bg) Shell.closeModal('tri-lightbox-bg');
|
||||
});
|
||||
bg.querySelector('.tri-lightbox-close')?.addEventListener('click', () => {
|
||||
Shell.closeModal('tri-lightbox-bg');
|
||||
});
|
||||
}
|
||||
bg.querySelector('#tri-lightbox-img').innerHTML =
|
||||
`<span class="ph-frame">${prodName} · 三视图(正/侧/背) · ${ver.label}</span>`;
|
||||
bg.querySelector('#tri-lightbox-label').textContent = ver.label;
|
||||
const tag = bg.querySelector('#tri-lightbox-tag');
|
||||
tag.hidden = !isAdopted;
|
||||
bg.querySelector('#tri-lightbox-meta').textContent = `// 生成于 ${ver.ts}`;
|
||||
Shell.openModal('tri-lightbox-bg');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -78,6 +78,9 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ov-edit.primary:hover { filter: brightness(1.05); background: var(--heat); color: var(--accent-white); }
|
||||
.ov-edit:disabled { cursor: not-allowed; color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); opacity: 1; }
|
||||
.ov-edit:disabled:hover { color: var(--heat); border-color: var(--heat-40); background: var(--heat-12); }
|
||||
.ov-edit:disabled svg { color: var(--heat); }
|
||||
|
||||
/* 编辑模式按钮组 (重置 + 取消 + 保存) */
|
||||
.ov-edit-group {
|
||||
@ -89,6 +92,154 @@
|
||||
.ov-card.editing .ov-edit-single { display: none; }
|
||||
.ov-card.editing .ov-edit-group { display: inline-flex; }
|
||||
|
||||
/* AI 生成三视图 · 按钮 + 弹出 panel(布局复刻 pipeline.html stage 2 三视图预览) */
|
||||
.ov-tri-wrap { position: relative; margin-left: auto; }
|
||||
/* 当 AI 入口存在时,编辑信息按钮不再独占 ml-auto,与 AI 按钮紧贴 */
|
||||
.ov-tri-wrap + .ov-edit-single { margin-left: 0; }
|
||||
.ov-tri-trigger { white-space: nowrap; }
|
||||
.ov-tri-trigger.is-open { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||||
.ov-card.editing .ov-tri-wrap { display: none; }
|
||||
|
||||
.ov-tri-pop {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px); right: 0;
|
||||
width: 360px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.06);
|
||||
padding: 14px 14px 12px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
z-index: 40;
|
||||
}
|
||||
.ov-tri-pop.show { display: flex; }
|
||||
.ov-tri-pop::before {
|
||||
content: ''; position: absolute;
|
||||
top: -5px; right: 36px;
|
||||
width: 9px; height: 9px;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--border-faint);
|
||||
border-top: 1px solid var(--border-faint);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.ov-tri-close {
|
||||
position: absolute;
|
||||
top: 8px; right: 8px;
|
||||
width: 22px; height: 22px;
|
||||
display: grid; place-items: center;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
z-index: 2;
|
||||
}
|
||||
.ov-tri-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--border-faint); }
|
||||
.ov-tri-close svg { width: 12px; height: 12px; }
|
||||
|
||||
/* 复刻 pipeline.html .prod-preview-* 内部样式 */
|
||||
.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; }
|
||||
.ov-tri-pop .prod-preview-img { aspect-ratio: 16/9; }
|
||||
.ov-tri-pop .prod-preview-foot { display: flex; align-items: center; gap: 8px; min-height: 30px; }
|
||||
.ov-tri-pop .prod-preview-history { display: none; flex-direction: column; gap: 6px; }
|
||||
.ov-tri-pop .prod-preview-history.show { display: flex; }
|
||||
.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; }
|
||||
.ov-tri-pop .prod-preview-history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }
|
||||
.ov-tri-pop .prod-preview-history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; scrollbar-width: thin; }
|
||||
.ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar { height: 4px; }
|
||||
.ov-tri-pop .prod-preview-history .h-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 2px; }
|
||||
.ov-tri-pop .prod-preview-history .h-thumb {
|
||||
flex: 0 0 auto;
|
||||
width: 72px; aspect-ratio: 16/9;
|
||||
background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);
|
||||
position: relative; cursor: pointer; transition: border-color var(--t-base);
|
||||
display: grid; place-items: center; overflow: hidden;
|
||||
}
|
||||
.ov-tri-pop .prod-preview-history .h-thumb:hover { border-color: var(--heat-40); }
|
||||
/* 已采用版本:主橙描边 + 「已采用」徽标 */
|
||||
.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); }
|
||||
/* 仅预览(未采用):黑色描边,无徽标 */
|
||||
.ov-tri-pop .prod-preview-history .h-thumb.previewing { border-color: var(--accent-black); border-width: 2px; }
|
||||
.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; }
|
||||
.ov-tri-pop .prod-preview-history .h-thumb.adopted .v { color: var(--heat); font-weight: 600; }
|
||||
.ov-tri-pop .prod-preview-history .h-thumb.previewing .v { color: var(--accent-black); font-weight: 600; }
|
||||
.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; }
|
||||
.ov-tri-pop .prod-preview-history .h-thumb.adopted .badge { display: block; }
|
||||
|
||||
/* 主图可点击放大 */
|
||||
.ov-tri-pop .prod-preview-img.is-zoomable { cursor: zoom-in; transition: border-color var(--t-base); position: relative; }
|
||||
.ov-tri-pop .prod-preview-img.is-zoomable:hover { border-color: var(--heat-40); }
|
||||
.ov-tri-pop .prod-preview-img.is-zoomable::after {
|
||||
content: '';
|
||||
position: absolute; top: 8px; right: 8px;
|
||||
width: 22px; height: 22px;
|
||||
background: rgba(21,20,15,.72) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='7'/><path d='M21 21l-4.3-4.3M8 11h6M11 8v6'/></svg>") center/14px no-repeat;
|
||||
border-radius: var(--r-sm);
|
||||
opacity: 0; transition: opacity var(--t-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
.ov-tri-pop .prod-preview-img.is-zoomable:hover::after { opacity: 1; }
|
||||
|
||||
/* 三视图放大查看 lightbox */
|
||||
#ov-tri-lightbox-bg { z-index: 80; }
|
||||
#ov-tri-lightbox-bg .tri-lightbox {
|
||||
position: relative;
|
||||
width: min(1100px, 92vw);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 18px 20px 20px;
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,.24);
|
||||
}
|
||||
.tri-lightbox-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px; letter-spacing: .04em; text-transform: uppercase;
|
||||
color: var(--black-alpha-56);
|
||||
padding-right: 32px;
|
||||
}
|
||||
.tri-lightbox-head .lb-ver { color: var(--heat); font-weight: 600; }
|
||||
.tri-lightbox-head .lb-tag {
|
||||
margin-left: 6px;
|
||||
padding: 2px 6px;
|
||||
background: var(--heat-12); color: var(--heat);
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.tri-lightbox-close {
|
||||
position: absolute;
|
||||
top: 12px; right: 12px;
|
||||
width: 28px; height: 28px;
|
||||
display: grid; place-items: center;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
z-index: 2;
|
||||
}
|
||||
.tri-lightbox-close:hover { background: var(--black-alpha-08); color: var(--accent-black); border-color: var(--black-alpha-12); }
|
||||
.tri-lightbox-close svg { width: 14px; height: 14px; }
|
||||
.tri-lightbox-img { aspect-ratio: 16/9; width: 100%; }
|
||||
.tri-lightbox-foot { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
|
||||
.tri-lightbox-foot .spc { flex: 1; }
|
||||
.tri-lightbox-foot kbd {
|
||||
display: inline-block;
|
||||
padding: 1px 5px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--black-alpha-72);
|
||||
}
|
||||
|
||||
/* 字段 view ↔ edit 状态切换 */
|
||||
.v-edit { display: none; }
|
||||
.ov-card.editing .v-static { display: none; }
|
||||
@ -771,6 +922,32 @@
|
||||
<div class="ov-card ov-main" id="ov-main-card">
|
||||
<div class="ov-h">
|
||||
<span class="ti">商品信息</span>
|
||||
<!-- AI 生成三视图 · 按钮 + 弹出 panel(view 模式可见) -->
|
||||
<div class="ov-tri-wrap">
|
||||
<button class="ov-edit ov-tri-trigger" type="button" id="ov-tri-btn" title="AI 生成商品三视图" aria-haspopup="dialog" aria-expanded="false">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>
|
||||
AI 生成三视图
|
||||
</button>
|
||||
<div class="ov-tri-pop" id="ov-tri-pop" role="dialog" aria-label="AI 生成三视图">
|
||||
<button class="ov-tri-close" type="button" id="ov-tri-close" aria-label="关闭">
|
||||
<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>
|
||||
</button>
|
||||
<div class="prod-preview-h">// 三视图预览 · <span id="ov-tri-status">待生成</span></div>
|
||||
<div class="placeholder prod-preview-img" id="ov-tri-img"><span class="ph-frame">// 尚未生成 · 点击下方按钮开始</span></div>
|
||||
<div class="prod-preview-foot" id="ov-tri-foot">
|
||||
<button class="ov-edit primary" type="button" id="ov-tri-start" style="height:28px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/></svg>
|
||||
生成
|
||||
</button>
|
||||
<span style="flex:1;"></span>
|
||||
<span class="mono" style="font-size:11px; color: var(--black-alpha-56);">~¥0.30 / 次</span>
|
||||
</div>
|
||||
<div class="prod-preview-history" id="ov-tri-history">
|
||||
<div class="h-lbl">// 历史版本 · <span class="ct" id="ov-tri-history-count">0</span> 版</div>
|
||||
<div class="h-row" id="ov-tri-history-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- view 模式: 单个 [编辑信息] -->
|
||||
<button class="ov-edit ov-edit-single" type="button" id="ov-edit-btn" title="编辑商品信息">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>
|
||||
@ -875,7 +1052,7 @@
|
||||
</div>
|
||||
<div class="qa-item" data-go="image-optimize" role="button" tabindex="0">
|
||||
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M3 12h18M5 5l14 14M5 19l14-14"/></svg></span>
|
||||
图片优化
|
||||
图片创作
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -896,7 +1073,7 @@
|
||||
<div class="pd-tabs">
|
||||
<button class="tab active" type="button" data-tab="assets">AI 生成素材</button>
|
||||
<button class="tab" type="button" data-tab="videos">视频项目</button>
|
||||
<button class="tab" type="button" data-tab="tasks">任务记录</button>
|
||||
<button class="tab" type="button" data-tab="tasks" hidden>任务记录</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== AI 生成素材 ===== -->
|
||||
@ -1143,6 +1320,25 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 三视图 · 放大查看 lightbox -->
|
||||
<div class="modal-bg" id="ov-tri-lightbox-bg" onclick="if(event.target===this)Shell.closeModal('ov-tri-lightbox-bg')">
|
||||
<div class="tri-lightbox" role="dialog" aria-label="三视图放大查看">
|
||||
<button class="tri-lightbox-close" type="button" onclick="Shell.closeModal('ov-tri-lightbox-bg')" aria-label="关闭">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<div class="tri-lightbox-head">
|
||||
// 三视图(正/侧/背) · <span class="lb-ver" id="ov-tri-lightbox-label">v1</span>
|
||||
<span class="lb-tag" id="ov-tri-lightbox-tag" hidden>已采用</span>
|
||||
</div>
|
||||
<div class="placeholder tri-lightbox-img" id="ov-tri-lightbox-img"></div>
|
||||
<div class="tri-lightbox-foot">
|
||||
<span id="ov-tri-lightbox-meta">// 生成于 --:--</span>
|
||||
<span class="spc"></span>
|
||||
<span><kbd>Esc</kbd> 关闭</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/shell.js?v=202605211643"></script>
|
||||
<script>
|
||||
// 从 URL ?product= 读出商品名,注入 crumb / h1 / 商品名字段
|
||||
@ -1181,10 +1377,10 @@
|
||||
document.querySelectorAll('.qa-item[data-go]').forEach(item => {
|
||||
item.style.cursor = 'pointer';
|
||||
let go = item.dataset.go;
|
||||
// 图片优化暂复用 model-photo.html?mode=tri (后端实际生成人物 / 商品三视图)
|
||||
// 图片创作 → 独立工作台 image-optimize.html(自由创作),带 product 作为提示词种子
|
||||
let url;
|
||||
if (go === 'image-optimize') {
|
||||
url = 'model-photo.html?mode=tri&t=' + Date.now() + '&product=' + encodeURIComponent(name);
|
||||
url = 'image-optimize.html?t=' + Date.now() + '&prompt=' + encodeURIComponent(name);
|
||||
} else {
|
||||
url = go + '.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
|
||||
}
|
||||
@ -1669,9 +1865,348 @@
|
||||
}
|
||||
})();
|
||||
|
||||
// 从 create 跳来时显示 toast
|
||||
// AI 生成三视图 · 按钮悬浮 panel(可重复打开 / X 关闭 / 点击外部关闭 / Esc 关闭)
|
||||
(function initTriView() {
|
||||
const btn = document.getElementById('ov-tri-btn');
|
||||
const pop = document.getElementById('ov-tri-pop');
|
||||
const closeBtn = document.getElementById('ov-tri-close');
|
||||
const startBtn = document.getElementById('ov-tri-start');
|
||||
const img = document.getElementById('ov-tri-img');
|
||||
const statusEl = document.getElementById('ov-tri-status');
|
||||
const foot = document.getElementById('ov-tri-foot');
|
||||
const history = document.getElementById('ov-tri-history');
|
||||
const historyRow = document.getElementById('ov-tri-history-row');
|
||||
const historyCount = document.getElementById('ov-tri-history-count');
|
||||
if (!btn || !pop || !closeBtn || !startBtn) return;
|
||||
|
||||
const versions = []; // [{ ts, label }]
|
||||
let previewIdx = -1; // 主图当前正在「预览」哪一版(浏览态)
|
||||
let adoptedIdx = -1; // 真正被「采用」的那一版 · 与素材库通过状态联动
|
||||
let generating = false;
|
||||
|
||||
function prodName() {
|
||||
return (document.getElementById('pd-name')?.textContent || '商品').trim();
|
||||
}
|
||||
|
||||
function open() {
|
||||
pop.classList.add('show');
|
||||
btn.classList.add('is-open');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
function close() {
|
||||
pop.classList.remove('show');
|
||||
btn.classList.remove('is-open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
function toggle() {
|
||||
if (pop.classList.contains('show')) close(); else open();
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
if (versions.length === 0) { history.classList.remove('show'); return; }
|
||||
history.classList.add('show');
|
||||
historyCount.textContent = versions.length;
|
||||
historyRow.innerHTML = versions.map((ver, i) => {
|
||||
const isAdopted = i === adoptedIdx;
|
||||
const isPreview = i === previewIdx;
|
||||
const cls = [
|
||||
isAdopted ? 'adopted' : '',
|
||||
isPreview && !isAdopted ? 'previewing' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
const titleParts = [ver.label, ver.ts];
|
||||
if (isAdopted) titleParts.push('已采用');
|
||||
else if (isPreview) titleParts.push('预览中');
|
||||
return `
|
||||
<div class="h-thumb ${cls}" data-idx="${i}" title="${titleParts.join(' · ')}">
|
||||
<span class="badge">已采用</span>
|
||||
<span class="v">${ver.label}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
historyRow.querySelectorAll('.h-thumb').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = Number(el.dataset.idx);
|
||||
if (idx === previewIdx) return;
|
||||
setPreview(idx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderMain() {
|
||||
if (previewIdx < 0) return;
|
||||
const ver = versions[previewIdx];
|
||||
const isAdopted = previewIdx === adoptedIdx;
|
||||
img.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
|
||||
img.classList.add('is-zoomable');
|
||||
img.title = '点击放大查看';
|
||||
statusEl.textContent = isAdopted
|
||||
? `${ver.label} · 已采用,不满意可重跑`
|
||||
: `${ver.label} · 预览中(未采用)`;
|
||||
foot.innerHTML = `
|
||||
<button class="ov-edit" type="button" id="ov-tri-rerun" style="height:28px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
|
||||
重跑
|
||||
</button>
|
||||
<button class="ov-edit ${isAdopted ? '' : 'primary'}" type="button" id="ov-tri-adopt" style="height:28px;" ${isAdopted ? 'disabled title="此版本已采用"' : 'title="将此版本设为唯一通过版本,其他版本变为不通过"'}>
|
||||
${isAdopted
|
||||
? '<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> 已采用'
|
||||
: '<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> 采用此版本'}
|
||||
</button>
|
||||
<span style="flex:1;"></span>
|
||||
<span class="mono" style="font-size:11px; color: var(--black-alpha-56);">~¥0.30 / 次</span>
|
||||
`;
|
||||
document.getElementById('ov-tri-rerun')?.addEventListener('click', start);
|
||||
document.getElementById('ov-tri-adopt')?.addEventListener('click', adoptPreview);
|
||||
}
|
||||
|
||||
function syncLibraryStatus() {
|
||||
const grid = document.querySelector('.tab-pane[data-pane="assets"] .asset-grid');
|
||||
if (!grid) return;
|
||||
const adoptedLabel = versions[adoptedIdx]?.label;
|
||||
grid.querySelectorAll('.asset-card[data-tri-version]').forEach(c => {
|
||||
const pill = c.querySelector('.pill');
|
||||
if (!pill) return;
|
||||
const isAdopted = c.dataset.triVersion === adoptedLabel;
|
||||
pill.className = 'pill ' + (isAdopted ? 'pass' : 'fail');
|
||||
pill.textContent = isAdopted ? '通过' : '不通过';
|
||||
pill.setAttribute('data-status', isAdopted ? 'pass' : 'fail');
|
||||
pill.setAttribute('title', isAdopted ? '当前采用版本' : '未被采用');
|
||||
});
|
||||
}
|
||||
|
||||
function appendLibraryCard(ver) {
|
||||
const grid = document.querySelector('.tab-pane[data-pane="assets"] .asset-grid');
|
||||
if (!grid) return;
|
||||
const now = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const dateStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'asset-card';
|
||||
card.dataset.triVersion = ver.label;
|
||||
// 新生成的版本默认 `不通过`,等用户点「采用此版本」才转为通过
|
||||
card.innerHTML = `
|
||||
<div class="thumb placeholder"><span class="type-pill">三视图</span><span class="ph-frame">${prodName()} · ${ver.label}</span></div>
|
||||
<div class="meta"><span class="pill fail" data-status="fail" title="未被采用">不通过</span><span class="date">${dateStr}</span></div>
|
||||
`;
|
||||
grid.prepend(card);
|
||||
// 更新「全部 AI 素材 (N)」计数
|
||||
const ct = document.querySelector('.pd-toolbar .total .ct');
|
||||
if (ct) {
|
||||
const m = ct.textContent.match(/(\d+)/);
|
||||
const n = m ? Number(m[1]) + 1 : 1;
|
||||
ct.textContent = `(${n})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换主图预览(不动采用状态、不动素材库)
|
||||
function setPreview(idx) {
|
||||
previewIdx = idx;
|
||||
renderHistory();
|
||||
renderMain();
|
||||
}
|
||||
|
||||
// 显式「采用」当前预览版本 · 同步素材库通过/不通过
|
||||
function adoptPreview() {
|
||||
if (previewIdx < 0) return;
|
||||
if (previewIdx === adoptedIdx) return;
|
||||
adoptedIdx = previewIdx;
|
||||
renderHistory();
|
||||
renderMain();
|
||||
syncLibraryStatus();
|
||||
if (window.Shell?.toast) {
|
||||
Shell.toast('已采用 ' + versions[adoptedIdx].label,
|
||||
prodName() + ' · 该版本通过,其余版本转为不通过');
|
||||
}
|
||||
}
|
||||
|
||||
function renderLoading() {
|
||||
img.innerHTML = `<div style="display:flex;flex-direction:column;gap:6px;align-items:center;"><div class="spinner"></div><span class="ph-frame" style="font-size:10.5px;">生成中</span></div>`;
|
||||
img.classList.remove('is-zoomable');
|
||||
img.removeAttribute('title');
|
||||
statusEl.textContent = '生成中 · 约 12s';
|
||||
foot.innerHTML = '<span class="mono" style="font-size:11px; color: var(--black-alpha-48);">// POST /assets/tri-view</span>';
|
||||
}
|
||||
|
||||
function openLightbox() {
|
||||
if (previewIdx < 0) return;
|
||||
const ver = versions[previewIdx];
|
||||
const isAdopted = previewIdx === adoptedIdx;
|
||||
const lbImg = document.getElementById('ov-tri-lightbox-img');
|
||||
const lbLabel = document.getElementById('ov-tri-lightbox-label');
|
||||
const lbTag = document.getElementById('ov-tri-lightbox-tag');
|
||||
const lbMeta = document.getElementById('ov-tri-lightbox-meta');
|
||||
if (lbImg) lbImg.innerHTML = `<span class="ph-frame">${prodName()} · 三视图(正/侧/背) · ${ver.label}</span>`;
|
||||
if (lbLabel) lbLabel.textContent = ver.label;
|
||||
if (lbTag) {
|
||||
lbTag.hidden = !isAdopted;
|
||||
lbTag.textContent = '已采用';
|
||||
}
|
||||
if (lbMeta) lbMeta.textContent = `// 生成于 ${ver.ts}`;
|
||||
window.Shell?.openModal?.('ov-tri-lightbox-bg');
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (generating) return;
|
||||
generating = true;
|
||||
open();
|
||||
renderLoading();
|
||||
setTimeout(() => {
|
||||
generating = false;
|
||||
const now = new Date();
|
||||
const ts = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
|
||||
const newVer = { ts, label: 'v' + (versions.length + 1) };
|
||||
versions.push(newVer);
|
||||
appendLibraryCard(newVer);
|
||||
const newIdx = versions.length - 1;
|
||||
previewIdx = newIdx;
|
||||
// 第一次生成 · 自动采用新版本(无选择可言);之后只切预览,不动采用
|
||||
if (adoptedIdx === -1) {
|
||||
adoptedIdx = newIdx;
|
||||
syncLibraryStatus();
|
||||
}
|
||||
renderHistory();
|
||||
renderMain();
|
||||
if (window.Shell?.toast) {
|
||||
const tip = (adoptedIdx === newIdx)
|
||||
? `${newVer.label} · 已采用并同步到素材库`
|
||||
: `${newVer.label} · 预览中 · 满意请点「采用此版本」`;
|
||||
Shell.toast('三视图已生成', `${prodName()} · ${tip}`);
|
||||
}
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', (e) => { e.stopPropagation(); toggle(); });
|
||||
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); close(); });
|
||||
startBtn.addEventListener('click', (e) => { e.stopPropagation(); start(); });
|
||||
pop.addEventListener('click', (e) => e.stopPropagation());
|
||||
img.addEventListener('click', (e) => {
|
||||
if (!img.classList.contains('is-zoomable')) return;
|
||||
e.stopPropagation();
|
||||
openLightbox();
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!pop.classList.contains('show')) return;
|
||||
if (pop.contains(e.target) || btn.contains(e.target)) return;
|
||||
close();
|
||||
});
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && pop.classList.contains('show')) close();
|
||||
});
|
||||
})();
|
||||
|
||||
// 从 create 跳来时显示 toast + 清空三个 tab 数据(新商品没有素材/项目/任务)
|
||||
if (location.search.includes('id=new')) {
|
||||
setTimeout(() => Shell.toast('商品已创建', '开始创建 AI 资产'), 200);
|
||||
|
||||
// 从 sessionStorage 读出 drawer 刚保存的完整 product,注入到「商品信息」卡
|
||||
(function injectFromSession() {
|
||||
let p = null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem('npd-last-created');
|
||||
if (raw) p = JSON.parse(raw);
|
||||
sessionStorage.removeItem('npd-last-created'); // 读完即清,避免污染
|
||||
} catch (e) { /* ignore */ }
|
||||
if (!p) return;
|
||||
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
|
||||
// 商品名称(URL 已经设过,这里兜底)
|
||||
if (p.name) {
|
||||
const nm = document.querySelector('[data-field="name"] .v-static');
|
||||
if (nm) nm.textContent = p.name;
|
||||
const nmI = document.querySelector('[data-field="name"] .v-input');
|
||||
if (nmI) nmI.value = p.name;
|
||||
}
|
||||
// 品类
|
||||
if (p.cat) {
|
||||
const catRow = document.querySelector('[data-field="cat"] .v-static');
|
||||
if (catRow) catRow.textContent = p.cat;
|
||||
const catSel = document.querySelector('[data-field="cat"] .v-select');
|
||||
if (catSel) {
|
||||
// 找到匹配 option 并 select;若没有则插入到首位
|
||||
let matched = false;
|
||||
[...catSel.options].forEach(o => { if (o.value === p.cat || o.textContent === p.cat) { o.selected = true; matched = true; } });
|
||||
if (!matched) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.cat; opt.textContent = p.cat; opt.selected = true;
|
||||
catSel.insertBefore(opt, catSel.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 目标人群(可空)
|
||||
const tgtRow = document.querySelector('[data-field="target"] .v-static');
|
||||
const tgtIn = document.querySelector('[data-field="target"] .v-input');
|
||||
if (tgtRow) tgtRow.textContent = p.target || '—';
|
||||
if (tgtIn) tgtIn.value = p.target || '';
|
||||
// 卖点
|
||||
if (Array.isArray(p.points)) {
|
||||
const blStatic = document.querySelector('[data-field="bullets"] .v-static');
|
||||
if (blStatic) {
|
||||
blStatic.innerHTML = p.points.length
|
||||
? p.points.map(t => `<span class="bullet">${esc(t)}</span>`).join('')
|
||||
: '<span class="bullet" style="color:var(--black-alpha-48)">—</span>';
|
||||
}
|
||||
}
|
||||
// 商品图片 — 替换 6 张占位为真实 dataUrl;数量按上传数定
|
||||
const grid = document.getElementById('ov-images-grid');
|
||||
const addBtn = document.getElementById('ov-img-add');
|
||||
if (grid) {
|
||||
// 移除现有所有 .thumb 占位(保留末尾 #ov-img-add)
|
||||
[...grid.querySelectorAll('.thumb')].forEach(t => t.remove());
|
||||
if (Array.isArray(p.images) && p.images.length) {
|
||||
p.images.forEach(img => {
|
||||
const t = document.createElement('div');
|
||||
t.className = 'thumb';
|
||||
t.style.cssText = 'background-image:url(' + img.dataUrl + ');background-size:cover;background-position:center;';
|
||||
if (addBtn) grid.insertBefore(t, addBtn);
|
||||
else grid.appendChild(t);
|
||||
});
|
||||
}
|
||||
// 同步计数
|
||||
const ct = document.querySelector('.ov-images-sub .sub-h .ct');
|
||||
if (ct) ct.textContent = '(' + ((p.images || []).length) + ')';
|
||||
}
|
||||
})();
|
||||
|
||||
const EMPTY_HTML = (txt) => `<div class="empty-filter">// NO DATA<br><span style="margin-top:6px;display:inline-block">${txt}</span></div>`;
|
||||
|
||||
// AI 生成素材
|
||||
const assetsPane = document.querySelector('.tab-pane[data-pane="assets"]');
|
||||
if (assetsPane) {
|
||||
const grid = assetsPane.querySelector('.asset-grid');
|
||||
if (grid) grid.outerHTML = EMPTY_HTML('还没有 AI 素材,使用右上角「图片生成」开始创建');
|
||||
const ct = assetsPane.querySelector('.total .ct');
|
||||
if (ct) ct.textContent = '(0)';
|
||||
const more = assetsPane.querySelector('.pd-more');
|
||||
if (more) more.remove();
|
||||
}
|
||||
|
||||
// 视频项目
|
||||
const videosPane = document.querySelector('.tab-pane[data-pane="videos"]');
|
||||
if (videosPane) {
|
||||
const grid = videosPane.querySelector('.asset-grid');
|
||||
if (grid) grid.outerHTML = EMPTY_HTML('还没有视频项目,前往工作台「新建项目」开始');
|
||||
const ct = videosPane.querySelector('.total .ct');
|
||||
if (ct) ct.textContent = '(0)';
|
||||
const more = videosPane.querySelector('.pd-more');
|
||||
if (more) more.remove();
|
||||
}
|
||||
|
||||
// 任务记录
|
||||
const tasksPane = document.querySelector('.tab-pane[data-pane="tasks"]');
|
||||
if (tasksPane) {
|
||||
tasksPane.querySelectorAll('.task-stat .v').forEach(el => {
|
||||
const small = el.querySelector('small');
|
||||
el.textContent = '0';
|
||||
if (small) el.appendChild(small);
|
||||
});
|
||||
const tbl = tasksPane.querySelector('.task-table');
|
||||
if (tbl) tbl.outerHTML = EMPTY_HTML('暂无任务记录');
|
||||
const ct = tasksPane.querySelector('.total .ct');
|
||||
if (ct) ct.textContent = '(0)';
|
||||
const more = tasksPane.querySelector('.pd-more');
|
||||
if (more) more.remove();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -140,7 +140,10 @@
|
||||
|
||||
/* ─── 卡片底栏 · V2.1 克制版 (线图标 + mono 文本) ─── */
|
||||
.product-footer {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
font-size: 11.5px;
|
||||
@ -148,16 +151,16 @@
|
||||
background: var(--background-base);
|
||||
}
|
||||
.product-footer .stat {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--r-sm);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: .02em;
|
||||
white-space: nowrap; /* 防止"素材"等中文被挤压成竖排 */
|
||||
flex-shrink: 0;
|
||||
border: 1px solid transparent;
|
||||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.product-footer .stat { justify-self: center; }
|
||||
.product-footer .stat[data-type] { cursor: pointer; }
|
||||
.product-footer .stat[data-type]:hover {
|
||||
background: var(--heat-12);
|
||||
@ -1019,9 +1022,11 @@ Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.h
|
||||
// ============== 注入用户新建的商品(来自工作台 / 商品库 drawer,写入 localStorage)==============
|
||||
// 必须在 const cards / TOTAL / CAT_COUNT 之前执行,让后续逻辑把它们当普通商品处理
|
||||
(function injectExtraProducts() {
|
||||
// 一次性清掉历史遗留的 localStorage 旧数据(用户上次会话误持久化的占位商品)
|
||||
try { localStorage.removeItem('fs-extra-products'); } catch (e) {}
|
||||
let pending;
|
||||
try {
|
||||
pending = JSON.parse(localStorage.getItem('fs-extra-products') || '[]');
|
||||
pending = JSON.parse(sessionStorage.getItem('fs-extra-products') || '[]');
|
||||
} catch (e) { return; }
|
||||
if (!Array.isArray(pending) || !pending.length) return;
|
||||
const grid = document.getElementById('product-grid');
|
||||
@ -1046,7 +1051,18 @@ Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.h
|
||||
<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>
|
||||
<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.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 class="placeholder product-thumb">
|
||||
<span class="tri-missing-badge" title="该商品尚未生成商品三视图,进入详情可在工作流第一步补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||||
<span class="ico" aria-hidden="true"></span>
|
||||
<span class="lbl-mono">缺三视图</span>
|
||||
<span class="tri-missing-pop" role="tooltip">
|
||||
<span class="pop-h">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
MISSING TRI-VIEW
|
||||
</span>
|
||||
<span class="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
|
||||
<span class="pop-tip">建议:进入 <b>商品详情</b> 先补齐三视图,再发起后续生成。</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ph-frame">${esc(p.name)} · 新建</span>
|
||||
</div>
|
||||
<div class="product-body">
|
||||
@ -1753,11 +1769,11 @@ saveBtn.addEventListener('click', () => {
|
||||
return;
|
||||
}
|
||||
const bullets = confirmedBullets;
|
||||
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
|
||||
// 自动从 storage 读出并 prepend 到 grid
|
||||
// 持久化到 sessionStorage('fs-extra-products') · 仅在当前标签页生命周期内有效
|
||||
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
|
||||
try {
|
||||
const KEY = 'fs-extra-products';
|
||||
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
|
||||
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
|
||||
list.push({
|
||||
id: 'pp-' + Date.now(),
|
||||
name, cat,
|
||||
@ -1768,7 +1784,7 @@ saveBtn.addEventListener('click', () => {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
localStorage.setItem(KEY, JSON.stringify(list));
|
||||
sessionStorage.setItem(KEY, JSON.stringify(list));
|
||||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||||
|
||||
// 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底消除"闪商品库"
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
.product-pick-row .product-pick { flex: 0 0 240px; min-width: 240px; }
|
||||
.product-pick-row .product-pick.lib { flex: 0 0 152px; min-width: 152px; min-height: 96px; }
|
||||
/* 底部开始 CTA */
|
||||
.wiz-start-bar { display: flex; justify-content: center; padding: 20px 0 8px; }
|
||||
.wiz-start-bar { display: flex; justify-content: flex-end; padding: 20px 0 8px; }
|
||||
.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); }
|
||||
.wiz-start-bar .btn-start:hover:not(.disabled) { box-shadow: var(--shadow-cta-hover); }
|
||||
.wiz-start-bar .btn-start.disabled { opacity: .4; cursor: not-allowed; }
|
||||
@ -121,6 +121,234 @@
|
||||
.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; }
|
||||
.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; }
|
||||
|
||||
/* ============================================================
|
||||
第 1 步 · 商品选择器(沿用 products.html 商品库的卡片与 toolbar 视觉)
|
||||
─ 命名空间 .pp- 前缀,避免与 products.html 冲突
|
||||
============================================================ */
|
||||
.pp-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.pp-toolbar .search-inline {
|
||||
flex: 1; min-width: 220px; max-width: 340px;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
height: 34px; padding: 0 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
.pp-toolbar .search-inline:focus-within { border-color: var(--heat-40); }
|
||||
.pp-toolbar .search-inline svg { width: 14px; height: 14px; color: var(--black-alpha-48); flex-shrink: 0; }
|
||||
.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; }
|
||||
.pp-toolbar .pp-chip-wrap { position: relative; }
|
||||
.pp-toolbar .pp-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
height: 34px; padding: 0 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 13px; font-family: inherit;
|
||||
color: var(--black-alpha-72);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pp-toolbar .pp-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.pp-toolbar .pp-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
|
||||
.pp-toolbar .pp-chip svg { width: 11px; height: 11px; opacity: .6; }
|
||||
.pp-toolbar .pp-menu {
|
||||
position: absolute; top: calc(100% + 4px); left: 0;
|
||||
min-width: 160px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,.06);
|
||||
padding: 4px;
|
||||
display: none;
|
||||
z-index: 20;
|
||||
}
|
||||
.pp-toolbar .pp-chip-wrap.open .pp-menu { display: block; }
|
||||
.pp-toolbar .pp-menu .mi {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12.5px;
|
||||
color: var(--accent-black);
|
||||
cursor: pointer;
|
||||
}
|
||||
.pp-toolbar .pp-menu .mi:hover { background: var(--background-lighter); }
|
||||
.pp-toolbar .pp-menu .mi.selected { color: var(--heat); font-weight: 600; }
|
||||
.pp-toolbar .pp-menu .mi-check { width: 12px; height: 12px; opacity: 0; }
|
||||
.pp-toolbar .pp-menu .mi.selected .mi-check { opacity: 1; }
|
||||
.pp-toolbar .pp-clear {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
height: 30px; padding: 0 10px;
|
||||
background: transparent; border: 0; border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56); font-size: 12.5px; font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pp-toolbar .pp-clear:hover { color: var(--accent-crimson); background: var(--crimson-bg, #fdebea); }
|
||||
.pp-toolbar .pp-clear svg { width: 11px; height: 11px; }
|
||||
.pp-toolbar .spacer { flex: 1; }
|
||||
.pp-view-tog { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
|
||||
.pp-view-tog button {
|
||||
width: 34px; height: 34px;
|
||||
background: var(--surface);
|
||||
border: 0; border-right: 1px solid var(--border-faint);
|
||||
cursor: pointer;
|
||||
color: var(--black-alpha-48);
|
||||
display: grid; place-items: center;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pp-view-tog button:last-child { border-right: 0; }
|
||||
.pp-view-tog button:hover { background: var(--background-lighter); color: var(--accent-black); }
|
||||
.pp-view-tog button.active { background: var(--heat-12); color: var(--heat); }
|
||||
.pp-view-tog button svg { width: 14px; height: 14px; }
|
||||
|
||||
.pp-result-meta {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
/* 网格 — 沿用 products.html 商品库卡片样式(复制必要部分)
|
||||
固定 4 列 → 每页 8 tile(createCard + 7 商品)正好两行 */
|
||||
.pp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
@media (max-width: 1100px) { .pp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 800px) { .pp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
.pp-grid .product-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
cursor: pointer; position: relative; overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
transition: background .15s, border-color .15s, transform .15s;
|
||||
}
|
||||
.pp-grid .product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
|
||||
.pp-grid .product-card.selected { border-color: var(--heat); background: var(--heat-12); }
|
||||
.pp-grid .product-card.selected::after {
|
||||
content: ''; position: absolute; top: 0; right: 0;
|
||||
width: 0; height: 0;
|
||||
border-top: 28px solid var(--heat);
|
||||
border-left: 28px solid transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
.pp-grid .product-card.selected::before {
|
||||
content: ''; position: absolute; top: 4px; right: 4px;
|
||||
width: 10px; height: 10px;
|
||||
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;
|
||||
z-index: 3;
|
||||
}
|
||||
.pp-grid .product-thumb { aspect-ratio: 1.4 / 1; }
|
||||
.pp-grid .product-body { padding: 14px 14px 12px; flex: 1; }
|
||||
.pp-grid .product-name {
|
||||
font-size: 14px; font-weight: 600; color: var(--accent-black);
|
||||
line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.pp-grid .product-cat {
|
||||
display: inline-flex; align-items: center;
|
||||
margin-top: 8px; padding: 2px 8px;
|
||||
background: var(--background-lighter); color: var(--black-alpha-72);
|
||||
border-radius: var(--r-sm); font-size: 11.5px;
|
||||
}
|
||||
.pp-grid .product-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px; color: var(--black-alpha-48);
|
||||
margin-top: 10px; letter-spacing: .02em;
|
||||
}
|
||||
.pp-grid .product-card.selected .product-cat { background: var(--surface); color: var(--heat); }
|
||||
|
||||
/* 创建新商品 空卡 */
|
||||
.pp-grid .pp-create-card {
|
||||
border: 1.5px dashed var(--black-alpha-24);
|
||||
border-radius: var(--r-md);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 10px; min-height: 220px;
|
||||
color: var(--black-alpha-48);
|
||||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.pp-grid .pp-create-card:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||||
.pp-grid .pp-create-card .pc-plus {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 50%;
|
||||
background: var(--heat); color: var(--accent-white);
|
||||
display: grid; place-items: center;
|
||||
transition: filter var(--t-base);
|
||||
}
|
||||
.pp-grid .pp-create-card:hover .pc-plus { filter: brightness(1.06); }
|
||||
.pp-grid .pp-create-card .pc-plus svg { width: 18px; height: 18px; }
|
||||
.pp-grid .pp-create-card .pc-t { font-size: 13px; font-weight: 600; }
|
||||
.pp-grid .pp-create-card .pc-d { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
|
||||
|
||||
/* 列表视图 */
|
||||
.pp-grid.list-view { display: flex; flex-direction: column; gap: 6px; }
|
||||
.pp-grid.list-view .product-card {
|
||||
flex-direction: row; align-items: center;
|
||||
}
|
||||
.pp-grid.list-view .product-thumb { width: 96px; aspect-ratio: 1.4 / 1; flex-shrink: 0; }
|
||||
.pp-grid.list-view .product-body { flex: 1; padding: 10px 14px; }
|
||||
.pp-grid.list-view .pp-create-card { flex-direction: row; min-height: 56px; gap: 12px; }
|
||||
.pp-grid.list-view .pp-create-card .pc-plus { width: 32px; height: 32px; }
|
||||
|
||||
/* 空筛选结果 */
|
||||
.pp-empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 48px 24px; text-align: center;
|
||||
border: 1px dashed var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--background-lighter);
|
||||
color: var(--black-alpha-48);
|
||||
font-size: 12.5px; font-family: var(--font-mono); letter-spacing: .02em;
|
||||
}
|
||||
.pp-empty .reset { display: inline-block; margin-top: 8px; color: var(--heat); cursor: pointer; text-decoration: underline; }
|
||||
|
||||
/* 分页 */
|
||||
.pp-pager {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
margin-top: 18px; padding-top: 14px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
font-size: 12.5px; color: var(--black-alpha-56);
|
||||
}
|
||||
.pp-pager .total { font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.pp-pager .pages { display: inline-flex; gap: 4px; margin-left: auto; }
|
||||
.pp-pager .pages button {
|
||||
min-width: 28px; height: 28px; padding: 0 8px;
|
||||
border: 1px solid var(--border-faint); background: var(--surface);
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer; font-size: 12.5px; color: var(--black-alpha-72); font-family: inherit;
|
||||
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pp-pager .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.pp-pager .pages button.active { background: var(--heat); color: var(--accent-white); border-color: var(--heat); font-weight: 600; }
|
||||
.pp-pager .pages button:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.pp-pager .pages .ellipsis {
|
||||
min-width: 22px; height: 28px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
color: var(--black-alpha-48); font-family: var(--font-mono);
|
||||
}
|
||||
.pp-pager .page-size {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
height: 28px; padding: 0 10px;
|
||||
background: var(--surface); border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer; font-family: inherit; font-size: 12.5px; color: var(--black-alpha-72);
|
||||
}
|
||||
.pp-pager .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.pp-pager .page-size svg { width: 10px; height: 10px; opacity: .6; }
|
||||
|
||||
/* 底部提示 */
|
||||
.pp-bottom-tip {
|
||||
margin-top: 14px;
|
||||
padding: 10px 14px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 12.5px; color: var(--black-alpha-56);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.pp-bottom-tip svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-48); }
|
||||
.pp-bottom-tip a { color: var(--heat); cursor: pointer; text-decoration: none; }
|
||||
.pp-bottom-tip a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Step 1 · product picker grid ── */
|
||||
.product-pick-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
||||
@media (max-width: 1100px) { .product-pick-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@ -391,13 +619,13 @@
|
||||
/* ---------- data ---------- */
|
||||
|
||||
const PRODUCTS = [
|
||||
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'] },
|
||||
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'] },
|
||||
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'] },
|
||||
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'] },
|
||||
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'] },
|
||||
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'] },
|
||||
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'] },
|
||||
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'], date: '2026-05-15' },
|
||||
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'], date: '2026-05-12' },
|
||||
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'], date: '2026-05-10' },
|
||||
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'], date: '2026-05-08' },
|
||||
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'], date: '2026-05-05' },
|
||||
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'], date: '2026-05-03' },
|
||||
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'], date: '2026-04-30' },
|
||||
];
|
||||
|
||||
const RECENT_IDS = ['mask', 'sun', 'coffee', 'earphone'];
|
||||
@ -434,6 +662,37 @@
|
||||
{ id: 'genz', name: '学生党', sub: 'Z 世代 18-24', metric: '平价快消', defaults: { duration: '0-10', style: 'compare' } },
|
||||
];
|
||||
|
||||
/* ---------- 合并其它页面创建的商品 ---------- */
|
||||
// 商品库 / 工作台 / 新建项目 都共用同一 drawer(assets/new-product-drawer.js),
|
||||
// 该 drawer 在 save() 时把新商品写入 sessionStorage['fs-extra-products']。
|
||||
// 这里在 PRODUCTS hardcoded 数据后,把 storage 中尚不在 PRODUCTS 的商品 unshift
|
||||
// 到列表头部 → 用户跨页面新建的商品在 step 1 也能立即看到。
|
||||
(function mergeExtraProducts() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('fs-extra-products');
|
||||
if (!raw) return;
|
||||
const list = JSON.parse(raw);
|
||||
if (!Array.isArray(list) || !list.length) return;
|
||||
const existingIds = new Set(PRODUCTS.map(p => p.id));
|
||||
// 按 createdAt 倒序(最新在前),逐个 unshift
|
||||
list.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)).forEach(x => {
|
||||
if (existingIds.has(x.id)) return;
|
||||
existingIds.add(x.id);
|
||||
PRODUCTS.unshift({
|
||||
id: x.id,
|
||||
name: x.name || '未命名商品',
|
||||
cat: x.cat || '未分类',
|
||||
price: null, // 表单未采集价格
|
||||
imgs: Math.max(1, x.assets || 0), // 用素材数兜底,至少 1
|
||||
points: Array.isArray(x.bullets) ? x.bullets : [],
|
||||
tags: x.target ? String(x.target).split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
|
||||
date: x.date || (x.createdAt ? new Date(x.createdAt).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10)),
|
||||
createdAt: x.createdAt || Date.now(),
|
||||
});
|
||||
});
|
||||
} catch (e) { /* storage 不可用就静默放弃 */ }
|
||||
})();
|
||||
|
||||
const USER_EMAIL = 'li@shop.com';
|
||||
const ACCOUNT_BALANCE = 327.40;
|
||||
|
||||
@ -444,6 +703,10 @@
|
||||
productId: null,
|
||||
pickSearch: '',
|
||||
pickCat: '全部',
|
||||
pickSort: 'recent', // recent | name | added
|
||||
pickView: 'grid', // grid | list
|
||||
pickPage: 1,
|
||||
pickPageSize: 7, // 固定 4 列 × 2 行 = 8 tile,首页含 createCard 占 1 位 → 7 商品
|
||||
sourceId: null,
|
||||
themeText: '',
|
||||
manualScript: '',
|
||||
@ -523,7 +786,7 @@
|
||||
state.projectName = p.name.split(' ')[0] + ' · 痛点种草 · v1';
|
||||
}
|
||||
state.points = {};
|
||||
p.points.forEach((pt, i) => { state.points[pt] = i < 2; });
|
||||
p.points.forEach(pt => { state.points[pt] = false; });
|
||||
render();
|
||||
}
|
||||
|
||||
@ -598,6 +861,7 @@
|
||||
onSave: function (p) {
|
||||
// p = { id, name, cat, target, points: string[], images: [...], imgs: N }
|
||||
// 适配 wizard 数据结构
|
||||
const now = Date.now();
|
||||
const product = {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
@ -606,6 +870,8 @@
|
||||
imgs: p.imgs,
|
||||
points: p.points,
|
||||
tags: p.target ? p.target.split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
|
||||
date: new Date(now).toISOString().slice(0, 10),
|
||||
createdAt: now,
|
||||
};
|
||||
// 置顶插入,让用户立刻看到
|
||||
PRODUCTS.unshift(product);
|
||||
@ -683,8 +949,13 @@
|
||||
window._wiz = {
|
||||
selectProduct, selectSource, goNext, goPrev, jumpTo, applyPreset, startGenerate, openNewProduct,
|
||||
openProdLib, closeProdLib,
|
||||
setSearch: v => { state.pickSearch = v; renderStep1Only(); },
|
||||
setCat: v => { state.pickCat = v; renderStep1Only(); },
|
||||
setSearch: v => { state.pickSearch = v; state.pickPage = 1; renderStep1Only(); },
|
||||
setCat: v => { state.pickCat = v; state.pickPage = 1; renderStep1Only(); },
|
||||
setSort: v => { state.pickSort = v; state.pickPage = 1; renderStep1Only(); },
|
||||
setView: v => { state.pickView = v; renderStep1Only(); },
|
||||
setPage: v => { state.pickPage = Math.max(1, v); renderStep1Only(); },
|
||||
setPageSize: v => { state.pickPageSize = v; state.pickPage = 1; renderStep1Only(); },
|
||||
clearPickFilters: () => { state.pickSearch=''; state.pickCat='全部'; state.pickPage=1; renderStep1Only(); },
|
||||
setTheme: v => { state.themeText = v; updateFootOnly(); updatePreviewLive(); },
|
||||
setScript: v => { state.manualScript = v; updateFootOnly(); },
|
||||
setName: v => { state.projectName = v; updatePreviewLive(); updateFootOnly(); updateRailOnly(); },
|
||||
@ -749,24 +1020,108 @@
|
||||
$('#rail').innerHTML = html;
|
||||
}
|
||||
|
||||
function productPickHTML(p) {
|
||||
function productCardHTML(p) {
|
||||
const selected = state.productId === p.id;
|
||||
return `<div class="product-pick${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="body">
|
||||
<div class="name">${esc(p.name)}</div>
|
||||
<div class="meta">${esc(p.cat)}${p.price != null ? ' · <b>¥' + p.price + '</b>' : ''} · ${p.imgs} 张图</div>
|
||||
<div class="tags">${p.tags.map(t => `<span class="tag-s">${esc(t)}</span>`).join('')}</div>
|
||||
return `<div class="product-card${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
|
||||
<div class="placeholder product-thumb"><span class="ph-frame">${esc(p.name)} · 1200×800</span></div>
|
||||
<div class="product-body">
|
||||
<div class="product-name">${esc(p.name)}</div>
|
||||
<div class="product-cat">${esc(p.cat)}</div>
|
||||
<div class="product-date">${esc(p.date || '')} 创建</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function pickerFilteredProducts() {
|
||||
const q = (state.pickSearch || '').trim().toLowerCase();
|
||||
let list = PRODUCTS.filter(p => {
|
||||
if (state.pickCat !== '全部' && p.cat !== state.pickCat) return false;
|
||||
if (q) {
|
||||
const blob = (p.name + ' ' + p.cat + ' ' + (p.tags || []).join(' ')).toLowerCase();
|
||||
if (!blob.includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (state.pickSort === 'name') {
|
||||
list = list.slice().sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
||||
} else if (state.pickSort === 'added') {
|
||||
list = list.slice();
|
||||
} else {
|
||||
// recent 排序优先级:
|
||||
// 1) 用户新建的商品(有 createdAt) — 按 createdAt 倒序,最新在最前(紧挨 createCard 按钮)
|
||||
// 2) RECENT_IDS 命中的商品 — 按命中顺序
|
||||
// 3) 其余 — 保持原始顺序
|
||||
const recent = new Map(RECENT_IDS.map((id, i) => [id, i]));
|
||||
function weight(p) {
|
||||
if (p.createdAt) return -p.createdAt; // 负值 → 最大的负数(最近创建)排最前
|
||||
if (recent.has(p.id)) return recent.get(p.id);
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
list = list.slice().sort((a, b) => weight(a) - weight(b));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function pickerPagerHTML(total) {
|
||||
const pageSize = state.pickPageSize;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const cur = Math.min(state.pickPage, totalPages);
|
||||
function pageBtn(n) {
|
||||
const act = n === cur ? ' active' : '';
|
||||
return `<button type="button" class="${act.trim()}" onclick="_wiz.setPage(${n})">${n}</button>`;
|
||||
}
|
||||
const items = [];
|
||||
items.push(`<button type="button"${cur === 1 ? ' disabled' : ''} onclick="_wiz.setPage(${cur - 1})">‹</button>`);
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) items.push(pageBtn(i));
|
||||
} else {
|
||||
items.push(pageBtn(1));
|
||||
if (cur > 3) items.push('<span class="ellipsis">…</span>');
|
||||
const lo = Math.max(2, cur - 1), hi = Math.min(totalPages - 1, cur + 1);
|
||||
for (let i = lo; i <= hi; i++) items.push(pageBtn(i));
|
||||
if (cur < totalPages - 2) items.push('<span class="ellipsis">…</span>');
|
||||
items.push(pageBtn(totalPages));
|
||||
}
|
||||
items.push(`<button type="button"${cur === totalPages ? ' disabled' : ''} onclick="_wiz.setPage(${cur + 1})">›</button>`);
|
||||
return `<div class="pp-pager">
|
||||
<span class="total">共 ${total} 条</span>
|
||||
<div class="pages">${items.join('')}</div>
|
||||
<span class="page-size">每页 ${pageSize} 条</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStep1() {
|
||||
// 已选 + 最近使用,合并去重
|
||||
const ids = new Set();
|
||||
if (state.productId) ids.add(state.productId);
|
||||
RECENT_IDS.forEach(id => ids.add(id));
|
||||
const recent = [...ids].map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean);
|
||||
const list = pickerFilteredProducts();
|
||||
const total = list.length;
|
||||
const pageSize = state.pickPageSize;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const cur = Math.min(state.pickPage, totalPages);
|
||||
const pageList = list.slice((cur - 1) * pageSize, cur * pageSize);
|
||||
|
||||
const cats = ['全部', ...new Set(PRODUCTS.map(p => p.cat))];
|
||||
const catChipActive = state.pickCat !== '全部';
|
||||
const sortLabels = { recent: '最近使用', name: '商品名称', added: '添加顺序' };
|
||||
const hasFilter = !!state.pickSearch || state.pickCat !== '全部';
|
||||
|
||||
const catMenu = cats.map(c => `<div class="mi${state.pickCat === c ? ' selected' : ''}" onclick="_wiz.setCat('${esc(c)}')">
|
||||
<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>
|
||||
<span>${esc(c)}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const sortMenu = Object.keys(sortLabels).map(k => `<div class="mi${state.pickSort === k ? ' selected' : ''}" onclick="_wiz.setSort('${k}')">
|
||||
<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>
|
||||
<span>${sortLabels[k]}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const cards = pageList.map(productCardHTML).join('');
|
||||
const createCard = `<div class="pp-create-card" onclick="_wiz.openNewProduct()">
|
||||
<div class="pc-plus"><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></div>
|
||||
<div class="pc-t">创建新商品</div>
|
||||
<div class="pc-d">// 在此添加一个新商品</div>
|
||||
</div>`;
|
||||
const gridContent = total === 0
|
||||
? (createCard + `<div class="pp-empty">// NO MATCH<br>没有符合筛选条件的商品 <span class="reset" onclick="_wiz.clearPickFilters()">[ 清空筛选 ]</span></div>`)
|
||||
: (createCard + cards);
|
||||
|
||||
return `<div class="wiz-pane active" data-step="1">
|
||||
<div class="wiz-step-h">
|
||||
@ -774,23 +1129,33 @@
|
||||
<p>从商品库选一个 SKU。它的主图与卖点会被 LLM 作为脚本/资产生成的素材。</p>
|
||||
</div>
|
||||
|
||||
<div class="pick-actions">
|
||||
<button class="cap-pill" type="button" onclick="_wiz.openNewProduct()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
添加商品
|
||||
</button>
|
||||
<button class="cap-pill" type="button" onclick="_wiz.openProdLib()">
|
||||
<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="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
|
||||
去商品库 <span style="font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-left: 2px;">// 共 ${PRODUCTS.length} 个</span>
|
||||
<div class="pp-toolbar">
|
||||
<div class="search-inline">
|
||||
<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>
|
||||
<input type="text" placeholder="搜索商品名称、标签" value="${esc(state.pickSearch)}" oninput="_wiz.setSearch(this.value)">
|
||||
</div>
|
||||
<div class="pp-chip-wrap" data-key="cat">
|
||||
<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')})">
|
||||
<span>${state.pickCat === '全部' ? '全部分类' : esc(state.pickCat)}</span>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="pp-menu">${catMenu}</div>
|
||||
</div>
|
||||
${hasFilter ? `<button class="pp-clear" type="button" onclick="_wiz.clearPickFilters()">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
清空筛选
|
||||
</button>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="pick-section-h">
|
||||
<span>最近使用</span>
|
||||
<span class="count">${recent.length}</span>
|
||||
</div>
|
||||
<div class="product-pick-row">
|
||||
${recent.map(productPickHTML).join('')}
|
||||
<div class="pp-result-meta">// 显示 ${pageList.length} / ${total} 个商品${hasFilter ? ' (已筛选)' : ''}</div>
|
||||
|
||||
<div class="pp-grid${state.pickView === 'list' ? ' list-view' : ''}">${gridContent}</div>
|
||||
|
||||
${total > pageSize ? pickerPagerHTML(total) : ''}
|
||||
|
||||
<div class="pp-bottom-tip">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>
|
||||
<span>找不到想要的商品?可<a onclick="_wiz.openNewProduct()">创建新商品</a>,或前往 <a href="products.html">商品库 · 管理商品</a></span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@ -1170,12 +1535,13 @@
|
||||
tmp.innerHTML = renderStep1();
|
||||
active.replaceWith(tmp.firstElementChild);
|
||||
}
|
||||
// refocus search input
|
||||
const inp = body.querySelector('.search-input input');
|
||||
if (inp && document.activeElement !== inp) {
|
||||
// refocus search input (旧 .search-input → 新 .pp-toolbar .search-inline)
|
||||
const inp = body.querySelector('.pp-toolbar .search-inline input')
|
||||
|| body.querySelector('.search-input input');
|
||||
if (inp && state.pickSearch && document.activeElement !== inp) {
|
||||
inp.focus();
|
||||
const v = inp.value;
|
||||
inp.setSelectionRange(v.length, v.length);
|
||||
try { inp.setSelectionRange(v.length, v.length); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1223,6 +1589,13 @@
|
||||
openNewProduct();
|
||||
});
|
||||
|
||||
// 全局点击 → 关闭 picker chip 菜单
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.pp-chip-wrap')) {
|
||||
document.querySelectorAll('.pp-chip-wrap.open').forEach(w => w.classList.remove('open'));
|
||||
}
|
||||
});
|
||||
|
||||
// initial render
|
||||
render();
|
||||
})();
|
||||
|
||||
@ -866,6 +866,21 @@ document.getElementById('search-input').addEventListener('input', e => {
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
// 从 URL ?filter=wip|done|fail|archived|all 接收外部跳转 (例如工作台 stat 卡)
|
||||
(function applyUrlFilter() {
|
||||
try {
|
||||
const q = new URLSearchParams(location.search);
|
||||
const f = q.get('filter');
|
||||
if (!f) return;
|
||||
const valid = ['all', 'wip', 'done', 'fail', 'archived'];
|
||||
if (!valid.includes(f)) return;
|
||||
state.filter = f;
|
||||
document.querySelectorAll('#status-tabs .tab').forEach(t =>
|
||||
t.classList.toggle('active', t.dataset.filter === f)
|
||||
);
|
||||
} catch (e) {}
|
||||
})();
|
||||
|
||||
applyFilter();
|
||||
|
||||
// ============================================================
|
||||
|
||||
@ -295,13 +295,6 @@
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">短信</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="lbl">营销 / 产品更新</div>
|
||||
<div class="val">
|
||||
<label class="switch"><input type="checkbox" id="n-marketing"><span class="slider"></span></label>
|
||||
<span class="static mono" style="font-size: 11.5px; color: var(--black-alpha-48);">邮件</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── 创作默认 ─── -->
|
||||
|
||||
1243
电商AI平台/team.html
1243
电商AI平台/team.html
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user