修复图片画布素材上传鉴权

统一侧栏上传入口走上传工作流

未登录素材上传先弹账号入口,不再打开文件选择器

补充上传鉴权回归测试和编辑器拆分文档记录
This commit is contained in:
2026-06-17 08:35:00 +08:00
parent e07002c1dc
commit be3d91f1c5
6 changed files with 67 additions and 12 deletions

View File

@@ -130,3 +130,4 @@
- 2026-06-17 前端拆分第十三阶段:新增 `useImageCanvasAssetExportWorkflow`,把画布素材导出状态、单图右键导出、整包 ZIP 组包、图片去重、读取失败记录、metadata / manifest 和下载链接副作用从主视图抽出;主视图保留右键目标解析和状态提示渲染。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,登录临时开发账号后下载按钮启用,点击后触发真实下载 `未命名画布-画布素材-20260617.zip` 并显示导出状态;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`,点击 `生成工具``生成图片` 对话框出现且 `AI画布工具栏` 保持可见,登录后控制台无前端 error。 - 2026-06-17 前端拆分第十三阶段:新增 `useImageCanvasAssetExportWorkflow`,把画布素材导出状态、单图右键导出、整包 ZIP 组包、图片去重、读取失败记录、metadata / manifest 和下载链接副作用从主视图抽出;主视图保留右键目标解析和状态提示渲染。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,登录临时开发账号后下载按钮启用,点击后触发真实下载 `未命名画布-画布素材-20260617.zip` 并显示导出状态;背景设置点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`,点击 `生成工具``生成图片` 对话框出现且 `AI画布工具栏` 保持可见,登录后控制台无前端 error。
- 2026-06-17 前端拆分第十四阶段:新增 `useImageCanvasLayerCommands`,把画布剪贴板、右键目标解析、复制 / 剪切 / 粘贴、创建副本、层级移动、分组 / 解组、显隐、锁定、翻转、删除选中图层、按 id 删除和单图导出委托从主视图抽出;主视图保留菜单定位、画布事件、生成、上传、项目持久化和实际导出下载。验证命令:`npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭后 `画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`,点击 `生成工具``Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后新标签素材、画布图层、返回项目入口、小地图和底部工具栏可见,控制台无前端 error。 - 2026-06-17 前端拆分第十四阶段:新增 `useImageCanvasLayerCommands`,把画布剪贴板、右键目标解析、复制 / 剪切 / 粘贴、创建副本、层级移动、分组 / 解组、显隐、锁定、翻转、删除选中图层、按 id 删除和单图导出委托从主视图抽出;主视图保留菜单定位、画布事件、生成、上传、项目持久化和实际导出下载。验证命令:`npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭后 `画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`,点击 `生成工具``Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后新标签素材、画布图层、返回项目入口、小地图和底部工具栏可见,控制台无前端 error。
- 2026-06-17 前端拆分第十五阶段:新增 `useImageCanvasGenerationWorkflow`,把生成入口、规范 / 角色 / 图标 / 修改 / 快速编辑 / 角色动画状态机、真实生成提交、结果落图、失败恢复和删除图层后的生成态清理从主视图抽出;主视图保留画布事件、浮层定位、上传、项目资源持久化和历史捕获。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` 面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`;点击 `生成工具``Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后上传素材成功,素材数增加,点击素材可加入画布,切换 `图层` 面板可看到对应图层,登录后控制台无前端 error。 - 2026-06-17 前端拆分第十五阶段:新增 `useImageCanvasGenerationWorkflow`,把生成入口、规范 / 角色 / 图标 / 修改 / 快速编辑 / 角色动画状态机、真实生成提交、结果落图、失败恢复和删除图层后的生成态清理从主视图抽出;主视图保留画布事件、浮层定位、上传、项目资源持久化和历史捕获。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` 面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`;点击 `生成工具``Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后上传素材成功,素材数增加,点击素材可加入画布,切换 `图层` 面板可看到对应图层,登录后控制台无前端 error。
- 2026-06-17 上传鉴权回归修正:普通素材上传入口在未登录时先打开 `账号入口`,不再先弹系统文件选择器;登录后用户再次点击上传即可打开文件选择器,避免浏览器拦截登录后异步触发的系统选择器。拖拽 / 已选中文件的续传逻辑仍保留,角色 / 图标生成参考图仍作为本地引用上传,不强制登录。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口``画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`;未登录点击侧栏上传直接弹登录,不出现文件选择器;登录后再次点击上传可以打开文件选择器并上传成功,素材计数从 4 增至 5`AI画布工具栏` 保持可见。

View File

@@ -109,6 +109,7 @@
- 承载图片画布上传工作流:隐藏文件 input、上传目标分发、未登录拦截和登录后续传、上传占位卡片、文件读取、素材落库、拖到画布建层、选中新图层、打开图层侧栏以及角色 / 图标生成参考图上传。 - 承载图片画布上传工作流:隐藏文件 input、上传目标分发、未登录拦截和登录后续传、上传占位卡片、文件读取、素材落库、拖到画布建层、选中新图层、打开图层侧栏以及角色 / 图标生成参考图上传。
- 主视图继续负责画布 drop 外层事件判断、素材库已有素材加入画布、项目资源持久化 hook 注入和画布历史捕获,避免上传 hook 反向成为画布全局状态真相。 - 主视图继续负责画布 drop 外层事件判断、素材库已有素材加入画布、项目资源持久化 hook 注入和画布历史捕获,避免上传 hook 反向成为画布全局状态真相。
- 该 hook 用独立单测覆盖登录续传、上传占位 / 成功回写、上传到画布建层、鉴权失败和生成参考图分发,主视图保留 DOM 级 smoke 覆盖侧栏上传、画布 drop 上传和文件夹定向上传。 - 该 hook 用独立单测覆盖登录续传、上传占位 / 成功回写、上传到画布建层、鉴权失败和生成参考图分发,主视图保留 DOM 级 smoke 覆盖侧栏上传、画布 drop 上传和文件夹定向上传。
- 普通素材上传入口必须在打开系统文件选择器前检查登录态;未登录时先弹 `账号入口`,登录后由用户再次点击上传入口打开选择器,避免浏览器拦截异步触发的系统文件选择器。角色 / 图标生成参考图只作为本地引用进入生成表单,不强制登录。
## 第十二阶段模块 ## 第十二阶段模块

View File

@@ -1648,7 +1648,6 @@ export function ImageCanvasEditorView() {
<ImageCanvasSidebarView <ImageCanvasSidebarView
activeSidebarPanel={activeSidebarPanel} activeSidebarPanel={activeSidebarPanel}
assetListRef={assetListRef} assetListRef={assetListRef}
uploadInputRef={uploadInputRef}
assetPointerDragRef={assetPointerDragRef} assetPointerDragRef={assetPointerDragRef}
suppressAssetClickRef={suppressAssetClickRef} suppressAssetClickRef={suppressAssetClickRef}
assets={assets} assets={assets}
@@ -1673,7 +1672,6 @@ export function ImageCanvasEditorView() {
setRenamingFolder={setRenamingFolder} setRenamingFolder={setRenamingFolder}
setRenamingAsset={setRenamingAsset} setRenamingAsset={setRenamingAsset}
setActiveUploadFolderId={setActiveUploadFolderId} setActiveUploadFolderId={setActiveUploadFolderId}
setUploadTarget={setUploadTarget}
setUploadDropTarget={setUploadDropTarget} setUploadDropTarget={setUploadDropTarget}
setAssetPointerDrag={setAssetPointerDrag} setAssetPointerDrag={setAssetPointerDrag}
setSelectedAssetIds={setSelectedAssetIds} setSelectedAssetIds={setSelectedAssetIds}
@@ -1684,6 +1682,7 @@ export function ImageCanvasEditorView() {
onAssetMarqueePointerUp={handleAssetMarqueePointerUp} onAssetMarqueePointerUp={handleAssetMarqueePointerUp}
updateAssetMoveDropFolder={updateAssetMoveDropFolder} updateAssetMoveDropFolder={updateAssetMoveDropFolder}
addUploadedFiles={addUploadedFiles} addUploadedFiles={addUploadedFiles}
requestUpload={requestUpload}
moveAssetToFolder={moveAssetToFolder} moveAssetToFolder={moveAssetToFolder}
commitNewAssetFolder={commitNewAssetFolder} commitNewAssetFolder={commitNewAssetFolder}
toggleAssetFolder={toggleAssetFolder} toggleAssetFolder={toggleAssetFolder}

View File

@@ -57,7 +57,6 @@ type UploadFilesOptions = {
type ImageCanvasSidebarViewProps = { type ImageCanvasSidebarViewProps = {
activeSidebarPanel: SidebarPanel | null; activeSidebarPanel: SidebarPanel | null;
assetListRef: RefObject<HTMLDivElement | null>; assetListRef: RefObject<HTMLDivElement | null>;
uploadInputRef: RefObject<HTMLInputElement | null>;
assetPointerDragRef: { current: AssetPointerDragState | null }; assetPointerDragRef: { current: AssetPointerDragState | null };
suppressAssetClickRef: { current: boolean }; suppressAssetClickRef: { current: boolean };
assets: EditorAsset[]; assets: EditorAsset[];
@@ -86,7 +85,6 @@ type ImageCanvasSidebarViewProps = {
SetStateAction<{ assetId: string; value: string } | null> SetStateAction<{ assetId: string; value: string } | null>
>; >;
setActiveUploadFolderId: Dispatch<SetStateAction<string>>; setActiveUploadFolderId: Dispatch<SetStateAction<string>>;
setUploadTarget: Dispatch<SetStateAction<UploadTarget>>;
setUploadDropTarget: Dispatch<SetStateAction<'canvas' | 'assets' | null>>; setUploadDropTarget: Dispatch<SetStateAction<'canvas' | 'assets' | null>>;
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>; setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>; setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
@@ -103,6 +101,7 @@ type ImageCanvasSidebarViewProps = {
) => void; ) => void;
updateAssetMoveDropFolder: (folderId: string | null) => void; updateAssetMoveDropFolder: (folderId: string | null) => void;
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void; addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
requestUpload: (target: UploadTarget) => void;
moveAssetToFolder: (assetId: string, folderId: string) => void; moveAssetToFolder: (assetId: string, folderId: string) => void;
commitNewAssetFolder: () => void | Promise<void>; commitNewAssetFolder: () => void | Promise<void>;
toggleAssetFolder: (folderId: string) => void; toggleAssetFolder: (folderId: string) => void;
@@ -133,7 +132,6 @@ type ImageCanvasSidebarViewProps = {
export function ImageCanvasSidebarView({ export function ImageCanvasSidebarView({
activeSidebarPanel, activeSidebarPanel,
assetListRef, assetListRef,
uploadInputRef,
assetPointerDragRef, assetPointerDragRef,
suppressAssetClickRef, suppressAssetClickRef,
assets, assets,
@@ -158,7 +156,6 @@ export function ImageCanvasSidebarView({
setRenamingFolder, setRenamingFolder,
setRenamingAsset, setRenamingAsset,
setActiveUploadFolderId, setActiveUploadFolderId,
setUploadTarget,
setUploadDropTarget, setUploadDropTarget,
setAssetPointerDrag, setAssetPointerDrag,
setSelectedAssetIds, setSelectedAssetIds,
@@ -169,6 +166,7 @@ export function ImageCanvasSidebarView({
onAssetMarqueePointerUp, onAssetMarqueePointerUp,
updateAssetMoveDropFolder, updateAssetMoveDropFolder,
addUploadedFiles, addUploadedFiles,
requestUpload,
moveAssetToFolder, moveAssetToFolder,
commitNewAssetFolder, commitNewAssetFolder,
toggleAssetFolder, toggleAssetFolder,
@@ -427,8 +425,7 @@ export function ImageCanvasSidebarView({
icon={ImagePlus} icon={ImagePlus}
onClick={() => { onClick={() => {
setActiveUploadFolderId(folder.id); setActiveUploadFolderId(folder.id);
setUploadTarget('asset'); requestUpload('asset');
uploadInputRef.current?.click();
}} }}
/> />
</div> </div>

View File

@@ -141,6 +141,9 @@ function UploadWorkflowHarness({
> >
</button> </button>
<button type="button" onClick={() => workflow.requestUpload('asset')}>
</button>
<button <button
type="button" type="button"
onClick={() => onClick={() =>
@@ -171,6 +174,12 @@ function UploadWorkflowHarness({
> >
</button> </button>
<button
type="button"
onClick={() => workflow.requestUpload('character-spec')}
>
</button>
</div> </div>
); );
} }
@@ -227,6 +236,40 @@ describe('useImageCanvasUploadWorkflow', () => {
}); });
}); });
it('opens login instead of the asset file picker when protected data is unavailable', () => {
const openEditorLoginModal = vi.fn();
render(
<UploadWorkflowHarness
canAccessProtectedData={false}
openEditorLoginModal={openEditorLoginModal}
/>,
);
const uploadInput = screen.getByLabelText('上传图片文件') as HTMLInputElement;
const clickUploadInput = vi.spyOn(uploadInput, 'click');
fireEvent.click(screen.getByRole('button', { name: '请求素材上传' }));
expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
expect(clickUploadInput).not.toHaveBeenCalled();
});
it('keeps generation reference uploads local and opens the file picker without login', () => {
const openEditorLoginModal = vi.fn();
render(
<UploadWorkflowHarness
canAccessProtectedData={false}
openEditorLoginModal={openEditorLoginModal}
/>,
);
const uploadInput = screen.getByLabelText('上传图片文件') as HTMLInputElement;
const clickUploadInput = vi.spyOn(uploadInput, 'click');
fireEvent.click(screen.getByRole('button', { name: '请求角色规范上传' }));
expect(openEditorLoginModal).not.toHaveBeenCalled();
expect(clickUploadInput).toHaveBeenCalledTimes(1);
});
it('creates an uploading asset card, adds a canvas layer, and patches the layer with the persisted asset id', async () => { it('creates an uploading asset card, adds a canvas layer, and patches the layer with the persisted asset id', async () => {
const deferredAsset = createDeferred<{ const deferredAsset = createDeferred<{
assetId: string; assetId: string;

View File

@@ -461,10 +461,24 @@ export function useImageCanvasUploadWorkflow({
], ],
); );
const requestUpload = useCallback((target: UploadTarget = 'asset') => { const openUploadPicker = useCallback(
setUploadTarget(target); (target: UploadTarget) => {
uploadInputRef.current?.click(); setUploadTarget(target);
}, []); uploadInputRef.current?.click();
},
[setUploadTarget],
);
const requestUpload = useCallback(
(target: UploadTarget = 'asset') => {
if (target === 'asset' && !canAccessProtectedDataRef.current) {
openEditorLoginModal();
return;
}
openUploadPicker(target);
},
[openEditorLoginModal, openUploadPicker],
);
const handleUploadInputChange = useCallback( const handleUploadInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => { (event: ChangeEvent<HTMLInputElement>) => {