From f3ab2f2ec0eef635c2ec20d3b41a960820a21d4b Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 18 Jul 2026 16:15:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=B9=B3=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E9=A1=B6=E6=A0=8F=E4=B8=8E=E6=88=91=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按新版创作页原布局恢复泥点入口与账号身份胶囊。 在桌面侧栏补回我的页签及游客、登录态个人页面。 个人页面只复用账号、钱包、统计、设置和法律信息等平台能力。 补充壳层与个人页面回归测试并更新退役边界文档。 --- .../shared-memory/project-overview.md | 2 +- ...构下线】旧创作模板业务退役方案-2026-07-17.md | 4 +- .../PlatformActiveProfileView.test.tsx | 65 +++++ .../PlatformActiveProfileView.tsx | 244 ++++++++++++++++++ .../PlatformEntryActiveFlowShell.test.tsx | 115 +++++++++ .../PlatformEntryActiveFlowShell.tsx | 136 +++++++--- .../platformActiveProfileModel.ts | 19 ++ tsconfig.typecheck-guardrails.json | 2 + vite.config.ts | 2 + vitest.config.ts | 2 + 10 files changed, 549 insertions(+), 42 deletions(-) create mode 100644 src/components/platform-entry/PlatformActiveProfileView.test.tsx create mode 100644 src/components/platform-entry/PlatformActiveProfileView.tsx create mode 100644 src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx create mode 100644 src/components/platform-entry/platformActiveProfileModel.ts diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index 6e8baa24d..8de7407b5 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -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` 是画布项目入口,“我的”继续承载账号、钱包、统计和通用设置等平台公共能力。 ## 当前后端路线 diff --git a/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md b/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md index 0db7c6fc2..fa93e6cc3 100644 --- a/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md +++ b/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md @@ -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。 diff --git a/src/components/platform-entry/PlatformActiveProfileView.test.tsx b/src/components/platform-entry/PlatformActiveProfileView.test.tsx new file mode 100644 index 000000000..b9d04417d --- /dev/null +++ b/src/components/platform-entry/PlatformActiveProfileView.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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(); + }); +}); diff --git a/src/components/platform-entry/PlatformActiveProfileView.tsx b/src/components/platform-entry/PlatformActiveProfileView.tsx new file mode 100644 index 000000000..bbf03adef --- /dev/null +++ b/src/components/platform-entry/PlatformActiveProfileView.tsx @@ -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(null); + const activeLegalDocument = activeLegalDocumentId + ? getLegalDocument(activeLegalDocumentId) + : null; + + if (!user) { + return ( +
+
+ +
+
+ +
+
+ 尚未登录 +
+ + 登录 + +
+
+
+
+
+ ); + } + + const avatarUrl = user.avatarUrl?.trim() || null; + const publicUserCode = resolveActivePublicUserCode(user); + + return ( +
+
+ +
+
+ +
+
+ {user.displayName} +
+
+ 陶泥号:{publicUserCode} +
+
+
+
+
+ +
+
+ {isLoadingDashboard ? ( + <> + + + + + ) : ( + <> + + + + + )} +
+
+ +
+
+ + + + +
+
+ + + setActiveLegalDocumentId(null)} + /> +
+ ); +} + +export default PlatformActiveProfileView; diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx new file mode 100644 index 000000000..e0d256e83 --- /dev/null +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx @@ -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: () => ( +
创作主页
+ ), +})); + +vi.mock('../project/ProjectGalleryView', () => ({ + ProjectGalleryView: () =>
项目
, +})); + +vi.mock('../image-editor/ImageCanvasEditorView', () => ({ + ImageCanvasEditorView: () =>
, +})); + +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( + , + ); + + 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'); + }); +}); diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx index 2d3b72bc2..6d9ec2a4a 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx @@ -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} /> + @@ -255,10 +293,14 @@ export function PlatformEntryFlowShellImpl({ - {isCreationStage ? '创作工具' : '项目'} + {isProfileStage + ? '我的' + : isCreationStage + ? '创作工具' + : '项目'} -
+
{isAuthenticated ? ( { - profileCenter.setIsRechargeOpen(true); - profileCenter.loadRechargeCenter(); - }} + onRecharge={openRecharge} onOpenLedger={profileCenter.openWalletLedgerPanel} /> - ) : ( - authUi?.openLoginModal()} + ) : null} +
@@ -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 ? ( + authUi?.openLoginModal()} + onOpenAccount={() => authUi?.openAccountModal()} + onOpenRecharge={openRecharge} + onOpenSettings={() => authUi?.openSettingsModal()} + onOpenWalletLedger={ + profileCenter.openWalletLedgerPanel + } + /> + ) : isCreationStage ? ( } > diff --git a/src/components/platform-entry/platformActiveProfileModel.ts b/src/components/platform-entry/platformActiveProfileModel.ts new file mode 100644 index 000000000..6bf73fd83 --- /dev/null +++ b/src/components/platform-entry/platformActiveProfileModel.ts @@ -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(); +} diff --git a/tsconfig.typecheck-guardrails.json b/tsconfig.typecheck-guardrails.json index 150dc4429..8fc9ddeba 100644 --- a/tsconfig.typecheck-guardrails.json +++ b/tsconfig.typecheck-guardrails.json @@ -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", diff --git a/vite.config.ts b/vite.config.ts index 95a3528b6..9fb329b3a 100644 --- a/vite.config.ts +++ b/vite.config.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"; diff --git a/vitest.config.ts b/vitest.config.ts index a60339bb7..9341167bf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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',