feat(model):优化模型价格设置功能

This commit is contained in:
qian committed 2026-08-20 01:03:48 +08:00
1 parent add95ca5ab
commit ee96a5b866
10 files changed
+181 -11

No files matched your search

+8
View File
@@ -128,6 +128,10 @@ type PricingGetReq struct {
type PricingGetRes struct {
List []PricingItem `json:"list"`
TimeSegments []TimeSegmentItem `json:"time_segments" dc:"时段定价列表(锚点行 time_segments)"`
// 以下为锚点行展示字段(仅 min_tokens=0 行生效)
PriceNote string `json:"price_note" dc:"价格说明(仅管理后台可见的内部备注)"`
DiscountLabel string `json:"discount_label" dc:"折扣标签(对外展示,如 7折起)"`
PriceChangeNote string `json:"price_change_note" dc:"价格调整说明(对外展示,提示价格有变动)"`
}
// PricingSetReq 设置模型定价(全量替换)
@@ -136,6 +140,10 @@ type PricingSetReq struct {
ModelID int64 `json:"model_id" in:"path" v:"required" dc:"模型ID"`
Items []PricingItem `json:"items" v:"required" dc:"定价列表"`
TimeSegments []TimeSegmentItem `json:"time_segments" dc:"时段定价(可选,全量替换;空数组清除时段配置)"`
// 以下为锚点行展示字段(全量替换语义:空=清除;price_note 不透出到租户端)
PriceNote string `json:"price_note" dc:"价格说明(仅内部可见)"`
DiscountLabel string `json:"discount_label" dc:"折扣标签(对外展示)"`
PriceChangeNote string `json:"price_change_note" dc:"价格调整说明(对外展示)"`
}
type PricingSetRes struct{}
+4
View File
@@ -60,6 +60,10 @@ type TenantAvailableModelItem struct {
PricingTiers []PricingTierItem `json:"pricing_tiers"`
// TimePrices 时段价目(平台配置了时段定价的模型才有;价格为换算后的展示价)
TimePrices []TimePriceItem `json:"time_prices"`
// DiscountLabel 折扣标签(平台定价锚点行配置的营销展示文案,如"7折起";NULL=不展示)
DiscountLabel *string `json:"discount_label"`
// PriceChangeNote 价格调整说明(对外提示价格有变动;NULL=不展示)
PriceChangeNote *string `json:"price_change_note"`
// AsyncImage 图片模型异步端点是否可用(提交 /v1/images/generations/async 后轮询取图)。
// 仅图片分类模型有意义;真异步厂商,或同步厂商且「同步图片异步化」开关开启时为 true。
AsyncImage bool `json:"async_image"`
+6
View File
@@ -34,6 +34,9 @@ type MdlPricingColumns struct {
CacheReadPrice string // 缓存读取每 1M token 价格(直接定价)
CacheCreationPrice string // 缓存创建每 1M token 价格(直接定价)
TimeSegments string // 时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{"name":"闲时","days":[1,2,3,4,5],"start_time":"00:00","end_time":"08:00","valid_from":"","valid_to":"","multiplier":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点
PriceNote string // 价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无
DiscountLabel string // 折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
PriceChangeNote string // 价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
}
// mdlPricingColumns holds the columns for the table mdl_pricing.
@@ -51,6 +54,9 @@ var mdlPricingColumns = MdlPricingColumns{
CacheReadPrice: "cache_read_price",
CacheCreationPrice: "cache_creation_price",
TimeSegments: "time_segments",
PriceNote: "price_note",
DiscountLabel: "discount_label",
PriceChangeNote: "price_change_note",
}
// NewMdlPricingDao creates and returns a new DAO object for table data access.
+59 -7
View File
@@ -452,6 +452,9 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v
CacheReadPrice float64 `json:"cache_read_price"`
CacheCreationPrice float64 `json:"cache_creation_price"`
TimeSegments string `json:"time_segments"`
PriceNote *string `json:"price_note"`
DiscountLabel *string `json:"discount_label"`
PriceChangeNote *string `json:"price_change_note"`
}
err := dao.MdlPricing.Ctx(ctx).
@@ -464,7 +467,9 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v
result := make([]v1.PricingItem, 0, len(rows))
var timeSegments []billing.TimeSegment
for _, r := range rows {
var priceNote, discountLabel, priceChangeNote string
for i := range rows {
r := rows[i]
result = append(result, v1.PricingItem{
BillingMode: r.BillingMode,
MinTokens: r.MinTokens,
@@ -475,15 +480,34 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v
CacheReadPrice: r.CacheReadPrice,
CacheCreationPrice: r.CacheCreationPrice,
})
// 时段定价只认锚点行(min_tokens=0),阶梯行上的残留忽略
if r.MinTokens == 0 && r.TimeSegments != "" && r.TimeSegments != "null" {
if err := json.Unmarshal([]byte(r.TimeSegments), &timeSegments); err != nil {
g.Log().Warningf(ctx, "admin: 模型 %d 时段定价解析失败: %v", req.ModelID, err)
timeSegments = nil
// 时段定价与展示字段只认锚点行(min_tokens=0),阶梯行上的残留忽略
if r.MinTokens == 0 {
if r.TimeSegments != "" && r.TimeSegments != "null" {
if err := json.Unmarshal([]byte(r.TimeSegments), &timeSegments); err != nil {
g.Log().Warningf(ctx, "admin: 模型 %d 时段定价解析失败: %v", req.ModelID, err)
timeSegments = nil
}
}
priceNote = derefStr(r.PriceNote)
discountLabel = derefStr(r.DiscountLabel)
priceChangeNote = derefStr(r.PriceChangeNote)
}
}
return &v1.PricingGetRes{List: result, TimeSegments: timeSegmentsToAPI(timeSegments)}, nil
return &v1.PricingGetRes{
List: result,
TimeSegments: timeSegmentsToAPI(timeSegments),
PriceNote: priceNote,
DiscountLabel: discountLabel,
PriceChangeNote: priceChangeNote,
}, nil
}
// derefStr 字符串指针安全解引用(NULL → 空串)
func derefStr(s *string) string {
if s == nil {
return ""
}
return *s
}
// timeSegmentsToAPI billing.TimeSegment → API 时段项
@@ -556,6 +580,29 @@ func writeTimeSegmentsForModel(ctx context.Context, modelDBID int64, items []v1.
return err
}
// writePricingDisplayFieldsForModel 把展示字段(价格说明/折扣标签/价格调整说明)写到定价锚点行(min_tokens=0)。
// 全量替换语义:空串=清除(写 NULL)。与 writeTimeSegmentsForModel 同理用列名直写而非 do 字段,
// 与 gf gen dao 生成物解耦。调用方需保证事务 ctx 传播。
func writePricingDisplayFieldsForModel(ctx context.Context, modelDBID int64, priceNote, discountLabel, priceChangeNote string) error {
trimOrNull := func(s string) any {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
return s
}
_, err := dao.MdlPricing.Ctx(ctx).
Where("model_id", modelDBID).
Where("min_tokens", 0).
Data(g.Map{
"price_note": trimOrNull(priceNote),
"discount_label": trimOrNull(discountLabel),
"price_change_note": trimOrNull(priceChangeNote),
}).
Update()
return err
}
// SetModelPricing 设置模型定价(全量替换)
func (s *sAdmin) SetModelPricing(ctx context.Context, req *v1.PricingSetReq) (*v1.PricingSetRes, error) {
// 时段定价校验(乘数范围/时间格式/星期/日期边界),fail-fast 返回用户可读错误
@@ -623,6 +670,11 @@ func (s *sAdmin) SetModelPricing(ctx context.Context, req *v1.PricingSetReq) (*v
return err
}
// 展示字段写锚点行(空=清除,全量替换语义;需在锚点行插入之后执行)
if err := writePricingDisplayFieldsForModel(ctx, req.ModelID, req.PriceNote, req.DiscountLabel, req.PriceChangeNote); err != nil {
return err
}
return nil
})
if err != nil {
+21 -2
View File
@@ -37,6 +37,8 @@ type tenantModelPriceRow struct {
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
BaseBillingMode string `json:"base_billing_mode"`
BasePerRequestPrice *float64 `json:"base_per_request_price"`
BaseDiscountLabel *string `json:"base_discount_label"`
BasePriceChangeNote *string `json:"base_price_change_note"`
CustomInputPrice *float64 `json:"custom_input_price"`
CustomOutputPrice *float64 `json:"custom_output_price"`
CustomCacheReadPrice *float64 `json:"custom_cache_read_price"`
@@ -53,6 +55,8 @@ type groupPriceRow struct {
BaseCacheReadPrice float64 `json:"base_cache_read_price"`
BaseCacheCreationPrice float64 `json:"base_cache_creation_price"`
BasePerRequestPrice *float64 `json:"base_per_request_price"`
BaseDiscountLabel *string `json:"base_discount_label"`
BasePriceChangeNote *string `json:"base_price_change_note"`
}
// baseTierRow 阶梯定价查询结果
@@ -83,6 +87,8 @@ type priceInfo struct {
BaseCacheCreationPrice float64
BaseBillingMode string
BasePerRequestPrice *float64
BaseDiscountLabel *string
BasePriceChangeNote *string
CustomInputPrice *float64
CustomOutputPrice *float64
CustomCacheReadPrice *float64
@@ -98,6 +104,8 @@ type groupPriceInfo struct {
BaseCacheReadPrice float64
BaseCacheCreationPrice float64
BasePerRequestPrice *float64
BaseDiscountLabel *string
BasePriceChangeNote *string
}
// ListAvailableModels 获取租户可用的模型列表
@@ -128,7 +136,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
LeftJoin("mdl_pricing p ON p.model_id = mdl_tenant_models.model_id AND p.min_tokens = 0").
Where("mdl_tenant_models.tenant_id", tenantID).
WhereIn("mdl_tenant_models.model_id", explicitDBIDs).
Fields("mdl_tenant_models.model_id AS model_db_id, mdl_tenant_models.id, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, p.per_request_price AS base_per_request_price, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers").
Fields("mdl_tenant_models.model_id AS model_db_id, mdl_tenant_models.id, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, p.per_request_price AS base_per_request_price, p.discount_label AS base_discount_label, p.price_change_note AS base_price_change_note, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers").
Scan(&priceResults)
if err != nil {
return nil, err
@@ -150,6 +158,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
BaseCacheCreationPrice: r.BaseCacheCreationPrice,
BaseBillingMode: r.BaseBillingMode,
BasePerRequestPrice: r.BasePerRequestPrice,
BaseDiscountLabel: r.BaseDiscountLabel,
BasePriceChangeNote: r.BasePriceChangeNote,
CustomInputPrice: r.CustomInputPrice,
CustomOutputPrice: r.CustomOutputPrice,
CustomCacheReadPrice: r.CustomCacheReadPrice,
@@ -172,7 +182,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
err = dao.MdlPricing.Ctx(ctx).
WhereIn("model_id", groupDBIDs).
Where("min_tokens", 0).
Fields("model_id, billing_mode AS base_billing_mode, input_price AS base_input_price, output_price AS base_output_price, cache_read_price AS base_cache_read_price, cache_creation_price AS base_cache_creation_price, per_request_price AS base_per_request_price").
Fields("model_id, billing_mode AS base_billing_mode, input_price AS base_input_price, output_price AS base_output_price, cache_read_price AS base_cache_read_price, cache_creation_price AS base_cache_creation_price, per_request_price AS base_per_request_price, discount_label AS base_discount_label, price_change_note AS base_price_change_note").
Scan(&groupPrices)
if err != nil {
return nil, err
@@ -186,6 +196,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
BaseCacheReadPrice: gp.BaseCacheReadPrice,
BaseCacheCreationPrice: gp.BaseCacheCreationPrice,
BasePerRequestPrice: gp.BasePerRequestPrice,
BaseDiscountLabel: gp.BaseDiscountLabel,
BasePriceChangeNote: gp.BasePriceChangeNote,
}
}
}
@@ -301,6 +313,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
OutputPrice: outputPrice,
CacheReadPrice: cacheReadPrice,
CacheCreationPrice: cacheCreationPrice,
DiscountLabel: pi.BaseDiscountLabel,
PriceChangeNote: pi.BasePriceChangeNote,
}
if effectiveBillingMode == "tiered" {
@@ -316,6 +330,7 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
billingMode := "token"
var inputPrice, outputPrice, cacheReadPrice, cacheCreationPrice, perRequestPrice *float64
var baseInputPrice, baseOutputPrice float64
var discountLabel, priceChangeNote *string
gp, ok := groupPriceMap[m.ModelDBID]
if ok {
billingMode = gp.BaseBillingMode
@@ -329,6 +344,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
cacheReadPrice = effectivePrice(nil, gp.BaseCacheReadPrice)
cacheCreationPrice = effectivePrice(nil, gp.BaseCacheCreationPrice)
perRequestPrice = gp.BasePerRequestPrice
discountLabel = gp.BaseDiscountLabel
priceChangeNote = gp.BasePriceChangeNote
}
item := v1.TenantAvailableModelItem{
@@ -347,6 +364,8 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
OutputPrice: outputPrice,
CacheReadPrice: cacheReadPrice,
CacheCreationPrice: cacheCreationPrice,
DiscountLabel: discountLabel,
PriceChangeNote: priceChangeNote,
}
if billingMode == "tiered" && ok {
item.PricingTiers = buildTiers("", baseInputPrice, baseOutputPrice, baseTiersMap[m.ModelDBID])
+3
View File
@@ -26,4 +26,7 @@ type MdlPricing struct {
CacheReadPrice any // 缓存读取每 1M token 价格(直接定价)
CacheCreationPrice any // 缓存创建每 1M token 价格(直接定价)
TimeSegments any // 时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{"name":"闲时","days":[1,2,3,4,5],"start_time":"00:00","end_time":"08:00","valid_from":"","valid_to":"","multiplier":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点
PriceNote any // 价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无
DiscountLabel any // 折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
PriceChangeNote any // 价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
}
+3
View File
@@ -24,4 +24,7 @@ type MdlPricing struct {
CacheReadPrice decimal.Decimal `json:"cache_read_price" orm:"cache_read_price" description:"缓存读取每 1M token 价格(直接定价)"` // 缓存读取每 1M token 价格(直接定价)
CacheCreationPrice decimal.Decimal `json:"cache_creation_price" orm:"cache_creation_price" description:"缓存创建每 1M token 价格(直接定价)"` // 缓存创建每 1M token 价格(直接定价)
TimeSegments string `json:"time_segments" orm:"time_segments" description:"时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{\"name\":\"闲时\",\"days\":[1,2,3,4,5],\"start_time\":\"00:00\",\"end_time\":\"08:00\",\"valid_from\":\"\",\"valid_to\":\"\",\"multiplier\":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点"` // 时段定价(JSONB 有序数组,仅 min_tokens=0 锚点行生效):[{"name":"闲时","days":[1,2,3,4,5],"start_time":"00:00","end_time":"08:00","valid_from":"","valid_to":"","multiplier":0.5}],按序先命中先生效,未命中=默认价(乘数 1.0),days 1=周一..7=周日 空=每天,end<start 表示跨零点
PriceNote string `json:"price_note" orm:"price_note" description:"价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无"` // 价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无
DiscountLabel string `json:"discount_label" orm:"discount_label" description:"折扣标签(对外展示,如\"7折起\"、\"限时5折\"),仅 min_tokens=0 锚点行使用,NULL/空=不展示"` // 折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
PriceChangeNote string `json:"price_change_note" orm:"price_change_note" description:"价格调整说明(对外展示,提示用户价格有变动,如\"9月1日起输入价下调\"),仅 min_tokens=0 锚点行使用,NULL/空=不展示"` // 价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示
}
@@ -0,0 +1,16 @@
-- +goose Up
-- 模型定价展示字段:价格说明(内部)+ 折扣标签 / 价格调整说明(对外展示)
ALTER TABLE mdl_pricing
ADD COLUMN IF NOT EXISTS price_note VARCHAR(500),
ADD COLUMN IF NOT EXISTS discount_label VARCHAR(50),
ADD COLUMN IF NOT EXISTS price_change_note VARCHAR(200);
COMMENT ON COLUMN mdl_pricing.price_note IS '价格说明(仅管理后台可见,调价背景等内部备注),仅 min_tokens=0 锚点行使用,NULL=无';
COMMENT ON COLUMN mdl_pricing.discount_label IS '折扣标签(对外展示,如"7折起"、"限时5折"),仅 min_tokens=0 锚点行使用,NULL/空=不展示';
COMMENT ON COLUMN mdl_pricing.price_change_note IS '价格调整说明(对外展示,提示用户价格有变动,如"9月1日起输入价下调"),仅 min_tokens=0 锚点行使用,NULL/空=不展示';
-- +goose Down
ALTER TABLE mdl_pricing
DROP COLUMN IF EXISTS price_change_note,
DROP COLUMN IF EXISTS discount_label,
DROP COLUMN IF EXISTS price_note;
+42 -1
View File
@@ -23,6 +23,11 @@ const editorSaving = ref(false)
const editorBillingMode = ref('token')
const editorItems = reactive<any[]>([])
// 展示字段(挂锚点行:内部备注 + 对外营销文案)
const editorPriceNote = ref('')
const editorDiscountLabel = ref('')
const editorPriceChangeNote = ref('')
const billingModeOptions = [
{ label: '按量计费 (Token)', value: 'token' },
{ label: '按次计费', value: 'per_request' },
@@ -46,6 +51,9 @@ function resetEditorDefaults() {
editorBillingMode.value = 'token'
editorItems.length = 0
editorItems.push(createEmptyItem('token'))
editorPriceNote.value = ''
editorDiscountLabel.value = ''
editorPriceChangeNote.value = ''
resetTimeSegments()
}
@@ -124,6 +132,9 @@ async function loadPricing() {
try {
const res: any = await request.get(`/admin/models/${props.modelId}/pricing`)
const list: any[] = res.data?.data?.list || []
editorPriceNote.value = res.data?.data?.price_note || ''
editorDiscountLabel.value = res.data?.data?.discount_label || ''
editorPriceChangeNote.value = res.data?.data?.price_change_note || ''
if (list.length > 0) {
editorBillingMode.value = list[0].billing_mode || 'token'
editorItems.length = 0
@@ -379,7 +390,13 @@ async function savePricing() {
cache_read_price: item.cache_read_price,
cache_creation_price: item.cache_creation_price,
}))
await request.put(`/admin/models/${props.modelId}/pricing`, { items, time_segments: segmentsPayload() })
await request.put(`/admin/models/${props.modelId}/pricing`, {
items,
time_segments: segmentsPayload(),
price_note: editorPriceNote.value.trim(),
discount_label: editorDiscountLabel.value.trim(),
price_change_note: editorPriceChangeNote.value.trim(),
})
Message.success('定价已保存')
emit('update:visible', false)
emit('saved')
@@ -748,6 +765,30 @@ watch(() => props.visible, (val) => {
</template>
</div>
<!-- 展示信息(折扣标签 / 价格调整说明对外展示,价格说明仅内部可见) -->
<div class="editor-section">
<div class="editor-section-header">
<h3>展示信息</h3>
<span class="section-hint">折扣标签与调整说明会展示给租户;价格说明仅管理后台可见</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<AFormItem label="折扣标签(对外展示)">
<AInput v-model="editorDiscountLabel" :maxlength="50" placeholder="如:7折起、限时5折" allow-clear />
</AFormItem>
<AFormItem label="价格调整说明(对外展示)">
<AInput v-model="editorPriceChangeNote" :maxlength="200" placeholder="如:9月1日起输入价下调 20%" allow-clear />
</AFormItem>
</div>
<AFormItem label="价格说明(仅内部可见)">
<ATextarea
v-model="editorPriceNote"
:max-length="500"
:auto-size="{ minRows: 2, maxRows: 4 }"
placeholder="调价背景、渠道成本等内部备注,不对外展示"
/>
</AFormItem>
</div>
</div>
</AForm>
</ASpin>
+19 -1
View File
@@ -51,6 +51,8 @@ interface ModelItem {
cache_creation_price: number | null
pricing_tiers: PricingTierItem[]
time_prices?: TimePriceItem[] | null
discount_label?: string | null
price_change_note?: string | null
}
const capabilityLabel: Record<string, string> = {
@@ -217,7 +219,7 @@ function timeWindow(tp: TimePriceItem): string {
function timePriceText(m: ModelItem, tp: TimePriceItem): string {
if (m.billing_mode === 'per_request') return `${formatPrice(tp.per_request_price ?? null)} /次`
if (m.billing_mode === 'tiered') return `${formatPrice(tp.input_price ?? null)} 起`
if (m.billing_mode === 'tiered') return `输入 ${formatPrice(tp.input_price ?? null)} · 输出 ${formatPrice(tp.output_price ?? null)} 起`
return `输入 ${formatPrice(tp.input_price ?? null)} · 输出 ${formatPrice(tp.output_price ?? null)}`
}
@@ -334,6 +336,13 @@ onMounted(fetchModels)
{{ categoryLabel[m.category] || m.category }}
</span>
<h3 class="text-sm font-semibold text-gray-900 truncate">{{ m.model_name || m.model_id }}</h3>
<span
v-if="m.discount_label"
class="shrink-0 px-1.5 py-0.5 text-[10px] font-semibold text-white rounded-md"
style="background: linear-gradient(135deg, #f43f5e, #fb923c);"
>
{{ m.discount_label }}
</span>
</div>
<!-- Model ID + Copy -->
@@ -382,6 +391,15 @@ onMounted(fetchModels)
</span>
</div>
<!-- 价格调整说明(平台调价提示,仅配置时展示) -->
<div
v-if="m.price_change_note"
class="mt-2.5 flex items-start gap-1.5 text-[11px] text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-2 py-1.5"
>
<Icon name="infoCircle" size="xs" class="shrink-0 mt-px text-amber-500" />
<span>{{ m.price_change_note }}</span>
</div>
<!-- Pricing (pushed to bottom) -->
<div v-if="hasPricing(m)" class="mt-auto pt-3 border-t border-gray-100">
<template v-if="m.billing_mode === 'token'">