优化画布顶部栏布局
收窄画布顶部栏高度和按钮间距 移除未命名画布下方的画布副标题 将画布标题和重命名入口贴近返回按钮 在顶部栏右端展示当前泥点余额 补充画布顶部栏测试和技术方案说明
This commit is contained in:
@@ -98,6 +98,8 @@ function createTopbarProps(): ImageCanvasTopbarViewProps {
|
||||
isProjectRenameSaving: false,
|
||||
projectRenameError: null,
|
||||
layers: [],
|
||||
walletBalanceLabel: '0泥点',
|
||||
isWalletBalanceLoading: false,
|
||||
assetExportStatus: null,
|
||||
isExportingAssets: false,
|
||||
setProjectRenameValue: vi.fn(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import JSZip from 'jszip';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
ApiClientError,
|
||||
@@ -38,6 +38,7 @@ const loadEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||
const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||
const saveEditorProjectLayoutMock = vi.hoisted(() => vi.fn());
|
||||
const getPlatformProfileDashboardMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
const actual = await vi.importActual<
|
||||
@@ -64,6 +65,10 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../services/platform-entry/platformProfileClient', () => ({
|
||||
getPlatformProfileDashboard: getPlatformProfileDashboardMock,
|
||||
}));
|
||||
|
||||
describe('ImageCanvasEditorView', () => {
|
||||
setupImageCanvasEditorViewTestLifecycle({
|
||||
generateEditorImageMock,
|
||||
@@ -84,6 +89,19 @@ describe('ImageCanvasEditorView', () => {
|
||||
saveEditorProjectLayoutMock,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
getPlatformProfileDashboardMock.mockResolvedValue({
|
||||
walletBalance: 1234,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getPlatformProfileDashboardMock.mockReset();
|
||||
});
|
||||
|
||||
it('loads the project from projectid query before falling back to recent project', async () => {
|
||||
loadEditorProjectMock.mockResolvedValueOnce({
|
||||
projectId: 'editor-project-query',
|
||||
@@ -157,6 +175,36 @@ describe('ImageCanvasEditorView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the live mud point balance in the canvas topbar when logged in', async () => {
|
||||
render(
|
||||
<AuthUiContext.Provider
|
||||
value={createAuthValue({
|
||||
user: {
|
||||
id: 'user-1',
|
||||
publicUserCode: 'U001',
|
||||
displayName: '测试用户',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: '138****0000',
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
},
|
||||
canAccessProtectedData: true,
|
||||
})}
|
||||
>
|
||||
<ImageCanvasEditorView />
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText('泥点余额 1,234泥点')).toBeTruthy();
|
||||
expect(getPlatformProfileDashboardMock).toHaveBeenCalledWith({
|
||||
authImpact: 'local',
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('opens the login modal immediately when entering the editor while logged out', async () => {
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
type EditorAssetSnapshot,
|
||||
loadEditorGenerationPricing,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
|
||||
import { formatDashboardCount } from '../rpg-entry/rpgEntryProfileDashboardPresentation';
|
||||
import {
|
||||
canvasAssetKindOrNull,
|
||||
generationInputsOrNull,
|
||||
@@ -66,6 +68,10 @@ import {
|
||||
export function ImageCanvasEditorView() {
|
||||
const authUi = useAuthUi();
|
||||
const [, setGenerationPricingVersion] = useState(0);
|
||||
const [walletBalanceLabel, setWalletBalanceLabel] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isWalletBalanceLoading, setIsWalletBalanceLoading] = useState(false);
|
||||
const editorRootRef = useRef<HTMLElement | null>(null);
|
||||
const canvasViewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const assetListRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -175,6 +181,66 @@ export function ImageCanvasEditorView() {
|
||||
window.location.reload();
|
||||
});
|
||||
}, [authUi]);
|
||||
useEffect(() => {
|
||||
if (!authUi?.canAccessProtectedData || !authUi.user) {
|
||||
setWalletBalanceLabel(null);
|
||||
setIsWalletBalanceLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let isMounted = true;
|
||||
let requestId = 0;
|
||||
|
||||
const refreshWalletBalance = () => {
|
||||
const currentRequestId = requestId + 1;
|
||||
requestId = currentRequestId;
|
||||
setIsWalletBalanceLoading(true);
|
||||
void getPlatformProfileDashboard({
|
||||
authImpact: 'local',
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
})
|
||||
.then((dashboard) => {
|
||||
if (!isMounted || currentRequestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
setWalletBalanceLabel(`${formatDashboardCount(dashboard.walletBalance)}泥点`);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!isMounted || currentRequestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
setWalletBalanceLabel(null);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!isMounted || currentRequestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
setIsWalletBalanceLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
refreshWalletBalance();
|
||||
|
||||
const handleWindowFocus = () => {
|
||||
refreshWalletBalance();
|
||||
};
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
refreshWalletBalance();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focus', handleWindowFocus);
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
window.removeEventListener('focus', handleWindowFocus);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [authUi?.canAccessProtectedData, authUi?.user?.id]);
|
||||
const {
|
||||
projectTitle,
|
||||
setProjectTitle,
|
||||
@@ -1179,6 +1245,8 @@ export function ImageCanvasEditorView() {
|
||||
isProjectRenameSaving,
|
||||
projectRenameError,
|
||||
layers,
|
||||
walletBalanceLabel,
|
||||
isWalletBalanceLoading,
|
||||
assetExportStatus,
|
||||
isExportingAssets,
|
||||
setProjectRenameValue,
|
||||
|
||||
@@ -38,6 +38,8 @@ function renderTopbar(
|
||||
isProjectRenameSaving: false,
|
||||
projectRenameError: null,
|
||||
layers: [],
|
||||
walletBalanceLabel: '1,234泥点',
|
||||
isWalletBalanceLoading: false,
|
||||
assetExportStatus: null,
|
||||
isExportingAssets: false,
|
||||
setProjectRenameValue: vi.fn(),
|
||||
@@ -60,6 +62,7 @@ describe('ImageCanvasTopbarView', () => {
|
||||
const props = renderTopbar();
|
||||
|
||||
expect(screen.getByRole('heading', { name: '默认项目' })).toBeTruthy();
|
||||
expect(screen.queryByText('画布')).toBeNull();
|
||||
expect(
|
||||
screen.getByRole('link', { name: '返回项目页面' }).getAttribute('href'),
|
||||
).toBe('/project');
|
||||
@@ -73,6 +76,19 @@ describe('ImageCanvasTopbarView', () => {
|
||||
expect(props.onOpenShortcuts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows the current mud point balance in the topbar', () => {
|
||||
renderTopbar({
|
||||
walletBalanceLabel: '1.2万泥点',
|
||||
});
|
||||
|
||||
const walletChip = screen.getByLabelText('泥点余额 1.2万泥点');
|
||||
|
||||
expect(walletChip.textContent).toBe('1.2万泥点');
|
||||
expect(walletChip.querySelector('img')?.getAttribute('src')).toBe(
|
||||
'/creation-home/topbar-wallet.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits, resets, and cancels project rename edits', () => {
|
||||
const props = renderTopbar({
|
||||
isRenamingProject: true,
|
||||
@@ -105,6 +121,8 @@ describe('ImageCanvasTopbarView', () => {
|
||||
isProjectRenameSaving={false}
|
||||
projectRenameError={null}
|
||||
layers={[]}
|
||||
walletBalanceLabel="0泥点"
|
||||
isWalletBalanceLoading={false}
|
||||
assetExportStatus={null}
|
||||
isExportingAssets={false}
|
||||
setProjectRenameValue={vi.fn()}
|
||||
@@ -131,6 +149,8 @@ describe('ImageCanvasTopbarView', () => {
|
||||
isProjectRenameSaving={false}
|
||||
projectRenameError={null}
|
||||
layers={[createLayer()]}
|
||||
walletBalanceLabel="0泥点"
|
||||
isWalletBalanceLoading={false}
|
||||
assetExportStatus={{
|
||||
tone: 'success',
|
||||
message: '画布素材已导出',
|
||||
|
||||
@@ -21,6 +21,8 @@ export type ImageCanvasTopbarViewProps = {
|
||||
isProjectRenameSaving: boolean;
|
||||
projectRenameError: string | null;
|
||||
layers: CanvasLayer[];
|
||||
walletBalanceLabel: string | null;
|
||||
isWalletBalanceLoading: boolean;
|
||||
assetExportStatus: AssetExportStatus | null;
|
||||
isExportingAssets: boolean;
|
||||
setProjectRenameValue: (value: string) => void;
|
||||
@@ -40,6 +42,8 @@ export function ImageCanvasTopbarView({
|
||||
isProjectRenameSaving,
|
||||
projectRenameError,
|
||||
layers,
|
||||
walletBalanceLabel,
|
||||
isWalletBalanceLoading,
|
||||
assetExportStatus,
|
||||
isExportingAssets,
|
||||
setProjectRenameValue,
|
||||
@@ -53,86 +57,91 @@ export function ImageCanvasTopbarView({
|
||||
const hasExportableLayer = layers.some(
|
||||
(layer) => layer.src.trim().length > 0,
|
||||
);
|
||||
const walletDisplayLabel = walletBalanceLabel ?? '--泥点';
|
||||
const walletAriaLabel = walletBalanceLabel
|
||||
? `泥点余额 ${walletBalanceLabel}`
|
||||
: '泥点余额读取中';
|
||||
|
||||
return (
|
||||
<div className="image-canvas-editor__topbar">
|
||||
<a
|
||||
className="image-canvas-editor__project-back-button"
|
||||
href="/project"
|
||||
aria-label="返回项目页面"
|
||||
title="返回项目"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<div className="image-canvas-editor__title-block">
|
||||
{isRenamingProject ? (
|
||||
<form
|
||||
className="image-canvas-editor__project-title-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
submitProjectRename(projectId);
|
||||
}}
|
||||
>
|
||||
<PlatformTextField
|
||||
aria-label="项目名称"
|
||||
value={projectRenameValue}
|
||||
autoFocus
|
||||
disabled={isProjectRenameSaving}
|
||||
className="image-canvas-editor__project-title-input"
|
||||
onChange={(event) => {
|
||||
setProjectRenameValue(event.target.value);
|
||||
resetProjectRenameError();
|
||||
<div className="image-canvas-editor__topbar-project">
|
||||
<a
|
||||
className="image-canvas-editor__project-back-button"
|
||||
href="/project"
|
||||
aria-label="返回项目页面"
|
||||
title="返回项目"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||
</a>
|
||||
<div className="image-canvas-editor__title-block">
|
||||
{isRenamingProject ? (
|
||||
<form
|
||||
className="image-canvas-editor__project-title-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
submitProjectRename(projectId);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelProjectRename();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EditorIconButton
|
||||
type="submit"
|
||||
label="保存项目名称"
|
||||
title="保存"
|
||||
icon={Check}
|
||||
disabled={isProjectRenameSaving}
|
||||
/>
|
||||
<EditorIconButton
|
||||
label="取消修改项目名称"
|
||||
title="取消"
|
||||
icon={X}
|
||||
disabled={isProjectRenameSaving}
|
||||
onClick={cancelProjectRename}
|
||||
/>
|
||||
{projectRenameError ? (
|
||||
<span
|
||||
className="image-canvas-editor__project-title-error"
|
||||
role="alert"
|
||||
>
|
||||
{projectRenameError}
|
||||
</span>
|
||||
) : null}
|
||||
</form>
|
||||
) : (
|
||||
<div className="image-canvas-editor__project-title-row">
|
||||
<button
|
||||
type="button"
|
||||
className="image-canvas-editor__project-title-button"
|
||||
onDoubleClick={startProjectRename}
|
||||
aria-label={`编辑项目名称${projectTitle}`}
|
||||
>
|
||||
<h1>{projectTitle}</h1>
|
||||
</button>
|
||||
<EditorIconButton
|
||||
className="image-canvas-editor__project-rename-button"
|
||||
label="编辑项目名称"
|
||||
title="编辑项目名称"
|
||||
icon={Pencil}
|
||||
onClick={startProjectRename}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span>画布</span>
|
||||
<PlatformTextField
|
||||
aria-label="项目名称"
|
||||
value={projectRenameValue}
|
||||
autoFocus
|
||||
disabled={isProjectRenameSaving}
|
||||
className="image-canvas-editor__project-title-input"
|
||||
onChange={(event) => {
|
||||
setProjectRenameValue(event.target.value);
|
||||
resetProjectRenameError();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
cancelProjectRename();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EditorIconButton
|
||||
type="submit"
|
||||
label="保存项目名称"
|
||||
title="保存"
|
||||
icon={Check}
|
||||
disabled={isProjectRenameSaving}
|
||||
/>
|
||||
<EditorIconButton
|
||||
label="取消修改项目名称"
|
||||
title="取消"
|
||||
icon={X}
|
||||
disabled={isProjectRenameSaving}
|
||||
onClick={cancelProjectRename}
|
||||
/>
|
||||
{projectRenameError ? (
|
||||
<span
|
||||
className="image-canvas-editor__project-title-error"
|
||||
role="alert"
|
||||
>
|
||||
{projectRenameError}
|
||||
</span>
|
||||
) : null}
|
||||
</form>
|
||||
) : (
|
||||
<div className="image-canvas-editor__project-title-row">
|
||||
<button
|
||||
type="button"
|
||||
className="image-canvas-editor__project-title-button"
|
||||
onDoubleClick={startProjectRename}
|
||||
aria-label={`编辑项目名称${projectTitle}`}
|
||||
>
|
||||
<h1>{projectTitle}</h1>
|
||||
</button>
|
||||
<EditorIconButton
|
||||
className="image-canvas-editor__project-rename-button"
|
||||
label="编辑项目名称"
|
||||
title="编辑项目名称"
|
||||
icon={Pencil}
|
||||
onClick={startProjectRename}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="image-canvas-editor__topbar-actions">
|
||||
<EditorIconButton
|
||||
@@ -158,6 +167,20 @@ export function ImageCanvasTopbarView({
|
||||
{assetExportStatus.message}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
<div
|
||||
className="image-canvas-editor__wallet-chip"
|
||||
aria-label={walletAriaLabel}
|
||||
aria-busy={isWalletBalanceLoading}
|
||||
aria-live="polite"
|
||||
>
|
||||
<img
|
||||
src="/creation-home/topbar-wallet.png"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
draggable={false}
|
||||
/>
|
||||
<span>{walletDisplayLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user