civitai 生成图选择器(Generated Image Picker)设计与 BlobData 状态重构指南
发布时间:2026/9/18 3:15:39 作者:尧图编辑部 阅读量:1,286
设计与 BlobData 状态重构指南)
civitai 生成图选择器Generated Image Picker设计与 BlobData 状态重构指南【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai导读本文基于 docs/features/generation-image-picker.md 展开系统讲解 civitai 前端生成面板中的**生成图选择器Generated Image Picker**设计如何把orchestratorImageSelect选择状态从字符串 ID 列表重构为直接持有BlobData对象并在此之上叠加一套可被任意功能激活的通用挑选流程首个消费者是 v2 生成表单的ImageUploadMultipleInput。读者将掌握选择 Store 的两种实现形态createSelectionStore 纯 zustand及其取舍、picker 状态的 API 设计、消费者迁移要点以及完整的落地顺序与验证清单。一、背景为什么需要一个通用的生成图选择器在 civitai 的生成工作流中用户经常需要从自己的生成队列Queue/信息流Feed中挑选图片用于多种场景上传到 img2img / upscale 表单输入参与挑战challenges投稿作为训练training素材批量发布、下载、删除等已有操作。在引入选择器之前用户只能手动拖拽图片到目标位置交互成本高且无法批量操作。设计目标是一个任何功能都能激活的通用选择器系统——同一个选择 Store既能承载现有的批量选择 → 下载/删除/发布能力又能扩展出限选数量 → 确认回填的 picker 模式两者共用同一份选中状态。现状对照本文写作时仓库中 src/components/ImageGeneration/utils/generationImage.select.ts 已落地了 BlobData 值存储 部分createSelectionStoreBlobDatagetKey而 picker 状态扩展startPicker/ImagePickerFooter等仍属于文档规划中的后续步骤读者可以按本文顺序继续推进。二、Store 重构从字符串 ID 到 BlobData picker 状态2.1 现状字符串 ID createSelectionStore旧实现将选中项以字符串 ID 记录// Stores: { wfId:stepName:imgId: true } const selectStore createSelectStorestring(generated-image-select);由此带来两个问题useSelection()返回的是{ workflowId, stepName, imageId }[]元组数组缺少图片的 URL、尺寸等实际数据GeneratedImageActions.getSelectedImages()必须做二次查询从请求数据里按 ID 反查BlobData既多一层遍历也让调用方依赖查询数据的形状。2.2 新方案纯 zustandBlobData 值 picker 状态import { create } from zustand; import { devtools } from zustand/middleware; import type { BlobData } from ~/shared/orchestrator/workflow-data; const makeKey (image: BlobData) ${image.workflowId}:${image.stepName}:${image.id}; interface OrchestratorImageSelectState { selected: Recordstring, BlobData; // Picker-specific state picker: { active: boolean; maxSelectable: number; onConfirm: ((images: BlobData[]) void) | null; }; } const useStore createOrchestratorImageSelectState()( devtools(() ({ selected: {}, picker: { active: false, maxSelectable: 0, onConfirm: null }, }), { name: generated-image-select }) );为什么不能用 immer因为BlobData使用了私有字段#step、#index而 immer 基于 Proxy 进行结构共享与写时复制Proxy 无法正确代理类实例的私有字段会在运行期抛错。Store 形状本身很简单selected是一个Recordstring, BlobDatapicker 是三个标量字段纯 zustand 配合浅拷贝即可不需要 immer。这一点可以从仓库源码得到印证src/shared/orchestrator/workflow-data.ts 中BlobData抽象类确实声明了#step: StepData与#index: number两个私有字段并在构造函数中通过Object.assign(this, data)填充公开字段url、id、available、nsfwLevel、blockedReason等。BlobData.from()工厂按data.type分发到ImageBlob/VideoBlob/AudioBlob/Model3DBlob子类。2.3 导出 API调用方签名不变数据更丰富export const orchestratorImageSelect { // Existing API (unchanged call sites) useSelection: () BlobData[], // was { workflowId, stepName, imageId }[] useIsSelected: (image: BlobData) boolean, // was (args: { workflowId, stepName, imageId }) useIsSelecting: () boolean, toggle: (image: BlobData, value?: boolean) void, setSelected: (images: BlobData[]) void, getSelected: () BlobData[], // Picker extensions usePickerActive: () boolean, usePickerMaxReached: () boolean, startPicker: (opts: { maxSelectable: number; onConfirm: (images: BlobData[]) void }) void, confirmPicker: () void, cancelPicker: () void, };设计要点存量 API 签名形态不变只是载荷从 ID 元组升级为完整的BlobData调用点的改动被压缩到最小usePickerActive/usePickerMaxReached是只读 Hook供各组件按 picker 状态渲染 UIstartPicker负责注入maxSelectable与onConfirm回调confirmPicker/cancelPicker是终结动作。2.4 与仓库现状的对照仓库当前实现 generationImage.select.ts 已经采用createSelectionStoreBlobData({ getKey, name: generated-image-select })并导出SelectionProvider、useActions、useSelection、useIsSelected、useIsSelecting、useSelectedCount、useRegisterOrder——即文档规划的 BlobData 值存储 部分已落地。底层通用选择 Store 位于 src/store/createSelectionStore.ts它提供了toggle普通点击切换并更新 shift 范围锚点selectGmail 风格 shift 连选范围切换范围注册在registerOrder/useRegisterOrder的分组内避免跨网格误选selectMany/setSelected/clear/getSelected细粒度的 selector Hook每个 Hook 只订阅一个切片单行切换只重渲染该行。三、消费者迁移GeneratedImage 与 GeneratedImageActions3.1GeneratedImage.tsx直接传image调用点从三字段元组改为直接传BlobData// Before: orchestratorImageSelect.useIsSelected({ workflowId: request.id, stepName: step.name, imageId: image.id }) orchestratorImageSelect.toggle({ workflowId: request.id, stepName: step.name, imageId: image.id }) // After: orchestratorImageSelect.useIsSelected(image) orchestratorImageSelect.toggle(image)当 picker 激活时单张图片的交互行为如下复选框照常反映选中状态同一 Store无需额外状态点击照常切换选中复用同一个toggle()调用toggle内部尊重picker.maxSelectable——已达上限且当前项未选中时直接 no-op始终显示复选框不再以isSelecting为门槛已达上限且当前项未选中时呈现禁用态外观。3.2GeneratedImageActions.tsx删掉二次查询// Before: const selected orchestratorImageSelect.useSelection(); // { workflowId, stepName, imageId }[] function getSelectedImages() { const selectedIds selected.map(x x.imageId); return data.flatMap(wf wf.succeededImages.filter(x selectedIds.includes(x.id))); } // After: const selected orchestratorImageSelect.useSelection(); // BlobData[] directly // getSelectedImages() is gone — selected is already what we needuseSelection()直接返回BlobData[]getSelectedImages()整个删除。仓库中的实际消费者 GeneratedImageActions.tsx 已经按此形态工作selected直接用于批量删除按workflowId/stepName/image.id聚合元数据更新、发布读取image.url、image.workflowId、image.step、下载downloadGeneratedImages(selected)并过滤image.mediaType ! audio。picker 激活时GeneratedImageActions的行为变化隐藏批量操作下载/删除/发布/批量工作流菜单显示Selecting images...上下文标签提示当前处于挑选模式而非批量管理模式。四、Image picker 完整流程4.1 激活从ImageUploadMultipleInput发起orchestratorImageSelect.startPicker({ maxSelectable: max - currentCount, onConfirm: (images) { const newValues images.map(img ({ url: img.url, width: img.width, height: img.height })); onChange?.([...(value ?? []), ...newValues]); }, }); // startPicker also calls generationGraphPanel.setView(queue)关键细节maxSelectable是动态计算的剩余容量max - currentCount即表单允许的总数减去已上传数保证挑选结果不会撑爆上限onConfirm把BlobData映射为表单值的形状{ url, width, height }并追加到现有值数组尾部startPicker内部还会调用generationGraphPanel.setView(queue)自动把生成面板切到 Queue 视图让用户立刻看到可挑选的图片列表。4.2 触发按钮放在 Dropzone 之外在ImageUploadMultipleInput的默认布局中把Dropzone包进一个relative容器按钮作为兄弟节点而非 Dropzone 的子节点点击时执行e.stopPropagation()按钮不是 Dropzone 的一部分点击不会触发文件选择弹窗relative定位让按钮可以浮在拖放区之上。仓库中的 ImageUploadMultipleInput.tsx 支持layout: default | url-input两种布局并具备max、slots、aspect、enableDrawing、warnOnMissingAiMetadata等 props生成图选择器按钮按计划通过新增的enableGeneratedImagePickerprop 接入默认布局。4.3 吸底操作条ImagePickerFooterImagePickerFooter渲染在ScrollableQueue/ScrollableFeed内部Queue/Feed 之后、ScrollArea 之内复用FormFooter的shadow-topper sticky bottom-0 z-10吸底模式展示{n} of {max} selected计数Cancel与Confirm两个操作按钮。吸底设计保证用户在长列表中滚动挑选时确认/取消动作始终可见可点。4.4 Tab 切换守卫picker 模式期间切换到generate tab 被阻止必须走 Cancel/Confirm 结束挑选Queue / Feed 之间可以自由切换挑选范围本身覆盖两个视图滚动位置和筛选互不影响。这个守卫防止用户半路退出而丢失挑选上下文同时保留在两类来源间补选图片的灵活性。4.5 Confirm / Cancel 语义confirmPicker(): // calls onConfirm(Object.values(selected)), resets state, setView(generate) cancelPicker(): // resets state (clears selection picker), setView(generate)confirmPicker()把当前selected的对象值BlobData[]交给onConfirm回调回填表单然后重置 Store 状态并切回 generate 视图cancelPicker()清空选中与 picker 状态不留残余选中同样切回 generate 视图——保证取消后表单和选择状态都回到干净起点。五、涉及文件清单与落地顺序5.1 文件修改清单#文件变更内容1src/components/ImageGeneration/utils/generationImage.select.ts重写纯 zustand、BlobData 存储、picker 状态2src/components/ImageGeneration/GeneratedImage.tsx简化 toggle/isSelected 调用直接传 BlobData增加 picker 感知的复选框/点击3src/components/ImageGeneration/GeneratedImageActions.tsx删除getSelectedImages()二次查询直接使用 BlobDatapicker 期间隐藏4新建src/components/ImageGeneration/ImagePickerFooter.tsx吸底操作条计数 Cancel Confirm5src/components/ImageGeneration/GenerationTabs.tsx在 ScrollableQueue/ScrollableFeed 中挂载ImagePickerFooter加 Tab 守卫6src/components/generation_v2/inputs/ImageUploadMultipleInput.tsx新增enableGeneratedImagePickerprop 触发按钮7src/components/generation_v2/GenerationForm.tsx把enableGeneratedImagePicker透传给ImagesInput5.2 推荐实施顺序按依赖关系从底层向上推进每步都可独立编译验证重写generationImage.select.ts—— BlobData Store picker 状态地基决定上层所有 API 形态更新GeneratedImage.tsx—— 传 BlobData 给 toggle/isSelected加 picker UI复选框始终显示、上限禁用态更新GeneratedImageActions.tsx—— 直接用 BlobData删掉查询picker 期间隐藏批量操作创建ImagePickerFooter.tsx—— 吸底计数与确认/取消更新GenerationTabs.tsx—— 挂载 footer Tab 切换守卫更新ImageUploadMultipleInput.tsx—— 加 prop 触发按钮更新GenerationForm.tsx—— 透传 prop接通端到端链路。六、验证清单文档给出的验证步骤覆盖回归与新功能两条线pnpm run typecheck—— 无类型错误回归测试既有批量选择 → 下载/删除/发布仍然正常Store 重构不能破坏旧能力picker 主链路点击 dropzone 上的按钮 → 在 queue 中选择图片 → confirm → 图片出现在表单中上限强制已上传 3/7 张时picker 允许最多再选 4 张maxSelectable max - currentCount生效超出即 no-op 禁用态取消语义Cancel 返回 generate 视图且表单无任何改动确认cancelPicker清空了选中与 picker 状态。七、总结一个 Store两种模式这套方案的核心价值在于复用普通模式批量选择 → 下载/删除/发布使用同一份selected状态与既有toggle/setSelectedAPIPicker 模式在同一 Store 上叠加picker: { active, maxSelectable, onConfirm }通过startPicker/confirmPicker/cancelPicker三个动作把挑选 → 回填变成任何表单输入都能调用的通用能力。状态层从字符串 ID 升级为BlobData后所有消费者都直接拿到url、width、height、workflowId、step等完整数据消除了二次查询也为未来挑战投稿、训练选图等更多从生成结果中选择的场景铺平了道路。实现时务必牢记BlobData的私有字段与 immer Proxy 的兼容性约束并严格按先底层 Store、后上层组件、最后接线的顺序推进配合文末的回归与新链路验证清单收尾。延伸阅读选择 Store 的完整实现Gmail 风格 shift 范围选择、分组注册、selector Hook 优化src/store/createSelectionStore.tsBlobData抽象类及子类工厂私有字段、NSFW 阻断、errored/displayable 语义src/shared/orchestrator/workflow-data.ts当前已迁移的 Store 实例与导出 APIsrc/components/ImageGeneration/utils/generationImage.select.ts批量操作的现实消费者下载/删除/发布/批量工作流src/components/ImageGeneration/GeneratedImageActions.tsx首个 picker 消费者组件的现有 props 与布局src/components/generation_v2/inputs/ImageUploadMultipleInput.tsx【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考