# 开发提示词：批量导出"购物车"（清单式收集 → 一键打包下载，离线版+在线版两端一致）

> 用途：把本文件交给 AI 会话执行。完成后按"验收标准"逐条核对，结果回写 OnebotCatalog\项目总览.md（进展记录 + 踩坑表 + 最后更新行）。
> 背景必读：OnebotCatalog\项目总览.md、OnebotCatalog\src\App\MainWindow.cs（桌面 4 标签页 + 参数面板下载区 + 参数表）、OnebotCatalog\web\app.js / index.html / style.css（网页 4 标签页）、OnebotCatalog\开发提示词-窗口内3D-WebView2方案B.md（提示词文体参照）。

## 一、角色与任务

你是资深 WPF/前端工程师，在既有项目 **OnebotCatalog**（C# / .NET Framework 4.8 代码式 WPF + 纯静态网页版）上实现**淘宝式购物车批量导出**：用户浏览选型过程中把多个型号"加入清单"（跨系列、跨型号，选型流程完全不受干扰），随时打开清单**一键打包下载**（zip 含全部 STEP + BOM 清单）。**两端功能、交互、文案完全一致**（项目已定规矩）。

## 二、需求定稿（语义先行，避免歧义）

1. **购物车 = 跨系列清单**：可加入不同系列的不同型号；同一型号重复加入 → 提示"已在清单中"（不清空、不加量、不跳转）。
2. **不影响选型（淘宝式，用户 2026-08-24 明确）**：加入清单是"旁路操作"——加入后当前选型状态、3D 视图、参数面板**完全不变**、不跳转、不清空。
   - **购物车生命周期**：网页 = 浏览器会话内（**刷新浏览器才清空**）；桌面 = 软件运行期间（重启软件才清空）。
   - **切换系列 / 切换型号 / 切标签页 / 搜索跳转 / 平替跳转 / 参数表任何操作，一律不触碰购物车**——购物车状态对象与选型状态彻底解耦（两端 cart 独立于 sel/series 状态）。
3. **三处入口（两端一致）**：
   - 参数面板下载区新增【加入清单】按钮（与【下载 STEP】【下载 PDF】并排，选型完成才显示）
   - 参数表每行行尾"+"小按钮（一键加该行，**不触发行双击/筛选**）
   - 顶部栏常驻【清单 (N)】按钮，N=件数实时角标，点击打开清单面板
4. **清单面板**：
   - 桌面：独立非模态 Window（Owner=主窗口），DataGrid 列表；网页：右侧滑出抽屉（遮罩点击关闭，不误触面板外按钮）
   - 列：型号 | 系列 | 规格摘要 | 文件名 | 数量(可编辑≥1，默认1) | 操作(删除)
   - 底部按钮：【清空】【下载全部 (zip + BOM)】；面板标题与角标实时同步件数
   - 数量语义定死：**zip 中每型号只打包一份 STEP**，数量只体现在 BOM.csv 的"数量"列（不做重复文件、不改文件名）
5. **一键打包（两端产物结构完全一致）**：
   - `ONEBOT_清单_<时间戳>.zip`：内含各型号 STEP（文件名=型号编码，保留原后缀）+ `BOM.csv` + `清单.txt`（人类可读版 BOM）
   - `BOM.csv` 列：序号, 型号编码, 系列代码, 系列名, 数量, 参数摘要(键=值 分号分隔), STEP文件名；**UTF-8 带 BOM**（Excel 中文不乱码）；字段含逗号/引号按 CSV 规范转义
6. **打包引擎**：
   - 桌面：System.IO.Compression.ZipArchive（net48 内置，零新依赖）
   - 网页：**JSZip**（MIT 开源，~100KB 单文件，放 `web\libs\jszip.min.js`，**需用户批准下载**，交付说明注明版本/来源/许可）——纯静态无后端，前端逐项 fetch 同源 STEP 二进制 → 内存打包 → `URL.createObjectURL` 触发下载
7. **整合旧功能**：桌面参数表多选 +【批量导出所选】改造为【加入清单】（勾选行批量加入，提示"已加入 N 项"），删除旧直接导出逻辑，统一走清单（避免两套批量路径）；网页参数表本就无批量导出，直接加"+"。

## 三、两端统一文案表（开发时直接抄，保证一致）

| 场景 | 中文 | 英文 |
|---|---|---|
| 加入按钮 | 加入清单 | Add to Cart |
| 顶栏角标 | 清单 (N) | Cart (N) |
| 重复加入提示 | 该型号已在清单中 | Already in cart |
| 加入成功 | 已加入清单 (共 N 项) | Added to cart (N items) |
| 批量加入 | 已加入清单 N 项 | N items added to cart |
| 面板标题 | 我的清单 | My Cart |
| 数量列 | 数量 | Qty |
| 删除 | 删除 | Remove |
| 清空 | 清空 | Clear All |
| 下载全部 | 下载全部 (zip + BOM) | Download All (zip + BOM) |
| 打包中 | 打包中... | Packing... |
| 空清单 | 清单为空, 请先加入型号 | Cart is empty |
| 部分失败 | 打包完成, 但以下型号文件缺失: xxx | Packed, but missing: xxx |

## 四、现状速览（改动前先读）

- 桌面 `src\App\MainWindow.cs`：`TryUpdateSelection()`（已选型号后 `_btnDownload/_btnDownloadPdf` 显隐、`_curModelCode` 平替过滤）；参数面板 actions 区（2026-08-24 加的 `_btnDownload/_btnDownloadPdf` StackPanel）；`BuildGridTab`（`_grid` 多选 + `ExportSelected()` + 右键列头筛选 + 双击 `NavigateToModel`）；顶栏 StackPanel `top`；4 标签页索引：0=3D/尺寸图, 1=参数表, 2=搜索, 3=平替对照
- 桌面 STEP 读取：`_pkg.ReadAsset(v.step)` / `OpcPackage.ExtractStepTo(v, path)`；Variant 有 `@params` 字典、`modelCode`、`step`
- 网页 `web\app.js`：`selectSeries`（重置 sel/ranges/tableFilter——**不得动 cart**）、`updateSelection`（下载按钮显隐 + `v.step` 相对路径如 `step/UCBM32.stp`）、`renderTable`（行点击带入选型 + 2026-08-24 列筛选，行尾加"+"注意 stopPropagation）
- 网页 STEP 位置：站点根 `step\` 目录（发布时从 .opc 解包），fetch 相对路径即可
- 版本号规范：网页 index.html 引 `app.js?v=N`、`style.css?v=M`，每次发布 +1；桌面改 .cs 后必跑 `sample-data\tools\fix-bom.ps1`

## 五、硬性约束（违反即返工）

1. 完全离线可用（两端）；桌面编译环境无 .NET SDK（csc + `lib\` DLL `/reference`，新库须放 lib\）
2. 两端交互/文案/输出格式一致（见文案表）；桌面中英双语 `_lang.T(zh,en)`，网页 `t(zh,en)`
3. 不影响选型主流程（需求第 2 条——验收重点）
4. 不改数据管线、.opc 格式、发布六步结构
5. JSZip 必须经用户批准下载并在交付说明注明版本/来源/许可（MIT）
6. 编辑含中文 .cs/.ps1 后必须 fix-bom；网页文件 UTF-8 无 BOM 即可
7. 客户版与开发版同一代码（`#if CUSTOMER_BUILD` 只控制维护入口）；清单功能两版都有

## 六、实施步骤

### 桌面（src\App）

1. **新文件 `Cart.cs`**（命名空间 Ounibo.Catalog.App）：
   `class CartItem { string ModelCode; string SeriesCode; string SeriesName; string StepPath; string ParamsSummary; int Qty = 1; }`
2. **状态**：`List<CartItem> _cart`；`int CartCount => _cart.Sum(x => x.Qty)`（角标显示**件数=数量总和**还是行数？——定死：角标显示行数，BOM 数量列体现 Qty，简单不混淆）
3. **加入清单**：`AddToCart(Variant v, Series s)`——重复判断（ModelCode 忽略大小写）→ 提示已存在；成功仅更新角标 + 状态栏，**严禁写 `_sel`**
4. **三处入口**：参数面板 actions 区【加入清单】（`TryUpdateSelection` 里随 `_btnDownload` 同显隐，点击用当前已解析的 variant）；参数表"+"列（`DataGridTemplateColumn` 内 Button，`Click` 里 `e.Handled` 防止干扰行选择/双击/右键筛选）；顶栏【清单 (N)】
5. **清单窗口** `CartWindow`：非模态 Window（Owner 主窗口），DataGrid 列：型号/系列/规格/数量(可编辑绑定 Qty)/删除(按钮列)；底部【清空】【下载全部 (zip + BOM)】；窗口关闭不丢数据（_cart 在主窗口）
6. **打包**：SaveFileDialog（默认名 `ONEBOT_清单_yyyyMMddHHmmss.zip`）→ ZipArchive：遍历 `_pkg.ReadAsset(item.StepPath)` 写 `ModelCode+原扩展名`；BOM.csv 与 清单.txt 用 UTF8Encoding(true)；打包完成后 MessageBox 提示路径
7. **整合**：`ExportSelected` 改为"选中行批量 AddToCart + 提示已加入 N 项"；`_btnExport` 文案改【加入清单】
8. 文案走文案表 + 状态栏反馈

### 网页（web\）

1. **引入 JSZip**：下载官方最新稳定版 jszip.min.js → `web\libs\jszip.min.js`；index.html `<script src="libs/jszip.min.js?v=1"></script>`；交付说明注明版本/来源/许可
2. **状态**：`state.cart = []`（结构同 CartItem，StepPath=相对路径）——**selectSeries/refreshOptions/renderTable/jumpToModel 全部不得触碰**
3. **三处入口**：`updateSelection()` 里下载区显【加入清单】；`renderTable` 每行尾"+"（`onclick` 内 `ev.stopPropagation()` 防触发行点击）；header 加【清单 (N)】角标按钮
4. **清单抽屉**：右侧 fixed 面板 + 半透明遮罩；行含数量 `<input type=number min=1>`、删除按钮；底部【清空】【下载全部 (zip + BOM)】；遮罩点击只关面板（面板内点击 `stopPropagation`）
5. **打包**：`new JSZip()` → 逐项 `await fetch(item.step)`（失败收集到 missing 列表**继续**）→ `zip.file(name, blob)` → `zip.file('BOM.csv', '﻿' + csv)`、`zip.file('清单.txt', txt)` → `generateAsync({type:'blob'})` → createObjectURL + a[download] 触发；打包中按钮文案"打包中..."并禁用防连点；missing 非空时提示"打包完成, 但以下型号文件缺失: xxx"
6. 文案走文案表；不调用 selectSeries/refreshOptions

### 发布与回归

1. 桌面：fix-bom → build.ps1（双版）→ 冒烟 → `--selftest` 31 项不变
2. 网页：index.html 版本号 +1 → 停 serve-web → publish.ps1 → 重启 serve-web
3. 交付说明：JSZip 来源/版本/许可 + 两端截图对照

## 七、验收标准（逐条核对）

1. 两端：选型号 A → 加入清单 → **选型界面无任何变化**（参数不清空、3D 不跳）→ 换系列选型号 B → 加入 → 角标 (2)；再切回系列 A、切所有标签页、搜索/平替跳转、参数表双击/列筛选，**清单仍 (2) 内容不变**（网页仅刷新、桌面仅重启才清空）
2. 重复加入同型号 → 提示"已在清单中"，件数不变
3. 参数表"+"与勾选批量加入正常，且**不干扰**行双击带入选型、右键列筛选、范围筛选
4. 清单面板：数量改 3 → 删除一项 → 清空 → 角标实时同步
5. 下载全部 → zip 含全部 STEP + BOM.csv + 清单.txt；Excel 打开中文无乱码、列齐全；数量 3 的型号 zip 中仍只有一份文件、BOM 数量列=3；两端产物结构一致
6. 网页打包中防连点；断网桌面全流程可用；STEP 缺失时跳过并明确提示缺失型号（不整体失败）
7. 自测 31 项不变；客户版发布目录运行正常
8. 两端文案完全一致（对照文案表，中英切换正确）

## 八、已知陷阱（本项目血泪 + 本功能专属）

| # | 陷阱 | 对策 |
|---|---|---|
| 1 | 中文文件/CSV 乱码 | BOM.csv 必须 UTF-8 BOM（桌面 UTF8Encoding(true) / 网页 '﻿' 前缀）；.cs/.ps1 改完 fix-bom |
| 2 | GUI exe 用 `&` 启动不等待，$LASTEXITCODE 空 | Start-Process -Wait -PassThru，核对 log 时间戳 |
| 3 | 发布目录被运行中程序锁 | 发布前停 serve-web 与运行中 exe（红线） |
| 4 | zip 内重名冲突（跨系列存在同名型号） | 打包前校验，冲突加后缀 `_系列码`，BOM 列同步注明 |
| 5 | 网页逐项 fetch 大 STEP 内存峰值 | 逐项 fetch 后立即 zip.file（JSZip 持有 blob，不必先全存数组）；避免一次性 concat |
| 6 | 空串枚举参数摘要显示 | 摘要统一走 displayValue/显示名映射，空值显示"(无)" |
| 7 | 加入清单误写 `_sel/state.sel` 导致选型被重置 | AddToCart 只读 variant；code review 时重点查 |
| 8 | 桌面 DataGridTemplateColumn 按钮点击干扰行选择/双击 | 按钮 Click 中 `e.Handled = true`；"+"按钮 `Focusable=false` 避免抢焦点 |
| 9 | 网页"+"按钮事件冒泡触发整行 onclick（带入选型） | `ev.stopPropagation()`；抽屉面板内所有点击同样处理 |
| 10 | 角标件数与 BOM 数量语义混淆 | 定死：角标=行数，Qty 只在 BOM 体现（需求第 4 条） |
| 11 | 清单窗口关掉后数据丢失 | 购物车状态放主窗口/state，面板只是视图 |
| 12 | 打包中用户再点加入/删除导致 zip 内容快照不一致 | 打包开始时先对清单做快照（ToArray），打包期间 UI 允许操作但只影响下一次下载 |

## 九、交付物

1. 两端可运行清单功能（bin\ 开发版 + 发布目录客户版 + 网页站）
2. `web\libs\jszip.min.js`（注明版本/来源/许可 MIT）
3. `src\App\Cart.cs` + MainWindow/网页改动（注释风格与现有一致）
4. 文档回写：项目总览（进展记录 + 踩坑 + 最后更新行）、维护手册、数据维护说明书（批量导出流程）
5. 本文件验收标准逐条核对结果

## 十、不做的事（范围外）

- 不做清单持久化（网页刷新/软件重启即清空；若要跨会话保留，用 localStorage/文件另立项）
- 不做价格/报价单/订单/账号对接（BOM 只是清单文本）
- 不做网页服务端打包（保持纯静态）
- 不改选型核心逻辑、数据管线、.opc 格式
