From f67f57b41557ead1f4d46b35fa605fda136dba9b Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 3 Jun 2026 18:06:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=B5=84=E9=87=91=E5=B1=95=E7=A4=BA=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 8 + docs/README.md | 2 + ...】ProfileFundsViewModel收口计划-2026-06-03.md | 31 ++++ .../RpgEntryHomeView.recharge.test.tsx | 15 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 67 +++----- .../rpgEntryProfileFundsViewModel.test.ts | 161 ++++++++++++++++++ .../rpgEntryProfileFundsViewModel.ts | 108 ++++++++++++ 7 files changed, 343 insertions(+), 49 deletions(-) create mode 100644 docs/technical/【前端架构】ProfileFundsViewModel收口计划-2026-06-03.md create mode 100644 src/components/rpg-entry/rpgEntryProfileFundsViewModel.test.ts create mode 100644 src/components/rpg-entry/rpgEntryProfileFundsViewModel.ts diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 11ec2081..fe0d7927 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1249,6 +1249,14 @@ - 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|edutainment"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "logged out home recommendation next starts the next puzzle work"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md`。 +## 2026-06-03 Profile Funds ViewModel 收口 + +- 背景:个人资金展示规则散在 `RpgEntryHomeView.tsx`,且账单来源 label 表漏掉后端契约已有的 `puzzle_author_incentive_claim`,会把原始枚举值直接外显。 +- 决策:新增 `src/components/rpg-entry/rpgEntryProfileFundsViewModel.ts` 作为个人资金展示 Module,Interface 收口账单来源文案、金额正负号、余额兜底、充值价格、商品主值与会员摘要;页面保留弹窗布局、支付流程、微信渠道和订单轮询副作用。 +- 影响范围:泥点账单弹窗、充值商品卡片、账户充值弹窗会员摘要。 +- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryProfileFundsViewModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "wallet ledger|profile recharge modal shows native qr code"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】ProfileFundsViewModel收口计划-2026-06-03.md`。 + ## 2026-05-26 前端不外露图片模型名 - 背景:拼图与相关结果页、生成进度和错误提示里直接显示 `gpt-image-2`、`gemini-3.1-flash-image-preview`、`image-2` 等名称,会把内部模型路由暴露给普通用户。 diff --git a/docs/README.md b/docs/README.md index 39969faa..28cc8586 100644 --- a/docs/README.md +++ b/docs/README.md @@ -53,6 +53,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 个人数据卡、钱包 chip 与“玩过”弹窗的计数、时长、作品类型和作品号展示收口到 `src/components/rpg-entry/rpgEntryProfileDashboardPresentation.ts`,规则见 [【前端架构】ProfileDashboardPresentation收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileDashboardPresentation%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 +个人资金展示的账单来源、金额正负号、余额兜底、充值价格、商品主值和会员摘要收口到 `src/components/rpg-entry/rpgEntryProfileFundsViewModel.ts`,规则见 [【前端架构】ProfileFundsViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileFundsViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 + ## 推荐阅读顺序 1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。 diff --git a/docs/technical/【前端架构】ProfileFundsViewModel收口计划-2026-06-03.md b/docs/technical/【前端架构】ProfileFundsViewModel收口计划-2026-06-03.md new file mode 100644 index 00000000..70a2bf60 --- /dev/null +++ b/docs/technical/【前端架构】ProfileFundsViewModel收口计划-2026-06-03.md @@ -0,0 +1,31 @@ +# 【前端架构】Profile Funds ViewModel 收口计划 + +## 背景 + +`RpgEntryHomeView.tsx` 原先直接维护钱包账单来源文案、金额正负号、账单余额兜底、充值价格、充值商品主值和会员摘要文案。这些规则散在页面 **Implementation** 内,且已与 `ProfileWalletLedgerEntry.sourceType` 契约产生漂移:后端可返回 `puzzle_author_incentive_claim`,页面没有对应中文 label,会把原始枚举值外显给用户。 + +## 决策 + +新增 `src/components/rpg-entry/rpgEntryProfileFundsViewModel.ts` 作为个人资金展示 **Module**。该 **Module** 的 **Interface** 收口为: + +- `getWalletLedgerSourceLabel(sourceType)`:统一账单来源中文文案,补齐 `puzzle_author_incentive_claim`。 +- `formatWalletLedgerAmount(amountDelta)`:统一账单金额正负号。 +- `buildWalletLedgerPresentation(ledger, fallbackBalance)`:统一余额兜底与账单行 presentation。 +- `formatRechargePrice(priceCents)` 与 `buildRechargeProductValueLabel(product)`:统一充值商品价格与主值文案。 +- `buildMembershipLabel(membership, formatTime)`:统一会员摘要文案,并保留页面现有时间格式 Adapter。 + +`RpgEntryHomeView.tsx` 只消费该 **Module** 输出,保留弹窗布局、支付流程、微信渠道和轮询副作用。资金展示规则的 **Locality** 收口到纯函数测试,后续新增账单来源或调整价格 / 会员文案时不再穿透页面 JSX。 + +## 约定 + +- 未知账单来源仍保留原始 sourceType 兜底,避免新后端枚举被空白吞掉。 +- 账单余额继续沿用既有口径:有账单时取第一条 `balanceAfter`,无账单时使用外部 fallback balance。 +- 本次只收展示 **Interface**,不迁移支付确认、微信跳转、订单轮询或弹窗状态。 + +## 验证 + +- `npm run test -- src/components/rpg-entry/rpgEntryProfileFundsViewModel.test.ts` +- `npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "wallet ledger|profile recharge modal shows native qr code"` +- 针对变更文件执行 ESLint +- `npm run typecheck` +- `npm run check:encoding` diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 989b581e..a9a42e9c 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -305,12 +305,21 @@ const { amountDelta: -1, balanceAfter: 29, sourceType: 'asset_operation_consume', + createdAt: '2026-05-03T08:00:00Z', }, { id: 'ledger-2', amountDelta: 30, balanceAfter: 30, sourceType: 'invite_invitee_reward', + createdAt: '2026-05-03T09:00:00Z', + }, + { + id: 'ledger-3', + amountDelta: 5, + balanceAfter: 35, + sourceType: 'puzzle_author_incentive_claim', + createdAt: '2026-05-03T10:00:00Z', }, ], })), @@ -413,11 +422,6 @@ const originalUserAgent = navigator.userAgent; const originalMaxTouchPoints = navigator.maxTouchPoints; const originalRequestAnimationFrame = window.requestAnimationFrame; const originalCancelAnimationFrame = window.cancelAnimationFrame; -const DEFAULT_PROFILE_CREATED_AT = '2026-04-01T00:00:00.000Z'; - -function buildFreshProfileCreatedAt() { - return new Date().toISOString(); -} function dispatchPointerEvent( target: HTMLElement, @@ -1169,6 +1173,7 @@ test('opens wallet ledger modal from narrative coin card', async () => { expect(screen.getByText('-1')).toBeTruthy(); expect(screen.getByText('填写邀请码奖励')).toBeTruthy(); expect(screen.getByText('+30')).toBeTruthy(); + expect(screen.getByText('拼图作者奖励')).toBeTruthy(); }); test('profile recharge modal shows native qr code on desktop web by default', async () => { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 3937c789..e681d018 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -141,6 +141,12 @@ import { formatPlayedWorkType, formatTotalPlayTimeHours, } from './rpgEntryProfileDashboardPresentation'; +import { + buildMembershipLabel, + buildRechargeProductValueLabel, + buildWalletLedgerPresentation, + formatRechargePrice, +} from './rpgEntryProfileFundsViewModel'; import { buildProfileTaskCardSummary, buildProfileTaskProgressLabel, @@ -2150,27 +2156,6 @@ function ProfileNicknameModal({ ); } -const WALLET_LEDGER_SOURCE_LABELS: Record = { - new_user_registration_reward: '注册赠送', - points_recharge: '泥点充值', - invite_inviter_reward: '邀请奖励', - invite_invitee_reward: '填写邀请码奖励', - snapshot_sync: '账户同步', - asset_operation_consume: '资产操作消耗', - asset_operation_refund: '资产操作退回', - redeem_code_reward: '兑换码奖励', - daily_task_reward: '每日任务奖励', -}; - -function formatWalletLedgerAmount(amountDelta: number) { - return amountDelta > 0 ? `+${amountDelta}` : `${amountDelta}`; -} - -function formatRechargePrice(priceCents: number) { - const yuan = priceCents / 100; - return `¥${Number.isInteger(yuan) ? yuan.toFixed(0) : yuan.toFixed(2)}`; -} - function clearWechatPayResultHash() { if (typeof window === 'undefined') { return; @@ -2378,12 +2363,8 @@ function RechargeProductCard({ onBuy: (product: ProfileRechargeProduct) => void; }) { const submitting = submittingProductId === product.productId; - const effectiveBonusPoints = product.bonusPoints; const badgeLabel = product.badgeLabel; - const value = - product.kind === 'points' - ? `${product.pointsAmount}${effectiveBonusPoints > 0 ? `+${effectiveBonusPoints}` : ''}泥点` - : `${product.durationDays}天`; + const value = buildRechargeProductValueLabel(product); return (