feat: 预约组件及半小时时间工具更新
This commit is contained in:
parent
dabe0a5fe0
commit
a0b636a297
@ -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}`
|
||||||
|
|||||||
@ -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}`
|
||||||
|
|||||||
@ -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
72
src/utils/halfHourTime.js
Normal 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'
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user