feat(channel): 模型标签输入 + $/MTok 价格单位 + 左开右闭区间 + i18n

- 模型输入改为标签列表(输入回车添加,支持粘贴批量导入)
- 价格显示单位改为 $/MTok(每百万 token),提交时自动转换
- Token 模式增加图片输出价格字段(适配 Gemini 图片模型按 token 计费)
- 区间边界改为左开右闭 (min, max],右边界包含
- 默认价格作为未命中区间时的回退价格
- 添加完整中英文 i18n 翻译
This commit is contained in:
erio
2026-03-30 02:24:54 +08:00
parent 983fe58959
commit dca0054e93
9 changed files with 375 additions and 224 deletions

View File

@@ -287,7 +287,7 @@ import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
import type { Channel, ChannelModelPricing, CreateChannelRequest, UpdateChannelRequest } from '@/api/admin/channels'
import type { PricingFormEntry } from '@/components/admin/channel/types'
import { toNullableNumber, apiIntervalsToForm, formIntervalsToAPI } from '@/components/admin/channel/types'
import { mTokToPerToken, perTokenToMTok, apiIntervalsToForm, formIntervalsToAPI } from '@/components/admin/channel/types'
import type { AdminGroup } from '@/types'
import type { Column } from '@/components/common/types'
import AppLayout from '@/components/layout/AppLayout.vue'
@@ -412,13 +412,12 @@ function toggleGroup(groupId: number) {
// ── Pricing helpers ──
function addPricingEntry() {
form.model_pricing.push({
modelsInput: '',
models: [],
billing_mode: 'token',
input_price: null,
output_price: null,
cache_write_price: null,
cache_read_price: null,
per_request_price: null,
image_output_price: null,
intervals: []
})
@@ -434,29 +433,28 @@ function removePricingEntry(idx: number) {
function formPricingToAPI(): ChannelModelPricing[] {
return form.model_pricing
.filter(e => e.modelsInput.trim())
.filter(e => e.models.length > 0)
.map(e => ({
models: e.modelsInput.split(',').map(m => m.trim()).filter(Boolean),
models: e.models,
billing_mode: e.billing_mode,
input_price: toNullableNumber(e.input_price),
output_price: toNullableNumber(e.output_price),
cache_write_price: toNullableNumber(e.cache_write_price),
cache_read_price: toNullableNumber(e.cache_read_price),
image_output_price: toNullableNumber(e.image_output_price),
input_price: mTokToPerToken(e.input_price),
output_price: mTokToPerToken(e.output_price),
cache_write_price: mTokToPerToken(e.cache_write_price),
cache_read_price: mTokToPerToken(e.cache_read_price),
image_output_price: mTokToPerToken(e.image_output_price),
intervals: formIntervalsToAPI(e.intervals || [])
}))
}
function apiPricingToForm(pricing: ChannelModelPricing[]): PricingFormEntry[] {
return pricing.map(p => ({
modelsInput: p.models.join(', '),
models: p.models || [],
billing_mode: p.billing_mode,
input_price: p.input_price,
output_price: p.output_price,
cache_write_price: p.cache_write_price,
cache_read_price: p.cache_read_price,
per_request_price: null,
image_output_price: p.image_output_price,
input_price: perTokenToMTok(p.input_price),
output_price: perTokenToMTok(p.output_price),
cache_write_price: perTokenToMTok(p.cache_write_price),
cache_read_price: perTokenToMTok(p.cache_read_price),
image_output_price: perTokenToMTok(p.image_output_price),
intervals: apiIntervalsToForm(p.intervals || [])
}))
}