模型广场:分时时段行明确不含高峰倍率口径并披露叠加

- 实扣倍率为 基础 × 高峰 × 分时;时段行价格与整表一致,按不含高峰的口径展示
- 分组启用高峰时,时段行 tooltip 披露与高峰窗口重叠的部分实付再乘高峰倍率
- PlazaGroupSection 把高峰窗口描述与倍率传入价格表
This commit is contained in:
feeeei
2026-08-24 11:23:58 +08:00
parent b07d85c497
commit f19095f96d
6 changed files with 81 additions and 6 deletions
@@ -61,6 +61,8 @@
:user-rate-multiplier="group.user_rate_multiplier ?? null"
:image-rate-independent="group.image_rate_independent"
:image-rate-multiplier="group.image_rate_multiplier"
:peak-window="peakWindow"
:peak-rate-multiplier="group.peak_rate_multiplier"
/>
<p v-else class="px-5 py-4 text-center text-sm text-gray-400 dark:text-dark-500">
{{ t('modelPlaza.detail.noModels') }}
@@ -88,14 +90,19 @@ const props = defineProps<{
const { t } = useI18n()
const appStore = useAppStore()
const peakNote = computed(() => {
/** 高峰窗口描述(含倍率与服务器时区标注);分组未启用高峰为空串。 */
const peakWindow = computed(() => {
if (!hasPeakRate(props.group)) return ''
const window = formatPeakRateWindow(
return formatPeakRateWindow(
props.group,
serverTimezoneLabel(appStore.cachedPublicSettings?.server_utc_offset)
)
})
const peakNote = computed(() => {
if (!peakWindow.value) return ''
return t('modelPlaza.detail.peakNote', {
window,
window: peakWindow.value,
multiplier: props.group.peak_rate_multiplier
})
})
@@ -308,6 +308,13 @@ const props = defineProps<{
/** 生图独立倍率:true 时图片计费模型的实付倍率取 imageRateMultiplier,不取分组/专属倍率。 */
imageRateIndependent?: boolean
imageRateMultiplier?: number | null
/**
* 高峰窗口描述(含倍率与服务器时区标注),空串/缺省 = 分组未启用高峰。
* 表格所有价格均为不含高峰因子的口径,该窗口仅用于分时时段行的 tooltip 披露:
* 与高峰重叠的部分实付还会再乘高峰倍率。
*/
peakWindow?: string
peakRateMultiplier?: number | null
}>()
const { t } = useI18n()
@@ -428,12 +435,22 @@ function timePeriods(m: PlazaModel): PlazaTimePricingPeriod[] {
return m.time_pricing?.periods ?? []
}
/** 时段行 tooltip:仅工作日生效的配置换用带周末回落说明的文案。 */
/**
* 时段行 tooltip:仅工作日生效的配置换用带周末回落说明的文案;
* 分组启用高峰倍率时追加披露——本行价格不含高峰因子,与高峰窗口重叠的部分实付再乘高峰倍率。
*/
function timePricingRowHint(m: PlazaModel): string {
const key = m.time_pricing?.weekdays_only
? 'modelPlaza.table.timePricingRowHintWeekdays'
: 'modelPlaza.table.timePricingRowHint'
return t(key, { timezone: m.time_pricing?.timezone })
let hint = t(key, { timezone: m.time_pricing?.timezone })
if (props.peakWindow) {
hint += t('modelPlaza.table.timePricingRowHintPeak', {
window: props.peakWindow,
multiplier: props.peakRateMultiplier ?? 1
})
}
return hint
}
/** “00:30–08:30”;整分钟的 HH:mm:ss 省略秒。 */
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import PlazaGroupSection from '../PlazaGroupSection.vue'
import PlazaModelPricingTable from '../PlazaModelPricingTable.vue'
import type { ModelPlazaGroup, PlazaModel } from '@/api/modelPlaza'
vi.mock('vue-i18n', async () => {
@@ -113,3 +114,26 @@ describe('PlazaGroupSection 长上下文说明', () => {
expect(wrapper.text()).not.toContain(NOTE)
})
})
describe('PlazaGroupSection 高峰配置传递', () => {
it('分组启用高峰时把窗口描述与倍率传给价格表', () => {
const wrapper = mountSection(
group({
subscription_type: 'subscription',
peak_rate_enabled: true,
peak_start: '14:00',
peak_end: '18:00',
peak_rate_multiplier: 1.5
})
)
const table = wrapper.findComponent(PlazaModelPricingTable)
// appStore mock 无 server_utc_offset,窗口描述不带时区标注
expect(table.props('peakWindow')).toBe('14:00-18:00 ×1.5')
expect(table.props('peakRateMultiplier')).toBe(1.5)
})
it('分组未启用高峰时窗口描述为空串', () => {
const wrapper = mountSection(group())
expect(wrapper.findComponent(PlazaModelPricingTable).props('peakWindow')).toBe('')
})
})
@@ -43,7 +43,12 @@ function mountTable(
models: PlazaModel[],
rateMultiplier: number,
userRateMultiplier?: number | null,
extraProps?: { imageRateIndependent?: boolean; imageRateMultiplier?: number | null }
extraProps?: {
imageRateIndependent?: boolean
imageRateMultiplier?: number | null
peakWindow?: string
peakRateMultiplier?: number | null
}
) {
return mount(PlazaModelPricingTable, {
props: { models, rateMultiplier, userRateMultiplier: userRateMultiplier ?? null, ...extraProps }
@@ -602,6 +607,25 @@ describe('PlazaModelPricingTable 分时计价', () => {
expect(wrapper.find('[title="modelPlaza.table.timePricingRowHint"]').exists()).toBe(true)
})
it('分组启用高峰倍率时时段行 tooltip 追加高峰披露,价格与倍率列保持不含高峰的口径', () => {
const wrapper = mountTable([timePricedModel()], 0.8, null, {
peakWindow: '14:00-18:00 ×1.5 (UTC+08:00)',
peakRateMultiplier: 1.5
})
const nightCells = wrapper.findAll('tbody tr')[1].findAll('td')
const title = nightCells[0].find('[title*="modelPlaza.table.timePricingRowHint"]').attributes('title')
expect(title).toContain('modelPlaza.table.timePricingRowHintPeak')
// 行内数字仍是 基础倍率 × 时段倍率(0.8 × 0.5),高峰只进披露不进价格
expect(nightCells[1].text()).toContain('$1.20')
expect(nightCells[7].text()).toContain('0.4x')
})
it('分组未启用高峰(peakWindow 缺省)时 tooltip 不含高峰披露', () => {
const wrapper = mountTable([timePricedModel()], 1)
const badge = wrapper.find('[title*="modelPlaza.table.timePricingRowHint"]')
expect(badge.attributes('title')).not.toContain('modelPlaza.table.timePricingRowHintPeak')
})
it('无分时倍率时只有一行,不渲染时段标注', () => {
const wrapper = mountTable([tokenModel()], 1)
expect(wrapper.findAll('tbody tr')).toHaveLength(1)
@@ -607,6 +607,8 @@ export default {
timePricingRowHint: 'Requests made within this period ({timezone} time) are billed at the prices in this row',
timePricingRowHintWeekdays:
'On weekdays (Mon–Fri) only, requests made within this period ({timezone} time) are billed at the prices in this row; weekends use the standard prices',
timePricingRowHintPeak:
'; prices in this row exclude the peak-hour rate — where this period overlaps the peak hours {window}, the overlapping portion is additionally multiplied by ×{multiplier}',
timePricingWeekdays: 'Weekdays',
timePricingRateHint: 'Effective rate {rate} × period multiplier {multiplier}',
paidPrice: 'Your Price (Discounted)',
@@ -612,6 +612,7 @@ export default {
timePricingRowHint: '按 {timezone} 时间,在该时段内发起的请求按本行价格计费',
timePricingRowHintWeekdays:
'按 {timezone} 时间,仅工作日(周一至周五)在该时段内发起的请求按本行价格计费,周末全天按标准价',
timePricingRowHintPeak: ';本行价格未含高峰倍率,与高峰时段 {window} 重叠的部分实付再乘 ×{multiplier}',
timePricingWeekdays: '工作日',
timePricingRateHint: '生效倍率 {rate} × 时段倍率 {multiplier}',
paidPrice: '实付价格(折后)',