合并主分支素材库多选更新

合入素材库多选操作与批量导出更新
保留画布生成配方分支改动并解决冲突
This commit is contained in:
2026-08-08 11:11:58 +08:00
32 changed files with 3496 additions and 1127 deletions
@@ -28,6 +28,10 @@
- 锁定、分组、翻转等状态不影响图片文件导出,但写入元数据。
- 空画布时按钮置灰,或点击后显示轻提示。
左侧素材库选择模式同时支持导出当前可见范围内的选中素材:只选中一个普通素材时直接下载原文件;选中多个素材时沿用画布素材 ZIP 的读取、去重、媒体分目录、序列帧、元数据、部分失败和浏览器下载能力,生成 `项目名-选中素材-YYYYMMDD-HHmmss.zip`,包内根目录为 `项目名-选中素材/`。单素材、单序列帧、选中素材 ZIP 和画布素材 ZIP 的下载文件名都必须包含到秒的本地时间戳,避免同一天重复导出时重名。序列帧层只要至少一帧具有可读 `imageSrc / objectKey` 即可进入导出计划,不要求层级 `src / objectKey`;单层、选中素材和画布集合导出必须共用同一互斥状态,避免并发下载覆盖进度与结果提示。该入口不改变中央画布和图层列表的选择 / 下载语义。
持久素材只要 `src``objectKey` 任一有效即可进入点击、Shift、框选和全选范围。`src` 为空但保留私有 `objectKey` 的素材,导出时复用统一资源读取链路换签;浏览器无法直读签名 URL 时继续回退同源 `/api/assets/read-bytes` 字节代理,不得因缺少临时展示地址而过滤。
暂不实现:
- 画布整体截图 PNG。
@@ -40,7 +44,7 @@
导出文件名:
```text
项目名-画布素材-YYYYMMDD.zip
项目名-画布素材-YYYYMMDD-HHmmss.zip
```
包内结构:
@@ -72,7 +76,7 @@
普通序列帧 ZIP 结构:
```text
角色动作-Sequence.zip
角色动作-Sequence-YYYYMMDD-HHmmss.zip
├─ frames/
│ ├─ frame-01.png
│ └─ frame-02.png
@@ -84,7 +88,7 @@
Spine JSON ZIP 结构:
```text
角色动作-SpineJSON.zip
角色动作-SpineJSON-YYYYMMDD-HHmmss.zip
├─ frames/
│ ├─ frame-01.png
│ └─ frame-02.png
@@ -164,6 +168,7 @@ assetObjectId > objectKey > sourceAssetId > src
2. 过滤无效图层,保留隐藏图层。
3. 按去重 key 合并图片源或序列帧源。
4. 对每个素材源读取 Blob
- 集合导出先按图层顺序完成去重和文件名规划,再用同一个四路并发读取器读取 / 转换普通素材与序列帧,最后按规划和原帧顺序写入 ZIP;不得让响应完成顺序改变导出目录或帧顺序。
- `data:image/...` 直接转换为 Blob。
- 同源或可访问 URL 使用 `fetch` 拉取 Blob。
- 私有 generated / OSS 素材先走 `/api/assets/read-url` 换签并由浏览器直接 `fetch` OSS 签名 URL;必须在同一保护边界内完整消费响应体,换签、请求、状态码或响应体读取任一阶段失败时,才 fallback 到同源 `/api/assets/read-bytes`,不得在只拿到 `2xx/206` 响应头后提前视为读取成功。
@@ -198,6 +203,10 @@ assetObjectId > objectKey > sourceAssetId > src
- 动作图层右键菜单把 `导出为` 作为一级入口,二级菜单提供 `序列帧导出(zip``Spine 导出(zip`
- 单图层普通序列帧 ZIP 包含 `frames/``preview.gif``metadata.json``manifest.txt`,且不包含 `skeleton.json`
- 序列帧导出的 `skeleton.json` 可被独立验证器解析并预览。
- 左侧素材库只选一个素材时直接下载该素材,不创建集合 ZIP。
- 左侧素材库选择多个素材时只把选中素材写入 `项目名-选中素材-YYYYMMDD-HHmmss.zip`,不混入未选素材,并继续覆盖部分失败和全部失败边界。
- 同一事件循环内重复触发集合导出时只启动一次读取和下载;单个选中素材下载成功后显示 `选中素材已导出`
- 多素材与长序列帧集合导出同时读取不超过四项,外层素材无需等待前一个序列全部完成,异步读取逆序完成时 ZIP 文件与帧顺序仍保持稳定。
## Spine JSON 验证器
@@ -226,7 +235,6 @@ npm run spine-export-validator:build
## 后续扩展
- 导出当前选中素材。
- 导出画布快照 PNG。
- 导出可恢复工程包。
- 导入工程包恢复画布。
@@ -204,7 +204,8 @@
- 画布素材 ZIP 的 `metadata.json` 只保存前端信息弹窗和导出文件列表可见的展示快照:项目标题、导出时间、图层标题、文件路径、类型、生成输入、模型显示名、Task 短 ID、Object 显示值、Resolution / 时长和导出错误。导出的生成输入只保留用户实际填写或选择的内容;系统默认兜底提示词、固定工作流提示词、内置图标描述、UI 提取素材固定提示词等内置提示词即使存在于历史 `generationInputs`,也不得写入导出元数据。不得把 `projectId``layerId``resourceId``sourceAssetId``sourceResourceId`、原始 `prompt` / `actualPrompt` / `provider` 或画布坐标、锁定、隐藏等布局状态写入导出元数据;Object 字段仅沿用信息弹窗当前可见值。
- `生成角色动作` 的完成结果按序列帧素材处理:`assetKind` 固定为 `character-animation`,图层主 `src` 使用首帧,前端内部据 `assetKind` 派生 `CanvasMediaType="image-sequence"` 选择播放器。`previewVideoPath` 只作为上游来源,不作为画布主媒体。下载和 ZIP 导出必须得到序列帧 ZIP / frames 目录,不能回退为预览视频或首帧 PNG;后端抽帧后逐帧保存带绿幕源图,上传时消费并释放原帧字节,再用该对象的 600 秒 OSS URL 完成透明化并落盘透明帧素材。
- 素材库和项目资源 API 不返回独立 `mediaType`;前端只按 `assetKind` 映射渲染器:`character-animation → image-sequence``video → video``audio/sound-effect/background-music → audio`、其余 → image,不按扩展名猜类型。最终动作 asset / resource 平铺返回 `imageSequenceFrames/imageSequenceDurationMs`;正式帧对象不含 `frameIndex`,数组位置是唯一播放顺序,帧数取数组长度,FPS 按帧数与毫秒时长即时推导。只有至少两帧且播放时长大于 0 的角色动作素材才能进入序列播放器,否则按损坏素材失败关闭。预览视频作为独立 `assetKind=video` 资源保存,最终动作以 `sourceResourceId` 保留来源关系,不重复保存路径。从素材库点击、HTML5 拖放或指针拖放到画布时必须复制完整字段并先创建正式 project resource;新 layout 只保存 resource 引用和几何,不保存帧集或预览视频,刷新后从 project resource 恢复循环播放。历史 `local-*` 自包含动作布局仍可读取。
- 素材面板支持按素材名、文件夹名、生成信息、用户可见模型、任务和媒体类型搜索,并支持选择模式框选,一次选中多个素材,并可批量移动或删除上传素材。Provider 与 `isEditorInternalProcessingModel(...)` 识别的内部处理模型不得进入搜索索引,避免出现隐藏字段可命中或命中但无可见匹配字段;原始审计元数据只保留给服务端和后台管理读取,普通用户(包括素材 owner)不得通过响应取得
- 素材面板支持按素材名、文件夹名、生成信息、用户可见模型、任务和媒体类型搜索,并支持选择模式框选。选择模式的选中集合以全部上传完成且媒体地址有效的可选素材为有效性边界,不因搜索、折叠或展开变化而收缩;只有素材被删除、进入上传中 / 失败态或媒体地址失效时才剔除对应选择及范围锚点。素材缩略图、标题、元数据和卡片空白区统一作为整行选择命中区:鼠标、键盘、触摸和笔输入单击都只切换当前素材,不替换其它已选素材;`Shift + 点击` 按当前可见顺序把连续区间增量加入现有选择,锚点当前不可见时退化为切换目标素材并建立新锚点。当前搜索结果的全选 / 取消全选只增量增删当前已展开的可见素材并保留其它选择,同时清空上次单项选择的范围锚点。触摸素材卡仍可切换选中态,但触摸列表空白区必须继续滚动,不能捕获为框选;鼠标 / 笔框选坐标必须包含列表滚动偏移,`pointerup` 提交最后坐标,`pointercancel` 只取消。内置、上传未完成、上传失败或无可读来源的素材行不得暴露可用的选择按钮。退出选择模式、关闭素材栏或切换到图层栏时必须同时清空选择锚点、选中集合和框选状态,非选择模式不得残留选中高亮
- 素材选择模式的批量工具栏是素材列表滚动区之外的固定底栏,始终显示跨搜索与折叠状态保留的全部已选数量、当前可见素材全选 / 取消全选、下载、删除和取消;下载和删除作用于完整选中集合,不得只消费当前可见 ID。删除集合中存在当前未显示素材时,必须先用危险确认弹窗明确展示全部删除数量和未显示数量,确认目标使用打开弹窗时的素材 ID 快照;选择集合、可见集合或选择模式漂移后关闭旧确认弹窗,确认前不得执行删除。工具栏自身禁止横向溢出和滚动,空间不足时按钮在工具栏内部换行。不得把工具栏作为滚动列表末项或依赖 `position: sticky` 的自然位置。移动端进入素材选择模式时必须提高素材侧栏高度并允许素材列表纵向滚动,为固定底栏之上至少保留一张素材卡的可操作空间,同时受当前视口高度上限约束,不得在横屏或嵌入式短视口中挤占画布。选择模式隐藏单素材下载 / 重命名动作,进入时清理已有拖拽 / 投放状态,并同时阻断素材行、文件夹空白区的内部素材移动和外部文件投放以及右键菜单,避免批量意图回退成单素材操作。Provider 与 `isEditorInternalProcessingModel(...)` 识别的内部处理模型不得进入搜索索引,避免出现隐藏字段可命中或命中但无可见匹配字段;原始审计元数据只保留给服务端和后台管理读取,普通用户(包括素材 owner)不得通过响应取得。
- 图层面板支持按图层名、生成信息、用户可见模型、任务和媒体类型搜索;Provider 与内部处理模型同样不得进入搜索索引。普通用户图层快照也不得携带这些内部字段。支持选择多个图层后创建图层组,组名和 groupId 随画布布局保存。
- 小地图支持拖拽视口框,拖动时画布 viewport 跟随移动;`pointermove` 更新必须通过 `requestAnimationFrame` 合帧,结束拖拽时 flush 最后一帧,避免高频 pointermove 直接压垮 React 渲染和项目持久化链路。
- 鼠标滚轮默认按设备原始 `deltaX / deltaY` 二维平移画布视口;当按住 Shift 且设备上报 `deltaX = 0` 时,视口输入适配层把 `deltaY` 映射为横向位移并将纵向位移置零,核心平移模型仍只消费归一化后的 X / Y。按住 Ctrl / Cmd 滚轮才缩放画布,并阻止浏览器页面缩放。缩放比例显示保持现有换算口径,最低可缩小到 `5%`
@@ -101,9 +101,12 @@
## 第十阶段模块
- `useImageCanvasAssetLibrary.ts`
- 承载账号级素材库状态模型:素材文件夹、素材列表、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 删除、素材选择模式、框选、多选删除、素材拖到文件夹和鉴权失败登录弹窗。
- 承载账号级素材库状态模型:素材文件夹、素材列表、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 删除、批量删除资源副作用、素材拖到文件夹和鉴权失败登录弹窗;不再持有选择集合、范围锚点或框选交互状态
- 主视图继续保留上传文件读取、上传占位卡片进度、拖到画布坐标、创建画布图层、工程资源持久化和画布图层清理;素材删除通过 `onDeleteAssets` 回调通知主视图清理关联图层。
- 该 hook 有独立单测覆盖素材库加载归一化、401 登录、新建文件夹临时 id 替换、素材移动、删除回调和多选删除,避免后续整理侧栏 JSX 时丢失素材库能力。
- `useImageCanvasAssetSelection.ts`
- 作为素材选择的唯一状态边界,统一持有选择模式、完整选中集合、范围锚点、可选素材有效性 reconcile、单项 / Shift / 当前可见全选增量、框选几何与框选生命周期;框选除 `pointerup / pointercancel` 外必须在匹配的 `lostpointercapture` 到达时只清理框选状态,不得再次释放已经丢失的 capture。该 hook 向批量下载 / 删除只暴露已经按素材顺序解析的 `selectedAssets`;删除入口根据当前 `visibleAssetIds` 识别未显示选择并在执行完整集合删除前弹出危险确认。
- 搜索和文件夹折叠只在侧栏产生 `visibleAssetIds` 并传入增量动作,不得直接修改或 reconcile 选择集合;选择行为测试集中在该 hook,素材库 model 不再维护第二套选择状态机。
## 第十一阶段模块
@@ -37,6 +37,7 @@
- 测试用 fixture 只包含本行为需要的字段。演化中的 payload 使用 `expect.objectContaining(...)` 或 helper 生成默认对象,避免一处契约加字段导致大量无关用例碎裂。
- 当测试是为防止历史回归,应在测试名或邻近注释中说明防的是什么行为,而不是记录实现步骤。
- 同一用例既要验证定时器调度参数,又要断言确定的中间帧或中间状态时,必须 mock 定时器回调或使用可控假时钟;不得让真实墙上时间在异步交互期间推进被断言的状态,否则本地通过的用例会在较慢 CI 中偶发失败。
- 测试 React effect 中注册的事件监听时,触发事件前先用可观测的 listener 调用确认注册已完成;异步请求已开始应用有界 `waitFor` 断言确认,不要用无界手工 Promise 等待一次性信号。解除挂起请求时将 Promise 收尾纳入异步 `act`,确保后续 React 更新在断言前已冲刷。
## 试点调整