显示画板顶部用户头像
在画板顶部泥点右侧显示当前用户头像、昵称和用户码。 头像入口复用账号弹窗,未登录时打开登录入口。 补充画板顶部头像入口的组件和集成测试。
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user