mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(billing): add image output range pricing support
This commit is contained in:
@@ -18,11 +18,20 @@ export interface PricingTier {
|
||||
cache_ttl_pricing?: CacheTTLPricing[]
|
||||
}
|
||||
|
||||
export type ImageOutputQuality = 'low' | 'medium' | 'high'
|
||||
|
||||
export interface ImageOutputPriceRange {
|
||||
up_to_pixels: number | null
|
||||
prices: Partial<Record<ImageOutputQuality, number>>
|
||||
label?: string | null
|
||||
}
|
||||
|
||||
/** 阶梯计费配置 */
|
||||
export interface TieredPricingConfig {
|
||||
tiers: PricingTier[]
|
||||
image_output_prices?: Record<string, Record<string, number>> | null
|
||||
image_output_price_default?: number | null
|
||||
image_output_price_ranges?: ImageOutputPriceRange[] | null
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
|
||||
@@ -948,9 +948,16 @@ async function handleSubmit() {
|
||||
function tieredPricingHasImageOutputPricing(pricing: TieredPricingConfig | null | undefined): boolean {
|
||||
if (!pricing) return false
|
||||
if (toFinitePrice(pricing.image_output_price_default) !== null) return true
|
||||
return Object.values(pricing.image_output_prices || {}).some((prices) => {
|
||||
if (Object.values(pricing.image_output_prices || {}).some((prices) => {
|
||||
if (!prices || typeof prices !== 'object') return false
|
||||
return Object.values(prices).some((price) => toFinitePrice(price) !== null)
|
||||
})) return true
|
||||
return (pricing.image_output_price_ranges || []).some((range) => {
|
||||
if (!range || typeof range !== 'object') return false
|
||||
const prices = range.prices && typeof range.prices === 'object'
|
||||
? range.prices
|
||||
: range as Record<string, unknown>
|
||||
return Object.values(prices).some((price) => toFinitePrice(price) !== null)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,13 @@
|
||||
>
|
||||
矩阵
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="imagePriceRangeEntries.length > 0"
|
||||
variant="outline"
|
||||
class="text-[10px] h-5 px-1.5"
|
||||
>
|
||||
区间
|
||||
</Badge>
|
||||
</div>
|
||||
<span
|
||||
v-if="imageOutputDefaultPrice !== null"
|
||||
@@ -206,6 +213,45 @@
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div
|
||||
v-if="imagePriceRangeEntries.length > 0"
|
||||
class="border rounded-lg overflow-hidden"
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow class="bg-muted/30">
|
||||
<TableHead class="text-xs h-9">
|
||||
上限像素
|
||||
</TableHead>
|
||||
<TableHead
|
||||
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
||||
:key="quality"
|
||||
class="text-xs h-9 text-right"
|
||||
>
|
||||
{{ quality }}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="entry in imagePriceRangeEntries"
|
||||
:key="entry.key"
|
||||
class="text-xs"
|
||||
>
|
||||
<TableCell class="py-2 font-mono">
|
||||
{{ formatPixelLimit(entry.upToPixels) }}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
||||
:key="`${entry.key}-${quality}`"
|
||||
class="py-2 text-right font-mono"
|
||||
>
|
||||
{{ formatImagePrice(entry.prices[quality]) }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 单阶梯(固定价格)展示 -->
|
||||
@@ -640,8 +686,26 @@ const imagePricingEntries = computed(() => {
|
||||
})).filter(entry => Object.values(entry.prices).some(price => price !== null))
|
||||
})
|
||||
|
||||
const imagePriceRangeEntries = computed(() => {
|
||||
const ranges = props.model?.default_tiered_pricing?.image_output_price_ranges
|
||||
if (!Array.isArray(ranges)) return []
|
||||
return ranges.map((range, index) => {
|
||||
const object = range && typeof range === 'object' ? range as Record<string, unknown> : {}
|
||||
const rawPrices = object.prices && typeof object.prices === 'object'
|
||||
? object.prices
|
||||
: object
|
||||
return {
|
||||
key: `${object.up_to_pixels ?? 'unbounded'}-${index}`,
|
||||
upToPixels: toFiniteNumber(object.up_to_pixels),
|
||||
prices: normalizeImageQualityPrices(rawPrices),
|
||||
}
|
||||
}).filter(entry => Object.values(entry.prices).some(price => price !== null))
|
||||
})
|
||||
|
||||
const hasImagePricing = computed(() =>
|
||||
imageOutputDefaultPrice.value !== null || imagePricingEntries.value.length > 0,
|
||||
imageOutputDefaultPrice.value !== null
|
||||
|| imagePricingEntries.value.length > 0
|
||||
|| imagePriceRangeEntries.value.length > 0,
|
||||
)
|
||||
|
||||
function normalizeImageQualityPrices(value: unknown): Record<typeof IMAGE_OUTPUT_QUALITIES[number], number | null> {
|
||||
@@ -665,6 +729,20 @@ function formatImageSize(value: string): string {
|
||||
return value.replace(/\s*[xX×]\s*/g, ' x ')
|
||||
}
|
||||
|
||||
function formatPixelLimit(value: number | null): string {
|
||||
return value === null ? '无上限' : `<= ${formatPixels(value)}`
|
||||
}
|
||||
|
||||
function formatPixels(value: number): string {
|
||||
if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(value % 1_000_000 === 0 ? 0 : 2)}M px`
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(0)}K px`
|
||||
}
|
||||
return `${value} px`
|
||||
}
|
||||
|
||||
const detailTab = ref('basic')
|
||||
|
||||
// 处理背景点击
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
class="rounded-lg border bg-muted/10 p-3 space-y-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<Label class="text-xs font-medium">图像输出矩阵 ($/张)</Label>
|
||||
<Label class="text-xs font-medium">图像输出计费 ($/张)</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Label class="text-xs text-muted-foreground">默认价</Label>
|
||||
<Input
|
||||
@@ -158,6 +158,10 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label class="text-xs text-muted-foreground">精确分辨率覆盖</Label>
|
||||
<span class="text-[11px] text-muted-foreground">优先匹配 size + quality</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[minmax(120px,1.1fr)_repeat(3,minmax(0,1fr))_32px] gap-2 text-xs text-muted-foreground">
|
||||
<span>分辨率</span>
|
||||
<span>low</span>
|
||||
@@ -208,6 +212,64 @@
|
||||
添加分辨率
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 border-t pt-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Label class="text-xs text-muted-foreground">像素区间</Label>
|
||||
<span class="text-[11px] text-muted-foreground">矩阵未命中时按宽×高落档</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[minmax(120px,1.1fr)_repeat(3,minmax(0,1fr))_32px] gap-2 text-xs text-muted-foreground">
|
||||
<span>上限像素</span>
|
||||
<span>low</span>
|
||||
<span>medium</span>
|
||||
<span>high</span>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
v-for="row in imageOutputPriceRangeRows"
|
||||
:key="row.id"
|
||||
class="grid grid-cols-[minmax(120px,1.1fr)_repeat(3,minmax(0,1fr))_32px] gap-2 items-center"
|
||||
>
|
||||
<Input
|
||||
:model-value="row.upToPixels"
|
||||
type="number"
|
||||
min="1"
|
||||
class="h-8 font-mono text-xs"
|
||||
placeholder="空=无上限"
|
||||
@update:model-value="(v) => updateImageOutputRangeLimit(row.id, v)"
|
||||
/>
|
||||
<Input
|
||||
v-for="quality in IMAGE_OUTPUT_QUALITIES"
|
||||
:key="`${row.id}-${quality}`"
|
||||
:model-value="getImageOutputRangePrice(row, quality)"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
class="h-8"
|
||||
placeholder="0"
|
||||
@update:model-value="(v) => updateImageOutputRangePrice(row.id, quality, v)"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
@click="removeImageOutputRangeRow(row.id)"
|
||||
>
|
||||
<X class="w-4 h-4 text-muted-foreground hover:text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
@click="addImageOutputRangeRow"
|
||||
>
|
||||
<Plus class="w-4 h-4 mr-2" />
|
||||
添加像素区间
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证提示 -->
|
||||
@@ -224,7 +286,7 @@
|
||||
import { ref, computed, watch, reactive } from 'vue'
|
||||
import { Plus, X } from 'lucide-vue-next'
|
||||
import { Button, Input, Label } from '@/components/ui'
|
||||
import type { TieredPricingConfig, PricingTier } from '@/api/endpoints/types'
|
||||
import type { TieredPricingConfig, PricingTier, ImageOutputPriceRange } from '@/api/endpoints/types'
|
||||
|
||||
type ImageOutputQuality = 'low' | 'medium' | 'high'
|
||||
type ImageOutputPriceRow = {
|
||||
@@ -232,8 +294,14 @@ type ImageOutputPriceRow = {
|
||||
size: string
|
||||
prices: Partial<Record<ImageOutputQuality, number>>
|
||||
}
|
||||
type ImageOutputPriceRangeRow = {
|
||||
id: string
|
||||
upToPixels: string
|
||||
prices: Partial<Record<ImageOutputQuality, number>>
|
||||
}
|
||||
|
||||
const DEFAULT_IMAGE_OUTPUT_SIZES = ['1024x1024', '1536x1024', '1024x1536']
|
||||
const DEFAULT_IMAGE_OUTPUT_PIXEL_LIMITS = [1_048_576, 1_572_864, 2_097_152]
|
||||
const IMAGE_OUTPUT_QUALITIES: ImageOutputQuality[] = ['low', 'medium', 'high']
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -249,9 +317,11 @@ const emit = defineEmits<{
|
||||
// 本地状态
|
||||
const localTiers = ref<PricingTier[]>([])
|
||||
const imageOutputPriceRows = ref<ImageOutputPriceRow[]>([])
|
||||
const imageOutputPriceRangeRows = ref<ImageOutputPriceRangeRow[]>([])
|
||||
const imageOutputPriceDefault = ref<string>('')
|
||||
const lastEmittedPricingJson = ref<string>('')
|
||||
let imageOutputPriceRowId = 0
|
||||
let imageOutputPriceRangeRowId = 0
|
||||
|
||||
// 跟踪每个阶梯的缓存价格是否被手动设置
|
||||
const cacheManuallySet = reactive<Record<number, { creation: boolean; read: boolean; cache1h: boolean }>>({})
|
||||
@@ -280,6 +350,7 @@ watch(
|
||||
if (newValue?.tiers) {
|
||||
localTiers.value = newValue.tiers.map(t => ({ ...t }))
|
||||
imageOutputPriceRows.value = createImageOutputPriceRows(newValue.image_output_prices)
|
||||
imageOutputPriceRangeRows.value = createImageOutputPriceRangeRows(newValue.image_output_price_ranges)
|
||||
imageOutputPriceDefault.value = newValue.image_output_price_default != null
|
||||
? String(newValue.image_output_price_default)
|
||||
: ''
|
||||
@@ -299,6 +370,7 @@ watch(
|
||||
output_price_per_1m: 0,
|
||||
}]
|
||||
imageOutputPriceRows.value = createImageOutputPriceRows(null)
|
||||
imageOutputPriceRangeRows.value = createImageOutputPriceRangeRows(null)
|
||||
imageOutputPriceDefault.value = ''
|
||||
cacheManuallySet[0] = { creation: false, read: false, cache1h: false }
|
||||
}
|
||||
@@ -524,6 +596,10 @@ function buildPricingConfig(tiers: PricingTier[]): TieredPricingConfig {
|
||||
if (Object.keys(matrix).length > 0) {
|
||||
config.image_output_prices = matrix
|
||||
}
|
||||
const ranges = normalizedImageOutputPriceRanges()
|
||||
if (ranges.length > 0) {
|
||||
config.image_output_price_ranges = ranges
|
||||
}
|
||||
const defaultPrice = parseOptionalFloat(imageOutputPriceDefault.value)
|
||||
if (defaultPrice != null) {
|
||||
config.image_output_price_default = defaultPrice
|
||||
@@ -551,6 +627,31 @@ function createImageOutputPriceRows(value: TieredPricingConfig['image_output_pri
|
||||
return DEFAULT_IMAGE_OUTPUT_SIZES.map(size => createImageOutputPriceRow(size))
|
||||
}
|
||||
|
||||
function createImageOutputPriceRangeRows(value: TieredPricingConfig['image_output_price_ranges']): ImageOutputPriceRangeRow[] {
|
||||
const rows: ImageOutputPriceRangeRow[] = []
|
||||
if (!Array.isArray(value)) {
|
||||
return rows
|
||||
}
|
||||
for (const range of value) {
|
||||
if (!range || typeof range !== 'object') continue
|
||||
const rowPrices: Partial<Record<ImageOutputQuality, number>> = {}
|
||||
const rawPrices = 'prices' in range && range.prices && typeof range.prices === 'object'
|
||||
? range.prices as Record<string, unknown>
|
||||
: range as Record<string, unknown>
|
||||
for (const quality of IMAGE_OUTPUT_QUALITIES) {
|
||||
const price = rawPrices[quality]
|
||||
if (typeof price === 'number' && Number.isFinite(price)) {
|
||||
rowPrices[quality] = price
|
||||
}
|
||||
}
|
||||
const upToPixels = 'up_to_pixels' in range && range.up_to_pixels != null
|
||||
? String(range.up_to_pixels)
|
||||
: ''
|
||||
rows.push(createImageOutputPriceRangeRow(upToPixels, rowPrices))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
function createImageOutputPriceRow(
|
||||
size = '',
|
||||
prices: Partial<Record<ImageOutputQuality, number>> = {},
|
||||
@@ -563,6 +664,18 @@ function createImageOutputPriceRow(
|
||||
}
|
||||
}
|
||||
|
||||
function createImageOutputPriceRangeRow(
|
||||
upToPixels = '',
|
||||
prices: Partial<Record<ImageOutputQuality, number>> = {},
|
||||
): ImageOutputPriceRangeRow {
|
||||
imageOutputPriceRangeRowId += 1
|
||||
return {
|
||||
id: `image-output-range-${imageOutputPriceRangeRowId}`,
|
||||
upToPixels,
|
||||
prices: { ...prices },
|
||||
}
|
||||
}
|
||||
|
||||
function normalizedImageOutputPrices(): Record<string, Record<string, number>> {
|
||||
const out: Record<string, Record<string, number>> = {}
|
||||
for (const row of imageOutputPriceRows.value) {
|
||||
@@ -578,12 +691,42 @@ function normalizedImageOutputPrices(): Record<string, Record<string, number>> {
|
||||
return out
|
||||
}
|
||||
|
||||
function normalizedImageOutputPriceRanges(): ImageOutputPriceRange[] {
|
||||
const ranges: ImageOutputPriceRange[] = []
|
||||
for (const row of imageOutputPriceRangeRows.value) {
|
||||
const prices: Partial<Record<ImageOutputQuality, number>> = {}
|
||||
for (const quality of IMAGE_OUTPUT_QUALITIES) {
|
||||
const price = row.prices[quality]
|
||||
if (price != null && Number.isFinite(price)) {
|
||||
prices[quality] = price
|
||||
}
|
||||
}
|
||||
if (Object.keys(prices).length === 0) continue
|
||||
ranges.push({
|
||||
up_to_pixels: parseOptionalInteger(row.upToPixels),
|
||||
prices,
|
||||
})
|
||||
}
|
||||
return ranges.sort((a, b) => {
|
||||
if (a.up_to_pixels == null && b.up_to_pixels == null) return 0
|
||||
if (a.up_to_pixels == null) return 1
|
||||
if (b.up_to_pixels == null) return -1
|
||||
return a.up_to_pixels - b.up_to_pixels
|
||||
})
|
||||
}
|
||||
|
||||
function parseOptionalFloat(value: string | number): number | null {
|
||||
if (value === '' || value === null || value === undefined) return null
|
||||
const number = typeof value === 'string' ? parseFloat(value) : value
|
||||
return Number.isFinite(number) ? number : null
|
||||
}
|
||||
|
||||
function parseOptionalInteger(value: string | number): number | null {
|
||||
if (value === '' || value === null || value === undefined) return null
|
||||
const number = typeof value === 'string' ? parseInt(value, 10) : value
|
||||
return Number.isFinite(number) && number > 0 ? Math.trunc(number) : null
|
||||
}
|
||||
|
||||
function normalizeImageOutputSize(size: string): string {
|
||||
return String(size || '').trim().replace(/\s*[xX×]\s*/g, 'x')
|
||||
}
|
||||
@@ -592,6 +735,10 @@ function getImageOutputPrice(row: ImageOutputPriceRow, quality: ImageOutputQuali
|
||||
return row.prices[quality] ?? ''
|
||||
}
|
||||
|
||||
function getImageOutputRangePrice(row: ImageOutputPriceRangeRow, quality: ImageOutputQuality): string | number {
|
||||
return row.prices[quality] ?? ''
|
||||
}
|
||||
|
||||
function updateImageOutputSize(rowId: string, value: string | number) {
|
||||
const row = imageOutputPriceRows.value.find(item => item.id === rowId)
|
||||
if (!row) return
|
||||
@@ -625,6 +772,39 @@ function removeImageOutputSizeRow(rowId: string) {
|
||||
syncToParent()
|
||||
}
|
||||
|
||||
function updateImageOutputRangeLimit(rowId: string, value: string | number) {
|
||||
const row = imageOutputPriceRangeRows.value.find(item => item.id === rowId)
|
||||
if (!row) return
|
||||
row.upToPixels = String(value ?? '')
|
||||
imageOutputPriceRangeRows.value = [...imageOutputPriceRangeRows.value]
|
||||
syncToParent()
|
||||
}
|
||||
|
||||
function updateImageOutputRangePrice(rowId: string, quality: ImageOutputQuality, value: string | number) {
|
||||
const row = imageOutputPriceRangeRows.value.find(item => item.id === rowId)
|
||||
if (!row) return
|
||||
const price = parseOptionalFloat(value)
|
||||
if (price == null) {
|
||||
delete row.prices[quality]
|
||||
} else {
|
||||
row.prices[quality] = price
|
||||
}
|
||||
imageOutputPriceRangeRows.value = [...imageOutputPriceRangeRows.value]
|
||||
syncToParent()
|
||||
}
|
||||
|
||||
function addImageOutputRangeRow() {
|
||||
const usedLimits = new Set(imageOutputPriceRangeRows.value.map(row => parseOptionalInteger(row.upToPixels)).filter((value): value is number => value !== null))
|
||||
const suggestedLimit = DEFAULT_IMAGE_OUTPUT_PIXEL_LIMITS.find(limit => !usedLimits.has(limit))
|
||||
imageOutputPriceRangeRows.value = [...imageOutputPriceRangeRows.value, createImageOutputPriceRangeRow(suggestedLimit ? String(suggestedLimit) : '')]
|
||||
syncToParent()
|
||||
}
|
||||
|
||||
function removeImageOutputRangeRow(rowId: string) {
|
||||
imageOutputPriceRangeRows.value = imageOutputPriceRangeRows.value.filter(row => row.id !== rowId)
|
||||
syncToParent()
|
||||
}
|
||||
|
||||
function updateImageOutputPriceDefault(value: string | number) {
|
||||
imageOutputPriceDefault.value = String(value ?? '')
|
||||
syncToParent()
|
||||
|
||||
@@ -574,9 +574,16 @@ function modelSupportsImageGeneration(model: {
|
||||
function tieredPricingHasImageOutputPricing(pricing: TieredPricingConfig | null | undefined): boolean {
|
||||
if (!pricing) return false
|
||||
if (toFinitePrice(pricing.image_output_price_default) !== null) return true
|
||||
return Object.values(pricing.image_output_prices || {}).some((prices) => {
|
||||
if (Object.values(pricing.image_output_prices || {}).some((prices) => {
|
||||
if (!prices || typeof prices !== 'object') return false
|
||||
return Object.values(prices).some((price) => toFinitePrice(price) !== null)
|
||||
})) return true
|
||||
return (pricing.image_output_price_ranges || []).some((range) => {
|
||||
if (!range || typeof range !== 'object') return false
|
||||
const prices = range.prices && typeof range.prices === 'object'
|
||||
? range.prices
|
||||
: range as Record<string, unknown>
|
||||
return Object.values(prices).some((price) => toFinitePrice(price) !== null)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -403,13 +403,9 @@
|
||||
{{ imageOutputBillingLabel }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="imageOutputMatrixEnabled && imagePriceKey"
|
||||
v-if="imageOutputPricingDescriptor"
|
||||
class="text-muted-foreground font-mono"
|
||||
>{{ imagePriceKey }}</span>
|
||||
<span
|
||||
v-else-if="imageOutputSize || imageOutputQuality"
|
||||
class="text-muted-foreground font-mono"
|
||||
>{{ [imageOutputSize, imageOutputQuality].filter(Boolean).join(' / ') }}</span>
|
||||
>{{ imageOutputPricingDescriptor }}</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
@@ -1482,6 +1478,15 @@ const imagePriceKey = computed(() => {
|
||||
return fallbackKey || null
|
||||
})
|
||||
|
||||
const imageOutputPriceBucket = computed(() =>
|
||||
getNestedString(billingResolvedDimensions.value, 'image_output_price_bucket'),
|
||||
)
|
||||
|
||||
const imageOutputPixels = computed(() =>
|
||||
getNestedNumber(billingResolvedDimensions.value, 'image_pixels')
|
||||
?? parseImageSizePixels(imageOutputSize.value),
|
||||
)
|
||||
|
||||
const imageOutputPricingMode = computed(() =>
|
||||
getNestedString(billingResolvedDimensions.value, 'image_output_pricing_mode'),
|
||||
)
|
||||
@@ -1489,18 +1494,43 @@ const imageOutputPricingMode = computed(() =>
|
||||
const imageOutputPricingEnabled = computed(() =>
|
||||
getNestedValue(billingResolvedDimensions.value, 'image_output_pricing_enabled') === true
|
||||
|| imageOutputPricingMode.value === 'matrix'
|
||||
|| imageOutputPricingMode.value === 'pixel_tiers'
|
||||
|| imageOutputPricingMode.value === 'per_image'
|
||||
|| imageOutputCostTotal.value > 0,
|
||||
)
|
||||
|
||||
const imageOutputMatrixEnabled = computed(() =>
|
||||
getNestedValue(billingResolvedDimensions.value, 'image_output_matrix_enabled') === true
|
||||
|| imageOutputPricingMode.value === 'matrix',
|
||||
)
|
||||
const imageOutputMatrixEnabled = computed(() => {
|
||||
if (imageOutputPricingMode.value) return imageOutputPricingMode.value === 'matrix'
|
||||
return getNestedValue(billingResolvedDimensions.value, 'image_output_matrix_enabled') === true
|
||||
})
|
||||
|
||||
const imageOutputBillingLabel = computed(() =>
|
||||
imageOutputMatrixEnabled.value ? '矩阵计费' : '默认计费',
|
||||
)
|
||||
const imageOutputRangeEnabled = computed(() => {
|
||||
if (imageOutputPricingMode.value) return imageOutputPricingMode.value === 'pixel_tiers'
|
||||
return getNestedValue(billingResolvedDimensions.value, 'image_output_range_enabled') === true
|
||||
})
|
||||
|
||||
const imageOutputBillingLabel = computed(() => {
|
||||
if (imageOutputMatrixEnabled.value) return '矩阵计费'
|
||||
if (imageOutputRangeEnabled.value) return '像素区间'
|
||||
return '默认计费'
|
||||
})
|
||||
|
||||
const imageOutputPricingDescriptor = computed(() => {
|
||||
if (imageOutputMatrixEnabled.value && imagePriceKey.value) return imagePriceKey.value
|
||||
|
||||
const parts: string[] = []
|
||||
if (imageOutputPriceBucket.value && imageOutputPriceBucket.value !== 'default') {
|
||||
parts.push(formatImagePriceBucket(imageOutputPriceBucket.value))
|
||||
}
|
||||
const sizeQuality = [imageOutputSize.value, imageOutputQuality.value].filter(Boolean).join(' / ')
|
||||
if (sizeQuality) parts.push(sizeQuality)
|
||||
if (imageOutputRangeEnabled.value && imageOutputPixels.value !== null) {
|
||||
parts.push(formatPixels(imageOutputPixels.value))
|
||||
}
|
||||
if (parts.length > 0) return parts.join(' · ')
|
||||
if (imageOutputPriceBucket.value === 'default') return '默认价'
|
||||
return null
|
||||
})
|
||||
|
||||
const hasImageBillingDetail = computed(() =>
|
||||
imageOutputPricingEnabled.value && (imageOutputCount.value > 0 || imageOutputCostTotal.value > 0),
|
||||
@@ -2206,6 +2236,34 @@ function formatNumber(num: number): string {
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
function parseImageSizePixels(size: string | null): number | null {
|
||||
if (!size) return null
|
||||
const normalized = size.trim().toLowerCase().replace(/\s+/g, '').replace(/×/g, 'x')
|
||||
const [widthText, heightText] = normalized.split('x')
|
||||
const width = Number(widthText)
|
||||
const height = Number(heightText)
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null
|
||||
return Math.trunc(width * height)
|
||||
}
|
||||
|
||||
function formatImagePriceBucket(bucket: string): string {
|
||||
if (bucket === 'default') return '默认价'
|
||||
if (bucket === 'unbounded') return '无上限'
|
||||
const match = bucket.match(/^<=([0-9]+)px$/)
|
||||
if (match) return `<= ${formatPixels(Number(match[1]))}`
|
||||
return bucket
|
||||
}
|
||||
|
||||
function formatPixels(value: number): string {
|
||||
if (value >= 1_000_000) {
|
||||
return `${(value / 1_000_000).toFixed(value % 1_000_000 === 0 ? 0 : 2)}M px`
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
return `${(value / 1_000).toFixed(0)}K px`
|
||||
}
|
||||
return `${value} px`
|
||||
}
|
||||
|
||||
// 格式化响应时间,自动选择合适的单位
|
||||
function formatResponseTime(ms: number): { value: string; unit: string } {
|
||||
if (ms >= 1_000) {
|
||||
|
||||
Reference in New Issue
Block a user