mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-30 16:23:31 +00:00
Merge pull request #3775 from heathermhuang/codex/grok-media-pricing-labels
fix: add Grok video pricing controls
This commit is contained in:
@@ -294,7 +294,8 @@ const billingModeOptions = ref<SelectOption[]>([
|
||||
{ value: null, label: t('admin.usage.allBillingModes') },
|
||||
{ value: 'token', label: t('admin.usage.billingModeToken') },
|
||||
{ value: 'per_request', label: t('admin.usage.billingModePerRequest') },
|
||||
{ value: 'image', label: t('admin.usage.billingModeImage') }
|
||||
{ value: 'image', label: t('admin.usage.billingModeImage') },
|
||||
{ value: 'video', label: t('admin.usage.billingModeVideo') }
|
||||
])
|
||||
|
||||
const emitChange = () => emit('change')
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
billingModeToken: 'Per Token',
|
||||
billingModePerRequest: 'Per Request',
|
||||
billingModeImage: 'Per Image',
|
||||
billingModeVideo: 'Per Video',
|
||||
inputPrice: 'Input',
|
||||
outputPrice: 'Output',
|
||||
cacheWritePrice: 'Cache Write',
|
||||
|
||||
@@ -841,6 +841,28 @@ export default {
|
||||
finalPricePreview: 'Final per-image price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
mediaPricing: {
|
||||
title: 'Image / Video Generation Pricing',
|
||||
description:
|
||||
'Configure Grok image and video generation access plus base media prices. Leave empty to use default prices.',
|
||||
allowImageGeneration: 'Allow image and video generation for this group',
|
||||
independentMultiplier: 'Use independent media multiplier',
|
||||
imageMultiplier: 'Media multiplier',
|
||||
modeHint:
|
||||
'By default, Grok media billing uses media price × current effective group multiplier. Independent mode uses media price × media multiplier. One video generation is billed as one media unit.',
|
||||
finalPricePreview: 'Final per-media-unit price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
videoPricing: {
|
||||
title: 'Video Generation Pricing',
|
||||
description: 'Configure Grok video generation base prices. Leave empty to use default video prices.',
|
||||
independentMultiplier: 'Use independent video multiplier',
|
||||
videoMultiplier: 'Video multiplier',
|
||||
modeHint:
|
||||
'By default, video billing uses video price × current effective group multiplier. Independent mode uses video price × video multiplier.',
|
||||
finalPricePreview: 'Final per-video price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
peakRate: {
|
||||
enable: 'Enable peak rate multiplier',
|
||||
peakStart: 'Peak start',
|
||||
|
||||
@@ -477,6 +477,7 @@ export default {
|
||||
billingModeToken: 'Token',
|
||||
billingModePerRequest: 'Per Request',
|
||||
billingModeImage: 'Image',
|
||||
billingModeVideo: 'Video',
|
||||
allBillingModes: 'All Billing Modes',
|
||||
ipAddress: 'IP',
|
||||
clickToViewBalance: 'Click to view balance history',
|
||||
|
||||
@@ -478,6 +478,7 @@ export default {
|
||||
billingModeToken: 'Per Token',
|
||||
billingModePerRequest: 'Per Request',
|
||||
billingModeImage: 'Per Image',
|
||||
billingModeVideo: 'Per Video',
|
||||
inputPrice: 'Input',
|
||||
outputPrice: 'Output',
|
||||
cacheWritePrice: 'Cache Write',
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
billingModeToken: '按 Token',
|
||||
billingModePerRequest: '按次',
|
||||
billingModeImage: '按图片',
|
||||
billingModeVideo: '按视频',
|
||||
inputPrice: '输入',
|
||||
outputPrice: '输出',
|
||||
cacheWritePrice: '缓存写入',
|
||||
|
||||
@@ -919,6 +919,27 @@ export default {
|
||||
finalPricePreview: '最终单张价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
mediaPricing: {
|
||||
title: '图片/视频生成计费',
|
||||
description: '配置 Grok 图片和视频生成能力及媒体基础单价,留空则使用默认价格',
|
||||
allowImageGeneration: '允许当前分组生图和视频生成',
|
||||
independentMultiplier: '媒体倍率独立',
|
||||
imageMultiplier: '媒体独立倍率',
|
||||
modeHint:
|
||||
'默认关闭独立倍率时,Grok 媒体费用 = 媒体价格 × 当前分组有效倍率;开启独立倍率后,Grok 媒体费用 = 媒体价格 × 媒体独立倍率。一次视频生成按 1 个媒体单位计费。',
|
||||
finalPricePreview: '最终单次媒体价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
videoPricing: {
|
||||
title: '视频生成计费',
|
||||
description: '配置 Grok 视频生成基础单价,留空则使用默认视频价格',
|
||||
independentMultiplier: '视频倍率独立',
|
||||
videoMultiplier: '视频独立倍率',
|
||||
modeHint:
|
||||
'默认关闭独立倍率时,视频费用 = 视频价格 × 当前分组有效倍率;开启独立倍率后,视频费用 = 视频价格 × 视频独立倍率。',
|
||||
finalPricePreview: '最终单次视频价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
peakRate: {
|
||||
enable: '启用高峰倍率',
|
||||
peakStart: '高峰开始',
|
||||
|
||||
@@ -538,6 +538,7 @@ export default {
|
||||
billingModeToken: '按量',
|
||||
billingModePerRequest: '按次',
|
||||
billingModeImage: '按次(图片)',
|
||||
billingModeVideo: '按次(视频)',
|
||||
allBillingModes: '全部计费模式',
|
||||
ipAddress: 'IP',
|
||||
clickToViewBalance: '点击查看充值记录',
|
||||
|
||||
@@ -483,6 +483,7 @@ export default {
|
||||
billingModeToken: '按 Token',
|
||||
billingModePerRequest: '按次',
|
||||
billingModeImage: '按图片',
|
||||
billingModeVideo: '按视频',
|
||||
inputPrice: '输入',
|
||||
outputPrice: '输出',
|
||||
cacheWritePrice: '缓存写入',
|
||||
|
||||
@@ -526,6 +526,11 @@ export interface Group {
|
||||
image_price_1k: number | null
|
||||
image_price_2k: number | null
|
||||
image_price_4k: number | null
|
||||
video_rate_independent: boolean
|
||||
video_rate_multiplier: number
|
||||
video_price_480p: number | null
|
||||
video_price_720p: number | null
|
||||
video_price_1080p: number | null
|
||||
// 高峰时段倍率配置
|
||||
peak_rate_enabled: boolean
|
||||
peak_start: string
|
||||
@@ -654,6 +659,11 @@ export interface CreateGroupRequest {
|
||||
image_price_1k?: number | null
|
||||
image_price_2k?: number | null
|
||||
image_price_4k?: number | null
|
||||
video_rate_independent?: boolean
|
||||
video_rate_multiplier?: number
|
||||
video_price_480p?: number | null
|
||||
video_price_720p?: number | null
|
||||
video_price_1080p?: number | null
|
||||
peak_rate_enabled?: boolean
|
||||
peak_start?: string
|
||||
peak_end?: string
|
||||
@@ -696,6 +706,11 @@ export interface UpdateGroupRequest {
|
||||
image_price_1k?: number | null
|
||||
image_price_2k?: number | null
|
||||
image_price_4k?: number | null
|
||||
video_rate_independent?: boolean
|
||||
video_rate_multiplier?: number
|
||||
video_price_480p?: number | null
|
||||
video_price_720p?: number | null
|
||||
video_price_1080p?: number | null
|
||||
peak_rate_enabled?: boolean
|
||||
peak_start?: string
|
||||
peak_end?: string
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
export const BILLING_MODE_TOKEN = 'token'
|
||||
export const BILLING_MODE_PER_REQUEST = 'per_request'
|
||||
export const BILLING_MODE_IMAGE = 'image'
|
||||
export const BILLING_MODE_VIDEO = 'video'
|
||||
|
||||
export function getBillingModeLabel(mode: string | null | undefined, t: (key: string) => string): string {
|
||||
switch (mode) {
|
||||
case BILLING_MODE_PER_REQUEST: return t('admin.usage.billingModePerRequest')
|
||||
case BILLING_MODE_IMAGE: return t('admin.usage.billingModeImage')
|
||||
case BILLING_MODE_VIDEO: return t('admin.usage.billingModeVideo')
|
||||
default: return t('admin.usage.billingModeToken')
|
||||
}
|
||||
}
|
||||
@@ -14,6 +16,7 @@ export function getBillingModeBadgeClass(mode: string | null | undefined): strin
|
||||
switch (mode) {
|
||||
case BILLING_MODE_PER_REQUEST: return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
case BILLING_MODE_IMAGE: return 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300'
|
||||
case BILLING_MODE_VIDEO: return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||
default: return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
}
|
||||
}
|
||||
@@ -25,7 +28,7 @@ interface ImageBillingRow {
|
||||
}
|
||||
|
||||
export function isImageUsage(row: Pick<ImageBillingRow, 'image_count' | 'billing_mode'> | null | undefined): boolean {
|
||||
return (row?.image_count ?? 0) > 0 && row?.billing_mode !== BILLING_MODE_TOKEN
|
||||
return (row?.image_count ?? 0) > 0 && row?.billing_mode !== BILLING_MODE_TOKEN && row?.billing_mode !== BILLING_MODE_VIDEO
|
||||
}
|
||||
|
||||
export function getDisplayBillingMode(row: Pick<ImageBillingRow, 'billing_mode' | 'image_count'> | null | undefined): string | null | undefined {
|
||||
|
||||
@@ -825,10 +825,10 @@
|
||||
<label
|
||||
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.groups.imagePricing.title") }}
|
||||
{{ t(imagePricingI18nKey(createForm.platform, "title")) }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t("admin.groups.imagePricing.description") }}
|
||||
{{ t(imagePricingI18nKey(createForm.platform, "description")) }}
|
||||
</p>
|
||||
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
@@ -837,7 +837,7 @@
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t("admin.groups.imagePricing.allowImageGeneration") }}
|
||||
{{ t(imagePricingI18nKey(createForm.platform, "allowImageGeneration")) }}
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
@@ -845,7 +845,7 @@
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t("admin.groups.imagePricing.independentMultiplier") }}
|
||||
{{ t(imagePricingI18nKey(createForm.platform, "independentMultiplier")) }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -853,7 +853,7 @@
|
||||
class="mb-4"
|
||||
>
|
||||
<label class="input-label">{{
|
||||
t("admin.groups.imagePricing.imageMultiplier")
|
||||
t(imagePricingI18nKey(createForm.platform, "imageMultiplier"))
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="createForm.image_rate_multiplier"
|
||||
@@ -873,7 +873,7 @@
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.134"
|
||||
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_1k')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -884,7 +884,7 @@
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.201"
|
||||
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_2k')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -895,16 +895,16 @@
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.268"
|
||||
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_4k')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.groups.imagePricing.modeHint") }}
|
||||
{{ t(imagePricingI18nKey(createForm.platform, "modeHint")) }}
|
||||
</p>
|
||||
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t("admin.groups.imagePricing.finalPricePreview") }}
|
||||
{{ t(imagePricingI18nKey(createForm.platform, "finalPricePreview")) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
@@ -969,6 +969,98 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 视频生成计费配置(仅 Grok 平台) -->
|
||||
<div
|
||||
v-if="supportsVideoPricingPlatform(createForm.platform)"
|
||||
class="border-t pt-4"
|
||||
>
|
||||
<label
|
||||
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t(videoPricingI18nKey("title")) }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t(videoPricingI18nKey("description")) }}
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
v-model="createForm.video_rate_independent"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t(videoPricingI18nKey("independentMultiplier")) }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="createForm.video_rate_independent"
|
||||
class="mb-4"
|
||||
>
|
||||
<label class="input-label">{{
|
||||
t(videoPricingI18nKey("videoMultiplier"))
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="createForm.video_rate_multiplier"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">480p ($)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_480p"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_480p')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">720p ($)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_720p"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_720p')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">1080p ($)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_1080p"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_1080p')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t(videoPricingI18nKey("modeHint")) }}
|
||||
</p>
|
||||
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t(videoPricingI18nKey("finalPricePreview")) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
v-for="item in createVideoFinalPricePreview"
|
||||
:key="item.label"
|
||||
>
|
||||
{{ item.label }}: {{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
|
||||
<div v-if="createForm.subscription_type === 'subscription'" class="border-t pt-4">
|
||||
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
@@ -2212,10 +2304,10 @@
|
||||
<label
|
||||
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t("admin.groups.imagePricing.title") }}
|
||||
{{ t(imagePricingI18nKey(editForm.platform, "title")) }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t("admin.groups.imagePricing.description") }}
|
||||
{{ t(imagePricingI18nKey(editForm.platform, "description")) }}
|
||||
</p>
|
||||
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
@@ -2224,7 +2316,7 @@
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t("admin.groups.imagePricing.allowImageGeneration") }}
|
||||
{{ t(imagePricingI18nKey(editForm.platform, "allowImageGeneration")) }}
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
@@ -2232,7 +2324,7 @@
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t("admin.groups.imagePricing.independentMultiplier") }}
|
||||
{{ t(imagePricingI18nKey(editForm.platform, "independentMultiplier")) }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -2240,7 +2332,7 @@
|
||||
class="mb-4"
|
||||
>
|
||||
<label class="input-label">{{
|
||||
t("admin.groups.imagePricing.imageMultiplier")
|
||||
t(imagePricingI18nKey(editForm.platform, "imageMultiplier"))
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="editForm.image_rate_multiplier"
|
||||
@@ -2260,7 +2352,7 @@
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.134"
|
||||
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_1k')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2271,7 +2363,7 @@
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.201"
|
||||
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_2k')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2282,16 +2374,16 @@
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.268"
|
||||
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_4k')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t("admin.groups.imagePricing.modeHint") }}
|
||||
{{ t(imagePricingI18nKey(editForm.platform, "modeHint")) }}
|
||||
</p>
|
||||
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t("admin.groups.imagePricing.finalPricePreview") }}
|
||||
{{ t(imagePricingI18nKey(editForm.platform, "finalPricePreview")) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
@@ -2356,6 +2448,98 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 视频生成计费配置(仅 Grok 平台) -->
|
||||
<div
|
||||
v-if="supportsVideoPricingPlatform(editForm.platform)"
|
||||
class="border-t pt-4"
|
||||
>
|
||||
<label
|
||||
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ t(videoPricingI18nKey("title")) }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t(videoPricingI18nKey("description")) }}
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input
|
||||
v-model="editForm.video_rate_independent"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
{{ t(videoPricingI18nKey("independentMultiplier")) }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="editForm.video_rate_independent"
|
||||
class="mb-4"
|
||||
>
|
||||
<label class="input-label">{{
|
||||
t(videoPricingI18nKey("videoMultiplier"))
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="editForm.video_rate_multiplier"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">480p ($)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_480p"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_480p')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">720p ($)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_720p"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_720p')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">1080p ($)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_1080p"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_1080p')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t(videoPricingI18nKey("modeHint")) }}
|
||||
</p>
|
||||
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t(videoPricingI18nKey("finalPricePreview")) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div
|
||||
v-for="item in editVideoFinalPricePreview"
|
||||
:key="item.label"
|
||||
>
|
||||
{{ item.label }}: {{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
|
||||
<div v-if="editForm.subscription_type === 'subscription'" class="border-t pt-4">
|
||||
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
@@ -3342,7 +3526,16 @@ import {
|
||||
} from "./groupsModelsList";
|
||||
import { createModelsListCandidatesTracker } from "./groupsModelsListCandidates";
|
||||
import { normalizeSupportedModelScopesForPlatform } from "./groupsSupportedModelScopes";
|
||||
import { supportsImagePricingPlatform } from "./groupsImagePricing";
|
||||
import {
|
||||
getDefaultImagePreviewPrice,
|
||||
getDefaultVideoPreviewPrice,
|
||||
getImagePricePlaceholder,
|
||||
getVideoPricePlaceholder,
|
||||
imagePricingI18nKey,
|
||||
supportsImagePricingPlatform,
|
||||
supportsVideoPricingPlatform,
|
||||
videoPricingI18nKey,
|
||||
} from "./groupsImagePricing";
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
@@ -3689,6 +3882,12 @@ const createForm = reactive({
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null,
|
||||
// 视频生成计费配置(仅 Grok 平台)
|
||||
video_rate_independent: false,
|
||||
video_rate_multiplier: 1,
|
||||
video_price_480p: null as number | null,
|
||||
video_price_720p: null as number | null,
|
||||
video_price_1080p: null as number | null,
|
||||
// 高峰时段倍率配置
|
||||
peak_rate_enabled: false,
|
||||
peak_start: "",
|
||||
@@ -4028,6 +4227,12 @@ const editForm = reactive({
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null,
|
||||
// 视频生成计费配置(仅 Grok 平台)
|
||||
video_rate_independent: false,
|
||||
video_rate_multiplier: 1,
|
||||
video_price_480p: null as number | null,
|
||||
video_price_720p: null as number | null,
|
||||
video_price_1080p: null as number | null,
|
||||
// 高峰时段倍率配置
|
||||
peak_rate_enabled: false,
|
||||
peak_start: "",
|
||||
@@ -4077,12 +4282,28 @@ type ImagePricingFormState = {
|
||||
peak_rate_multiplier: number;
|
||||
};
|
||||
|
||||
type VideoPricingFormState = {
|
||||
platform: GroupPlatform;
|
||||
rate_multiplier: number;
|
||||
video_rate_independent: boolean;
|
||||
video_rate_multiplier: number;
|
||||
video_price_480p: number | string | null;
|
||||
video_price_720p: number | string | null;
|
||||
video_price_1080p: number | string | null;
|
||||
};
|
||||
|
||||
const imagePricingTiers = [
|
||||
{ key: "image_price_1k", label: "1K" },
|
||||
{ key: "image_price_2k", label: "2K" },
|
||||
{ key: "image_price_4k", label: "4K" },
|
||||
] as const;
|
||||
|
||||
const videoPricingTiers = [
|
||||
{ key: "video_price_480p", label: "480p" },
|
||||
{ key: "video_price_720p", label: "720p" },
|
||||
{ key: "video_price_1080p", label: "1080p" },
|
||||
] as const;
|
||||
|
||||
const normalizePreviewNumber = (value: number | string | null | undefined, fallback = 0) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return fallback;
|
||||
@@ -4091,6 +4312,14 @@ const normalizePreviewNumber = (value: number | string | null | undefined, fallb
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
};
|
||||
|
||||
const parsePreviewPrice = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const formatImagePricePreview = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return t("admin.groups.imagePricing.notConfigured");
|
||||
@@ -4102,28 +4331,64 @@ const formatImagePricePreview = (value: number | string | null | undefined) => {
|
||||
return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`;
|
||||
};
|
||||
|
||||
const formatVideoPricePreview = (value: number | string | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return t("admin.groups.videoPricing.notConfigured");
|
||||
}
|
||||
const price = Number(value);
|
||||
if (!Number.isFinite(price) || price < 0) {
|
||||
return t("admin.groups.videoPricing.notConfigured");
|
||||
}
|
||||
return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`;
|
||||
};
|
||||
|
||||
const buildImageFinalPricePreview = (form: ImagePricingFormState) => {
|
||||
const imageMultiplier = form.image_rate_independent
|
||||
? normalizePreviewNumber(form.image_rate_multiplier, 1)
|
||||
: normalizePreviewNumber(form.rate_multiplier, 1);
|
||||
const multiplier = imageMultiplier;
|
||||
return imagePricingTiers.map((tier) => {
|
||||
const basePrice = normalizePreviewNumber(form[tier.key]);
|
||||
const basePrice =
|
||||
parsePreviewPrice(form[tier.key]) ??
|
||||
getDefaultImagePreviewPrice(form.platform, tier.key);
|
||||
return {
|
||||
label: tier.label,
|
||||
value: basePrice > 0
|
||||
value: basePrice !== null
|
||||
? formatImagePricePreview(basePrice * multiplier)
|
||||
: t("admin.groups.imagePricing.notConfigured"),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const buildVideoFinalPricePreview = (form: VideoPricingFormState) => {
|
||||
const multiplier = form.video_rate_independent
|
||||
? normalizePreviewNumber(form.video_rate_multiplier, 1)
|
||||
: normalizePreviewNumber(form.rate_multiplier, 1);
|
||||
return videoPricingTiers.map((tier) => {
|
||||
const basePrice =
|
||||
parsePreviewPrice(form[tier.key]) ??
|
||||
getDefaultVideoPreviewPrice(form.platform, tier.key);
|
||||
return {
|
||||
label: tier.label,
|
||||
value: basePrice !== null
|
||||
? formatVideoPricePreview(basePrice * multiplier)
|
||||
: t("admin.groups.videoPricing.notConfigured"),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createImageFinalPricePreview = computed(() =>
|
||||
buildImageFinalPricePreview(createForm),
|
||||
);
|
||||
const editImageFinalPricePreview = computed(() =>
|
||||
buildImageFinalPricePreview(editForm),
|
||||
);
|
||||
const createVideoFinalPricePreview = computed(() =>
|
||||
buildVideoFinalPricePreview(createForm),
|
||||
);
|
||||
const editVideoFinalPricePreview = computed(() =>
|
||||
buildVideoFinalPricePreview(editForm),
|
||||
);
|
||||
|
||||
const resetDisabledBatchImagePricing = (
|
||||
form: Pick<
|
||||
@@ -4345,6 +4610,11 @@ const closeCreateModal = () => {
|
||||
createForm.image_price_1k = null;
|
||||
createForm.image_price_2k = null;
|
||||
createForm.image_price_4k = null;
|
||||
createForm.video_rate_independent = false;
|
||||
createForm.video_rate_multiplier = 1;
|
||||
createForm.video_price_480p = null;
|
||||
createForm.video_price_720p = null;
|
||||
createForm.video_price_1080p = null;
|
||||
createForm.peak_rate_enabled = false;
|
||||
createForm.peak_start = "";
|
||||
createForm.peak_end = "";
|
||||
@@ -4445,6 +4715,9 @@ const handleCreateGroup = async () => {
|
||||
requestData.batch_image_hold_multiplier = normalizeRateMultiplier(
|
||||
requestData.batch_image_hold_multiplier,
|
||||
);
|
||||
requestData.video_rate_multiplier = normalizeRateMultiplier(
|
||||
requestData.video_rate_multiplier,
|
||||
);
|
||||
requestData.peak_rate_enabled = createForm.peak_rate_enabled;
|
||||
requestData.peak_start = createForm.peak_start;
|
||||
requestData.peak_end = createForm.peak_end;
|
||||
@@ -4493,6 +4766,11 @@ const handleEdit = async (group: AdminGroup) => {
|
||||
editForm.image_price_1k = group.image_price_1k;
|
||||
editForm.image_price_2k = group.image_price_2k;
|
||||
editForm.image_price_4k = group.image_price_4k;
|
||||
editForm.video_rate_independent = group.video_rate_independent ?? false;
|
||||
editForm.video_rate_multiplier = group.video_rate_multiplier ?? 1;
|
||||
editForm.video_price_480p = group.video_price_480p;
|
||||
editForm.video_price_720p = group.video_price_720p;
|
||||
editForm.video_price_1080p = group.video_price_1080p;
|
||||
editForm.peak_rate_enabled = group.peak_rate_enabled ?? false;
|
||||
editForm.peak_start = group.peak_start ?? "";
|
||||
editForm.peak_end = group.peak_end ?? "";
|
||||
@@ -4545,6 +4823,11 @@ const closeEditModal = () => {
|
||||
editForm.peak_start = "";
|
||||
editForm.peak_end = "";
|
||||
editForm.peak_rate_multiplier = 1.0;
|
||||
editForm.video_rate_independent = false;
|
||||
editForm.video_rate_multiplier = 1;
|
||||
editForm.video_price_480p = null;
|
||||
editForm.video_price_720p = null;
|
||||
editForm.video_price_1080p = null;
|
||||
resetMessagesDispatchFormState(editForm);
|
||||
resetModelsListState(editModelsListState);
|
||||
};
|
||||
@@ -4610,6 +4893,9 @@ const handleUpdateGroup = async () => {
|
||||
payload.batch_image_hold_multiplier = normalizeRateMultiplier(
|
||||
payload.batch_image_hold_multiplier,
|
||||
);
|
||||
payload.video_rate_multiplier = normalizeRateMultiplier(
|
||||
payload.video_rate_multiplier,
|
||||
);
|
||||
payload.peak_rate_enabled = editForm.peak_rate_enabled;
|
||||
payload.peak_start = editForm.peak_start;
|
||||
payload.peak_end = editForm.peak_end;
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getDefaultImagePreviewPrice,
|
||||
getDefaultVideoPreviewPrice,
|
||||
getImagePricePlaceholder,
|
||||
getVideoPricePlaceholder,
|
||||
imagePricingPlatforms,
|
||||
imagePricingI18nKey,
|
||||
supportsImagePricingPlatform,
|
||||
supportsVideoPricingPlatform,
|
||||
videoPricingI18nKey,
|
||||
} from "../groupsImagePricing";
|
||||
|
||||
describe("groups image pricing platform support", () => {
|
||||
it("includes Grok media groups", () => {
|
||||
it("includes Grok image groups", () => {
|
||||
expect(supportsImagePricingPlatform("grok")).toBe(true);
|
||||
expect(imagePricingPlatforms.has("grok")).toBe(true);
|
||||
});
|
||||
|
||||
it("enables video pricing controls for Grok only", () => {
|
||||
expect(supportsVideoPricingPlatform("grok")).toBe(true);
|
||||
expect(supportsVideoPricingPlatform("openai")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps non-media group platforms out of the image pricing controls", () => {
|
||||
expect(supportsImagePricingPlatform("anthropic")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps image and video pricing copy separate", () => {
|
||||
expect(imagePricingI18nKey("grok", "title")).toBe(
|
||||
"admin.groups.imagePricing.title",
|
||||
);
|
||||
expect(videoPricingI18nKey("title")).toBe("admin.groups.videoPricing.title");
|
||||
});
|
||||
|
||||
it("uses Grok media defaults instead of generic image fallback placeholders", () => {
|
||||
expect(getImagePricePlaceholder("grok", "image_price_1k")).toBe("0.02");
|
||||
expect(getImagePricePlaceholder("grok", "image_price_2k")).toBe("0.02");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_480p")).toBe("0.08");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_720p")).toBe("0.14");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_1080p")).toBe("0.25");
|
||||
});
|
||||
|
||||
it("keeps non-Grok image placeholders on the generic image card", () => {
|
||||
expect(getImagePricePlaceholder("openai", "image_price_1k")).toBe("0.134");
|
||||
expect(getDefaultImagePreviewPrice("openai", "image_price_2k")).toBe(0.201);
|
||||
expect(getDefaultVideoPreviewPrice("openai", "video_price_480p")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,3 +7,85 @@ export const imagePricingPlatforms = new Set([
|
||||
|
||||
export const supportsImagePricingPlatform = (platform: string): boolean =>
|
||||
imagePricingPlatforms.has(platform);
|
||||
|
||||
export const supportsVideoPricingPlatform = (platform: string): boolean =>
|
||||
platform === "grok";
|
||||
|
||||
export const imagePricingI18nKey = (_platform: string, key: string): string =>
|
||||
`admin.groups.imagePricing.${key}`;
|
||||
|
||||
export const videoPricingI18nKey = (key: string): string =>
|
||||
`admin.groups.videoPricing.${key}`;
|
||||
|
||||
type ImagePricingTierKey = "image_price_1k" | "image_price_2k" | "image_price_4k";
|
||||
type VideoPricingTierKey =
|
||||
| "video_price_480p"
|
||||
| "video_price_720p"
|
||||
| "video_price_1080p";
|
||||
|
||||
const defaultImagePricePlaceholders: Record<
|
||||
string,
|
||||
Record<ImagePricingTierKey, string>
|
||||
> = {
|
||||
default: {
|
||||
image_price_1k: "0.134",
|
||||
image_price_2k: "0.201",
|
||||
image_price_4k: "0.268",
|
||||
},
|
||||
grok: {
|
||||
image_price_1k: "0.02",
|
||||
image_price_2k: "0.02",
|
||||
image_price_4k: "0.02",
|
||||
},
|
||||
};
|
||||
|
||||
const defaultVideoPricePlaceholders: Record<
|
||||
string,
|
||||
Record<VideoPricingTierKey, string>
|
||||
> = {
|
||||
grok: {
|
||||
video_price_480p: "0.08",
|
||||
video_price_720p: "0.14",
|
||||
video_price_1080p: "0.25",
|
||||
},
|
||||
};
|
||||
|
||||
export const getImagePricePlaceholder = (
|
||||
platform: string,
|
||||
tier: ImagePricingTierKey,
|
||||
): string => {
|
||||
const card = defaultImagePricePlaceholders[platform] ?? defaultImagePricePlaceholders.default;
|
||||
return card[tier];
|
||||
};
|
||||
|
||||
export const getVideoPricePlaceholder = (
|
||||
platform: string,
|
||||
tier: VideoPricingTierKey,
|
||||
): string => {
|
||||
const card = defaultVideoPricePlaceholders[platform];
|
||||
return card?.[tier] ?? "";
|
||||
};
|
||||
|
||||
export const getDefaultImagePreviewPrice = (
|
||||
platform: string,
|
||||
tier: ImagePricingTierKey,
|
||||
): number | null => {
|
||||
const placeholder = getImagePricePlaceholder(platform, tier);
|
||||
if (placeholder === "") {
|
||||
return null;
|
||||
}
|
||||
const value = Number(placeholder);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
};
|
||||
|
||||
export const getDefaultVideoPreviewPrice = (
|
||||
platform: string,
|
||||
tier: VideoPricingTierKey,
|
||||
): number | null => {
|
||||
const placeholder = getVideoPricePlaceholder(platform, tier);
|
||||
if (placeholder === "") {
|
||||
return null;
|
||||
}
|
||||
const value = Number(placeholder);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
};
|
||||
|
||||
@@ -389,6 +389,7 @@ const billingModeOptions = computed<SelectOption[]>(() => [
|
||||
{ value: 'token', label: t('admin.usage.billingModeToken') },
|
||||
{ value: 'per_request', label: t('admin.usage.billingModePerRequest') },
|
||||
{ value: 'image', label: t('admin.usage.billingModeImage') },
|
||||
{ value: 'video', label: t('admin.usage.billingModeVideo') },
|
||||
])
|
||||
|
||||
const apiKeys = ref<ApiKey[]>([])
|
||||
|
||||
Reference in New Issue
Block a user