feat: 套餐有效期支持日/周/月单位,订阅履约改用兑换码流程,UI层次感优化

- Prisma: SubscriptionPlan 新增 validityUnit 字段 (day/week/month)
- 新增 subscription-utils.ts 计算实际天数及格式化显示
- Sub2API client createAndRedeem 支持 subscription 类型 (group_id, validity_days)
- 订阅履约从 assignSubscription 改为 createAndRedeem,在 Sub2API 留痕
- 订单创建动态计算天数(月单位按自然月差值)
- 管理后台表单支持有效期数值+单位下拉
- 前端 ChannelCard 渠道卡片视觉层次优化(模型标签渐变、倍率突出、闪电图标)
- 按量付费 banner 改为渐变背景+底部倍率说明标签
- 帮助/客服信息区块添加到充值、订阅、支付全流程页面
- 移除系统配置独立页面入口,subscriptions API 返回用户信息
This commit is contained in:
erio
2026-03-13 21:19:22 +08:00
parent 9096271307
commit 687336cfd8
16 changed files with 672 additions and 1027 deletions

View File

@@ -0,0 +1,90 @@
export type ValidityUnit = 'day' | 'week' | 'month';
/**
* 根据数值和单位计算实际有效天数。
* - day: 直接返回
* - week: value * 7
* - month: 从 fromDate 到 value 个月后同一天的天数差
*/
export function computeValidityDays(value: number, unit: ValidityUnit, fromDate?: Date): number {
if (unit === 'day') return value;
if (unit === 'week') return value * 7;
// month: 计算到 value 个月后同一天的天数差
const from = fromDate ?? new Date();
const target = new Date(from);
target.setMonth(target.getMonth() + value);
return Math.round((target.getTime() - from.getTime()) / (1000 * 60 * 60 * 24));
}
/**
* 智能格式化有效期显示文本。
* - unit=month, value=1 → 包月 / Monthly
* - unit=month, value=3 → 包3月 / 3 Months
* - unit=week, value=2 → 包2周 / 2 Weeks
* - unit=day, value=30 → 包月 / Monthly (特殊处理)
* - unit=day, value=90 → 包90天 / 90 Days
*/
export function formatValidityLabel(
value: number,
unit: ValidityUnit,
locale: 'zh' | 'en',
): string {
if (unit === 'month') {
if (value === 1) return locale === 'zh' ? '包月' : 'Monthly';
return locale === 'zh' ? `${value}` : `${value} Months`;
}
if (unit === 'week') {
if (value === 1) return locale === 'zh' ? '包周' : 'Weekly';
return locale === 'zh' ? `${value}` : `${value} Weeks`;
}
// day
if (value === 30) return locale === 'zh' ? '包月' : 'Monthly';
return locale === 'zh' ? `${value}` : `${value} Days`;
}
/**
* 智能格式化有效期后缀(用于价格展示)。
* - unit=month, value=1 → /月 / /mo
* - unit=month, value=3 → /3月 / /3mo
* - unit=week, value=2 → /2周 / /2wk
* - unit=day, value=30 → /月 / /mo
* - unit=day, value=90 → /90天 / /90d
*/
export function formatValiditySuffix(
value: number,
unit: ValidityUnit,
locale: 'zh' | 'en',
): string {
if (unit === 'month') {
if (value === 1) return locale === 'zh' ? '/月' : '/mo';
return locale === 'zh' ? `/${value}` : `/${value}mo`;
}
if (unit === 'week') {
if (value === 1) return locale === 'zh' ? '/周' : '/wk';
return locale === 'zh' ? `/${value}` : `/${value}wk`;
}
// day
if (value === 30) return locale === 'zh' ? '/月' : '/mo';
return locale === 'zh' ? `/${value}` : `/${value}d`;
}
/**
* 格式化有效期列表展示文本(管理后台表格用)。
* - unit=day → "30 天"
* - unit=week → "2 周"
* - unit=month → "1 月"
*/
export function formatValidityDisplay(
value: number,
unit: ValidityUnit,
locale: 'zh' | 'en',
): string {
const unitLabels: Record<ValidityUnit, { zh: string; en: string }> = {
day: { zh: '天', en: 'day(s)' },
week: { zh: '周', en: 'week(s)' },
month: { zh: '月', en: 'month(s)' },
};
const label = locale === 'zh' ? unitLabels[unit].zh : unitLabels[unit].en;
return `${value} ${label}`;
}