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

@@ -1,125 +1,66 @@
<template>
<div class="flex items-start gap-2 rounded border border-gray-200 bg-white p-2 dark:border-dark-500 dark:bg-dark-700">
<!-- Token mode: context range + prices -->
<!-- Token mode: context range + prices ($/MTok) -->
<template v-if="mode === 'token'">
<div class="w-20">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.minTokens', 'Min (K)') }}</label>
<input
:value="interval.min_tokens"
@input="emitField('min_tokens', toInt(($event.target as HTMLInputElement).value))"
type="number"
min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">Min</label>
<input :value="interval.min_tokens" @input="emitField('min_tokens', toInt(($event.target as HTMLInputElement).value))"
type="number" min="0" class="input mt-0.5 text-xs" />
</div>
<div class="w-20">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.maxTokens', 'Max (K)') }}</label>
<input
:value="interval.max_tokens ?? ''"
@input="emitField('max_tokens', toIntOrNull(($event.target as HTMLInputElement).value))"
type="number"
min="0"
class="input mt-0.5 text-xs"
:placeholder="'∞'"
/>
<label class="text-xs text-gray-400">Max <span class="text-gray-300">()</span></label>
<input :value="interval.max_tokens ?? ''" @input="emitField('max_tokens', toIntOrNull(($event.target as HTMLInputElement).value))"
type="number" min="0" class="input mt-0.5 text-xs" :placeholder="'∞'" />
</div>
<div class="flex-1">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.inputPrice', 'Input') }}</label>
<input
:value="interval.input_price"
@input="emitField('input_price', ($event.target as HTMLInputElement).value)"
type="number"
step="any" min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.inputPrice', '输入') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.input_price" @input="emitField('input_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
<div class="flex-1">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.outputPrice', 'Output') }}</label>
<input
:value="interval.output_price"
@input="emitField('output_price', ($event.target as HTMLInputElement).value)"
type="number"
step="any" min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.outputPrice', '输出') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.output_price" @input="emitField('output_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
<div class="flex-1">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWritePrice', 'Cache W') }}</label>
<input
:value="interval.cache_write_price"
@input="emitField('cache_write_price', ($event.target as HTMLInputElement).value)"
type="number"
step="any" min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheWritePrice', '缓存W') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.cache_write_price" @input="emitField('cache_write_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
<div class="flex-1">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheReadPrice', 'Cache R') }}</label>
<input
:value="interval.cache_read_price"
@input="emitField('cache_read_price', ($event.target as HTMLInputElement).value)"
type="number"
step="any" min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.cacheReadPrice', '缓存R') }} <span class="text-gray-300">$/M</span></label>
<input :value="interval.cache_read_price" @input="emitField('cache_read_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
</template>
<!-- Per-request / Image mode: tier label + price -->
<!-- Per-request / Image mode: tier label + context range + price -->
<template v-else>
<div class="w-24">
<label class="text-xs text-gray-400">
{{ mode === 'image'
? t('admin.channels.form.resolution', 'Resolution')
: t('admin.channels.form.tierLabel', 'Tier')
}}
{{ mode === 'image' ? t('admin.channels.form.resolution', '分辨率') : t('admin.channels.form.tierLabel', '层级') }}
</label>
<input
:value="interval.tier_label"
@input="emitField('tier_label', ($event.target as HTMLInputElement).value)"
type="text"
class="input mt-0.5 text-xs"
:placeholder="mode === 'image' ? '1K / 2K / 4K' : ''"
/>
<input :value="interval.tier_label" @input="emitField('tier_label', ($event.target as HTMLInputElement).value)"
type="text" class="input mt-0.5 text-xs" :placeholder="mode === 'image' ? '1K / 2K / 4K' : ''" />
</div>
<div class="w-20">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.minTokens', 'Min') }}</label>
<input
:value="interval.min_tokens"
@input="emitField('min_tokens', toInt(($event.target as HTMLInputElement).value))"
type="number"
min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">Min</label>
<input :value="interval.min_tokens" @input="emitField('min_tokens', toInt(($event.target as HTMLInputElement).value))"
type="number" min="0" class="input mt-0.5 text-xs" />
</div>
<div class="w-20">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.maxTokens', 'Max') }}</label>
<input
:value="interval.max_tokens ?? ''"
@input="emitField('max_tokens', toIntOrNull(($event.target as HTMLInputElement).value))"
type="number"
min="0"
class="input mt-0.5 text-xs"
:placeholder="'∞'"
/>
<label class="text-xs text-gray-400">Max <span class="text-gray-300">()</span></label>
<input :value="interval.max_tokens ?? ''" @input="emitField('max_tokens', toIntOrNull(($event.target as HTMLInputElement).value))"
type="number" min="0" class="input mt-0.5 text-xs" :placeholder="'∞'" />
</div>
<div class="flex-1">
<label class="text-xs text-gray-400">{{ t('admin.channels.form.perRequestPrice', 'Price') }}</label>
<input
:value="interval.per_request_price"
@input="emitField('per_request_price', ($event.target as HTMLInputElement).value)"
type="number"
step="any" min="0"
class="input mt-0.5 text-xs"
/>
<label class="text-xs text-gray-400">{{ t('admin.channels.form.perRequestPrice', '单次价格') }} <span class="text-gray-300">$</span></label>
<input :value="interval.per_request_price" @input="emitField('per_request_price', ($event.target as HTMLInputElement).value)"
type="number" step="any" min="0" class="input mt-0.5 text-xs" />
</div>
</template>
<button
type="button"
@click="emit('remove')"
class="mt-4 rounded p-0.5 text-gray-400 hover:text-red-500"
>
<button type="button" @click="emit('remove')" class="mt-4 rounded p-0.5 text-gray-400 hover:text-red-500">
<Icon name="x" size="sm" />
</button>
</div>