恢复平台账号顶栏与我的页签
按新版创作页原布局恢复泥点入口与账号身份胶囊。 在桌面侧栏补回我的页签及游客、登录态个人页面。 个人页面只复用账号、钱包、统计、设置和法律信息等平台能力。 补充壳层与个人页面回归测试并更新退役边界文档。
This commit is contained in:
@@ -21,7 +21,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
|
||||
- 小程序 WebView 外壳:`miniprogram/`。
|
||||
- 法律文本:`media/files/user_agreement.md`、`media/files/privacy_policy.md`、`media/files/disclaimer.md`。
|
||||
|
||||
移动端一级 Tab:`推荐 / 发现 / 我的`。桌面端导航保留 `创作` 并新增 `项目`,其中 `/creation` 是独立创作工具主页,`/project` 是画布项目入口。
|
||||
移动端一级 Tab:`推荐 / 发现 / 我的`。桌面端导航保留 `创作 / 项目 / 我的`,其中 `/creation` 是独立创作工具主页,`/project` 是画布项目入口,“我的”继续承载账号、钱包、统计和通用设置等平台公共能力。
|
||||
|
||||
## 当前后端路线
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- `migration.rs` 中相关表的迁移白名单、表名兼容和字段目录。
|
||||
- 为历史审计、迁移、资产归属核对所必需的最小只读表定义;不得借兼容读取重新暴露旧创作、发布、公开详情或运行接口。
|
||||
- 编辑器、项目、账号、钱包、资产、HostBridge、运维和安全等平台公共能力。
|
||||
- 新版 `/creation` 创作工具主页、`creation-home` 展示组件与静态资产,以及桌面端“创作 / 项目”公共侧边栏;这些能力只读取编辑器项目和公开素材接口,不恢复旧模板入口。
|
||||
- 新版 `/creation` 创作工具主页、`creation-home` 展示组件与静态资产,以及桌面端“创作 / 项目 / 我的”公共侧边栏;“我的”只承载账号、钱包、统计和通用设置等平台能力,不恢复旧模板入口或旧作品架。
|
||||
- 旧页面、测试、素材、handler、service、worker、生成 bindings 和纯业务 crate 的源码目录;它们仅用于历史追溯,不属于任何正式入口或编译目标。
|
||||
|
||||
## 编译边界
|
||||
@@ -51,7 +51,7 @@
|
||||
## 验收
|
||||
|
||||
- 旧 URL 不再命中旧页面或后端路由。
|
||||
- `/creation` 与 `/project` 显示公共侧边栏,新创作主页只调用编辑器项目和公开素材接口。
|
||||
- `/creation` 与 `/project` 显示“创作 / 项目 / 我的”公共侧边栏,新创作主页只调用编辑器项目和公开素材接口;顶栏保持公共泥点入口与账号胶囊,不重新拼装平行账号按钮组。
|
||||
- Vite 构建产物和依赖图不包含旧前端业务目录。
|
||||
- `cargo tree` 中不存在纯模板 crate 或专属运行态 crate。
|
||||
- `spacetime-module` 编译结果仍包含全部历史表,但不包含任何旧模板 reducer、procedure 和业务 view。
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PlatformActiveProfileView } from './PlatformActiveProfileView';
|
||||
|
||||
const callbacks = {
|
||||
onLogin: vi.fn(),
|
||||
onOpenAccount: vi.fn(),
|
||||
onOpenRecharge: vi.fn(),
|
||||
onOpenSettings: vi.fn(),
|
||||
onOpenWalletLedger: vi.fn(),
|
||||
};
|
||||
|
||||
describe('PlatformActiveProfileView', () => {
|
||||
it('shows the login entry for guests', () => {
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={null}
|
||||
isLoadingDashboard={false}
|
||||
user={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(screen.getByText('尚未登录')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '登录' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders active account, wallet, and settings capabilities', () => {
|
||||
render(
|
||||
<PlatformActiveProfileView
|
||||
{...callbacks}
|
||||
dashboard={{
|
||||
walletBalance: 108,
|
||||
totalPlayTimeMs: 5_400_000,
|
||||
playedWorldCount: 7,
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
}}
|
||||
isLoadingDashboard={false}
|
||||
user={{
|
||||
id: 'user-1',
|
||||
publicUserCode: '100001',
|
||||
displayName: '测试玩家',
|
||||
avatarUrl: null,
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
wechatBound: false,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('测试玩家')).toBeTruthy();
|
||||
expect(screen.getByText('陶泥号:100001')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '泥点余额 108' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: /泥点充值/u })).toBeTruthy();
|
||||
expect(
|
||||
screen.getAllByRole('button', { name: /账号与安全/u }),
|
||||
).toHaveLength(2);
|
||||
expect(screen.getByRole('button', { name: /通用设置/u })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
Coins,
|
||||
History,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
UserRound,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import profileClockImage from '../../../media/profile/_Image (1).png';
|
||||
import profileGamepadImage from '../../../media/profile/_Image (2).png';
|
||||
import profileStillLifeImage from '../../../media/profile/_Image (3).png';
|
||||
import profileCoinsImage from '../../../media/profile/_Image (4).png';
|
||||
import profileMascotImage from '../../../media/profile/_Image (9).png';
|
||||
import profilePointImage from '../../../media/profile/_Image.png';
|
||||
import type { AuthUser } from '../../../packages/shared/src/contracts/auth';
|
||||
import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { LegalDocumentModal } from '../common/LegalDocumentModal';
|
||||
import {
|
||||
getLegalDocument,
|
||||
type LegalDocumentId,
|
||||
} from '../common/legalDocuments';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { resolveActivePublicUserCode } from './platformActiveProfileModel';
|
||||
import {
|
||||
ProfileLegalSection,
|
||||
ProfileShortcutButton,
|
||||
ProfileStatCard,
|
||||
ProfileStatCardSkeleton,
|
||||
} from './PlatformProfilePrimitives';
|
||||
|
||||
type PlatformActiveProfileViewProps = {
|
||||
dashboard: ProfileDashboardSummary | null;
|
||||
isLoadingDashboard: boolean;
|
||||
onLogin: () => void;
|
||||
onOpenAccount: () => void;
|
||||
onOpenRecharge: () => void;
|
||||
onOpenSettings: () => void;
|
||||
onOpenWalletLedger: () => void;
|
||||
user: AuthUser | null | undefined;
|
||||
};
|
||||
|
||||
function formatDashboardCount(value: number) {
|
||||
return Math.max(0, Math.round(value)).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatTotalPlayTime(value: number) {
|
||||
const hours = Math.max(0, Math.round(value / 360000) / 10);
|
||||
return `${hours.toLocaleString('zh-CN', {
|
||||
maximumFractionDigits: 1,
|
||||
})}小时`;
|
||||
}
|
||||
|
||||
export function PlatformActiveProfileView({
|
||||
dashboard,
|
||||
isLoadingDashboard,
|
||||
onLogin,
|
||||
onOpenAccount,
|
||||
onOpenRecharge,
|
||||
onOpenSettings,
|
||||
onOpenWalletLedger,
|
||||
user,
|
||||
}: PlatformActiveProfileViewProps) {
|
||||
const [activeLegalDocumentId, setActiveLegalDocumentId] =
|
||||
useState<LegalDocumentId | null>(null);
|
||||
const activeLegalDocument = activeLegalDocumentId
|
||||
? getLegalDocument(activeLegalDocumentId)
|
||||
: null;
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<main
|
||||
className="platform-profile-page platform-remap-surface mx-auto w-full max-w-4xl"
|
||||
aria-label="我的"
|
||||
>
|
||||
<section className="platform-profile-header">
|
||||
<img
|
||||
src={profileStillLifeImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="platform-profile-scene-decor"
|
||||
/>
|
||||
<div className="platform-profile-header__identity">
|
||||
<div className="platform-profile-header__identity-row flex min-w-0 items-center gap-4">
|
||||
<img
|
||||
src={profileMascotImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="platform-profile-avatar h-[5.15rem] w-[5.15rem] shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="text-base font-black text-[var(--platform-text-strong)]">
|
||||
尚未登录
|
||||
</div>
|
||||
<PlatformActionButton className="mt-3" onClick={onLogin}>
|
||||
登录
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const avatarUrl = user.avatarUrl?.trim() || null;
|
||||
const publicUserCode = resolveActivePublicUserCode(user);
|
||||
|
||||
return (
|
||||
<main
|
||||
className="platform-profile-page platform-remap-surface mx-auto w-full max-w-4xl space-y-4 pb-2"
|
||||
aria-label="我的"
|
||||
>
|
||||
<section className="platform-profile-header">
|
||||
<img
|
||||
src={profileStillLifeImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="platform-profile-scene-decor"
|
||||
/>
|
||||
<div className="platform-profile-header__identity">
|
||||
<div className="platform-profile-header__identity-row flex min-w-0 items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="platform-profile-avatar h-[5.15rem] w-[5.15rem] shrink-0 overflow-hidden rounded-full"
|
||||
aria-label="账号与安全"
|
||||
onClick={onOpenAccount}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={profileMascotImage}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<div className="platform-profile-header__text min-w-0">
|
||||
<div className="truncate text-[18px] font-black leading-tight text-[var(--platform-text-strong)]">
|
||||
{user.displayName}
|
||||
</div>
|
||||
<div className="mt-2 text-[12px] text-[var(--platform-text-base)]">
|
||||
陶泥号:{publicUserCode}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-profile-stats-panel" aria-label="我的数据">
|
||||
<div className="platform-profile-stats-grid grid grid-cols-3 gap-3">
|
||||
{isLoadingDashboard ? (
|
||||
<>
|
||||
<ProfileStatCardSkeleton />
|
||||
<ProfileStatCardSkeleton />
|
||||
<ProfileStatCardSkeleton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ProfileStatCard
|
||||
cardKey="wallet"
|
||||
label="泥点余额"
|
||||
value={
|
||||
dashboard
|
||||
? formatDashboardCount(dashboard.walletBalance)
|
||||
: '暂不可用'
|
||||
}
|
||||
icon={Coins}
|
||||
imageSrc={profilePointImage}
|
||||
onClick={onOpenWalletLedger}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playTime"
|
||||
label="累计游戏时长"
|
||||
value={
|
||||
dashboard
|
||||
? formatTotalPlayTime(dashboard.totalPlayTimeMs)
|
||||
: '暂不可用'
|
||||
}
|
||||
icon={History}
|
||||
imageSrc={profileClockImage}
|
||||
/>
|
||||
<ProfileStatCard
|
||||
cardKey="playedWorks"
|
||||
label="已玩游戏数量"
|
||||
value={
|
||||
dashboard
|
||||
? `${formatDashboardCount(dashboard.playedWorldCount)}个`
|
||||
: '暂不可用'
|
||||
}
|
||||
icon={UserRound}
|
||||
imageSrc={profileGamepadImage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="platform-profile-shortcut-panel" aria-label="常用功能">
|
||||
<div className="platform-profile-shortcut-grid grid w-full grid-cols-4">
|
||||
<ProfileShortcutButton
|
||||
label="泥点充值"
|
||||
subLabel="充值泥点"
|
||||
icon={Coins}
|
||||
imageSrc={profileCoinsImage}
|
||||
onClick={onOpenRecharge}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="使用详情"
|
||||
subLabel="查看泥点账单"
|
||||
icon={History}
|
||||
onClick={onOpenWalletLedger}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="账号与安全"
|
||||
subLabel="管理账号"
|
||||
icon={ShieldCheck}
|
||||
onClick={onOpenAccount}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="通用设置"
|
||||
subLabel="偏好设置"
|
||||
icon={Settings}
|
||||
onClick={onOpenSettings}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ProfileLegalSection onOpenDocument={setActiveLegalDocumentId} />
|
||||
<LegalDocumentModal
|
||||
document={activeLegalDocument}
|
||||
open={Boolean(activeLegalDocument)}
|
||||
onClose={() => setActiveLegalDocumentId(null)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformActiveProfileView;
|
||||
@@ -0,0 +1,115 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PlatformEntryFlowShellImpl } from './PlatformEntryActiveFlowShell';
|
||||
|
||||
const authUiMock = vi.hoisted(() => ({
|
||||
value: {
|
||||
user: null,
|
||||
canAccessProtectedData: false,
|
||||
openLoginModal: vi.fn(),
|
||||
openAccountModal: vi.fn(),
|
||||
openSettingsModal: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => authUiMock.value,
|
||||
}));
|
||||
|
||||
vi.mock('../creation-home/CreationLandingView', () => ({
|
||||
CreationLandingView: () => (
|
||||
<main aria-label="陶泥儿创作主页">创作主页</main>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../project/ProjectGalleryView', () => ({
|
||||
ProjectGalleryView: () => <main aria-label="项目">项目</main>,
|
||||
}));
|
||||
|
||||
vi.mock('../image-editor/ImageCanvasEditorView', () => ({
|
||||
ImageCanvasEditorView: () => <main aria-label="图片画布编辑器" />,
|
||||
}));
|
||||
|
||||
vi.mock('../../services/platform-entry/platformProfileClient', () => ({
|
||||
getPlatformProfileDashboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./usePlatformProfileCenterController', () => ({
|
||||
usePlatformProfileCenterController: () => ({
|
||||
buyRechargeProduct: vi.fn(),
|
||||
closeNativeWechatPayment: vi.fn(),
|
||||
confirmNativeWechatPayment: vi.fn(),
|
||||
isLoadingRechargeCenter: false,
|
||||
isLoadingWalletLedger: false,
|
||||
isRechargeOpen: false,
|
||||
isWalletLedgerOpen: false,
|
||||
loadRechargeCenter: vi.fn(),
|
||||
nativeWechatPayment: null,
|
||||
openWalletLedgerPanel: vi.fn(),
|
||||
rechargeCenter: null,
|
||||
rechargeError: null,
|
||||
rechargePaymentResult: null,
|
||||
setIsRechargeOpen: vi.fn(),
|
||||
setIsWalletLedgerOpen: vi.fn(),
|
||||
setRechargePaymentResult: vi.fn(),
|
||||
submittingRechargeProductId: null,
|
||||
walletLedger: null,
|
||||
walletLedgerError: null,
|
||||
wechatRechargeOrderConfirmationState: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PlatformEntryActiveFlowShell', () => {
|
||||
beforeEach(() => {
|
||||
window.history.replaceState(null, '', '/creation');
|
||||
authUiMock.value.openLoginModal.mockReset();
|
||||
});
|
||||
|
||||
it('keeps the active desktop rail and the shared account capsule', async () => {
|
||||
const setSelectionStage = vi.fn();
|
||||
const { container } = render(
|
||||
<PlatformEntryFlowShellImpl
|
||||
selectionStage="creation-home"
|
||||
setSelectionStage={setSelectionStage}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('main', { name: '陶泥儿创作主页' }),
|
||||
).toBeTruthy();
|
||||
const navigation = screen.getByRole('navigation', { name: '平台导航' });
|
||||
expect(
|
||||
within(navigation)
|
||||
.getAllByRole('button')
|
||||
.map((button) => button.getAttribute('aria-label')),
|
||||
).toEqual(['创作', '项目', '我的']);
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '创作' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
|
||||
const topbar = container.querySelector('.platform-desktop-topbar');
|
||||
expect(topbar).toBeTruthy();
|
||||
expect(
|
||||
topbar?.querySelectorAll('button.platform-desktop-search'),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
within(topbar as HTMLElement).queryByRole('button', { name: '设置' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
|
||||
|
||||
expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
|
||||
expect(window.location.pathname).toBe('/');
|
||||
expect(setSelectionStage).toHaveBeenCalledWith('platform', { path: '/' });
|
||||
expect(
|
||||
within(navigation)
|
||||
.getByRole('button', { name: '我的' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('page');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
FolderKanban,
|
||||
LogIn,
|
||||
Palette,
|
||||
Settings,
|
||||
UserRound,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
@@ -22,8 +20,11 @@ import {
|
||||
} from '../../routing/activeAppPageRoutes';
|
||||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import {
|
||||
resolveActivePublicUserCode,
|
||||
resolveActiveUserAvatarLabel,
|
||||
} from './platformActiveProfileModel';
|
||||
import { PlatformActiveProfileView } from './PlatformActiveProfileView';
|
||||
import type { PlatformEntryFlowShellProps } from './platformEntryActiveTypes';
|
||||
import { PlatformProfileRechargeModal } from './PlatformProfileRechargeModal';
|
||||
import { PlatformProfileWalletLedgerModal } from './PlatformProfileWalletLedgerModal';
|
||||
@@ -136,6 +137,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
null,
|
||||
);
|
||||
const [isLoadingDashboard, setIsLoadingDashboard] = useState(false);
|
||||
const [isProfileStage, setIsProfileStage] = useState(false);
|
||||
|
||||
const refreshDashboard = useCallback(async () => {
|
||||
if (!authUi?.user || !authUi.canAccessProtectedData) {
|
||||
@@ -156,6 +158,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
void refreshDashboard();
|
||||
}, [refreshDashboard]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionStage !== 'platform') {
|
||||
setIsProfileStage(false);
|
||||
}
|
||||
}, [selectionStage]);
|
||||
|
||||
const profileCenter = usePlatformProfileCenterController({
|
||||
activeTab: 'project',
|
||||
isAuthenticated: Boolean(authUi?.user),
|
||||
@@ -166,15 +174,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
|
||||
const openCreation = useCallback(() => {
|
||||
setIsProfileStage(false);
|
||||
pushAppHistoryPath('/creation');
|
||||
setSelectionStage('creation-home', { path: '/creation' });
|
||||
}, [setSelectionStage]);
|
||||
|
||||
const openProjects = useCallback(() => {
|
||||
setIsProfileStage(false);
|
||||
pushAppHistoryPath('/project');
|
||||
setSelectionStage('project', { path: '/project' });
|
||||
}, [setSelectionStage]);
|
||||
|
||||
const openProfile = useCallback(() => {
|
||||
setIsProfileStage(true);
|
||||
pushAppHistoryPath('/');
|
||||
setSelectionStage('platform', { path: '/' });
|
||||
}, [setSelectionStage]);
|
||||
|
||||
const openEditorProject = useCallback(
|
||||
(projectId: string, options?: { guide?: boolean; tool?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -211,12 +227,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const isAuthenticated = Boolean(authUi?.user);
|
||||
const balance =
|
||||
dashboard?.walletBalance ??
|
||||
profileCenter.rechargeCenter?.mudPointBalance?.totalPoints ??
|
||||
profileCenter.rechargeCenter?.walletBalance ??
|
||||
dashboard?.walletBalance ??
|
||||
null;
|
||||
const isCreationStage =
|
||||
selectionStage === 'platform' || selectionStage === 'creation-home';
|
||||
!isProfileStage &&
|
||||
(selectionStage === 'platform' || selectionStage === 'creation-home');
|
||||
const avatarUrl = authUi?.user?.avatarUrl?.trim() || null;
|
||||
const avatarLabel = resolveActiveUserAvatarLabel(authUi?.user);
|
||||
const publicUserCode = resolveActivePublicUserCode(authUi?.user);
|
||||
const openRecharge = () => {
|
||||
profileCenter.setIsRechargeOpen(true);
|
||||
profileCenter.loadRechargeCenter();
|
||||
};
|
||||
const openUserSurface = () => {
|
||||
if (isAuthenticated) {
|
||||
authUi?.openAccountModal();
|
||||
return;
|
||||
}
|
||||
authUi?.openLoginModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -246,6 +277,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
label="项目"
|
||||
onClick={openProjects}
|
||||
/>
|
||||
<ActiveRailButton
|
||||
active={isProfileStage}
|
||||
icon={UserRound}
|
||||
iconSrc="/creation-home/nav-profile.png"
|
||||
label="我的"
|
||||
onClick={openProfile}
|
||||
/>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@@ -255,10 +293,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
<ActivePlatformBrand />
|
||||
</div>
|
||||
<span className="hidden truncate text-base font-black text-[var(--platform-text-strong)] lg:block">
|
||||
{isCreationStage ? '创作工具' : '项目'}
|
||||
{isProfileStage
|
||||
? '我的'
|
||||
: isCreationStage
|
||||
? '创作工具'
|
||||
: '项目'}
|
||||
</span>
|
||||
|
||||
<div className="platform-desktop-topbar__actions flex shrink-0 items-center gap-2">
|
||||
<div className="platform-desktop-topbar__actions flex shrink-0 items-center gap-3">
|
||||
{isAuthenticated ? (
|
||||
<PlatformMudPointWalletEntry
|
||||
variant="desktop"
|
||||
@@ -271,40 +313,43 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileCenter.isLoadingRechargeCenter
|
||||
}
|
||||
error={profileCenter.rechargeError}
|
||||
className="platform-desktop-create-wallet-chip"
|
||||
onRequestDetails={profileCenter.loadRechargeCenter}
|
||||
onRecharge={() => {
|
||||
profileCenter.setIsRechargeOpen(true);
|
||||
profileCenter.loadRechargeCenter();
|
||||
}}
|
||||
onRecharge={openRecharge}
|
||||
onOpenLedger={profileCenter.openWalletLedgerPanel}
|
||||
/>
|
||||
) : (
|
||||
<PlatformActionButton
|
||||
size="xs"
|
||||
onClick={() => authUi?.openLoginModal()}
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-desktop-search flex items-center gap-3 px-3 py-2.5 text-left"
|
||||
>
|
||||
<LogIn className="h-4 w-4" aria-hidden="true" />
|
||||
登录
|
||||
</PlatformActionButton>
|
||||
)}
|
||||
<PlatformIconButton
|
||||
label={isAuthenticated ? '账号' : '登录'}
|
||||
icon={
|
||||
<UserRound className="h-5 w-5" aria-hidden="true" />
|
||||
}
|
||||
onClick={() => {
|
||||
if (isAuthenticated) {
|
||||
authUi?.openAccountModal();
|
||||
return;
|
||||
}
|
||||
authUi?.openLoginModal();
|
||||
<span
|
||||
className="flex h-11 w-11 items-center justify-center overflow-hidden rounded-full text-base font-black text-white"
|
||||
style={{
|
||||
background: 'var(--platform-profile-avatar-fill)',
|
||||
boxShadow: 'var(--platform-profile-avatar-shadow)',
|
||||
}}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<PlatformIconButton
|
||||
label="设置"
|
||||
icon={<Settings className="h-5 w-5" aria-hidden="true" />}
|
||||
onClick={() => authUi?.openSettingsModal()}
|
||||
/>
|
||||
) : (
|
||||
avatarLabel
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-semibold text-[var(--platform-text-strong)]">
|
||||
{authUi?.user?.displayName || '登录'}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-[var(--platform-text-soft)]">
|
||||
{authUi?.user ? publicUserCode : '账号入口'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -312,7 +357,20 @@ export function PlatformEntryFlowShellImpl({
|
||||
id={isCreationStage ? 'platform-tab-panel-create' : undefined}
|
||||
className={`platform-tab-panel min-h-0 min-w-0 flex-1 overflow-auto ${isCreationStage ? '' : 'px-3 py-4 sm:px-6 sm:py-6'}`}
|
||||
>
|
||||
{isCreationStage ? (
|
||||
{isProfileStage ? (
|
||||
<PlatformActiveProfileView
|
||||
dashboard={dashboard}
|
||||
isLoadingDashboard={isLoadingDashboard}
|
||||
user={authUi?.user}
|
||||
onLogin={() => authUi?.openLoginModal()}
|
||||
onOpenAccount={() => authUi?.openAccountModal()}
|
||||
onOpenRecharge={openRecharge}
|
||||
onOpenSettings={() => authUi?.openSettingsModal()}
|
||||
onOpenWalletLedger={
|
||||
profileCenter.openWalletLedgerPanel
|
||||
}
|
||||
/>
|
||||
) : isCreationStage ? (
|
||||
<Suspense
|
||||
fallback={<LoadingPanel label="正在加载创作主页..." />}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { AuthUser } from '../../../packages/shared/src/contracts/auth';
|
||||
|
||||
export function resolveActivePublicUserCode(
|
||||
user: AuthUser | null | undefined,
|
||||
) {
|
||||
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 resolveActiveUserAvatarLabel(
|
||||
user: AuthUser | null | undefined,
|
||||
) {
|
||||
return (user?.displayName || '叙').slice(0, 1).toUpperCase();
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
"src/components/image-editor/**/*.test.tsx",
|
||||
"src/components/project/**/*.test.ts",
|
||||
"src/components/project/**/*.test.tsx",
|
||||
"src/components/platform-entry/PlatformActiveProfileView.test.tsx",
|
||||
"src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx",
|
||||
"src/services/image-editor/**/*.test.ts",
|
||||
"src/services/image-editor/**/*.test.tsx",
|
||||
"src/services/external-generation/**/*.test.ts",
|
||||
|
||||
@@ -20,6 +20,8 @@ const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
|
||||
@source "./components/project";
|
||||
@source "./components/platform-entry/PlatformEntryFlowShell.tsx";
|
||||
@source "./components/platform-entry/PlatformEntryActiveFlowShell.tsx";
|
||||
@source "./components/platform-entry/PlatformActiveProfileView.tsx";
|
||||
@source "./components/platform-entry/PlatformProfilePrimitives.tsx";
|
||||
@source "./components/platform-entry/PlatformProfileModalShell.tsx";
|
||||
@source "./components/platform-entry/PlatformProfileRechargeModal.tsx";
|
||||
@source "./components/platform-entry/PlatformProfileWalletLedgerModal.tsx";
|
||||
|
||||
@@ -32,6 +32,8 @@ export default defineConfig({
|
||||
'src/components/image-editor/**/*.test.tsx',
|
||||
'src/components/project/**/*.test.ts',
|
||||
'src/components/project/**/*.test.tsx',
|
||||
'src/components/platform-entry/PlatformActiveProfileView.test.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx',
|
||||
'src/components/platform-entry/PlatformProfileModalShell.test.tsx',
|
||||
'src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRechargeModal.test.tsx',
|
||||
|
||||
Reference in New Issue
Block a user