Merge pull request #6089 from lyen1688/feat/channel-time-pricing-weekdays

新增渠道时间段定价工作日生效规则
This commit is contained in:
Wesley Liddick
2026-08-24 10:21:47 +08:00
committed by GitHub
13 changed files with 149 additions and 31 deletions
+1
View File
@@ -33,6 +33,7 @@ export interface ChannelTimePricingPeriod {
export interface ChannelTimePricing {
timezone: string
weekdays_only?: boolean
periods: ChannelTimePricingPeriod[]
}
@@ -1,22 +1,40 @@
<template>
<section class="mt-3 border-t border-gray-200 pt-3 dark:border-dark-600">
<div class="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div class="min-w-0 flex-1 sm:max-w-sm">
<div class="min-w-0 flex-1 sm:max-w-2xl">
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400">
{{ t('admin.channels.form.timePricing') }}
</label>
<label class="mt-2 block text-xs text-gray-400">
{{ t('admin.channels.form.timezone') }}
</label>
<Select
:model-value="modelValue.timezone"
:options="timezoneOptions"
:aria-label="t('admin.channels.form.timezone')"
searchable
creatable
class="mt-1 w-full"
@update:model-value="updateTimezone"
/>
<div class="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="min-w-0">
<label class="block text-xs text-gray-400">
{{ t('admin.channels.form.timezone') }}
</label>
<Select
:model-value="modelValue.timezone"
:options="timezoneOptions"
:aria-label="t('admin.channels.form.timezone')"
data-testid="time-pricing-timezone"
searchable
creatable
class="mt-1 w-full"
@update:model-value="updateTimezone"
/>
</div>
<div class="min-w-0">
<label class="block text-xs text-gray-400">
{{ t('admin.channels.form.timePricingDayScope') }}
</label>
<Select
:model-value="modelValue.weekdays_only"
:options="dayScopeOptions"
:aria-label="t('admin.channels.form.timePricingDayScope')"
data-testid="time-pricing-day-scope"
class="mt-1 w-full"
@update:model-value="updateDayScope"
/>
</div>
</div>
</div>
<button
type="button"
@@ -99,7 +117,7 @@
</template>
<script setup lang="ts">
import { getCurrentInstance } from 'vue'
import { computed, getCurrentInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import Select from '@/components/common/Select.vue'
import Icon from '@/components/icons/Icon.vue'
@@ -122,10 +140,19 @@ const timezoneOptions = COMMON_TIMEZONES.map(value => {
return { value, label: offset ? `${value} (${offset})` : value }
})
const dayScopeOptions = computed(() => [
{ value: false, label: t('admin.channels.form.timePricingEveryDay') },
{ value: true, label: t('admin.channels.form.timePricingWeekdaysOnly') },
])
function updateTimezone(value: string | number | boolean | null) {
emit('update:modelValue', { ...props.modelValue, timezone: String(value ?? '') })
}
function updateDayScope(value: string | number | boolean | null) {
emit('update:modelValue', { ...props.modelValue, weekdays_only: value === true })
}
function normalizeClockTime(value: string): string {
const normalized = value.replace(/:/g, ':')
return normalized === '24:00:00' ? '00:00:00' : normalized
@@ -12,7 +12,7 @@ const SelectStub = {
name: 'Select',
props: ['modelValue', 'options'],
emits: ['update:modelValue'],
template: '<button type="button" data-testid="timezone-select" />',
template: '<button type="button" v-bind="$attrs" />',
}
describe('TimePricingSection', () => {
@@ -20,6 +20,28 @@ describe('TimePricingSection', () => {
vi.clearAllMocks()
})
it('offers every-day and weekdays-only scopes', async () => {
const value = createDefaultTimePricingForm()
const wrapper = mount(TimePricingSection, {
props: { modelValue: value },
global: { stubs: { Select: SelectStub, Icon: true } },
})
const selects = wrapper.findAllComponents(SelectStub)
expect(selects).toHaveLength(2)
expect(selects[1].props('modelValue')).toBe(false)
expect(selects[1].props('options')).toEqual([
{ value: false, label: 'admin.channels.form.timePricingEveryDay' },
{ value: true, label: 'admin.channels.form.timePricingWeekdaysOnly' },
])
selects[1].vm.$emit('update:modelValue', true)
expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toEqual({
...value,
weekdays_only: true,
})
})
it('adds a neutral period immutably', async () => {
const value = createDefaultTimePricingForm()
const wrapper = mount(TimePricingSection, {
@@ -31,6 +53,7 @@ describe('TimePricingSection', () => {
expect(wrapper.emitted('update:modelValue')?.[0]?.[0]).toEqual({
timezone: 'Asia/Shanghai',
weekdays_only: false,
periods: [{ start_time: '', end_time: '', multiplier: '1.00' }],
})
expect(value.periods).toHaveLength(0)
@@ -146,15 +146,24 @@ describe('validateIntervals', () => {
describe('time pricing', () => {
it('uses a disabled Shanghai default', () => {
const form = createDefaultTimePricingForm()
expect(form).toEqual({ timezone: 'Asia/Shanghai', periods: [] })
expect(form).toEqual({ timezone: 'Asia/Shanghai', periods: [], weekdays_only: false })
expect(formTimePricingToAPI(form)).toBeNull()
})
it('round-trips and formats multiplier', () => {
const form = apiTimePricingToForm({
it('defaults missing API day scope to every day', () => {
expect(apiTimePricingToForm({
timezone: 'Asia/Shanghai',
periods: [{ start_time: '09:00', end_time: '12:00', multiplier: 2 }],
}).weekdays_only).toBe(false)
})
it('round-trips day scope and formats multiplier', () => {
const form = apiTimePricingToForm({
timezone: 'Asia/Shanghai',
weekdays_only: true,
periods: [{ start_time: '09:00', end_time: '12:00', multiplier: 2 }],
})
expect(form.weekdays_only).toBe(true)
expect(form.periods[0]).toEqual({
start_time: '09:00:00',
end_time: '12:00:00',
@@ -162,6 +171,7 @@ describe('time pricing', () => {
})
expect(formTimePricingToAPI(form)).toEqual({
timezone: 'Asia/Shanghai',
weekdays_only: true,
periods: [{ start_time: '09:00:00', end_time: '12:00:00', multiplier: 2 }],
})
})
@@ -42,6 +42,7 @@ export interface TimePricingPeriodFormEntry {
export interface TimePricingFormEntry {
timezone: string
weekdays_only: boolean
periods: TimePricingPeriodFormEntry[]
}
@@ -66,13 +67,14 @@ export const COMMON_TIMEZONES = [
]
export function createDefaultTimePricingForm(): TimePricingFormEntry {
return { timezone: DEFAULT_TIME_PRICING_TIMEZONE, periods: [] }
return { timezone: DEFAULT_TIME_PRICING_TIMEZONE, weekdays_only: false, periods: [] }
}
export function apiTimePricingToForm(value: ChannelTimePricing | null | undefined): TimePricingFormEntry {
if (!value) return createDefaultTimePricingForm()
return {
timezone: value.timezone || DEFAULT_TIME_PRICING_TIMEZONE,
weekdays_only: value.weekdays_only === true,
periods: (value.periods || []).map(period => ({
start_time: LEGACY_CLOCK_TIME.test(period.start_time) ? `${period.start_time}:00` : period.start_time,
end_time: LEGACY_CLOCK_TIME.test(period.end_time) ? `${period.end_time}:00` : period.end_time,
@@ -86,6 +88,7 @@ export function formTimePricingToAPI(value: TimePricingFormEntry | null | undefi
const timezone = typeof value.timezone === 'string' ? value.timezone.trim() : ''
return {
timezone,
weekdays_only: value.weekdays_only === true,
periods: value.periods.map(period => ({
start_time: period.start_time,
end_time: period.end_time,
@@ -140,6 +140,9 @@ export default {
intervals: 'Context Intervals (optional)',
timePricing: 'Time-based pricing (optional)',
timezone: 'Time zone',
timePricingDayScope: 'Effective days',
timePricingEveryDay: 'Every day (Monday-Sunday, including weekends)',
timePricingWeekdaysOnly: 'Weekdays only (Monday-Friday)',
addTimePeriod: 'Add period',
startTime: 'Start time',
endTime: 'End time',
@@ -140,6 +140,9 @@ export default {
intervals: '上下文区间定价(可选)',
timePricing: '时间段定价(可选)',
timezone: '时区',
timePricingDayScope: '生效日',
timePricingEveryDay: '每日生效(周一至周日,含周末)',
timePricingWeekdaysOnly: '仅工作日生效(周一至周五)',
addTimePeriod: '添加时间段',
startTime: '开始时间',
endTime: '结束时间',