feat: 调整默认排课节奏并限制时间选择

第一节 08:00-09:00,之后从 09:30 起每小时一节至 21:30;排课时间仅可选整点或半点。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-06 20:12:52 +08:00
parent 14d7c03b05
commit 15d96c722b
7 changed files with 168 additions and 46 deletions

View File

@@ -40,9 +40,10 @@
<view class="slot-body">
<view class="time-section">
<picker
mode="time"
:value="slot.startTime"
@change="(e: any) => updateSlotTime(slot, 'startTime', e.detail.value)"
mode="multiSelector"
:range="timePickerRange"
:value="timeToPickerIndex(slot.startTime)"
@change="(e: any) => updateSlotTime(slot, 'startTime', pickerIndexToTime(e.detail.value))"
>
<view class="time-display">
<text class="time-text">{{ slot.startTime }}</text>
@@ -50,9 +51,10 @@
</picker>
<text class="time-separator"></text>
<picker
mode="time"
:value="slot.endTime"
@change="(e: any) => updateSlotTime(slot, 'endTime', e.detail.value)"
mode="multiSelector"
:range="timePickerRange"
:value="timeToPickerIndex(slot.endTime)"
@change="(e: any) => updateSlotTime(slot, 'endTime', pickerIndexToTime(e.detail.value))"
>
<view class="time-display">
<text class="time-text">{{ slot.endTime }}</text>
@@ -112,8 +114,9 @@
<view class="modal-field">
<text class="modal-label">开始时间</text>
<picker
mode="time"
:value="addForm.startTime"
mode="multiSelector"
:range="timePickerRange"
:value="timeToPickerIndex(addForm.startTime)"
@change="onAddStartTimeChange"
>
<view class="picker-display">
@@ -162,6 +165,14 @@ import { getSystemLayout } from '../../utils/system'
import { useAdminStore } from '../../stores/admin'
import { formatDate } from '../../utils/format'
import DateSelector from '../../components/DateSelector.vue'
import {
SCHEDULE_TIME_PICKER_RANGE,
timeToPickerIndex,
pickerIndexToTime,
addOneHourCapped,
} from '../../utils/schedule-time'
const timePickerRange = SCHEDULE_TIME_PICKER_RANGE
interface EditableSlot {
readonly key: string
@@ -186,8 +197,8 @@ const showAddModal = ref(false)
const editableSlots = ref<EditableSlot[]>([])
const addForm = ref({
startTime: '09:00',
endTime: '10:00',
startTime: '09:30',
endTime: '10:30',
capacityStr: '1',
})
@@ -270,23 +281,14 @@ function removeSlot(slot: EditableSlot) {
// ── Add slot ──────────────────────────────────────────────
/** 将 "HH:mm" 加一小时,最大 23:59 */
function addOneHour(time: string): string {
const [h, m] = time.split(':').map(Number)
const newH = Math.min(h + 1, 23)
// 如果原本就是 23:xx结束时间设为 23:59
if (h >= 23) return '23:59'
return String(newH).padStart(2, '0') + ':' + String(m).padStart(2, '0')
}
function onAddStartTimeChange(e: any) {
const start = e.detail.value as string
const start = pickerIndexToTime(e.detail.value as number[])
addForm.value.startTime = start
addForm.value.endTime = addOneHour(start)
addForm.value.endTime = addOneHourCapped(start)
}
function openAddModal() {
addForm.value = { startTime: '09:00', endTime: '10:00', capacityStr: '1' }
addForm.value = { startTime: '09:30', endTime: '10:30', capacityStr: '1' }
showAddModal.value = true
}
@@ -305,6 +307,10 @@ function submitAdd() {
uni.showToast({ title: '请选择时间', icon: 'none' })
return
}
if (addForm.value.startTime >= addForm.value.endTime) {
uni.showToast({ title: '结束时间须晚于开始时间', icon: 'none' })
return
}
if (isNaN(capacity) || capacity < 1) {
uni.showToast({ title: '请填写有效容量', icon: 'none' })
return

View File

@@ -28,7 +28,12 @@
</view>
<view class="form-row">
<text class="form-label">开始时间</text>
<picker mode="time" :value="addForm.startTime" @change="(e: any) => addForm.startTime = e.detail.value">
<picker
mode="multiSelector"
:range="timePickerRange"
:value="timeToPickerIndex(addForm.startTime)"
@change="(e: any) => addForm.startTime = pickerIndexToTime(e.detail.value)"
>
<view class="picker-display">
<text class="picker-text">{{ addForm.startTime || '请选择' }}</text>
<text class="picker-arrow"></text>
@@ -37,7 +42,12 @@
</view>
<view class="form-row">
<text class="form-label">结束时间</text>
<picker mode="time" :value="addForm.endTime" @change="(e: any) => addForm.endTime = e.detail.value">
<picker
mode="multiSelector"
:range="timePickerRange"
:value="timeToPickerIndex(addForm.endTime)"
@change="(e: any) => addForm.endTime = pickerIndexToTime(e.detail.value)"
>
<view class="picker-display">
<text class="picker-text">{{ addForm.endTime || '请选择' }}</text>
<text class="picker-arrow"></text>
@@ -128,7 +138,7 @@
</picker>
</view>
</view>
<text class="gen-hint">将按默认时间表每天 8:00-22:00每小时一节自动生成所选日期范围内的时段</text>
<text class="gen-hint">将按默认时间表8:00-9:00之后从 9:30 起每小时一节至 21:30自动生成所选日期范围内的时段</text>
<view class="action-wrap">
<view class="action-btn" :class="{ 'action-btn--loading': submitting }" @tap="submitGenerate">
<text class="action-btn-text">{{ submitting ? '生成中...' : '批量生成' }}</text>
@@ -145,6 +155,13 @@ import { getSystemLayout } from '../../utils/system'
import { useAdminStore } from '../../stores/admin'
import { formatDate } from '../../utils/format'
import type { TimeSlot } from '@mp-pilates/shared'
import {
SCHEDULE_TIME_PICKER_RANGE,
timeToPickerIndex,
pickerIndexToTime,
} from '../../utils/schedule-time'
const timePickerRange = SCHEDULE_TIME_PICKER_RANGE
const adminStore = useAdminStore()
@@ -157,8 +174,8 @@ const slotsLoading = ref(false)
// ── Add slot form ────────────────────────────────────────────────
const addForm = ref({
date: formatDate(new Date()),
startTime: '09:00',
endTime: '10:00',
startTime: '09:30',
endTime: '10:30',
capacityStr: '10',
})
@@ -168,6 +185,10 @@ async function submitAddSlot() {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
if (addForm.value.startTime >= addForm.value.endTime) {
uni.showToast({ title: '结束时间须晚于开始时间', icon: 'none' })
return
}
const capacity = parseInt(addForm.value.capacityStr, 10)
submitting.value = true
try {

View File

@@ -0,0 +1,67 @@
import {
DEFAULT_SCHEDULE_START_TIME,
DEFAULT_SCHEDULE_END_TIME,
SCHEDULE_MINUTE_STEP,
} from '@mp-pilates/shared'
function buildHourOptions(): string[] {
const startHour = parseInt(DEFAULT_SCHEDULE_START_TIME.slice(0, 2), 10)
const endHour = parseInt(DEFAULT_SCHEDULE_END_TIME.slice(0, 2), 10)
const list: string[] = []
for (let h = startHour; h <= endHour; h++) {
list.push(String(h).padStart(2, '0'))
}
return list
}
function buildMinuteOptions(): string[] {
const list: string[] = []
for (let m = 0; m < 60; m += SCHEDULE_MINUTE_STEP) {
list.push(String(m).padStart(2, '0'))
}
return list
}
export const SCHEDULE_HOUR_OPTIONS = buildHourOptions()
export const SCHEDULE_MINUTE_OPTIONS = buildMinuteOptions()
export const SCHEDULE_TIME_PICKER_RANGE = [SCHEDULE_HOUR_OPTIONS, SCHEDULE_MINUTE_OPTIONS]
export function timeToMinutes(time: string): number {
const [h, m] = time.split(':').map(Number)
return h * 60 + (m || 0)
}
export function minutesToTime(total: number): string {
const h = Math.floor(total / 60)
const m = total % 60
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
}
export function timeToPickerIndex(time: string): number[] {
let total = timeToMinutes(time || DEFAULT_SCHEDULE_START_TIME)
const minTotal = timeToMinutes(DEFAULT_SCHEDULE_START_TIME)
const maxTotal = timeToMinutes(DEFAULT_SCHEDULE_END_TIME)
total = Math.max(minTotal, Math.min(maxTotal, total))
total = Math.round(total / SCHEDULE_MINUTE_STEP) * SCHEDULE_MINUTE_STEP
total = Math.max(minTotal, Math.min(maxTotal, total))
const hour = String(Math.floor(total / 60)).padStart(2, '0')
const minute = String(total % 60).padStart(2, '0')
return [
Math.max(0, SCHEDULE_HOUR_OPTIONS.indexOf(hour)),
Math.max(0, SCHEDULE_MINUTE_OPTIONS.indexOf(minute)),
]
}
export function pickerIndexToTime(value: number[]): string {
const hour = SCHEDULE_HOUR_OPTIONS[value[0]] ?? SCHEDULE_HOUR_OPTIONS[0]
const minute = SCHEDULE_MINUTE_OPTIONS[value[1]] ?? SCHEDULE_MINUTE_OPTIONS[0]
return `${hour}:${minute}`
}
/** 将 "HH:mm" 加一小时,不超过当日最晚结束时间 21:30 */
export function addOneHourCapped(time: string): string {
const next = timeToMinutes(time) + 60
const cap = timeToMinutes(DEFAULT_SCHEDULE_END_TIME)
return minutesToTime(Math.min(next, cap))
}