修复图片画布素材上传鉴权
统一侧栏上传入口走上传工作流 未登录素材上传先弹账号入口,不再打开文件选择器 补充上传鉴权回归测试和编辑器拆分文档记录
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 前端拆分第十四阶段:新增 `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 上传鉴权回归修正:普通素材上传入口在未登录时先打开 `账号入口`,不再先弹系统文件选择器;登录后用户再次点击上传即可打开文件选择器,避免浏览器拦截登录后异步触发的系统选择器。拖拽 / 已选中文件的续传逻辑仍保留,角色 / 图标生成参考图仍作为本地引用上传,不强制登录。验证命令:`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、上传目标分发、未登录拦截和登录后续传、上传占位卡片、文件读取、素材落库、拖到画布建层、选中新图层、打开图层侧栏,以及角色 / 图标生成参考图上传。
|
||||
- 主视图继续负责画布 drop 外层事件判断、素材库已有素材加入画布、项目资源持久化 hook 注入和画布历史捕获,避免上传 hook 反向成为画布全局状态真相。
|
||||
- 该 hook 用独立单测覆盖登录续传、上传占位 / 成功回写、上传到画布建层、鉴权失败和生成参考图分发,主视图保留 DOM 级 smoke 覆盖侧栏上传、画布 drop 上传和文件夹定向上传。
|
||||
- 普通素材上传入口必须在打开系统文件选择器前检查登录态;未登录时先弹 `账号入口`,登录后由用户再次点击上传入口打开选择器,避免浏览器拦截异步触发的系统文件选择器。角色 / 图标生成参考图只作为本地引用进入生成表单,不强制登录。
|
||||
|
||||
## 第十二阶段模块
|
||||
|
||||
|
||||
@@ -1648,7 +1648,6 @@ export function ImageCanvasEditorView() {
|
||||
<ImageCanvasSidebarView
|
||||
activeSidebarPanel={activeSidebarPanel}
|
||||
assetListRef={assetListRef}
|
||||
uploadInputRef={uploadInputRef}
|
||||
assetPointerDragRef={assetPointerDragRef}
|
||||
suppressAssetClickRef={suppressAssetClickRef}
|
||||
assets={assets}
|
||||
@@ -1673,7 +1672,6 @@ export function ImageCanvasEditorView() {
|
||||
setRenamingFolder={setRenamingFolder}
|
||||
setRenamingAsset={setRenamingAsset}
|
||||
setActiveUploadFolderId={setActiveUploadFolderId}
|
||||
setUploadTarget={setUploadTarget}
|
||||
setUploadDropTarget={setUploadDropTarget}
|
||||
setAssetPointerDrag={setAssetPointerDrag}
|
||||
setSelectedAssetIds={setSelectedAssetIds}
|
||||
@@ -1684,6 +1682,7 @@ export function ImageCanvasEditorView() {
|
||||
onAssetMarqueePointerUp={handleAssetMarqueePointerUp}
|
||||
updateAssetMoveDropFolder={updateAssetMoveDropFolder}
|
||||
addUploadedFiles={addUploadedFiles}
|
||||
requestUpload={requestUpload}
|
||||
moveAssetToFolder={moveAssetToFolder}
|
||||
commitNewAssetFolder={commitNewAssetFolder}
|
||||
toggleAssetFolder={toggleAssetFolder}
|
||||
|
||||
@@ -57,7 +57,6 @@ type UploadFilesOptions = {
|
||||
type ImageCanvasSidebarViewProps = {
|
||||
activeSidebarPanel: SidebarPanel | null;
|
||||
assetListRef: RefObject<HTMLDivElement | null>;
|
||||
uploadInputRef: RefObject<HTMLInputElement | null>;
|
||||
assetPointerDragRef: { current: AssetPointerDragState | null };
|
||||
suppressAssetClickRef: { current: boolean };
|
||||
assets: EditorAsset[];
|
||||
@@ -86,7 +85,6 @@ type ImageCanvasSidebarViewProps = {
|
||||
SetStateAction<{ assetId: string; value: string } | null>
|
||||
>;
|
||||
setActiveUploadFolderId: Dispatch<SetStateAction<string>>;
|
||||
setUploadTarget: Dispatch<SetStateAction<UploadTarget>>;
|
||||
setUploadDropTarget: Dispatch<SetStateAction<'canvas' | 'assets' | null>>;
|
||||
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
|
||||
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
|
||||
@@ -103,6 +101,7 @@ type ImageCanvasSidebarViewProps = {
|
||||
) => void;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
|
||||
requestUpload: (target: UploadTarget) => void;
|
||||
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
||||
commitNewAssetFolder: () => void | Promise<void>;
|
||||
toggleAssetFolder: (folderId: string) => void;
|
||||
@@ -133,7 +132,6 @@ type ImageCanvasSidebarViewProps = {
|
||||
export function ImageCanvasSidebarView({
|
||||
activeSidebarPanel,
|
||||
assetListRef,
|
||||
uploadInputRef,
|
||||
assetPointerDragRef,
|
||||
suppressAssetClickRef,
|
||||
assets,
|
||||
@@ -158,7 +156,6 @@ export function ImageCanvasSidebarView({
|
||||
setRenamingFolder,
|
||||
setRenamingAsset,
|
||||
setActiveUploadFolderId,
|
||||
setUploadTarget,
|
||||
setUploadDropTarget,
|
||||
setAssetPointerDrag,
|
||||
setSelectedAssetIds,
|
||||
@@ -169,6 +166,7 @@ export function ImageCanvasSidebarView({
|
||||
onAssetMarqueePointerUp,
|
||||
updateAssetMoveDropFolder,
|
||||
addUploadedFiles,
|
||||
requestUpload,
|
||||
moveAssetToFolder,
|
||||
commitNewAssetFolder,
|
||||
toggleAssetFolder,
|
||||
@@ -427,8 +425,7 @@ export function ImageCanvasSidebarView({
|
||||
icon={ImagePlus}
|
||||
onClick={() => {
|
||||
setActiveUploadFolderId(folder.id);
|
||||
setUploadTarget('asset');
|
||||
uploadInputRef.current?.click();
|
||||
requestUpload('asset');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +141,9 @@ function UploadWorkflowHarness({
|
||||
>
|
||||
上传素材
|
||||
</button>
|
||||
<button type="button" onClick={() => workflow.requestUpload('asset')}>
|
||||
请求素材上传
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
@@ -171,6 +174,12 @@ function UploadWorkflowHarness({
|
||||
>
|
||||
选择角色规范
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => workflow.requestUpload('character-spec')}
|
||||
>
|
||||
请求角色规范上传
|
||||
</button>
|
||||
</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 () => {
|
||||
const deferredAsset = createDeferred<{
|
||||
assetId: string;
|
||||
|
||||
@@ -461,10 +461,24 @@ export function useImageCanvasUploadWorkflow({
|
||||
],
|
||||
);
|
||||
|
||||
const requestUpload = useCallback((target: UploadTarget = 'asset') => {
|
||||
setUploadTarget(target);
|
||||
uploadInputRef.current?.click();
|
||||
}, []);
|
||||
const openUploadPicker = useCallback(
|
||||
(target: UploadTarget) => {
|
||||
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(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
Reference in New Issue
Block a user