修复图片画布素材上传鉴权
统一侧栏上传入口走上传工作流 未登录素材上传先弹账号入口,不再打开文件选择器 补充上传鉴权回归测试和编辑器拆分文档记录
This commit is contained in:
@@ -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画布工具栏` 保持可见。
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
- 承载图片画布上传工作流:隐藏文件 input、上传目标分发、未登录拦截和登录后续传、上传占位卡片、文件读取、素材落库、拖到画布建层、选中新图层、打开图层侧栏,以及角色 / 图标生成参考图上传。
|
- 承载图片画布上传工作流:隐藏文件 input、上传目标分发、未登录拦截和登录后续传、上传占位卡片、文件读取、素材落库、拖到画布建层、选中新图层、打开图层侧栏,以及角色 / 图标生成参考图上传。
|
||||||
- 主视图继续负责画布 drop 外层事件判断、素材库已有素材加入画布、项目资源持久化 hook 注入和画布历史捕获,避免上传 hook 反向成为画布全局状态真相。
|
- 主视图继续负责画布 drop 外层事件判断、素材库已有素材加入画布、项目资源持久化 hook 注入和画布历史捕获,避免上传 hook 反向成为画布全局状态真相。
|
||||||
- 该 hook 用独立单测覆盖登录续传、上传占位 / 成功回写、上传到画布建层、鉴权失败和生成参考图分发,主视图保留 DOM 级 smoke 覆盖侧栏上传、画布 drop 上传和文件夹定向上传。
|
- 该 hook 用独立单测覆盖登录续传、上传占位 / 成功回写、上传到画布建层、鉴权失败和生成参考图分发,主视图保留 DOM 级 smoke 覆盖侧栏上传、画布 drop 上传和文件夹定向上传。
|
||||||
|
- 普通素材上传入口必须在打开系统文件选择器前检查登录态;未登录时先弹 `账号入口`,登录后由用户再次点击上传入口打开选择器,避免浏览器拦截异步触发的系统文件选择器。角色 / 图标生成参考图只作为本地引用进入生成表单,不强制登录。
|
||||||
|
|
||||||
## 第十二阶段模块
|
## 第十二阶段模块
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -461,10 +461,24 @@ export function useImageCanvasUploadWorkflow({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestUpload = useCallback((target: UploadTarget = 'asset') => {
|
const openUploadPicker = useCallback(
|
||||||
|
(target: UploadTarget) => {
|
||||||
setUploadTarget(target);
|
setUploadTarget(target);
|
||||||
uploadInputRef.current?.click();
|
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>) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user