新增会员与泥点共享弹层并改版钱包入口
- PlatformProfileRechargeModal 重写为「会员订阅 / 泥点充值」双 Tab,含三池概览、套餐卡、周期切换、会员状态、套餐对比表与确认订单页 - 新增 membershipCopy 收敛数量估算、推荐标签、模型权限与并发展示文案,价格与权益仍读后端目录 - 新增 membershipProduct 提供由档位与周期拼出的会员 productId,避免与组件混导出 - PlatformMudPointWalletEntry 入口改为「可用总额 + 行内月度/永久 + 会员与泥点」,使用详情与兑换码保留为次级入口 - 同步改写共享弹层测试,覆盖双 Tab、升级报价与确认页 Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -88,6 +88,14 @@ export function PlatformMudPointWalletEntry({
|
||||
displayedBalance === null ? '--' : formatMudPointCount(displayedBalance);
|
||||
const detailsRequestKey = `balance:${balance ?? breakdown?.totalPoints ?? 'unknown'}`;
|
||||
const monthlyResetLabel = formatMudPointResetAt(breakdown?.monthlyResetsAt);
|
||||
const monthlyInlineLabel = formatMudPointCount(
|
||||
breakdown?.monthlyPoints ?? 0,
|
||||
true,
|
||||
);
|
||||
const permanentInlineLabel = formatMudPointCount(
|
||||
breakdown?.permanentPoints ?? 0,
|
||||
true,
|
||||
);
|
||||
|
||||
const cancelPendingClose = useCallback(() => {
|
||||
if (closeTimerRef.current !== null) {
|
||||
@@ -192,7 +200,9 @@ export function PlatformMudPointWalletEntry({
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 items-center gap-1.5 px-2 font-black outline-none transition-colors hover:bg-white/70 focus-visible:bg-white/80"
|
||||
aria-label={`泥点 ${exactBalanceLabel}`}
|
||||
aria-label={`泥点账户:可用总额 ${exactBalanceLabel},月度 ${formatMudPointCount(
|
||||
breakdown?.monthlyPoints ?? 0,
|
||||
)},永久 ${formatMudPointCount(breakdown?.permanentPoints ?? 0)}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="dialog"
|
||||
aria-busy={isLoading}
|
||||
@@ -218,8 +228,16 @@ export function PlatformMudPointWalletEntry({
|
||||
className={`${isCompact ? 'h-[1.05rem] w-[1.05rem]' : 'h-[1.2rem] w-[1.2rem]'} shrink-0 object-cover mix-blend-multiply`}
|
||||
/>
|
||||
<span className="truncate whitespace-nowrap">
|
||||
泥点 {balanceLabel}
|
||||
可用总额 {balanceLabel}
|
||||
</span>
|
||||
{!isCompact && breakdown ? (
|
||||
<span className="hidden shrink-0 items-center gap-1 truncate whitespace-nowrap text-[10px] font-bold text-[var(--platform-text-soft)] sm:inline-flex">
|
||||
<span aria-hidden="true" className="opacity-40">
|
||||
|
|
||||
</span>
|
||||
月度 {monthlyInlineLabel} · 永久 {permanentInlineLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
<span
|
||||
className="my-1.5 w-px shrink-0 bg-[rgba(196,153,120,0.36)]"
|
||||
@@ -233,7 +251,7 @@ export function PlatformMudPointWalletEntry({
|
||||
onRecharge();
|
||||
}}
|
||||
>
|
||||
充值
|
||||
会员与泥点
|
||||
</button>
|
||||
{onRedeemCode ? (
|
||||
<>
|
||||
@@ -258,13 +276,13 @@ export function PlatformMudPointWalletEntry({
|
||||
{isOpen ? (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label="泥点账户详情"
|
||||
aria-label="会员与泥点账户详情"
|
||||
onMouseEnter={isCompact ? undefined : cancelPendingClose}
|
||||
className="absolute right-0 top-[calc(100%+0.5rem)] z-[95] w-[min(19rem,calc(100vw-1rem))] overflow-hidden rounded-[1.25rem] border border-[rgba(194,145,111,0.46)] bg-[linear-gradient(180deg,#fffdfa,#fff7ef)] text-left shadow-[0_1.15rem_2.8rem_rgba(76,44,27,0.14),0_0.18rem_0.55rem_rgba(112,62,32,0.07)]"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="text-[15px] font-black text-[var(--platform-text-strong)]">
|
||||
泥点 {exactBalanceLabel}
|
||||
可用总额 {exactBalanceLabel}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -274,7 +292,7 @@ export function PlatformMudPointWalletEntry({
|
||||
onRecharge();
|
||||
}}
|
||||
>
|
||||
充值
|
||||
会员与泥点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,6 +25,26 @@ function buildNormalMembership() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildActiveMembership(
|
||||
plan: 'plus' | 'max',
|
||||
cycleKind: 'monthly' | 'yearly',
|
||||
) {
|
||||
return {
|
||||
status: 'active' as const,
|
||||
plan,
|
||||
startedAt: '2026-01-01T00:00:00+08:00',
|
||||
expiresAt: '2027-01-01T00:00:00+08:00',
|
||||
updatedAt: '2026-04-01T00:00:00+08:00',
|
||||
cycleStartedAt: '2026-03-20T09:00:00+08:00',
|
||||
cycleResetsAt: '2026-04-20T09:00:00+08:00',
|
||||
cycleGrantedPoints: 1_150,
|
||||
cycleRemainingPoints: 615,
|
||||
cycleIndex: 4,
|
||||
cycleCount: 12,
|
||||
cycleKind,
|
||||
};
|
||||
}
|
||||
|
||||
/** 三池余额 fixture:每日免费 / 月度 / 永久。 */
|
||||
function buildMudPointBalance(totalPoints: number) {
|
||||
return {
|
||||
@@ -54,7 +74,7 @@ function buildPointProduct(
|
||||
};
|
||||
}
|
||||
|
||||
/// 六档充点商品:¥1 = 10 泥点,没有首充赠送。
|
||||
// 六档充点商品:¥1 = 10 泥点,没有首充赠送。
|
||||
function buildPointProducts(): ProfileRechargeProduct[] {
|
||||
return [60, 180, 300, 680, 1_280, 3_280].map((pointsAmount) =>
|
||||
buildPointProduct({
|
||||
@@ -67,20 +87,44 @@ function buildPointProducts(): ProfileRechargeProduct[] {
|
||||
);
|
||||
}
|
||||
|
||||
function buildMembershipPlan() {
|
||||
return {
|
||||
plan: 'starter' as const,
|
||||
title: 'Starter',
|
||||
rank: 1,
|
||||
monthPriceCents: 3_900,
|
||||
yearPriceCents: 39_000,
|
||||
periodPoints: 400,
|
||||
modelAccess: 'basic' as const,
|
||||
concurrentJobLimit: 1,
|
||||
sortOrder: 10,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
const STARTER_PLAN = {
|
||||
plan: 'starter' as const,
|
||||
title: 'Starter',
|
||||
rank: 1,
|
||||
monthPriceCents: 3_900,
|
||||
yearPriceCents: 39_000,
|
||||
periodPoints: 400,
|
||||
modelAccess: 'basic' as const,
|
||||
concurrentJobLimit: 1,
|
||||
sortOrder: 10,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const PLUS_PLAN = {
|
||||
...STARTER_PLAN,
|
||||
plan: 'plus' as const,
|
||||
title: 'Plus',
|
||||
rank: 2,
|
||||
monthPriceCents: 9_900,
|
||||
yearPriceCents: 99_000,
|
||||
periodPoints: 1_150,
|
||||
modelAccess: 'full' as const,
|
||||
concurrentJobLimit: 3,
|
||||
sortOrder: 20,
|
||||
};
|
||||
|
||||
const MAX_PLAN = {
|
||||
...STARTER_PLAN,
|
||||
plan: 'max' as const,
|
||||
title: 'Max',
|
||||
rank: 4,
|
||||
monthPriceCents: 69_900,
|
||||
yearPriceCents: 699_000,
|
||||
periodPoints: 8_650,
|
||||
modelAccess: 'full' as const,
|
||||
concurrentJobLimit: 128,
|
||||
sortOrder: 40,
|
||||
};
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
default: {
|
||||
@@ -89,7 +133,134 @@ vi.mock('qrcode', () => ({
|
||||
}));
|
||||
|
||||
describe('PlatformProfileRechargeModal', () => {
|
||||
test('renders the six point products and forwards the selected buy action', async () => {
|
||||
test('renders the membership plans and forwards a first-time purchase', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitMembershipCheckout = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformProfileRechargeModal
|
||||
center={{
|
||||
mudPointBalance: buildMudPointBalance(29),
|
||||
membership: buildNormalMembership(),
|
||||
pointProducts: buildPointProducts(),
|
||||
membershipPlans: [STARTER_PLAN, PLUS_PLAN],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
nativePayment={null}
|
||||
membershipQuote={null}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={vi.fn()}
|
||||
onRequestMembershipQuote={vi.fn()}
|
||||
onSubmitMembershipCheckout={onSubmitMembershipCheckout}
|
||||
onConfirmNativePayment={vi.fn()}
|
||||
onCloseNativePayment={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '会员与泥点' })).toBeTruthy();
|
||||
expect(screen.getByText('可用总额 29 泥点')).toBeTruthy();
|
||||
const tablist = screen.getByRole('tablist', { name: '会员与泥点' });
|
||||
expect(
|
||||
within(tablist)
|
||||
.getByRole('tab', { name: '会员订阅' })
|
||||
.getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(screen.getAllByText('Starter').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Plus').length).toBeGreaterThan(0);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Starter 月付 开通会员' }),
|
||||
);
|
||||
|
||||
const checkout = screen.getByRole('dialog', { name: '确认订单' });
|
||||
expect(within(checkout).getByText('¥39')).toBeTruthy();
|
||||
expect(within(checkout).getByText('400(首期)')).toBeTruthy();
|
||||
expect(within(checkout).getByText('微信支付')).toBeTruthy();
|
||||
expect(within(checkout).queryByText(/报价已失效/)).toBeNull();
|
||||
expect(within(checkout).queryByText(/倒计时/)).toBeNull();
|
||||
|
||||
await user.click(
|
||||
within(checkout).getByRole('button', { name: '支付 ¥39' }),
|
||||
);
|
||||
|
||||
expect(onSubmitMembershipCheckout).toHaveBeenCalledWith(
|
||||
'starter',
|
||||
'monthly',
|
||||
);
|
||||
});
|
||||
|
||||
test('requests the server quote and shows the upgrade amount', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRequestMembershipQuote = vi.fn();
|
||||
const onSubmitMembershipCheckout = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformProfileRechargeModal
|
||||
center={{
|
||||
mudPointBalance: buildMudPointBalance(615),
|
||||
membership: buildActiveMembership('plus', 'yearly'),
|
||||
pointProducts: buildPointProducts(),
|
||||
membershipPlans: [STARTER_PLAN, PLUS_PLAN, MAX_PLAN],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: true,
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
nativePayment={null}
|
||||
membershipQuote={{
|
||||
currentPlan: 'plus',
|
||||
targetPlan: 'max',
|
||||
cycleKind: 'yearly',
|
||||
cycleIndex: 4,
|
||||
cycleCount: 12,
|
||||
amountCents: 10_000,
|
||||
grantedPointsDelta: 1_250,
|
||||
monthlyBalanceAfter: 1_865,
|
||||
expiresAt: '2027-01-01T09:00:00+08:00',
|
||||
cycleResetsAt: '2026-04-20T09:00:00+08:00',
|
||||
remainingFullMonths: 8,
|
||||
}}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={vi.fn()}
|
||||
onRequestMembershipQuote={onRequestMembershipQuote}
|
||||
onSubmitMembershipCheckout={onSubmitMembershipCheckout}
|
||||
onConfirmNativePayment={vi.fn()}
|
||||
onCloseNativePayment={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Plus 年付 当前套餐' }),
|
||||
).toHaveProperty('disabled', true);
|
||||
expect(screen.getByText('当前 Plus · 年付')).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Max 年付 补差升级' }));
|
||||
|
||||
expect(onRequestMembershipQuote).toHaveBeenCalledWith('max');
|
||||
|
||||
const checkout = screen.getByRole('dialog', { name: '确认套餐变更' });
|
||||
expect(within(checkout).getByText('¥100')).toBeTruthy();
|
||||
expect(within(checkout).getByText('本期补发月度泥点')).toBeTruthy();
|
||||
|
||||
await user.click(
|
||||
within(checkout).getByRole('button', { name: '补差升级 ¥100' }),
|
||||
);
|
||||
|
||||
expect(onSubmitMembershipCheckout).toHaveBeenCalledWith('max', 'yearly');
|
||||
});
|
||||
|
||||
test('recharge tab picks a point product and forwards the buy action', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onBuy = vi.fn();
|
||||
|
||||
@@ -99,7 +270,7 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
mudPointBalance: buildMudPointBalance(29),
|
||||
membership: buildNormalMembership(),
|
||||
pointProducts: buildPointProducts(),
|
||||
membershipPlans: [buildMembershipPlan()],
|
||||
membershipPlans: [STARTER_PLAN],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
}}
|
||||
@@ -107,58 +278,50 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
nativePayment={null}
|
||||
membershipQuote={null}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={onBuy}
|
||||
onRequestMembershipQuote={vi.fn()}
|
||||
onSubmitMembershipCheckout={vi.fn()}
|
||||
onConfirmNativePayment={vi.fn()}
|
||||
onCloseNativePayment={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '购买更多泥点' });
|
||||
expect(within(dialog).getByText('当前余额 29 泥点')).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '60泥点 ¥6 购买' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '180泥点 ¥18 购买' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', {
|
||||
name: '300泥点 ¥30 购买',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', {
|
||||
name: '680泥点 ¥68 购买',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).queryAllByText('首充加赠')).toHaveLength(0);
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '1,280泥点 ¥128 购买' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '3,280泥点 ¥328 购买' }),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).queryByRole('tablist')).toBeNull();
|
||||
expect(within(dialog).queryByText('Starter')).toBeNull();
|
||||
await user.click(screen.getByRole('tab', { name: '泥点充值' }));
|
||||
|
||||
const products = screen.getByRole('radiogroup', { name: '泥点充值档位' });
|
||||
await user.click(
|
||||
within(dialog).getByRole('button', { name: '180泥点 ¥18 购买' }),
|
||||
);
|
||||
expect(onBuy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ productId: 'points_180' }),
|
||||
within(products).getByRole('radio', { name: '60泥点 ¥6' }),
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '充值 ¥6' }));
|
||||
|
||||
const checkout = screen.getByRole('dialog', { name: '确认订单' });
|
||||
expect(within(checkout).getByText('60 永久泥点')).toBeTruthy();
|
||||
|
||||
await user.click(within(checkout).getByRole('button', { name: '充值 ¥6' }));
|
||||
|
||||
expect(onBuy).toHaveBeenCalledTimes(1);
|
||||
expect(onBuy.mock.calls[0][0]).toMatchObject({
|
||||
productId: 'points_60',
|
||||
priceCents: 600,
|
||||
});
|
||||
});
|
||||
|
||||
test('shows the point-product empty state without exposing membership purchase', () => {
|
||||
test('shows the empty state when the recharge tab has no point product', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<PlatformProfileRechargeModal
|
||||
center={{
|
||||
mudPointBalance: buildMudPointBalance(0),
|
||||
mudPointBalance: buildMudPointBalance(29),
|
||||
membership: buildNormalMembership(),
|
||||
pointProducts: [],
|
||||
membershipPlans: [buildMembershipPlan()],
|
||||
membershipPlans: [STARTER_PLAN],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
}}
|
||||
@@ -166,17 +329,23 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
nativePayment={null}
|
||||
membershipQuote={null}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={vi.fn()}
|
||||
onRequestMembershipQuote={vi.fn()}
|
||||
onSubmitMembershipCheckout={vi.fn()}
|
||||
onConfirmNativePayment={vi.fn()}
|
||||
onCloseNativePayment={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: '泥点充值' }));
|
||||
|
||||
expect(screen.getAllByText('Starter').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('暂无可购买套餐')).toBeTruthy();
|
||||
expect(screen.queryByText('Starter')).toBeNull();
|
||||
expect(screen.queryByRole('tablist')).toBeNull();
|
||||
});
|
||||
|
||||
test('opens native payment QR code in a separate dialog', async () => {
|
||||
@@ -197,6 +366,9 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
isLoading={false}
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
membershipQuote={null}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
nativePayment={{
|
||||
orderId: 'rcg_native_001',
|
||||
productTitle: '100泥点',
|
||||
@@ -209,13 +381,15 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={vi.fn()}
|
||||
onRequestMembershipQuote={vi.fn()}
|
||||
onSubmitMembershipCheckout={vi.fn()}
|
||||
onConfirmNativePayment={onConfirmNativePayment}
|
||||
onCloseNativePayment={onCloseNativePayment}
|
||||
/>,
|
||||
);
|
||||
|
||||
const rechargeDialog = screen.getByRole('dialog', {
|
||||
name: '购买更多泥点',
|
||||
name: '会员与泥点',
|
||||
});
|
||||
const paymentDialog = screen.getByRole('dialog', {
|
||||
name: '微信扫码支付',
|
||||
@@ -264,6 +438,9 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
isLoading={false}
|
||||
error={null}
|
||||
submittingProductId={null}
|
||||
membershipQuote={null}
|
||||
isLoadingMembershipQuote={false}
|
||||
membershipQuoteError={null}
|
||||
nativePayment={{
|
||||
orderId: 'rcg_native_expired',
|
||||
productTitle: '100泥点',
|
||||
@@ -275,6 +452,8 @@ describe('PlatformProfileRechargeModal', () => {
|
||||
onClose={vi.fn()}
|
||||
onRetry={vi.fn()}
|
||||
onBuy={vi.fn()}
|
||||
onRequestMembershipQuote={vi.fn()}
|
||||
onSubmitMembershipCheckout={vi.fn()}
|
||||
onConfirmNativePayment={onConfirmNativePayment}
|
||||
onCloseNativePayment={vi.fn()}
|
||||
/>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
ProfileMembershipModelAccess,
|
||||
ProfileMembershipPlan,
|
||||
} from '../../contracts/runtime';
|
||||
|
||||
/**
|
||||
* 会员侧的纯展示文案。
|
||||
*
|
||||
* 价格、每期泥点、模型权限、并发上限、标题的唯一真相源是后端 `profile_membership_plan` 目录,
|
||||
* 这里只放后端没有、也不该由后端承担的市场表达(数量估算、推荐标签、省费口径)。
|
||||
*/
|
||||
export const RECOMMENDED_MEMBERSHIP_PLAN: ProfileMembershipPlan = 'plus';
|
||||
|
||||
/** 并发上限哨兵值:与后端 `MEMBERSHIP_UNLIMITED_CONCURRENCY` 对齐,`>= 128` 视为不设上限。 */
|
||||
export const UNLIMITED_CONCURRENT_JOB_LIMIT = 128;
|
||||
|
||||
type MembershipPlanCopy = {
|
||||
/** 每月预计可完成的数量估算。 */
|
||||
estimateCount: string;
|
||||
/** 数量估算对应的项目形态。 */
|
||||
estimateKind: string;
|
||||
/** 数量估算的补充说明。 */
|
||||
estimateAlt: string;
|
||||
};
|
||||
|
||||
const MEMBERSHIP_PLAN_COPY: Partial<
|
||||
Record<ProfileMembershipPlan, MembershipPlanCopy>
|
||||
> = {
|
||||
starter: {
|
||||
estimateCount: '1–2 个',
|
||||
estimateKind: '简单小游戏原型',
|
||||
estimateAlt: '先验证核心玩法与基础交互',
|
||||
},
|
||||
plus: {
|
||||
estimateCount: '2–4 个',
|
||||
estimateKind: '轻量微抖小游戏',
|
||||
estimateAlt: '按可玩版本估算',
|
||||
},
|
||||
pro: {
|
||||
estimateCount: '1–2 个',
|
||||
estimateKind: '中等项目 Demo',
|
||||
estimateAlt: '或约 7–14 个轻量小游戏',
|
||||
},
|
||||
max: {
|
||||
estimateCount: '1–2 个',
|
||||
estimateKind: '独立游戏可玩章节',
|
||||
estimateAlt: '或约 2–5 个中等项目 Demo',
|
||||
},
|
||||
};
|
||||
|
||||
const FALLBACK_PLAN_COPY: MembershipPlanCopy = {
|
||||
estimateCount: '按实际创作量',
|
||||
estimateKind: '以作品规模为准',
|
||||
estimateAlt: '具体消耗取决于创作复杂度',
|
||||
};
|
||||
|
||||
export function resolveMembershipPlanCopy(
|
||||
plan: ProfileMembershipPlan,
|
||||
): MembershipPlanCopy {
|
||||
return MEMBERSHIP_PLAN_COPY[plan] ?? FALLBACK_PLAN_COPY;
|
||||
}
|
||||
|
||||
export function resolveMembershipModelAccessLabel(
|
||||
modelAccess: ProfileMembershipModelAccess,
|
||||
): string {
|
||||
return modelAccess === 'full' ? '基础及高性能模型' : '基础模型';
|
||||
}
|
||||
|
||||
/** 套餐卡里的并发文案。 */
|
||||
export function resolveMembershipConcurrencyLabel(
|
||||
concurrentJobLimit: number,
|
||||
): string {
|
||||
return concurrentJobLimit >= UNLIMITED_CONCURRENT_JOB_LIMIT
|
||||
? '独立任务并发不设套餐上限'
|
||||
: `同时运行 ${concurrentJobLimit} 个独立任务`;
|
||||
}
|
||||
|
||||
/** 对比表里的并发短文案。 */
|
||||
export function resolveMembershipConcurrencyShortLabel(
|
||||
concurrentJobLimit: number,
|
||||
): string {
|
||||
return concurrentJobLimit >= UNLIMITED_CONCURRENT_JOB_LIMIT
|
||||
? '不设套餐上限'
|
||||
: `${concurrentJobLimit} 个`;
|
||||
}
|
||||
|
||||
export function isUnlimitedConcurrency(concurrentJobLimit: number): boolean {
|
||||
return concurrentJobLimit >= UNLIMITED_CONCURRENT_JOB_LIMIT;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
ProfileMembershipCycleKind,
|
||||
ProfileMembershipPlan,
|
||||
} from '../../contracts/runtime';
|
||||
|
||||
/**
|
||||
* 会员商品没有配置行,`product_id` 由「档位 + 周期」拼出,与后端
|
||||
* `parse_runtime_profile_membership_product_id` 双向对应。
|
||||
*/
|
||||
export function resolveMembershipProductId(
|
||||
plan: ProfileMembershipPlan,
|
||||
cycleKind: ProfileMembershipCycleKind,
|
||||
) {
|
||||
return `membership-${plan}-${cycleKind}`;
|
||||
}
|
||||
Reference in New Issue
Block a user