diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx index 623d6d43a..ed8e85a31 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx @@ -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(), }; } diff --git a/src/components/image-editor/ImageCanvasEditorView.test.tsx b/src/components/image-editor/ImageCanvasEditorView.test.tsx index 2f358c611..50b60b0b3 100644 --- a/src/components/image-editor/ImageCanvasEditorView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.test.tsx @@ -205,6 +205,42 @@ describe('ImageCanvasEditorView', () => { }); }); + it('opens the account modal from the canvas topbar avatar entry', async () => { + const openAccountModal = vi.fn(); + render( + + + , + ); + + 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( setIsShortcutDialogOpen(true), onOpenWallet: openRechargeOrRewardCodeModal, + onOpenAccount: () => { + if (authUi?.user) { + authUi.openAccountModal(); + return; + } + authUi?.openLoginModal(); + }, }; const stageProps = { projectId, diff --git a/src/components/image-editor/ImageCanvasTopbarView.test.tsx b/src/components/image-editor/ImageCanvasTopbarView.test.tsx index 93f8ca7ca..b9f2c584b 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.test.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.test.tsx @@ -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()} />, ); diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index e4a6ff24d..f1b10df33 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -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; 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 (
@@ -185,6 +214,27 @@ export function ImageCanvasTopbarView({ /> {walletDisplayLabel} +
); diff --git a/src/index.css b/src/index.css index 61ba236fd..a6088efc0 100644 --- a/src/index.css +++ b/src/index.css @@ -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 {