显示画板顶部用户头像

在画板顶部泥点右侧显示当前用户头像、昵称和用户码。

头像入口复用账号弹窗,未登录时打开登录入口。

补充画板顶部头像入口的组件和集成测试。
This commit is contained in:
2026-06-29 18:07:44 +08:00
parent 7417370ae9
commit dbf38f3a8c
6 changed files with 216 additions and 0 deletions
@@ -101,6 +101,7 @@ function createTopbarProps(): ImageCanvasTopbarViewProps {
layers: [],
walletBalanceLabel: '0泥点',
isWalletBalanceLoading: false,
currentUser: null,
assetExportStatus: null,
isExportingAssets: false,
setProjectRenameValue: vi.fn(),
@@ -111,6 +112,7 @@ function createTopbarProps(): ImageCanvasTopbarViewProps {
exportCanvasAssets: vi.fn(),
onOpenShortcuts: vi.fn(),
onOpenWallet: vi.fn(),
onOpenAccount: vi.fn(),
};
}
@@ -205,6 +205,42 @@ describe('ImageCanvasEditorView', () => {
});
});
it('opens the account modal from the canvas topbar avatar entry', async () => {
const openAccountModal = vi.fn();
render(
<AuthUiContext.Provider
value={createAuthValue({
user: {
id: 'user-1',
publicUserCode: 'U001',
displayName: '测试用户',
avatarUrl: 'data:image/png;base64,AVATAR',
phoneNumberMasked: '138****0000',
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
},
canAccessProtectedData: true,
openAccountModal,
})}
>
<ImageCanvasEditorView />
</AuthUiContext.Provider>,
);
const accountButton = await screen.findByRole('button', {
name: '账号 测试用户',
});
expect(accountButton.querySelector('img')?.getAttribute('src')).toBe(
'data:image/png;base64,AVATAR',
);
fireEvent.click(accountButton);
expect(openAccountModal).toHaveBeenCalledTimes(1);
});
it('opens the same account wallet entry from the canvas topbar mud point button', async () => {
render(
<AuthUiContext.Provider
@@ -1649,6 +1649,7 @@ export function ImageCanvasEditorView() {
layers,
walletBalanceLabel,
isWalletBalanceLoading,
currentUser: authUi?.user,
assetExportStatus,
isExportingAssets,
setProjectRenameValue,
@@ -1659,6 +1660,13 @@ export function ImageCanvasEditorView() {
exportCanvasAssets,
onOpenShortcuts: () => setIsShortcutDialogOpen(true),
onOpenWallet: openRechargeOrRewardCodeModal,
onOpenAccount: () => {
if (authUi?.user) {
authUi.openAccountModal();
return;
}
authUi?.openLoginModal();
},
};
const stageProps = {
projectId,
@@ -40,6 +40,7 @@ function renderTopbar(
layers: [],
walletBalanceLabel: '1,234泥点',
isWalletBalanceLoading: false,
currentUser: null,
assetExportStatus: null,
isExportingAssets: false,
setProjectRenameValue: vi.fn(),
@@ -50,6 +51,7 @@ function renderTopbar(
exportCanvasAssets: vi.fn(),
onOpenShortcuts: vi.fn(),
onOpenWallet: vi.fn(),
onOpenAccount: vi.fn(),
...overrides,
};
@@ -95,6 +97,28 @@ describe('ImageCanvasTopbarView', () => {
expect(props.onOpenWallet).toHaveBeenCalledTimes(1);
});
it('shows the current user avatar beside the mud point balance', () => {
const props = renderTopbar({
currentUser: {
id: 'user-1',
publicUserCode: 'SY-00000003',
displayName: '厉冰都',
avatarUrl: 'data:image/png;base64,AVATAR',
},
});
const accountButton = screen.getByRole('button', { name: '账号 厉冰都' });
expect(accountButton.textContent).toContain('厉冰都');
expect(accountButton.textContent).toContain('SY-00000003');
expect(accountButton.querySelector('img')?.getAttribute('src')).toBe(
'data:image/png;base64,AVATAR',
);
fireEvent.click(accountButton);
expect(props.onOpenAccount).toHaveBeenCalledTimes(1);
});
it('submits, resets, and cancels project rename edits', () => {
const props = renderTopbar({
isRenamingProject: true,
@@ -129,6 +153,7 @@ describe('ImageCanvasTopbarView', () => {
layers={[]}
walletBalanceLabel="0泥点"
isWalletBalanceLoading={false}
currentUser={null}
assetExportStatus={null}
isExportingAssets={false}
setProjectRenameValue={vi.fn()}
@@ -139,6 +164,7 @@ describe('ImageCanvasTopbarView', () => {
exportCanvasAssets={exportCanvasAssets}
onOpenShortcuts={vi.fn()}
onOpenWallet={vi.fn()}
onOpenAccount={vi.fn()}
/>,
);
@@ -158,6 +184,7 @@ describe('ImageCanvasTopbarView', () => {
layers={[createLayer()]}
walletBalanceLabel="0泥点"
isWalletBalanceLoading={false}
currentUser={null}
assetExportStatus={{
tone: 'success',
message: '画布素材已导出',
@@ -171,6 +198,7 @@ describe('ImageCanvasTopbarView', () => {
exportCanvasAssets={exportCanvasAssets}
onOpenShortcuts={vi.fn()}
onOpenWallet={vi.fn()}
onOpenAccount={vi.fn()}
/>,
);
@@ -23,6 +23,15 @@ export type ImageCanvasTopbarViewProps = {
layers: CanvasLayer[];
walletBalanceLabel: string | null;
isWalletBalanceLoading: boolean;
currentUser:
| {
id: string;
publicUserCode: string;
displayName: string;
avatarUrl: string | null;
}
| null
| undefined;
assetExportStatus: AssetExportStatus | null;
isExportingAssets: boolean;
setProjectRenameValue: (value: string) => void;
@@ -33,8 +42,20 @@ export type ImageCanvasTopbarViewProps = {
exportCanvasAssets: () => void | Promise<void>;
onOpenShortcuts: () => void;
onOpenWallet: () => void;
onOpenAccount: () => void;
};
function buildCanvasUserCode(
user: ImageCanvasTopbarViewProps['currentUser'],
) {
if (user?.publicUserCode?.trim()) {
return user.publicUserCode.trim();
}
const raw =
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() || '00000000';
return `SY-${raw.slice(-8).padStart(8, '0')}`;
}
export function ImageCanvasTopbarView({
projectId,
projectTitle,
@@ -45,6 +66,7 @@ export function ImageCanvasTopbarView({
layers,
walletBalanceLabel,
isWalletBalanceLoading,
currentUser,
assetExportStatus,
isExportingAssets,
setProjectRenameValue,
@@ -55,6 +77,7 @@ export function ImageCanvasTopbarView({
exportCanvasAssets,
onOpenShortcuts,
onOpenWallet,
onOpenAccount,
}: ImageCanvasTopbarViewProps) {
const hasExportableLayer = layers.some(
(layer) => layer.src.trim().length > 0,
@@ -63,6 +86,12 @@ export function ImageCanvasTopbarView({
const walletAriaLabel = walletBalanceLabel
? `泥点余额 ${walletBalanceLabel}`
: '泥点余额读取中';
const userDisplayName = currentUser?.displayName?.trim() || '登录';
const userAvatarUrl = currentUser?.avatarUrl?.trim() || null;
const userAvatarLabel = userDisplayName.slice(0, 1).toUpperCase();
const userCodeLabel = currentUser
? buildCanvasUserCode(currentUser)
: '账号入口';
return (
<div className="image-canvas-editor__topbar">
@@ -185,6 +214,27 @@ export function ImageCanvasTopbarView({
/>
<span>{walletDisplayLabel}</span>
</button>
<button
type="button"
className="image-canvas-editor__account-chip"
onClick={onOpenAccount}
aria-label={`账号 ${userDisplayName}`}
>
<span
className="image-canvas-editor__account-avatar"
aria-hidden="true"
>
{userAvatarUrl ? (
<img src={userAvatarUrl} alt="" draggable={false} />
) : (
userAvatarLabel
)}
</span>
<span className="image-canvas-editor__account-meta">
<span>{userDisplayName}</span>
<span>{userCodeLabel}</span>
</span>
</button>
</div>
</div>
);
+92
View File
@@ -5207,6 +5207,81 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
mix-blend-mode: multiply;
}
.image-canvas-editor__account-chip {
display: inline-flex;
min-width: 0;
height: 2.04rem;
flex: 0 1 auto;
align-items: center;
gap: 0.42rem;
border: 1px solid rgba(214, 184, 159, 0.72);
border-radius: 999px;
background: rgba(255, 250, 244, 0.82);
padding: 0 0.68rem 0 0.24rem;
color: #4f2f23;
line-height: 1;
transition:
transform 160ms ease,
background-color 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease;
}
.image-canvas-editor__account-chip:hover,
.image-canvas-editor__account-chip:focus-visible {
transform: translateY(-1px);
border-color: rgba(214, 184, 159, 0.92);
background: rgba(255, 250, 244, 0.96);
box-shadow: 0 0.28rem 0.78rem rgba(112, 62, 32, 0.12);
}
.image-canvas-editor__account-avatar {
display: inline-grid;
width: 1.66rem;
height: 1.66rem;
flex: 0 0 auto;
place-items: center;
overflow: hidden;
border-radius: 999px;
background: var(--platform-profile-avatar-fill);
color: #ffffff;
font-size: 0.76rem;
font-weight: 900;
box-shadow: var(--platform-profile-avatar-shadow);
}
.image-canvas-editor__account-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-canvas-editor__account-meta {
display: grid;
min-width: 0;
max-width: 8.4rem;
gap: 0.12rem;
text-align: left;
}
.image-canvas-editor__account-meta span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-canvas-editor__account-meta span:first-child {
color: #4f2f23;
font-size: 0.76rem;
font-weight: 880;
}
.image-canvas-editor__account-meta span:last-child {
color: #8a6d5c;
font-size: 0.66rem;
font-weight: 760;
}
.image-canvas-editor__zoom-menu-wrap {
margin-left: auto;
position: relative;
@@ -8987,6 +9062,23 @@ button.image-canvas-editor__reference-chip:disabled {
height: 1.02rem;
}
.image-canvas-editor__account-chip {
width: 1.94rem;
height: 1.94rem;
padding: 0;
justify-content: center;
}
.image-canvas-editor__account-avatar {
width: 1.38rem;
height: 1.38rem;
font-size: 0.68rem;
}
.image-canvas-editor__account-meta {
display: none;
}
.image-canvas-editor__floating-toolbar button,
.image-canvas-editor__bottom-toolbar button,
.image-canvas-editor__reset-button {