refactor: 收口每日任务 ViewModel
This commit is contained in:
@@ -58,6 +58,14 @@
|
|||||||
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run typecheck`、`npm run check:encoding`、相关文件 ESLint 通过。
|
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run typecheck`、`npm run check:encoding`、相关文件 ESLint 通过。
|
||||||
- 关联文档:`docs/technical/【前端架构】PublicGalleryViewModel收口计划-2026-06-03.md`。
|
- 关联文档:`docs/technical/【前端架构】PublicGalleryViewModel收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
|
## 2026-06-03 Profile Task ViewModel 收口
|
||||||
|
|
||||||
|
- 背景:`RpgEntryHomeView.tsx` 同时持有每日任务卡片和任务中心弹窗的任务选择、进度 clamp、奖励兜底、状态标签和按钮文案,导致任务展示规则和 JSX 缠在一起。
|
||||||
|
- 决策:新增 `src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts`,把 `selectProfileTaskCenterTasks`、`selectProfileTaskCardTask`、`buildProfileTaskCardSummary`、`buildProfileTaskProgressLabel`、`getProfileTaskStatusLabel` 和 `getProfileTaskClaimButtonLabel` 收口为每日任务 ViewModel Interface。任务中心仍只展示一条 claimable / incomplete 优先任务,任务卡按可操作、claimed、非 disabled 的顺序兜底。
|
||||||
|
- 影响范围:RPG 首页“每日任务”卡片、任务中心弹窗、后续任务状态和任务展示文案调整。
|
||||||
|
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryProfileTaskViewModel.test.ts`、`npm run typecheck`、`npm run check:encoding`、相关文件 ESLint 通过。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
## 2026-06-03 最近创作只复用创作模板入口
|
## 2026-06-03 最近创作只复用创作模板入口
|
||||||
|
|
||||||
- 背景:底部加号创作入口的“最近创作”最初由真实作品架摘要驱动,但页面曾按作品标题、摘要和生成状态渲染独立最近创作卡,和其它模板页签的卡片样式及点击语义不一致。
|
- 背景:底部加号创作入口的“最近创作”最初由真实作品架摘要驱动,但页面曾按作品标题、摘要和生成状态渲染独立最近创作卡,和其它模板页签的卡片样式及点击语义不一致。
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
公开作品分类、搜索、跨来源去重、今日筛选、排行排序和时间戳解析收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】PublicGalleryViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91PublicGalleryViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
公开作品分类、搜索、跨来源去重、今日筛选、排行排序和时间戳解析收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】PublicGalleryViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91PublicGalleryViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
|
每日任务卡片与任务中心弹窗的任务选择、进度、状态标签和按钮文案收口到 `src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts`,规则见 [【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileTaskViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
## 推荐阅读顺序
|
## 推荐阅读顺序
|
||||||
|
|
||||||
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
|
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
|
||||||
|
|||||||
29
docs/technical/【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md
Normal file
29
docs/technical/【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 【前端架构】Profile Task ViewModel 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`RpgEntryHomeView.tsx` 的“每日任务”卡片与任务弹窗共用同一批展示规则:任务优先级、可领取 / 未完成选择、进度 clamp、奖励兜底、状态标签和按钮文案。原先这些规则散在巨型页面 **Implementation** 中,UI JSX 既要渲染,又要知道任务状态排序和兜底口径。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
新增 `src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts`,作为每日任务展示模型 **Module**。该 **Module** 的 **Interface** 收口为:
|
||||||
|
|
||||||
|
- `selectProfileTaskCenterTasks(tasks)`:统一任务中心只展示一条可操作任务,按 claimable / incomplete 优先级并保持原始顺序。
|
||||||
|
- `selectProfileTaskCardTask(tasks)`:统一任务卡兜底顺序,先可操作,再 claimed,再非 disabled。
|
||||||
|
- `buildProfileTaskCardSummary(center)`:统一任务卡的奖励、阈值、进度百分比与动作文案。
|
||||||
|
- `buildProfileTaskProgressLabel(task)`、`getProfileTaskStatusLabel(status)`、`getProfileTaskClaimButtonLabel(task, isClaiming)`:统一任务弹窗中的进度、状态和按钮文案。
|
||||||
|
|
||||||
|
`RpgEntryHomeView.tsx` 只消费这些 ViewModel 函数,保留弹窗、按钮和点击处理。每日任务展示规则的 **Locality** 转移到 ViewModel **Module** 与纯测试,后续新增任务状态或修改展示优先级不再穿透 UI。
|
||||||
|
|
||||||
|
## 约定
|
||||||
|
|
||||||
|
- 任务中心只露出当前最需要用户处理的一条任务。
|
||||||
|
- 任务进度必须按 `0..threshold` clamp,避免异常后端进度撑破卡片进度条。
|
||||||
|
- `pause` / `claim` 等副作用仍留在页面和后端 client;ViewModel 只做展示派生。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- `npm run test -- src/components/rpg-entry/rpgEntryProfileTaskViewModel.test.ts`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
|
- 针对变更文件执行 ESLint
|
||||||
@@ -71,7 +71,6 @@ import type {
|
|||||||
ProfileReferralInviteCenterResponse,
|
ProfileReferralInviteCenterResponse,
|
||||||
ProfileSaveArchiveSummary,
|
ProfileSaveArchiveSummary,
|
||||||
ProfileTaskCenterResponse,
|
ProfileTaskCenterResponse,
|
||||||
ProfileTaskItem,
|
|
||||||
ProfileWalletLedgerResponse,
|
ProfileWalletLedgerResponse,
|
||||||
RedeemProfileRewardCodeResponse,
|
RedeemProfileRewardCodeResponse,
|
||||||
WechatMiniProgramPayParams,
|
WechatMiniProgramPayParams,
|
||||||
@@ -135,6 +134,13 @@ import {
|
|||||||
import { getInitialPlatformDesktopLayout } from '../platform-entry/platformEntryResponsive';
|
import { getInitialPlatformDesktopLayout } from '../platform-entry/platformEntryResponsive';
|
||||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||||
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
|
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
|
||||||
|
import {
|
||||||
|
buildProfileTaskCardSummary,
|
||||||
|
buildProfileTaskProgressLabel,
|
||||||
|
getProfileTaskClaimButtonLabel,
|
||||||
|
getProfileTaskStatusLabel,
|
||||||
|
selectProfileTaskCenterTasks,
|
||||||
|
} from './rpgEntryProfileTaskViewModel';
|
||||||
import {
|
import {
|
||||||
buildPlatformRankingEntries,
|
buildPlatformRankingEntries,
|
||||||
buildPublicCategoryGroups,
|
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_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_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
|
||||||
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
|
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;
|
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 ProfileReferralPanel = 'invite' | 'redeem' | 'community';
|
||||||
type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
|
type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
|
||||||
type BarcodeDetectorLike = {
|
type BarcodeDetectorLike = {
|
||||||
@@ -2861,13 +2809,6 @@ function WalletLedgerModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROFILE_TASK_STATUS_LABELS: Record<ProfileTaskItem['status'], string> = {
|
|
||||||
incomplete: '未完成',
|
|
||||||
claimable: '可领取',
|
|
||||||
claimed: '已领取',
|
|
||||||
disabled: '已停用',
|
|
||||||
};
|
|
||||||
|
|
||||||
function ProfileTaskCenterModal({
|
function ProfileTaskCenterModal({
|
||||||
center,
|
center,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -2947,7 +2888,7 @@ function ProfileTaskCenterModal({
|
|||||||
{tasks.map((task) => {
|
{tasks.map((task) => {
|
||||||
const isClaimable = task.status === 'claimable';
|
const isClaimable = task.status === 'claimable';
|
||||||
const isClaiming = claimingTaskId === task.taskId;
|
const isClaiming = claimingTaskId === task.taskId;
|
||||||
const progressLabel = `${Math.min(task.progressCount, task.threshold)}/${task.threshold}`;
|
const progressLabel = buildProfileTaskProgressLabel(task);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -2968,7 +2909,7 @@ function ProfileTaskCenterModal({
|
|||||||
+{task.rewardPoints}
|
+{task.rewardPoints}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-[11px] font-semibold text-[var(--platform-text-soft)]">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2978,13 +2919,7 @@ function ProfileTaskCenterModal({
|
|||||||
onClick={() => onClaim(task.taskId)}
|
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"
|
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
|
{getProfileTaskClaimButtonLabel(task, isClaiming)}
|
||||||
? '领取中'
|
|
||||||
: task.status === 'claimed'
|
|
||||||
? '已领取'
|
|
||||||
: isClaimable
|
|
||||||
? '领取'
|
|
||||||
: '未完成'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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