feat: 预约组件及半小时时间工具更新

This commit is contained in:
MaDaLei 2026-04-14 22:12:07 +08:00
parent dabe0a5fe0
commit a0b636a297
4 changed files with 150 additions and 17 deletions

View File

@ -68,7 +68,13 @@
<text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text> <text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text>
</view> </view>
</picker> </picker>
<picker mode="time" :value="appointmentTime" @change="onAppointmentTimeOnlyChange" class="c-time-half"> <picker
mode="selector"
:range="HALF_HOUR_TIME_SLOTS"
:value="halfHourTimeIndex"
@change="onAppointmentTimeOnlyChange"
class="c-time-half"
>
<view class="c-input c-picker"> <view class="c-input c-picker">
<text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text> <text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text>
</view> </view>
@ -90,6 +96,12 @@ import { createAppointment, getServiceTypeList, getPetList, getStoreList } from
import AppIcon from '../../components/AppIcon.vue' import AppIcon from '../../components/AppIcon.vue'
import { getUserSession, getStoreSession, setStoreSession } from '../../utils/session.js' import { getUserSession, getStoreSession, setStoreSession } from '../../utils/session.js'
import { haversineKm } from '../../utils/geo.js' import { haversineKm } from '../../utils/geo.js'
import {
HALF_HOUR_TIME_SLOTS,
normalizeToHalfHour,
halfHourPickerIndex,
suggestHalfHourForYMD
} from '../../utils/halfHourTime.js'
const userInfo = getUserSession() const userInfo = getUserSession()
const storeInfo = getStoreSession() const storeInfo = getStoreSession()
@ -239,17 +251,25 @@ const appointmentDate = computed(() => {
const appointmentTime = computed(() => { const appointmentTime = computed(() => {
const raw = newAppt.value.appointmentTime || '' const raw = newAppt.value.appointmentTime || ''
if (!raw.includes('T')) return '' if (!raw.includes('T')) return ''
return (raw.split('T')[1] || '').slice(0, 5) const part = (raw.split('T')[1] || '').slice(0, 5)
return normalizeToHalfHour(part)
}) })
const halfHourTimeIndex = computed(() => halfHourPickerIndex(appointmentTime.value))
const onAppointmentDateChange = (e) => { const onAppointmentDateChange = (e) => {
const date = e?.detail?.value || '' const date = e?.detail?.value || ''
const time = appointmentTime.value || '00:00' if (!date) {
newAppt.value.appointmentTime = date ? `${date}T${time}` : '' newAppt.value.appointmentTime = ''
return
}
const time = normalizeToHalfHour(suggestHalfHourForYMD(date))
newAppt.value.appointmentTime = `${date}T${time}`
} }
const onAppointmentTimeOnlyChange = (e) => { const onAppointmentTimeOnlyChange = (e) => {
const time = e?.detail?.value || '' const idx = Number(e.detail.value)
const time = HALF_HOUR_TIME_SLOTS[idx]
const date = appointmentDate.value const date = appointmentDate.value
if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return } if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return }
newAppt.value.appointmentTime = `${date}T${time}` newAppt.value.appointmentTime = `${date}T${time}`

View File

@ -147,7 +147,13 @@
<text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text> <text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text>
</view> </view>
</picker> </picker>
<picker mode="time" :value="appointmentTime" @change="onAppointmentTimeOnlyChange" class="c-time-half"> <picker
mode="selector"
:range="HALF_HOUR_TIME_SLOTS"
:value="halfHourTimeIndex"
@change="onAppointmentTimeOnlyChange"
class="c-time-half"
>
<view class="c-input c-picker"> <view class="c-input c-picker">
<text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text> <text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text>
</view> </view>
@ -182,6 +188,12 @@ import { getUserSession, getStoreSession } from '../../utils/session.js'
import { useNavigator } from '../../composables/useNavigator.js' import { useNavigator } from '../../composables/useNavigator.js'
import { formatDateTimeCN } from '../../utils/datetime.js' import { formatDateTimeCN } from '../../utils/datetime.js'
import { getAppointmentStatusText } from '../../utils/appointment.js' import { getAppointmentStatusText } from '../../utils/appointment.js'
import {
HALF_HOUR_TIME_SLOTS,
normalizeToHalfHour,
halfHourPickerIndex,
suggestHalfHourForYMD
} from '../../utils/halfHourTime.js'
const userInfo = getUserSession() const userInfo = getUserSession()
const storeInfo = getStoreSession() const storeInfo = getStoreSession()
@ -225,9 +237,12 @@ const appointmentDate = computed(() => {
const appointmentTime = computed(() => { const appointmentTime = computed(() => {
const raw = newAppt.value.appointmentTime || '' const raw = newAppt.value.appointmentTime || ''
if (!raw.includes('T')) return '' if (!raw.includes('T')) return ''
return (raw.split('T')[1] || '').slice(0, 5) const part = (raw.split('T')[1] || '').slice(0, 5)
return normalizeToHalfHour(part)
}) })
const halfHourTimeIndex = computed(() => halfHourPickerIndex(appointmentTime.value))
const filteredOrders = computed(() => orders.value.filter(o => { const filteredOrders = computed(() => orders.value.filter(o => {
if (currentStatus.value === 'new') return o.status === 'new' if (currentStatus.value === 'new') return o.status === 'new'
if (currentStatus.value === 'doing') return o.status === 'doing' if (currentStatus.value === 'doing') return o.status === 'doing'
@ -237,12 +252,17 @@ const filteredOrders = computed(() => orders.value.filter(o => {
const onAppointmentDateChange = (e) => { const onAppointmentDateChange = (e) => {
const date = e?.detail?.value || '' const date = e?.detail?.value || ''
const time = appointmentTime.value || '00:00' if (!date) {
newAppt.value.appointmentTime = date ? `${date}T${time}` : '' newAppt.value.appointmentTime = ''
return
}
const time = normalizeToHalfHour(suggestHalfHourForYMD(date))
newAppt.value.appointmentTime = `${date}T${time}`
} }
const onAppointmentTimeOnlyChange = (e) => { const onAppointmentTimeOnlyChange = (e) => {
const time = e?.detail?.value || '' const idx = Number(e.detail.value)
const time = HALF_HOUR_TIME_SLOTS[idx]
const date = appointmentDate.value const date = appointmentDate.value
if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return } if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return }
newAppt.value.appointmentTime = `${date}T${time}` newAppt.value.appointmentTime = `${date}T${time}`

View File

@ -33,7 +33,13 @@
<text :class="{ placeholder: !serviceDate }">{{ serviceDate || '日期' }}</text> <text :class="{ placeholder: !serviceDate }">{{ serviceDate || '日期' }}</text>
</view> </view>
</picker> </picker>
<picker mode="time" :value="serviceTimePart" @change="onServiceTimeChange" class="time-picker"> <picker
mode="selector"
:range="HALF_HOUR_TIME_SLOTS"
:value="halfHourServiceTimeIndex"
@change="onServiceTimeChange"
class="time-picker"
>
<view class="form-input form-picker"> <view class="form-input form-picker">
<text :class="{ placeholder: !serviceTimePart }">{{ serviceTimePart || '时间' }}</text> <text :class="{ placeholder: !serviceTimePart }">{{ serviceTimePart || '时间' }}</text>
</view> </view>
@ -117,6 +123,13 @@ import TabBar from '../../components/TabBar.vue'
import AppIcon from '../../components/AppIcon.vue' import AppIcon from '../../components/AppIcon.vue'
import { getUserSession, getStoreSession } from '../../utils/session.js' import { getUserSession, getStoreSession } from '../../utils/session.js'
import { useNavigator } from '../../composables/useNavigator.js' import { useNavigator } from '../../composables/useNavigator.js'
import {
HALF_HOUR_TIME_SLOTS,
DEFAULT_HALF_HOUR_TIME,
normalizeToHalfHour,
halfHourPickerIndex,
suggestHalfHourForYMD
} from '../../utils/halfHourTime.js'
const userInfo = getUserSession() const userInfo = getUserSession()
const storeInfo = getStoreSession() const storeInfo = getStoreSession()
@ -196,17 +209,25 @@ const serviceDate = computed(() => {
const serviceTimePart = computed(() => { const serviceTimePart = computed(() => {
const raw = report.value.appointmentTime || '' const raw = report.value.appointmentTime || ''
if (!raw.includes('T')) return '' if (!raw.includes('T')) return ''
return (raw.split('T')[1] || '').slice(0, 5) const part = (raw.split('T')[1] || '').slice(0, 5)
return normalizeToHalfHour(part)
}) })
const halfHourServiceTimeIndex = computed(() => halfHourPickerIndex(serviceTimePart.value))
const onServiceDateChange = (e) => { const onServiceDateChange = (e) => {
const date = e?.detail?.value || '' const date = e?.detail?.value || ''
const time = serviceTimePart.value || '00:00' if (!date) {
report.value.appointmentTime = date ? `${date}T${time}` : '' report.value.appointmentTime = ''
return
}
const time = normalizeToHalfHour(suggestHalfHourForYMD(date))
report.value.appointmentTime = `${date}T${time}`
} }
const onServiceTimeChange = (e) => { const onServiceTimeChange = (e) => {
const time = e?.detail?.value || '' const idx = Number(e.detail.value)
const time = HALF_HOUR_TIME_SLOTS[idx]
const date = serviceDate.value const date = serviceDate.value
if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return } if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return }
report.value.appointmentTime = `${date}T${time}` report.value.appointmentTime = `${date}T${time}`
@ -262,8 +283,8 @@ onMounted(async () => {
const raw = String(prefill.appointmentTime) const raw = String(prefill.appointmentTime)
if (raw.includes('T')) { if (raw.includes('T')) {
const [d, rest] = raw.split('T') const [d, rest] = raw.split('T')
const hm = (rest || '').match(/^(\d{1,2}:\d{2})/)?.[1] || '00:00' const hm = (rest || '').match(/^(\d{1,2}:\d{2})/)?.[1] || DEFAULT_HALF_HOUR_TIME
report.value.appointmentTime = `${d}T${hm}` report.value.appointmentTime = `${d}T${normalizeToHalfHour(hm)}`
} }
} }
uni.removeStorageSync('petstore_report_prefill') uni.removeStorageSync('petstore_report_prefill')

72
src/utils/halfHourTime.js Normal file
View File

@ -0,0 +1,72 @@
/** 未选时间时的默认展示与选中日期的默认时刻 */
export const DEFAULT_HALF_HOUR_TIME = '09:00'
/** 全天半小时间隔00:00 … 23:30 */
export const HALF_HOUR_TIME_SLOTS = (() => {
const a = []
for (let h = 0; h < 24; h++) {
const p = (n) => String(n).padStart(2, '0')
a.push(`${p(h)}:00`)
a.push(`${p(h)}:30`)
}
return a
})()
/**
* HH:mm 规范到最近的半点档空值或无法解析时返回 {@link DEFAULT_HALF_HOUR_TIME}
*/
export function normalizeToHalfHour(hhmm) {
if (hhmm == null || hhmm === '') return DEFAULT_HALF_HOUR_TIME
if (typeof hhmm !== 'string') return DEFAULT_HALF_HOUR_TIME
const m = hhmm.match(/^(\d{1,2}):(\d{2})$/)
if (!m) return DEFAULT_HALF_HOUR_TIME
let h = parseInt(m[1], 10)
let mi = parseInt(m[2], 10)
if (Number.isNaN(h) || Number.isNaN(mi)) return DEFAULT_HALF_HOUR_TIME
h = Math.min(23, Math.max(0, h))
mi = Math.min(59, Math.max(0, mi))
const total = h * 60 + mi
const snapped = Math.min(1410, Math.round(total / 30) * 30)
const H = Math.floor(snapped / 60)
const M = snapped % 60
const key = `${String(H).padStart(2, '0')}:${String(M).padStart(2, '0')}`
return HALF_HOUR_TIME_SLOTS.includes(key) ? key : DEFAULT_HALF_HOUR_TIME
}
/** 供 picker mode=selector 的 :value索引 */
export function halfHourPickerIndex(hhmm) {
const n = normalizeToHalfHour(hhmm)
const i = HALF_HOUR_TIME_SLOTS.indexOf(n)
return i >= 0 ? i : 0
}
const pad2 = (n) => String(n).padStart(2, '0')
/** 本地日历 YYYY-MM-DD */
export function ymdLocal(d = new Date()) {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
}
/**
* 当前时刻之后的下一个半点档含恰落在整点/半点时取该档
* 若已超过 23:30返回 null
*/
export function getNextHalfHourAfterNow(now = new Date()) {
const total = now.getHours() * 60 + now.getMinutes() + now.getSeconds() / 60
let slotM = Math.ceil(total / 30) * 30
if (slotM >= 24 * 60) return null
const H = Math.floor(slotM / 60)
const M = slotM % 60
return `${pad2(H)}:${pad2(M)}`
}
/**
* 选中日期的默认时刻当天 当前时刻往后的下一个半点档其它日期 09:00
* 当天已无可用半点时理论上仅深夜退回 23:30
*/
export function suggestHalfHourForYMD(dateStr) {
if (!dateStr || typeof dateStr !== 'string') return DEFAULT_HALF_HOUR_TIME
if (dateStr !== ymdLocal(new Date())) return DEFAULT_HALF_HOUR_TIME
const next = getNextHalfHourAfterNow()
return next != null ? next : '23:30'
}