refactor: 收口每日任务 ViewModel
This commit is contained in:
@@ -71,7 +71,6 @@ import type {
|
||||
ProfileReferralInviteCenterResponse,
|
||||
ProfileSaveArchiveSummary,
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileTaskItem,
|
||||
ProfileWalletLedgerResponse,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
WechatMiniProgramPayParams,
|
||||
@@ -135,6 +134,13 @@ import {
|
||||
import { getInitialPlatformDesktopLayout } from '../platform-entry/platformEntryResponsive';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
|
||||
import {
|
||||
buildProfileTaskCardSummary,
|
||||
buildProfileTaskProgressLabel,
|
||||
getProfileTaskClaimButtonLabel,
|
||||
getProfileTaskStatusLabel,
|
||||
selectProfileTaskCenterTasks,
|
||||
} from './rpgEntryProfileTaskViewModel';
|
||||
import {
|
||||
buildPlatformRankingEntries,
|
||||
buildPublicCategoryGroups,
|
||||
@@ -277,66 +283,8 @@ const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
||||
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
|
||||
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
|
||||
const PROFILE_TASK_STATUS_PRIORITY_RANK: Record<
|
||||
ProfileTaskItem['status'],
|
||||
number
|
||||
> = {
|
||||
claimable: 2,
|
||||
incomplete: 1,
|
||||
disabled: 0,
|
||||
claimed: -1,
|
||||
};
|
||||
const PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS = 10;
|
||||
const PROFILE_QR_SCAN_INTERVAL_MS = 360;
|
||||
|
||||
function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
|
||||
return tasks
|
||||
.map((task, index) => ({ task, index }))
|
||||
.filter(
|
||||
({ task }) =>
|
||||
task.status === 'claimable' || task.status === 'incomplete',
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
PROFILE_TASK_STATUS_PRIORITY_RANK[right.task.status] -
|
||||
PROFILE_TASK_STATUS_PRIORITY_RANK[left.task.status] ||
|
||||
left.index - right.index,
|
||||
)
|
||||
.slice(0, 1)
|
||||
.map(({ task }) => task);
|
||||
}
|
||||
|
||||
function selectProfileTaskCardTask(tasks: ProfileTaskItem[]) {
|
||||
return (
|
||||
selectProfileTaskCenterTasks(tasks)[0] ??
|
||||
tasks.find((task) => task.status === 'claimed') ??
|
||||
tasks.find((task) => task.status !== 'disabled') ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function buildProfileTaskCardSummary(center: ProfileTaskCenterResponse | null) {
|
||||
const task = selectProfileTaskCardTask(center?.tasks ?? []);
|
||||
const threshold = Math.max(1, task?.threshold ?? 1);
|
||||
const progressCount = Math.min(task?.progressCount ?? 0, threshold);
|
||||
const rewardPoints =
|
||||
task?.rewardPoints ?? PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS;
|
||||
const actionLabel =
|
||||
task?.status === 'claimable'
|
||||
? '领取'
|
||||
: task?.status === 'claimed'
|
||||
? '已完成'
|
||||
: '去完成';
|
||||
|
||||
return {
|
||||
actionLabel,
|
||||
progressCount,
|
||||
progressPercent: Math.round((progressCount / threshold) * 100),
|
||||
rewardPoints,
|
||||
threshold,
|
||||
};
|
||||
}
|
||||
|
||||
type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
|
||||
type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
|
||||
type BarcodeDetectorLike = {
|
||||
@@ -2861,13 +2809,6 @@ function WalletLedgerModal({
|
||||
);
|
||||
}
|
||||
|
||||
const PROFILE_TASK_STATUS_LABELS: Record<ProfileTaskItem['status'], string> = {
|
||||
incomplete: '未完成',
|
||||
claimable: '可领取',
|
||||
claimed: '已领取',
|
||||
disabled: '已停用',
|
||||
};
|
||||
|
||||
function ProfileTaskCenterModal({
|
||||
center,
|
||||
isLoading,
|
||||
@@ -2947,7 +2888,7 @@ function ProfileTaskCenterModal({
|
||||
{tasks.map((task) => {
|
||||
const isClaimable = task.status === 'claimable';
|
||||
const isClaiming = claimingTaskId === task.taskId;
|
||||
const progressLabel = `${Math.min(task.progressCount, task.threshold)}/${task.threshold}`;
|
||||
const progressLabel = buildProfileTaskProgressLabel(task);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -2968,7 +2909,7 @@ function ProfileTaskCenterModal({
|
||||
+{task.rewardPoints}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] font-semibold text-[var(--platform-text-soft)]">
|
||||
{PROFILE_TASK_STATUS_LABELS[task.status]}
|
||||
{getProfileTaskStatusLabel(task.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2978,13 +2919,7 @@ function ProfileTaskCenterModal({
|
||||
onClick={() => onClaim(task.taskId)}
|
||||
className="platform-primary-button mt-3 w-full rounded-2xl px-4 py-2.5 text-sm font-black disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isClaiming
|
||||
? '领取中'
|
||||
: task.status === 'claimed'
|
||||
? '已领取'
|
||||
: isClaimable
|
||||
? '领取'
|
||||
: '未完成'}
|
||||
{getProfileTaskClaimButtonLabel(task, isClaiming)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
127
src/components/rpg-entry/rpgEntryProfileTaskViewModel.test.ts
Normal file
127
src/components/rpg-entry/rpgEntryProfileTaskViewModel.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import type {
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileTaskItem,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
buildProfileTaskCardSummary,
|
||||
buildProfileTaskProgressLabel,
|
||||
getProfileTaskClaimButtonLabel,
|
||||
getProfileTaskStatusLabel,
|
||||
selectProfileTaskCardTask,
|
||||
selectProfileTaskCenterTasks,
|
||||
} from './rpgEntryProfileTaskViewModel';
|
||||
|
||||
function buildTask(
|
||||
overrides: Partial<ProfileTaskItem> = {},
|
||||
): ProfileTaskItem {
|
||||
return {
|
||||
taskId: 'task-1',
|
||||
title: '游玩一次',
|
||||
description: '完成一次游戏',
|
||||
eventKey: 'work_play_start',
|
||||
cycle: 'daily',
|
||||
threshold: 1,
|
||||
progressCount: 0,
|
||||
rewardPoints: 10,
|
||||
status: 'incomplete',
|
||||
dayKey: 20260603,
|
||||
claimedAt: null,
|
||||
updatedAt: '2026-06-03T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildCenter(
|
||||
tasks: ProfileTaskItem[],
|
||||
): ProfileTaskCenterResponse {
|
||||
return {
|
||||
dayKey: 20260603,
|
||||
walletBalance: 12,
|
||||
tasks,
|
||||
updatedAt: '2026-06-03T00:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
test('profile task ViewModel selects one actionable task by status priority and original order', () => {
|
||||
const firstIncomplete = buildTask({
|
||||
taskId: 'incomplete-1',
|
||||
status: 'incomplete',
|
||||
});
|
||||
const secondIncomplete = buildTask({
|
||||
taskId: 'incomplete-2',
|
||||
status: 'incomplete',
|
||||
});
|
||||
const claimable = buildTask({
|
||||
taskId: 'claimable-1',
|
||||
status: 'claimable',
|
||||
});
|
||||
|
||||
expect(
|
||||
selectProfileTaskCenterTasks([
|
||||
firstIncomplete,
|
||||
secondIncomplete,
|
||||
claimable,
|
||||
]),
|
||||
).toEqual([claimable]);
|
||||
expect(selectProfileTaskCenterTasks([firstIncomplete, secondIncomplete])).toEqual(
|
||||
[firstIncomplete],
|
||||
);
|
||||
});
|
||||
|
||||
test('profile task ViewModel falls back from card task to claimed and enabled tasks', () => {
|
||||
const claimed = buildTask({ taskId: 'claimed-1', status: 'claimed' });
|
||||
const disabled = buildTask({ taskId: 'disabled-1', status: 'disabled' });
|
||||
const incomplete = buildTask({
|
||||
taskId: 'incomplete-1',
|
||||
status: 'incomplete',
|
||||
});
|
||||
|
||||
expect(selectProfileTaskCardTask([disabled, claimed])).toBe(claimed);
|
||||
expect(selectProfileTaskCardTask([disabled, incomplete])).toBe(incomplete);
|
||||
expect(selectProfileTaskCardTask([disabled])).toBeNull();
|
||||
});
|
||||
|
||||
test('profile task ViewModel builds card summary with reward fallback and clamped progress', () => {
|
||||
expect(buildProfileTaskCardSummary(null)).toEqual({
|
||||
actionLabel: '去完成',
|
||||
progressCount: 0,
|
||||
progressPercent: 0,
|
||||
rewardPoints: 10,
|
||||
threshold: 1,
|
||||
});
|
||||
|
||||
expect(
|
||||
buildProfileTaskCardSummary(
|
||||
buildCenter([
|
||||
buildTask({
|
||||
progressCount: 5,
|
||||
rewardPoints: 25,
|
||||
status: 'claimable',
|
||||
threshold: 3,
|
||||
}),
|
||||
]),
|
||||
),
|
||||
).toEqual({
|
||||
actionLabel: '领取',
|
||||
progressCount: 3,
|
||||
progressPercent: 100,
|
||||
rewardPoints: 25,
|
||||
threshold: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('profile task ViewModel exposes task labels for the modal', () => {
|
||||
const task = buildTask({ progressCount: -1, threshold: 0 });
|
||||
|
||||
expect(getProfileTaskStatusLabel('claimable')).toBe('可领取');
|
||||
expect(buildProfileTaskProgressLabel(task)).toBe('0/1');
|
||||
expect(getProfileTaskClaimButtonLabel(task, true)).toBe('领取中');
|
||||
expect(getProfileTaskClaimButtonLabel(buildTask({ status: 'claimed' }), false)).toBe(
|
||||
'已领取',
|
||||
);
|
||||
expect(
|
||||
getProfileTaskClaimButtonLabel(buildTask({ status: 'claimable' }), false),
|
||||
).toBe('领取');
|
||||
});
|
||||
107
src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts
Normal file
107
src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type {
|
||||
ProfileTaskCenterResponse,
|
||||
ProfileTaskItem,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
|
||||
const PROFILE_TASK_STATUS_PRIORITY_RANK: Record<
|
||||
ProfileTaskItem['status'],
|
||||
number
|
||||
> = {
|
||||
claimable: 2,
|
||||
incomplete: 1,
|
||||
disabled: 0,
|
||||
claimed: -1,
|
||||
};
|
||||
const PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS = 10;
|
||||
const PROFILE_TASK_STATUS_LABELS: Record<ProfileTaskItem['status'], string> = {
|
||||
incomplete: '未完成',
|
||||
claimable: '可领取',
|
||||
claimed: '已领取',
|
||||
disabled: '已停用',
|
||||
};
|
||||
|
||||
export type ProfileTaskCardSummary = {
|
||||
actionLabel: string;
|
||||
progressCount: number;
|
||||
progressPercent: number;
|
||||
rewardPoints: number;
|
||||
threshold: number;
|
||||
};
|
||||
|
||||
export function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
|
||||
return tasks
|
||||
.map((task, index) => ({ task, index }))
|
||||
.filter(
|
||||
({ task }) =>
|
||||
task.status === 'claimable' || task.status === 'incomplete',
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
PROFILE_TASK_STATUS_PRIORITY_RANK[right.task.status] -
|
||||
PROFILE_TASK_STATUS_PRIORITY_RANK[left.task.status] ||
|
||||
left.index - right.index,
|
||||
)
|
||||
.slice(0, 1)
|
||||
.map(({ task }) => task);
|
||||
}
|
||||
|
||||
export function selectProfileTaskCardTask(tasks: ProfileTaskItem[]) {
|
||||
return (
|
||||
selectProfileTaskCenterTasks(tasks)[0] ??
|
||||
tasks.find((task) => task.status === 'claimed') ??
|
||||
tasks.find((task) => task.status !== 'disabled') ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function getProfileTaskStatusLabel(status: ProfileTaskItem['status']) {
|
||||
return PROFILE_TASK_STATUS_LABELS[status];
|
||||
}
|
||||
|
||||
export function buildProfileTaskProgressLabel(task: ProfileTaskItem) {
|
||||
const threshold = Math.max(1, task.threshold);
|
||||
const progressCount = Math.min(Math.max(0, task.progressCount), threshold);
|
||||
return `${progressCount}/${threshold}`;
|
||||
}
|
||||
|
||||
export function getProfileTaskClaimButtonLabel(
|
||||
task: ProfileTaskItem,
|
||||
isClaiming: boolean,
|
||||
) {
|
||||
if (isClaiming) {
|
||||
return '领取中';
|
||||
}
|
||||
|
||||
if (task.status === 'claimed') {
|
||||
return '已领取';
|
||||
}
|
||||
|
||||
return task.status === 'claimable' ? '领取' : '未完成';
|
||||
}
|
||||
|
||||
export function buildProfileTaskCardSummary(
|
||||
center: ProfileTaskCenterResponse | null,
|
||||
): ProfileTaskCardSummary {
|
||||
const task = selectProfileTaskCardTask(center?.tasks ?? []);
|
||||
const threshold = Math.max(1, task?.threshold ?? 1);
|
||||
const progressCount = Math.min(
|
||||
Math.max(0, task?.progressCount ?? 0),
|
||||
threshold,
|
||||
);
|
||||
const rewardPoints =
|
||||
task?.rewardPoints ?? PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS;
|
||||
const actionLabel =
|
||||
task?.status === 'claimable'
|
||||
? '领取'
|
||||
: task?.status === 'claimed'
|
||||
? '已完成'
|
||||
: '去完成';
|
||||
|
||||
return {
|
||||
actionLabel,
|
||||
progressCount,
|
||||
progressPercent: Math.round((progressCount / threshold) * 100),
|
||||
rewardPoints,
|
||||
threshold,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user