feat: 图片生成计费功能
- 新增 Group 图片价格配置(image_price_1k/2k/4k) - BillingService 新增 CalculateImageCost 方法 - AntigravityGatewayService 支持识别图片生成模型并按次计费 - UsageLog 新增 image_count 和 image_size 字段 - 前端分组管理支持配置图片价格(antigravity 和 gemini 平台) - 图片计费复用通用计费能力(余额检查、扣费、倍率、订阅限额)
This commit is contained in:
@@ -403,7 +403,8 @@ export default {
|
||||
exportExcelFailed: 'Failed to export usage data',
|
||||
billingType: 'Billing',
|
||||
balance: 'Balance',
|
||||
subscription: 'Subscription'
|
||||
subscription: 'Subscription',
|
||||
imageUnit: ' images'
|
||||
},
|
||||
|
||||
// Redeem
|
||||
@@ -811,6 +812,10 @@ export default {
|
||||
defaultValidityDays: 'Default Validity (Days)',
|
||||
validityHint: 'Number of days the subscription is valid when assigned to a user',
|
||||
noLimit: 'No limit'
|
||||
},
|
||||
imagePricing: {
|
||||
title: 'Image Generation Pricing',
|
||||
description: 'Configure pricing for gemini-3-pro-image model. Leave empty to use default prices.'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -399,7 +399,8 @@ export default {
|
||||
exportExcelFailed: '使用数据导出失败',
|
||||
billingType: '消费类型',
|
||||
balance: '余额',
|
||||
subscription: '订阅'
|
||||
subscription: '订阅',
|
||||
imageUnit: '张'
|
||||
},
|
||||
|
||||
// Redeem
|
||||
@@ -900,6 +901,10 @@ export default {
|
||||
defaultValidityDays: '默认有效期(天)',
|
||||
validityHint: '分配给用户时订阅的有效天数',
|
||||
noLimit: '无限制'
|
||||
},
|
||||
imagePricing: {
|
||||
title: '图片生成计费',
|
||||
description: '配置 gemini-3-pro-image 模型的图片生成价格,留空则使用默认价格'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -239,6 +239,10 @@ export interface Group {
|
||||
daily_limit_usd: number | null
|
||||
weekly_limit_usd: number | null
|
||||
monthly_limit_usd: number | null
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
image_price_1k: number | null
|
||||
image_price_2k: number | null
|
||||
image_price_4k: number | null
|
||||
account_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -537,6 +541,11 @@ export interface UsageLog {
|
||||
stream: boolean
|
||||
duration_ms: number
|
||||
first_token_ms: number | null
|
||||
|
||||
// 图片生成字段
|
||||
image_count: number
|
||||
image_size: string | null
|
||||
|
||||
created_at: string
|
||||
|
||||
user?: User
|
||||
|
||||
@@ -398,6 +398,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片生成计费配置(antigravity 和 gemini 平台) -->
|
||||
<div v-if="createForm.platform === 'antigravity' || createForm.platform === 'gemini'" class="border-t pt-4">
|
||||
<label class="block mb-2 font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.imagePricing.title') }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t('admin.groups.imagePricing.description') }}
|
||||
</p>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">1K ($)</label>
|
||||
<input
|
||||
v-model.number="createForm.image_price_1k"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.134"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">2K ($)</label>
|
||||
<input
|
||||
v-model.number="createForm.image_price_2k"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.134"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">4K ($)</label>
|
||||
<input
|
||||
v-model.number="createForm.image_price_4k"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.268"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
@@ -601,6 +646,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片生成计费配置(antigravity 和 gemini 平台) -->
|
||||
<div v-if="editForm.platform === 'antigravity' || editForm.platform === 'gemini'" class="border-t pt-4">
|
||||
<label class="block mb-2 font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.groups.imagePricing.title') }}
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t('admin.groups.imagePricing.description') }}
|
||||
</p>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">1K ($)</label>
|
||||
<input
|
||||
v-model.number="editForm.image_price_1k"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.134"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">2K ($)</label>
|
||||
<input
|
||||
v-model.number="editForm.image_price_2k"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.134"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">4K ($)</label>
|
||||
<input
|
||||
v-model.number="editForm.image_price_4k"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="input"
|
||||
placeholder="0.268"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
@@ -758,7 +848,11 @@ const createForm = reactive({
|
||||
subscription_type: 'standard' as SubscriptionType,
|
||||
daily_limit_usd: null as number | null,
|
||||
weekly_limit_usd: null as number | null,
|
||||
monthly_limit_usd: null as number | null
|
||||
monthly_limit_usd: null as number | null,
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
@@ -771,7 +865,11 @@ const editForm = reactive({
|
||||
subscription_type: 'standard' as SubscriptionType,
|
||||
daily_limit_usd: null as number | null,
|
||||
weekly_limit_usd: null as number | null,
|
||||
monthly_limit_usd: null as number | null
|
||||
monthly_limit_usd: null as number | null,
|
||||
// 图片生成计费配置(仅 antigravity 平台使用)
|
||||
image_price_1k: null as number | null,
|
||||
image_price_2k: null as number | null,
|
||||
image_price_4k: null as number | null
|
||||
})
|
||||
|
||||
// 根据分组类型返回不同的删除确认消息
|
||||
@@ -838,6 +936,9 @@ const closeCreateModal = () => {
|
||||
createForm.daily_limit_usd = null
|
||||
createForm.weekly_limit_usd = null
|
||||
createForm.monthly_limit_usd = null
|
||||
createForm.image_price_1k = null
|
||||
createForm.image_price_2k = null
|
||||
createForm.image_price_4k = null
|
||||
}
|
||||
|
||||
const handleCreateGroup = async () => {
|
||||
@@ -872,6 +973,9 @@ const handleEdit = (group: Group) => {
|
||||
editForm.daily_limit_usd = group.daily_limit_usd
|
||||
editForm.weekly_limit_usd = group.weekly_limit_usd
|
||||
editForm.monthly_limit_usd = group.monthly_limit_usd
|
||||
editForm.image_price_1k = group.image_price_1k
|
||||
editForm.image_price_2k = group.image_price_2k
|
||||
editForm.image_price_4k = group.image_price_4k
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -361,7 +361,26 @@
|
||||
</template>
|
||||
|
||||
<template #cell-tokens="{ row }">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- 图片生成请求 -->
|
||||
<div v-if="row.image_count > 0" class="flex items-center gap-1.5">
|
||||
<svg
|
||||
class="h-4 w-4 text-indigo-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ row.image_count }}{{ $t('usage.imageUnit') }}</span>
|
||||
<span class="text-gray-400">({{ row.image_size || '2K' }})</span>
|
||||
</div>
|
||||
<!-- Token 请求 -->
|
||||
<div v-else class="flex items-center gap-1.5">
|
||||
<div class="space-y-1.5 text-sm">
|
||||
<!-- Input / Output Tokens -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -219,7 +219,26 @@
|
||||
</template>
|
||||
|
||||
<template #cell-tokens="{ row }">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- 图片生成请求 -->
|
||||
<div v-if="row.image_count > 0" class="flex items-center gap-1.5">
|
||||
<svg
|
||||
class="h-4 w-4 text-indigo-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ row.image_count }}{{ $t('usage.imageUnit') }}</span>
|
||||
<span class="text-gray-400">({{ row.image_size || '2K' }})</span>
|
||||
</div>
|
||||
<!-- Token 请求 -->
|
||||
<div v-else class="flex items-center gap-1.5">
|
||||
<div class="space-y-1.5 text-sm">
|
||||
<!-- Input / Output Tokens -->
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user