mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix: 高峰倍率全链路透传、计费术语修正与边界处理
- 高峰倍率信息透传至可用渠道、支付计划、结算信息等 API,前端 GroupBadge / GroupOptionItem / SubscriptionPlanCard / PaymentView / SubscriptionsView 统一展示高峰时段与倍率标签 - 修正计费术语:"文本倍率" → "token 倍率",明确高峰倍率同时作用于 token 计费的图片 token,图片按次计费不受高峰影响 - 允许高峰倍率 multiplier=0,支持高峰时段免费策略 - 切换分组类型为 standard 时自动清除高峰倍率配置 - 长上下文计费与标准计费路径改用内部实现,移除冗余中间调用 - 前端高峰倍率相关控件文案改为 i18n - 新增多组高峰倍率相关单元测试
This commit is contained in:
@@ -14,6 +14,10 @@ export interface UserAvailableGroup {
|
||||
subscription_type: string
|
||||
/** 分组默认倍率。用户专属倍率(若有)通过 /groups/rates 获取后在前端 join。 */
|
||||
rate_multiplier: number
|
||||
peak_rate_enabled: boolean
|
||||
peak_start: string
|
||||
peak_end: string
|
||||
peak_rate_multiplier: number
|
||||
/** true = 专属分组(小范围授权);false = 公开分组。 */
|
||||
is_exclusive: boolean
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
:platform="key.group.platform"
|
||||
:subscription-type="key.group.subscription_type"
|
||||
:rate-multiplier="key.group.rate_multiplier"
|
||||
:peak-rate-enabled="key.group.peak_rate_enabled"
|
||||
:peak-start="key.group.peak_start"
|
||||
:peak-end="key.group.peak_end"
|
||||
:peak-rate-multiplier="key.group.peak_rate_multiplier"
|
||||
/>
|
||||
<span v-else class="text-gray-400 italic">{{ t('admin.users.none') }}</span>
|
||||
<svg v-if="updatingKeyIds.has(key.id)" class="h-3 w-3 animate-spin text-primary-500" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
@@ -88,6 +92,10 @@
|
||||
:platform="group.platform"
|
||||
:subscription-type="group.subscription_type"
|
||||
:rate-multiplier="group.rate_multiplier"
|
||||
:peak-rate-enabled="group.peak_rate_enabled"
|
||||
:peak-start="group.peak_start"
|
||||
:peak-end="group.peak_end"
|
||||
:peak-rate-multiplier="group.peak_rate_multiplier"
|
||||
:description="group.description"
|
||||
:selected="selectedKeyForGroup?.group_id === group.id"
|
||||
/>
|
||||
|
||||
@@ -85,16 +85,28 @@
|
||||
<Icon name="shield" size="xs" class="h-3 w-3" />
|
||||
{{ t('availableChannels.exclusive') }}
|
||||
</span>
|
||||
<GroupBadge
|
||||
<div
|
||||
v-for="g in exclusiveGroups(section)"
|
||||
:key="`ex-${g.id}`"
|
||||
:name="g.name"
|
||||
:platform="g.platform as GroupPlatform"
|
||||
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
||||
:rate-multiplier="g.rate_multiplier"
|
||||
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
||||
always-show-rate
|
||||
/>
|
||||
class="inline-flex flex-wrap items-center gap-1"
|
||||
>
|
||||
<GroupBadge
|
||||
:name="g.name"
|
||||
:platform="g.platform as GroupPlatform"
|
||||
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
||||
:rate-multiplier="g.rate_multiplier"
|
||||
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
||||
always-show-rate
|
||||
/>
|
||||
<span
|
||||
v-if="hasPeakRate(g)"
|
||||
class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
|
||||
:title="peakRateTitle(g)"
|
||||
>
|
||||
<Icon name="clock" size="xs" class="h-3 w-3" />
|
||||
{{ peakRateLabel(g) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="publicGroups(section).length > 0"
|
||||
@@ -107,16 +119,28 @@
|
||||
<Icon name="globe" size="xs" class="h-3 w-3" />
|
||||
{{ t('availableChannels.public') }}
|
||||
</span>
|
||||
<GroupBadge
|
||||
<div
|
||||
v-for="g in publicGroups(section)"
|
||||
:key="`pub-${g.id}`"
|
||||
:name="g.name"
|
||||
:platform="g.platform as GroupPlatform"
|
||||
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
||||
:rate-multiplier="g.rate_multiplier"
|
||||
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
||||
always-show-rate
|
||||
/>
|
||||
class="inline-flex flex-wrap items-center gap-1"
|
||||
>
|
||||
<GroupBadge
|
||||
:name="g.name"
|
||||
:platform="g.platform as GroupPlatform"
|
||||
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
|
||||
:rate-multiplier="g.rate_multiplier"
|
||||
:user-rate-multiplier="userGroupRates[g.id] ?? null"
|
||||
always-show-rate
|
||||
/>
|
||||
<span
|
||||
v-if="hasPeakRate(g)"
|
||||
class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
|
||||
:title="peakRateTitle(g)"
|
||||
>
|
||||
<Icon name="clock" size="xs" class="h-3 w-3" />
|
||||
{{ peakRateLabel(g) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="section.groups.length === 0" class="text-xs text-gray-400">-</span>
|
||||
</div>
|
||||
@@ -186,4 +210,16 @@ function exclusiveGroups(section: UserChannelPlatformSection): UserAvailableGrou
|
||||
function publicGroups(section: UserChannelPlatformSection): UserAvailableGroup[] {
|
||||
return section.groups.filter((g) => !g.is_exclusive)
|
||||
}
|
||||
|
||||
function hasPeakRate(group: UserAvailableGroup): boolean {
|
||||
return Boolean(group.peak_rate_enabled && group.peak_start && group.peak_end)
|
||||
}
|
||||
|
||||
function peakRateLabel(group: UserAvailableGroup): string {
|
||||
return `${group.peak_start}-${group.peak_end} ${group.peak_rate_multiplier}x`
|
||||
}
|
||||
|
||||
function peakRateTitle(group: UserAvailableGroup): string {
|
||||
return `高峰倍率:${group.peak_start}-${group.peak_end} ${group.peak_rate_multiplier}x;token 计费的图片 token 同样适用,图片按次计费不受高峰影响`
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
{{ labelText }}
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="hasPeakRate" :class="peakRateClass" :title="peakRateTitle">
|
||||
{{ peakRateText }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -35,6 +38,10 @@ interface Props {
|
||||
subscriptionType?: SubscriptionType
|
||||
rateMultiplier?: number
|
||||
userRateMultiplier?: number | null // 用户专属倍率
|
||||
peakRateEnabled?: boolean
|
||||
peakStart?: string
|
||||
peakEnd?: string
|
||||
peakRateMultiplier?: number
|
||||
showRate?: boolean
|
||||
daysRemaining?: number | null // 剩余天数(订阅类型时使用)
|
||||
/**
|
||||
@@ -50,6 +57,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showRate: true,
|
||||
daysRemaining: null,
|
||||
userRateMultiplier: null,
|
||||
peakRateEnabled: false,
|
||||
alwaysShowRate: false
|
||||
})
|
||||
|
||||
@@ -67,6 +75,18 @@ const hasCustomRate = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const hasPeakRate = computed(() => {
|
||||
return Boolean(props.showRate && props.peakRateEnabled && props.peakStart && props.peakEnd)
|
||||
})
|
||||
|
||||
const peakRateText = computed(() => {
|
||||
return `${props.peakStart}-${props.peakEnd} ×${props.peakRateMultiplier ?? 1}`
|
||||
})
|
||||
|
||||
const peakRateTitle = computed(() => {
|
||||
return `高峰倍率:${peakRateText.value}`
|
||||
})
|
||||
|
||||
// 是否显示右侧标签
|
||||
const showLabel = computed(() => {
|
||||
if (!props.showRate) return false
|
||||
@@ -127,6 +147,10 @@ const labelClass = computed(() => {
|
||||
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
|
||||
})
|
||||
|
||||
const peakRateClass = computed(() => {
|
||||
return 'px-1.5 py-0.5 rounded text-[10px] font-semibold bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
})
|
||||
|
||||
// Badge color based on platform and subscription type
|
||||
const badgeClass = computed(() => {
|
||||
if (props.platform === 'anthropic') {
|
||||
|
||||
@@ -24,16 +24,25 @@
|
||||
|
||||
<!-- Right: rate pill + checkmark (vertically centered to first row) -->
|
||||
<div class="flex shrink-0 items-center gap-2 pt-0.5">
|
||||
<!-- Rate pill (platform color) -->
|
||||
<span v-if="rateMultiplier !== undefined" :class="['inline-flex items-center whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold', ratePillClass]">
|
||||
<template v-if="hasCustomRate">
|
||||
<span class="mr-1 line-through opacity-50">{{ rateMultiplier }}x</span>
|
||||
<span class="font-bold">{{ userRateMultiplier }}x</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ rateMultiplier }}x {{ t('admin.groups.rateLabel') }}
|
||||
</template>
|
||||
</span>
|
||||
<div class="flex shrink-0 flex-col items-end gap-1">
|
||||
<!-- Rate pill (platform color) -->
|
||||
<span v-if="rateMultiplier !== undefined" :class="['inline-flex items-center whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold', ratePillClass]">
|
||||
<template v-if="hasCustomRate">
|
||||
<span class="mr-1 line-through opacity-50">{{ rateMultiplier }}x</span>
|
||||
<span class="font-bold">{{ userRateMultiplier }}x</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ rateMultiplier }}x {{ t('admin.groups.rateLabel') }}
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
v-if="hasPeakRate"
|
||||
class="inline-flex items-center whitespace-nowrap rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
|
||||
:title="peakRateTitle"
|
||||
>
|
||||
{{ peakRateText }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Checkmark -->
|
||||
<svg
|
||||
v-if="showCheckmark && selected"
|
||||
@@ -63,6 +72,10 @@ interface Props {
|
||||
subscriptionType?: SubscriptionType
|
||||
rateMultiplier?: number
|
||||
userRateMultiplier?: number | null
|
||||
peakRateEnabled?: boolean
|
||||
peakStart?: string
|
||||
peakEnd?: string
|
||||
peakRateMultiplier?: number
|
||||
description?: string | null
|
||||
selected?: boolean
|
||||
showCheckmark?: boolean
|
||||
@@ -72,7 +85,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
subscriptionType: 'standard',
|
||||
selected: false,
|
||||
showCheckmark: true,
|
||||
userRateMultiplier: null
|
||||
userRateMultiplier: null,
|
||||
peakRateEnabled: false
|
||||
})
|
||||
|
||||
// Whether user has a custom rate different from default
|
||||
@@ -85,6 +99,18 @@ const hasCustomRate = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const hasPeakRate = computed(() => {
|
||||
return Boolean(props.peakRateEnabled && props.peakStart && props.peakEnd)
|
||||
})
|
||||
|
||||
const peakRateText = computed(() => {
|
||||
return `${props.peakStart}-${props.peakEnd} ×${props.peakRateMultiplier ?? 1}`
|
||||
})
|
||||
|
||||
const peakRateTitle = computed(() => {
|
||||
return `高峰倍率:${peakRateText.value}`
|
||||
})
|
||||
|
||||
// Rate pill color matches platform badge color
|
||||
const ratePillClass = computed(() => {
|
||||
switch (props.platform) {
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.rate') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ rateDisplay }}</span>
|
||||
</div>
|
||||
<div v-if="hasPeakRate" class="col-span-2 flex items-center justify-between gap-2">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.peakRate') }}</span>
|
||||
<span class="text-right font-medium text-amber-700 dark:text-amber-300">{{ peakRateDisplay }}</span>
|
||||
</div>
|
||||
<div v-if="plan.daily_limit_usd != null" class="flex items-center justify-between">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.dailyLimit') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">${{ plan.daily_limit_usd }}</span>
|
||||
@@ -140,6 +144,14 @@ const rateDisplay = computed(() => {
|
||||
return `×${Number(rate.toPrecision(10))}`
|
||||
})
|
||||
|
||||
const hasPeakRate = computed(() => {
|
||||
return Boolean(props.plan.peak_rate_enabled && props.plan.peak_start && props.plan.peak_end)
|
||||
})
|
||||
|
||||
const peakRateDisplay = computed(() => {
|
||||
return `${props.plan.peak_start}-${props.plan.peak_end} ×${props.plan.peak_rate_multiplier ?? 1}`
|
||||
})
|
||||
|
||||
const MODEL_SCOPE_LABELS: Record<string, string> = {
|
||||
claude: 'Claude',
|
||||
gemini_text: 'Gemini',
|
||||
|
||||
@@ -2279,6 +2279,13 @@ export default {
|
||||
finalPricePreview: 'Final per-image price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
peakRate: {
|
||||
enable: 'Enable peak rate multiplier',
|
||||
peakStart: 'Peak start',
|
||||
peakEnd: 'Peak end',
|
||||
peakMultiplier: 'Peak multiplier',
|
||||
multiplierHint: 'Applies to token billing multiplier; image tokens in token billing are also affected. 0 means peak token requests are billed at 0x.'
|
||||
},
|
||||
modelsList: {
|
||||
title: 'Custom /v1/models Model List',
|
||||
hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.',
|
||||
@@ -7264,6 +7271,7 @@ export default {
|
||||
planFeatures: 'Features',
|
||||
planCard: {
|
||||
rate: 'Rate',
|
||||
peakRate: 'Peak Rate',
|
||||
dailyLimit: 'Daily',
|
||||
weeklyLimit: 'Weekly',
|
||||
monthlyLimit: 'Monthly',
|
||||
|
||||
@@ -2361,6 +2361,13 @@ export default {
|
||||
finalPricePreview: '最终单张价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
peakRate: {
|
||||
enable: '启用高峰倍率',
|
||||
peakStart: '高峰开始',
|
||||
peakEnd: '高峰结束',
|
||||
peakMultiplier: '高峰倍率',
|
||||
multiplierHint: '作用于 token 计费倍率;token 计费的图片 token 同样适用,0 表示高峰 token 请求按 0 倍计费'
|
||||
},
|
||||
modelsList: {
|
||||
title: '自定义 /v1/models 模型列表',
|
||||
hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。',
|
||||
@@ -7444,6 +7451,7 @@ export default {
|
||||
planFeatures: '功能特性',
|
||||
planCard: {
|
||||
rate: '倍率',
|
||||
peakRate: '高峰倍率',
|
||||
dailyLimit: '日限额',
|
||||
weeklyLimit: '周限额',
|
||||
monthlyLimit: '月限额',
|
||||
|
||||
@@ -108,6 +108,10 @@ export interface SubscriptionPlan {
|
||||
group_platform?: string
|
||||
group_name?: string
|
||||
rate_multiplier?: number
|
||||
peak_rate_enabled?: boolean
|
||||
peak_start?: string
|
||||
peak_end?: string
|
||||
peak_rate_multiplier?: number
|
||||
daily_limit_usd?: number | null
|
||||
weekly_limit_usd?: number | null
|
||||
monthly_limit_usd?: number | null
|
||||
|
||||
@@ -866,7 +866,7 @@
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>启用高峰时段倍率</span>
|
||||
<span>{{ t("admin.groups.peakRate.enable") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -874,7 +874,7 @@
|
||||
class="mb-4 grid grid-cols-3 gap-3"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label">高峰开始</label>
|
||||
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
|
||||
<input
|
||||
v-model="createForm.peak_start"
|
||||
type="time"
|
||||
@@ -882,7 +882,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">高峰结束</label>
|
||||
<label class="input-label">{{ t("admin.groups.peakRate.peakEnd") }}</label>
|
||||
<input
|
||||
v-model="createForm.peak_end"
|
||||
type="time"
|
||||
@@ -890,7 +890,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">高峰倍率</label>
|
||||
<label class="input-label">{{ t("admin.groups.peakRate.peakMultiplier") }}</label>
|
||||
<input
|
||||
v-model.number="createForm.peak_rate_multiplier"
|
||||
type="number"
|
||||
@@ -898,6 +898,7 @@
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="1"
|
||||
:title="t('admin.groups.peakRate.multiplierHint')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2204,7 +2205,7 @@
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>启用高峰时段倍率</span>
|
||||
<span>{{ t("admin.groups.peakRate.enable") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -2212,7 +2213,7 @@
|
||||
class="mb-4 grid grid-cols-3 gap-3"
|
||||
>
|
||||
<div>
|
||||
<label class="input-label">高峰开始</label>
|
||||
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
|
||||
<input
|
||||
v-model="editForm.peak_start"
|
||||
type="time"
|
||||
@@ -2220,7 +2221,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">高峰结束</label>
|
||||
<label class="input-label">{{ t("admin.groups.peakRate.peakEnd") }}</label>
|
||||
<input
|
||||
v-model="editForm.peak_end"
|
||||
type="time"
|
||||
@@ -2228,7 +2229,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">高峰倍率</label>
|
||||
<label class="input-label">{{ t("admin.groups.peakRate.peakMultiplier") }}</label>
|
||||
<input
|
||||
v-model.number="editForm.peak_rate_multiplier"
|
||||
type="number"
|
||||
@@ -2236,6 +2237,7 @@
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="1"
|
||||
:title="t('admin.groups.peakRate.multiplierHint')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,6 +144,10 @@
|
||||
:subscription-type="row.group.subscription_type"
|
||||
:rate-multiplier="row.group.rate_multiplier"
|
||||
:user-rate-multiplier="userGroupRates[row.group.id]"
|
||||
:peak-rate-enabled="row.group.peak_rate_enabled"
|
||||
:peak-start="row.group.peak_start"
|
||||
:peak-end="row.group.peak_end"
|
||||
:peak-rate-multiplier="row.group.peak_rate_multiplier"
|
||||
/>
|
||||
<span v-else class="text-sm text-gray-400 dark:text-dark-500">{{
|
||||
t('keys.noGroup')
|
||||
@@ -454,6 +458,10 @@
|
||||
:subscription-type="(option as unknown as GroupOption).subscriptionType"
|
||||
:rate-multiplier="(option as unknown as GroupOption).rate"
|
||||
:user-rate-multiplier="(option as unknown as GroupOption).userRate"
|
||||
:peak-rate-enabled="(option as unknown as GroupOption).peakRateEnabled"
|
||||
:peak-start="(option as unknown as GroupOption).peakStart"
|
||||
:peak-end="(option as unknown as GroupOption).peakEnd"
|
||||
:peak-rate-multiplier="(option as unknown as GroupOption).peakRateMultiplier"
|
||||
/>
|
||||
<span v-else class="text-gray-400">{{ t('keys.selectGroup') }}</span>
|
||||
</template>
|
||||
@@ -464,6 +472,10 @@
|
||||
:subscription-type="(option as unknown as GroupOption).subscriptionType"
|
||||
:rate-multiplier="(option as unknown as GroupOption).rate"
|
||||
:user-rate-multiplier="(option as unknown as GroupOption).userRate"
|
||||
:peak-rate-enabled="(option as unknown as GroupOption).peakRateEnabled"
|
||||
:peak-start="(option as unknown as GroupOption).peakStart"
|
||||
:peak-end="(option as unknown as GroupOption).peakEnd"
|
||||
:peak-rate-multiplier="(option as unknown as GroupOption).peakRateMultiplier"
|
||||
:description="(option as unknown as GroupOption).description"
|
||||
:selected="selected"
|
||||
/>
|
||||
@@ -1059,6 +1071,10 @@
|
||||
:subscription-type="option.subscriptionType"
|
||||
:rate-multiplier="option.rate"
|
||||
:user-rate-multiplier="option.userRate"
|
||||
:peak-rate-enabled="option.peakRateEnabled"
|
||||
:peak-start="option.peakStart"
|
||||
:peak-end="option.peakEnd"
|
||||
:peak-rate-multiplier="option.peakRateMultiplier"
|
||||
:description="option.description"
|
||||
:selected="
|
||||
selectedKeyForGroup?.group_id === option.value ||
|
||||
@@ -1123,6 +1139,10 @@ interface GroupOption {
|
||||
description: string | null
|
||||
rate: number
|
||||
userRate: number | null
|
||||
peakRateEnabled: boolean
|
||||
peakStart: string
|
||||
peakEnd: string
|
||||
peakRateMultiplier: number
|
||||
subscriptionType: SubscriptionType
|
||||
platform: GroupPlatform
|
||||
}
|
||||
@@ -1351,6 +1371,10 @@ const groupOptions = computed(() =>
|
||||
description: group.description,
|
||||
rate: group.rate_multiplier,
|
||||
userRate: userGroupRates.value[group.id] ?? null,
|
||||
peakRateEnabled: group.peak_rate_enabled,
|
||||
peakStart: group.peak_start,
|
||||
peakEnd: group.peak_end,
|
||||
peakRateMultiplier: group.peak_rate_multiplier,
|
||||
subscriptionType: group.subscription_type,
|
||||
platform: group.platform
|
||||
}))
|
||||
|
||||
@@ -121,6 +121,12 @@
|
||||
<span :class="['text-lg font-bold', planTextClass]">×{{ selectedPlan.rate_multiplier ?? 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="planHasPeakRate(selectedPlan)">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('payment.planCard.peakRate') }}</span>
|
||||
<div class="text-sm font-semibold text-amber-700 dark:text-amber-300">
|
||||
{{ planPeakRateLabel(selectedPlan) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedPlan.daily_limit_usd != null">
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('payment.planCard.dailyLimit') }}</span>
|
||||
<div class="text-lg font-semibold text-gray-800 dark:text-gray-200">${{ selectedPlan.daily_limit_usd }}</div>
|
||||
@@ -194,6 +200,7 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-3 text-[11px] text-gray-400 dark:text-gray-500">
|
||||
<span>{{ t('payment.planCard.rate') }}: ×{{ sub.group?.rate_multiplier ?? 1 }}</span>
|
||||
<span v-if="subscriptionHasPeakRate(sub)">{{ t('payment.planCard.peakRate') }}: {{ subscriptionPeakRateLabel(sub) }}</span>
|
||||
<span v-if="sub.group?.daily_limit_usd == null && sub.group?.weekly_limit_usd == null && sub.group?.monthly_limit_usd == null">{{ t('payment.planCard.quota') }}: {{ t('payment.planCard.unlimited') }}</span>
|
||||
<span v-if="sub.expires_at">{{ t('userSubscriptions.daysRemaining', { days: getDaysRemaining(sub.expires_at) }) }}</span>
|
||||
<span v-else>{{ t('userSubscriptions.noExpiration') }}</span>
|
||||
@@ -297,6 +304,16 @@ function getDaysRemaining(expiresAt: string): number {
|
||||
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)))
|
||||
}
|
||||
|
||||
function subscriptionHasPeakRate(sub: { group?: { peak_rate_enabled?: boolean; peak_start?: string; peak_end?: string } | null }): boolean {
|
||||
const group = sub.group
|
||||
return Boolean(group?.peak_rate_enabled && group.peak_start && group.peak_end)
|
||||
}
|
||||
|
||||
function subscriptionPeakRateLabel(sub: { group?: { peak_start?: string; peak_end?: string; peak_rate_multiplier?: number } | null }): string {
|
||||
const group = sub.group
|
||||
return `${group?.peak_start}-${group?.peak_end} ×${group?.peak_rate_multiplier ?? 1}`
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const submitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
@@ -695,6 +712,14 @@ const planValiditySuffix = computed(() => {
|
||||
return `${selectedPlan.value.validity_days}${t('payment.days')}`
|
||||
})
|
||||
|
||||
function planHasPeakRate(plan: SubscriptionPlan): boolean {
|
||||
return Boolean(plan.peak_rate_enabled && plan.peak_start && plan.peak_end)
|
||||
}
|
||||
|
||||
function planPeakRateLabel(plan: SubscriptionPlan): string {
|
||||
return `${plan.peak_start}-${plan.peak_end} ×${plan.peak_rate_multiplier ?? 1}`
|
||||
}
|
||||
|
||||
function selectPlan(plan: SubscriptionPlan) {
|
||||
selectedPlan.value = plan
|
||||
errorMessage.value = ''
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
<p v-if="subscription.group?.description" class="mt-0.5 text-xs text-gray-500 dark:text-dark-400">
|
||||
{{ subscription.group.description }}
|
||||
</p>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-gray-400 dark:text-gray-500">
|
||||
<span>{{ t('payment.planCard.rate') }}: ×{{ subscription.group?.rate_multiplier ?? 1 }}</span>
|
||||
<span v-if="subscriptionHasPeakRate(subscription)" class="text-amber-700 dark:text-amber-300">
|
||||
{{ t('payment.planCard.peakRate') }}: {{ subscriptionPeakRateLabel(subscription) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -271,6 +277,16 @@ const appStore = useAppStore()
|
||||
const subscriptions = ref<UserSubscription[]>([])
|
||||
const loading = ref(true)
|
||||
|
||||
function subscriptionHasPeakRate(subscription: UserSubscription): boolean {
|
||||
const group = subscription.group
|
||||
return Boolean(group?.peak_rate_enabled && group.peak_start && group.peak_end)
|
||||
}
|
||||
|
||||
function subscriptionPeakRateLabel(subscription: UserSubscription): string {
|
||||
const group = subscription.group
|
||||
return `${group?.peak_start}-${group?.peak_end} ×${group?.peak_rate_multiplier ?? 1}`
|
||||
}
|
||||
|
||||
async function loadSubscriptions() {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
Reference in New Issue
Block a user