66178edc7c
- 新增每日免费泥点独立余额桶,按北京时间零点固定重置20泥点 - 按每日免费、会员限时、永久顺序扣费并支持跨日退款按原分桶回补 - 统一主站与图片画板泥点入口,隐藏限时泥点、任务入口和会员购买入口 - 收敛四档充值商品与泥点购买弹窗,并接入后台动态商品配置 - 同步余额read model、SpacetimeDB迁移绑定、回归测试和工程文档 - 将泥点明细浮层圆角匹配首页卡片视觉规范
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
import type {
|
|
ProfileMembership,
|
|
ProfileRechargeProduct,
|
|
ProfileWalletLedgerEntry,
|
|
ProfileWalletLedgerResponse,
|
|
} from '../../../packages/shared/src/contracts/runtime';
|
|
|
|
const PROFILE_WALLET_LEDGER_SOURCE_LABELS = {
|
|
new_user_registration_reward: '注册赠送',
|
|
points_recharge: '泥点充值',
|
|
invite_inviter_reward: '邀请奖励',
|
|
invite_invitee_reward: '填写邀请码奖励',
|
|
snapshot_sync: '账户同步',
|
|
membership_period_grant: '会员周期发放',
|
|
membership_period_reset: '会员周期重置',
|
|
daily_free_grant: '每日免费发放',
|
|
daily_free_reset: '每日免费重置',
|
|
asset_operation_consume: '资产操作消耗',
|
|
asset_operation_refund: '资产操作退回',
|
|
redeem_code_reward: '兑换码奖励',
|
|
puzzle_author_incentive_claim: '拼图作者奖励',
|
|
daily_task_reward: '每日任务奖励',
|
|
} satisfies Record<ProfileWalletLedgerEntry['sourceType'], string>;
|
|
|
|
export type ProfileWalletLedgerEntryPresentation = {
|
|
amountLabel: string;
|
|
balanceLabel: string;
|
|
createdAt: string;
|
|
id: string;
|
|
isIncome: boolean;
|
|
sourceLabel: string;
|
|
};
|
|
|
|
export type ProfileWalletLedgerPresentation = {
|
|
balance: number;
|
|
balanceLabel: string;
|
|
entries: ProfileWalletLedgerEntryPresentation[];
|
|
};
|
|
|
|
export function getWalletLedgerSourceLabel(
|
|
sourceType: string | null | undefined,
|
|
) {
|
|
const normalizedSourceType = sourceType?.trim() ?? '';
|
|
if (!normalizedSourceType) {
|
|
return '未知来源';
|
|
}
|
|
|
|
return (
|
|
PROFILE_WALLET_LEDGER_SOURCE_LABELS[
|
|
normalizedSourceType as ProfileWalletLedgerEntry['sourceType']
|
|
] ?? normalizedSourceType
|
|
);
|
|
}
|
|
|
|
export function formatWalletLedgerAmount(amountDelta: number) {
|
|
return amountDelta > 0 ? `+${amountDelta}` : `${amountDelta}`;
|
|
}
|
|
|
|
export function buildWalletLedgerEntryPresentation(
|
|
entry: ProfileWalletLedgerEntry,
|
|
): ProfileWalletLedgerEntryPresentation {
|
|
return {
|
|
amountLabel: formatWalletLedgerAmount(entry.amountDelta),
|
|
balanceLabel: `余额 ${entry.balanceAfter}`,
|
|
createdAt: entry.createdAt,
|
|
id: entry.id,
|
|
isIncome: entry.amountDelta > 0,
|
|
sourceLabel: getWalletLedgerSourceLabel(entry.sourceType),
|
|
};
|
|
}
|
|
|
|
export function buildWalletLedgerPresentation(
|
|
ledger: ProfileWalletLedgerResponse | null,
|
|
fallbackBalance: number,
|
|
): ProfileWalletLedgerPresentation {
|
|
const entries = ledger?.entries ?? [];
|
|
const balance = entries[0]?.balanceAfter ?? fallbackBalance;
|
|
|
|
return {
|
|
balance,
|
|
balanceLabel: `${balance}泥点`,
|
|
entries: entries.map(buildWalletLedgerEntryPresentation),
|
|
};
|
|
}
|
|
|
|
export function formatRechargePrice(priceCents: number) {
|
|
const yuan = priceCents / 100;
|
|
return `¥${Number.isInteger(yuan) ? yuan.toFixed(0) : yuan.toFixed(2)}`;
|
|
}
|
|
|
|
function formatMembershipPeriodPrefix(periodDays: number) {
|
|
if (periodDays === 30) {
|
|
return '每月';
|
|
}
|
|
if (periodDays === 7) {
|
|
return '每周';
|
|
}
|
|
if (periodDays > 0) {
|
|
return `每${periodDays}天`;
|
|
}
|
|
return '每周期';
|
|
}
|
|
|
|
export function buildRechargeProductValueLabel(product: ProfileRechargeProduct) {
|
|
if (product.kind === 'membership') {
|
|
const membershipPeriodPoints = product.membershipPeriodPoints;
|
|
const membershipPeriodDays =
|
|
product.membershipPeriodDays > 0
|
|
? product.membershipPeriodDays
|
|
: product.durationDays;
|
|
return membershipPeriodPoints > 0
|
|
? `${formatMembershipPeriodPrefix(membershipPeriodDays)}${membershipPeriodPoints}泥点`
|
|
: `${product.durationDays}天`;
|
|
}
|
|
|
|
return `${product.pointsAmount}${
|
|
product.bonusPoints > 0 ? `+${product.bonusPoints}` : ''
|
|
}泥点`;
|
|
}
|
|
|
|
export function getMembershipTierLabel(tier: ProfileMembership['tier']) {
|
|
switch (tier) {
|
|
case 'starter':
|
|
return 'Starter';
|
|
case 'basic':
|
|
return 'Basic';
|
|
case 'pro':
|
|
return 'Pro';
|
|
case 'ultimate':
|
|
return 'Ultimate';
|
|
case 'month':
|
|
return '月卡';
|
|
case 'season':
|
|
return '季卡';
|
|
case 'year':
|
|
return '年卡';
|
|
default:
|
|
return '普通用户';
|
|
}
|
|
}
|
|
|
|
export function buildMembershipLabel(
|
|
membership: ProfileMembership | null | undefined,
|
|
formatTime: (value: string) => string,
|
|
) {
|
|
if (membership?.status !== 'active') {
|
|
return '普通用户';
|
|
}
|
|
|
|
const tierLabel =
|
|
membership.tier === 'normal' ? '会员' : getMembershipTierLabel(membership.tier);
|
|
const resetLabel = membership.cycleResetsAt
|
|
? ` · ${formatTime(membership.cycleResetsAt)}重置`
|
|
: '';
|
|
if (membership.tier === 'normal') {
|
|
return membership.expiresAt
|
|
? `会员至 ${formatTime(membership.expiresAt)}${resetLabel}`
|
|
: `会员已生效${resetLabel}`;
|
|
}
|
|
return membership.expiresAt
|
|
? `${tierLabel} 至 ${formatTime(membership.expiresAt)}${resetLabel}`
|
|
: `${tierLabel} 已生效${resetLabel}`;
|
|
}
|
|
|
|
export function buildMembershipCycleLabel(
|
|
membership: ProfileMembership | null | undefined,
|
|
formatTime: (value: string) => string,
|
|
) {
|
|
if (membership?.status !== 'active') {
|
|
return '普通用户';
|
|
}
|
|
|
|
if (membership.cycleRemainingPoints > 0) {
|
|
const resetLabel = membership.cycleResetsAt
|
|
? ` · ${formatTime(membership.cycleResetsAt)}重置`
|
|
: '';
|
|
return `本周期剩余 ${membership.cycleRemainingPoints} 泥点${resetLabel}`;
|
|
}
|
|
|
|
return buildMembershipLabel(membership, formatTime);
|
|
}
|