feat: book against service duration

This commit is contained in:
malei 2026-08-01 22:56:31 +08:00
parent 9a12ee07b0
commit d29ece3a85
7 changed files with 117 additions and 32 deletions

View File

@ -86,9 +86,9 @@ export const getAppointmentDetail = (id) => get('/appointment/detail', { id })
// 创建预约登录人身份始终由后端派生customer 传选中门店,门店代客预约传 customerUserId 或 customerPhone // 创建预约登录人身份始终由后端派生customer 传选中门店,门店代客预约传 customerUserId 或 customerPhone
export const createAppointment = (data) => post('/appointment/create', data) export const createAppointment = (data) => post('/appointment/create', data)
/** 门店某日可预约半小时档(可约 / 已满 / 已过);公开接口 */ /** 门店某日可预约开始时段;按服务时长与门店容量计算,公开接口。 */
export const getAppointmentAvailableSlots = (storeId, date) => export const getAppointmentAvailableSlots = (storeId, date, serviceType) =>
get('/appointment/available-slots', { storeId, date }) get('/appointment/available-slots', { storeId, date, serviceType })
// 开始服务staffUserId 由后端从上下文派生;保留旧签名兼容,不传 staffUserId // 开始服务staffUserId 由后端从上下文派生;保留旧签名兼容,不传 staffUserId
export const startAppointment = (appointmentId/*, staffUserId */) => export const startAppointment = (appointmentId/*, staffUserId */) =>
@ -147,8 +147,9 @@ export const getServiceTypeList = (storeId) => {
return get('/service-type/list', params) return get('/service-type/list', params)
} }
// 创建服务类型 // 创建服务类型storeId 仅保留旧签名兼容,后端从登录态派生)
export const createServiceType = (storeId, name) => post('/service-type/create', { storeId, name }) export const createServiceType = (storeId, name, durationMinutes) =>
post('/service-type/create', { storeId, name, durationMinutes })
// 删除服务类型 // 删除服务类型
export const deleteServiceType = (id) => del(`/service-type/delete?id=${id}`) export const deleteServiceType = (id) => del(`/service-type/delete?id=${id}`)

View File

@ -38,6 +38,10 @@
<text class="label">预约时间</text> <text class="label">预约时间</text>
<text class="value time">{{ timeDisplay }}</text> <text class="value time">{{ timeDisplay }}</text>
</view> </view>
<view v-if="detail.endTime" class="detail-row">
<text class="label">预计结束</text>
<text class="value time">{{ endTimeDisplay }} {{ detail.durationMinutes || 60 }} 分钟</text>
</view>
<view v-if="detail.remark" class="detail-row block"> <view v-if="detail.remark" class="detail-row block">
<text class="label">备注</text> <text class="label">备注</text>
<text class="value remark">{{ detail.remark }}</text> <text class="value remark">{{ detail.remark }}</text>
@ -84,6 +88,7 @@ const navSafeStyle = (() => {
const statusText = computed(() => (detail.value ? getAppointmentStatusText(detail.value.status) : '')) const statusText = computed(() => (detail.value ? getAppointmentStatusText(detail.value.status) : ''))
const timeDisplay = computed(() => formatDateTimeCN(detail.value?.appointmentTime)) const timeDisplay = computed(() => formatDateTimeCN(detail.value?.appointmentTime))
const endTimeDisplay = computed(() => formatDateTimeCN(detail.value?.endTime))
const tagClass = (status) => getAppointmentTagClass(status) const tagClass = (status) => getAppointmentTagClass(status)

View File

@ -73,7 +73,7 @@
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">服务类型</text> <text class="app-form-label">服务类型</text>
<picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value"> <picker mode="selector" :range="serviceTypes" range-key="label" @change="onServiceTypeChange">
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'app-form-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text>
<text class="app-form-arrow"></text> <text class="app-form-arrow"></text>
@ -330,8 +330,9 @@ const creatingAppt = ref(false)
/** 与日期选择器同步,避免无号源时丢失已选日期 */ /** 与日期选择器同步,避免无号源时丢失已选日期 */
const selectedDateStr = ref(ymdLocal()) const selectedDateStr = ref(ymdLocal())
const availableTimes = ref([]) const availableTimes = ref([])
const availableSlotEnds = ref({})
const loadingSlots = ref(false) const loadingSlots = ref(false)
const bookingWindowHint = ref('请选择半小时时间段;同一时段仅接待一单,已满或已过期不可选') const bookingWindowHint = ref('请选择服务后查看可预约开始时间;系统会按服务时长校验完整容量')
const normalizeText = (s) => (s || '').toString().trim().toLowerCase() const normalizeText = (s) => (s || '').toString().trim().toLowerCase()
@ -397,11 +398,13 @@ const slotPickerIndex = computed(() => {
/** 滚轮选项展示为「09:0009:30」 */ /** 滚轮选项展示为「09:0009:30」 */
const availableSlotLabels = computed(() => const availableSlotLabels = computed(() =>
availableTimes.value.map((t) => formatHalfHourSlotRange(t)) availableTimes.value.map((t) => `${t}${availableSlotEnds.value[t] || formatHalfHourSlotRange(t).slice(6)}`)
) )
const selectedSlotRangeLabel = computed(() => const selectedSlotRangeLabel = computed(() =>
appointmentTime.value ? formatHalfHourSlotRange(appointmentTime.value) : '' appointmentTime.value
? `${appointmentTime.value}${availableSlotEnds.value[appointmentTime.value] || formatHalfHourSlotRange(appointmentTime.value).slice(6)}`
: ''
) )
const showTryTomorrow = computed( const showTryTomorrow = computed(
@ -433,20 +436,24 @@ const loadAvailableSlots = async () => {
} }
loadingSlots.value = true loadingSlots.value = true
try { try {
const res = await getAppointmentAvailableSlots(storeId, date) const res = await getAppointmentAvailableSlots(storeId, date, newAppt.value.serviceType || undefined)
loadingSlots.value = false loadingSlots.value = false
if (res.code !== 200 || !res.data?.slots) { if (res.code !== 200 || !res.data?.slots) {
availableTimes.value = [] availableTimes.value = []
availableSlotEnds.value = {}
uni.showToast({ title: res.message || '号源加载失败', icon: 'none' }) uni.showToast({ title: res.message || '号源加载失败', icon: 'none' })
return return
} }
const ds = res.data.dayStart const ds = res.data.dayStart
const le = res.data.lastSlotStart const closing = res.data.closingTime
if (ds != null && le != null) { if (ds != null && closing != null) {
const a = String(ds).slice(0, 5) const a = String(ds).slice(0, 5)
const b = String(le).slice(0, 5) const b = String(closing).slice(0, 5)
bookingWindowHint.value = `本店可约 ${a}${b};以下为半小时时段,同一时段仅一单,已满或已过不可选` const mins = Number(res.data.durationMinutes || 60)
const capacity = Number(res.data.bookingCapacity || 1)
bookingWindowHint.value = `营业容量 ${a}${b};本服务约 ${mins} 分钟,门店并发接待 ${capacity}`
} }
availableSlotEnds.value = Object.fromEntries(res.data.slots.map((s) => [s.time, s.endTime]))
availableTimes.value = res.data.slots.filter((s) => s.available).map((s) => s.time) availableTimes.value = res.data.slots.filter((s) => s.available).map((s) => s.time)
if (!availableTimes.value.length) { if (!availableTimes.value.length) {
newAppt.value.appointmentTime = '' newAppt.value.appointmentTime = ''
@ -466,6 +473,7 @@ const loadAvailableSlots = async () => {
} catch (_) { } catch (_) {
loadingSlots.value = false loadingSlots.value = false
availableTimes.value = [] availableTimes.value = []
availableSlotEnds.value = {}
uni.showToast({ title: '号源加载失败', icon: 'none' }) uni.showToast({ title: '号源加载失败', icon: 'none' })
} }
} }
@ -494,12 +502,25 @@ const onAppointmentTimeOnlyChange = (e) => {
newAppt.value.appointmentTime = `${date}T${time}` newAppt.value.appointmentTime = `${date}T${time}`
} }
const onServiceTypeChange = (e) => {
const selected = serviceTypes.value[Number(e.detail.value)]
newAppt.value.serviceType = selected?.value || ''
loadAvailableSlots()
}
const loadServiceTypesForSelected = async () => { const loadServiceTypesForSelected = async () => {
const row = orderedStores.value[selectedStoreIndex.value] const row = orderedStores.value[selectedStoreIndex.value]
const sid = row?.id const sid = row?.id
const res = await getServiceTypeList(sid ?? undefined) const res = await getServiceTypeList(sid ?? undefined)
if (res.code === 200 && Array.isArray(res.data)) { if (res.code === 200 && Array.isArray(res.data)) {
serviceTypes.value = res.data.map((s) => ({ label: s.name, value: s.name })) serviceTypes.value = res.data.map((s) => ({
label: `${s.name} · 约${Number(s.durationMinutes || 60)}分钟`,
value: s.name,
durationMinutes: Number(s.durationMinutes || 60)
}))
if (newAppt.value.serviceType && !serviceTypes.value.some((s) => s.value === newAppt.value.serviceType)) {
newAppt.value.serviceType = ''
}
if (sid) { if (sid) {
try { try {
const last = uni.getStorageSync(lastServiceStorageKey(sid)) const last = uni.getStorageSync(lastServiceStorageKey(sid))

View File

@ -228,7 +228,7 @@
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">服务类型</text> <text class="app-form-label">服务类型</text>
<picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value"> <picker mode="selector" :range="serviceTypes" range-key="label" @change="onStaffServiceTypeChange">
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'app-form-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text>
<text class="app-form-arrow"></text> <text class="app-form-arrow"></text>
@ -256,7 +256,7 @@
</view> </view>
</picker> </picker>
</view> </view>
<text class="c-store-hint home-slot-hint">每行半小时时段约满不可选</text> <text class="c-store-hint home-slot-hint">按服务时长与门店并发容量计算约满不可选</text>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">备注可选</text> <text class="app-form-label">备注可选</text>
@ -421,6 +421,7 @@ const appointmentTime = computed(() => {
/** B 端弹窗:按半小时档选可约时段 */ /** B 端弹窗:按半小时档选可约时段 */
const staffApptDateStr = ref('') const staffApptDateStr = ref('')
const staffAvailableTimes = ref([]) const staffAvailableTimes = ref([])
const staffAvailableSlotEnds = ref({})
const staffLoadingSlots = ref(false) const staffLoadingSlots = ref(false)
const staffSlotPickerIndex = computed(() => { const staffSlotPickerIndex = computed(() => {
@ -430,11 +431,13 @@ const staffSlotPickerIndex = computed(() => {
}) })
const staffAvailableSlotLabels = computed(() => const staffAvailableSlotLabels = computed(() =>
staffAvailableTimes.value.map((t) => formatHalfHourSlotRange(t)) staffAvailableTimes.value.map((t) => `${t}${staffAvailableSlotEnds.value[t] || formatHalfHourSlotRange(t).slice(6)}`)
) )
const staffSelectedSlotRangeLabel = computed(() => const staffSelectedSlotRangeLabel = computed(() =>
appointmentTime.value ? formatHalfHourSlotRange(appointmentTime.value) : '' appointmentTime.value
? `${appointmentTime.value}${staffAvailableSlotEnds.value[appointmentTime.value] || formatHalfHourSlotRange(appointmentTime.value).slice(6)}`
: ''
) )
const filteredOrders = computed(() => orders.value.filter(o => { const filteredOrders = computed(() => orders.value.filter(o => {
@ -525,12 +528,14 @@ const loadStaffAvailableSlots = async () => {
staffApptDateStr.value = date staffApptDateStr.value = date
staffLoadingSlots.value = true staffLoadingSlots.value = true
try { try {
const res = await getAppointmentAvailableSlots(storeInfo.id, date) const res = await getAppointmentAvailableSlots(storeInfo.id, date, newAppt.value.serviceType || undefined)
staffLoadingSlots.value = false staffLoadingSlots.value = false
if (res.code !== 200 || !res.data?.slots) { if (res.code !== 200 || !res.data?.slots) {
staffAvailableTimes.value = [] staffAvailableTimes.value = []
staffAvailableSlotEnds.value = {}
return return
} }
staffAvailableSlotEnds.value = Object.fromEntries(res.data.slots.map((s) => [s.time, s.endTime]))
staffAvailableTimes.value = res.data.slots.filter((s) => s.available).map((s) => s.time) staffAvailableTimes.value = res.data.slots.filter((s) => s.available).map((s) => s.time)
if (!staffAvailableTimes.value.length) { if (!staffAvailableTimes.value.length) {
newAppt.value.appointmentTime = '' newAppt.value.appointmentTime = ''
@ -550,6 +555,7 @@ const loadStaffAvailableSlots = async () => {
} catch (_) { } catch (_) {
staffLoadingSlots.value = false staffLoadingSlots.value = false
staffAvailableTimes.value = [] staffAvailableTimes.value = []
staffAvailableSlotEnds.value = {}
} }
} }
@ -559,6 +565,12 @@ const openStaffNewApptDialog = async () => {
await loadStaffAvailableSlots() await loadStaffAvailableSlots()
} }
const onStaffServiceTypeChange = (e) => {
const selected = serviceTypes.value[Number(e.detail.value)]
newAppt.value.serviceType = selected?.value || ''
loadStaffAvailableSlots()
}
const onStaffApptDateChange = (e) => { const onStaffApptDateChange = (e) => {
const date = e?.detail?.value || '' const date = e?.detail?.value || ''
if (!date) { if (!date) {
@ -654,7 +666,11 @@ const loadServiceTypes = async () => {
if (!storeInfo.id) return if (!storeInfo.id) return
const res = await getServiceTypeList(storeInfo.id) const res = await getServiceTypeList(storeInfo.id)
if (res.code === 200) { if (res.code === 200) {
serviceTypes.value = res.data.map(s => ({ label: s.name, value: s.name })) serviceTypes.value = res.data.map(s => ({
label: `${s.name} · 约${Number(s.durationMinutes || 60)}分钟`,
value: s.name,
durationMinutes: Number(s.durationMinutes || 60)
}))
} }
} }

View File

@ -17,7 +17,7 @@
</view> </view>
<view class="sched-hero-text"> <view class="sched-hero-text">
<text class="sched-hero-title">时段一览</text> <text class="sched-hero-title">时段一览</text>
<text class="sched-hero-desc">绿色为客户预约橙色为到店占用灰色为暂停线上预约点空白格可手动占用点占用可取消</text> <text class="sched-hero-desc">绿色为客户预约橙色为到店占用灰色为暂停线上预约每行显示已用/总容量</text>
</view> </view>
</view> </view>
@ -48,7 +48,7 @@
<view class="sched-main"> <view class="sched-main">
<template v-if="row.kind === 'appointment' && row.appointment"> <template v-if="row.kind === 'appointment' && row.appointment">
<text class="sched-line-title">{{ row.appointment.petName || '宠物' }} · {{ row.appointment.serviceType || '服务' }}</text> <text class="sched-line-title">{{ row.appointment.petName || '宠物' }} · {{ row.appointment.serviceType || '服务' }}</text>
<text class="sched-line-sub">{{ apptStatusText(row.appointment.status) }}</text> <text class="sched-line-sub">{{ apptStatusText(row.appointment.status) }} · 容量 {{ row.usedCapacity || 0 }}/{{ row.totalCapacity || 1 }}</text>
</template> </template>
<template v-else-if="row.kind === 'block' && row.block"> <template v-else-if="row.kind === 'block' && row.block">
<text class="sched-line-title">{{ blockTitle(row.block) }}</text> <text class="sched-line-title">{{ blockTitle(row.block) }}</text>
@ -61,7 +61,7 @@
</view> </view>
</view> </view>
<view class="sched-foot-hint">营业时间一致每半小时一档占用后客户端该档不可再约</view> <view class="sched-foot-hint">营业容量时段一致服务和到店占用会跨半小时桶消耗名额暂停预约会关闭全部名额</view>
</view> </view>
</view> </view>
</template> </template>
@ -212,6 +212,7 @@ function onRowTap(row) {
const res = await createScheduleBlock({ const res = await createScheduleBlock({
storeId: storeInfo.id, storeId: storeInfo.id,
slotStart, slotStart,
durationMinutes: 30,
blockType, blockType,
note: '', note: '',
createdByUserId: userInfo.id createdByUserId: userInfo.id

View File

@ -37,6 +37,7 @@
<view class="st-name-row"> <view class="st-name-row">
<span class="st-dot"><AppIcon name="service" :size="14" /></span> <span class="st-dot"><AppIcon name="service" :size="14" /></span>
<text class="st-name">{{ s.name }}</text> <text class="st-name">{{ s.name }}</text>
<text class="st-duration"> {{ s.durationMinutes || 60 }} 分钟</text>
<view v-if="!s.storeId" class="van-tag van-tag--success van-tag--small system-tag">系统默认</view> <view v-if="!s.storeId" class="van-tag van-tag--success van-tag--small system-tag">系统默认</view>
</view> </view>
</view> </view>
@ -58,6 +59,13 @@
<view class="popup-desc">建议使用简洁明确的命名方便前台下单与报告展示</view> <view class="popup-desc">建议使用简洁明确的命名方便前台下单与报告展示</view>
<view class="field-label">服务类型名称</view> <view class="field-label">服务类型名称</view>
<input v-model="newName" class="van-field" placeholder="请输入" /> <input v-model="newName" class="van-field" placeholder="请输入" />
<view class="field-label st-duration-label">预计服务时长</view>
<picker mode="selector" :range="DURATION_LABELS" :value="durationPickerIndex" @change="onDurationChange">
<view class="app-form-input app-form-picker">
<text>{{ newDuration }} 分钟</text>
<text class="app-form-arrow"></text>
</view>
</picker>
</view> </view>
<view class="popup-footer"> <view class="popup-footer">
<view class="popup-actions"> <view class="popup-actions">
@ -71,7 +79,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import AppIcon from '../../components/AppIcon.vue' import AppIcon from '../../components/AppIcon.vue'
import { navigateTo } from '../../utils/globalState.js' import { navigateTo } from '../../utils/globalState.js'
import { getServiceTypeList, createServiceType, deleteServiceType } from '../../api/index.js' import { getServiceTypeList, createServiceType, deleteServiceType } from '../../api/index.js'
@ -102,6 +110,10 @@ const navSafeStyle = (() => {
const serviceTypes = ref([]) const serviceTypes = ref([])
const showNew = ref(false) const showNew = ref(false)
const newName = ref('') const newName = ref('')
const DURATION_OPTIONS = Array.from({ length: 16 }, (_, i) => (i + 1) * 30)
const DURATION_LABELS = DURATION_OPTIONS.map((minutes) => `${minutes} 分钟`)
const newDuration = ref(60)
const durationPickerIndex = computed(() => Math.max(0, DURATION_OPTIONS.indexOf(newDuration.value)))
const swipedId = ref(null) const swipedId = ref(null)
const touchStartX = ref(0) const touchStartX = ref(0)
const touchCurrentX = ref(0) const touchCurrentX = ref(0)
@ -111,12 +123,17 @@ const load = async () => {
if (res.code === 200) serviceTypes.value = res.data if (res.code === 200) serviceTypes.value = res.data
} }
const onDurationChange = (e) => {
newDuration.value = DURATION_OPTIONS[Number(e.detail.value)] || 60
}
const confirmAdd = async () => { const confirmAdd = async () => {
if (!newName.value) { uni.showToast({ title: '请输入服务类型名称', icon: 'none' }); return } if (!newName.value) { uni.showToast({ title: '请输入服务类型名称', icon: 'none' }); return }
const res = await createServiceType(storeInfo.id, newName.value) const res = await createServiceType(storeInfo.id, newName.value, newDuration.value)
if (res.code === 200) { if (res.code === 200) {
uni.showToast({ title: '添加成功', icon: 'success' }) uni.showToast({ title: '添加成功', icon: 'success' })
newName.value = '' newName.value = ''
newDuration.value = 60
showNew.value = false showNew.value = false
load() load()
} else { } else {
@ -193,6 +210,8 @@ onMounted(() => load())
background: var(--c-slate-100); background: var(--c-slate-100);
} }
.st-name { font-size: 15px; font-weight: 600; color: var(--c-gray-800); } .st-name { font-size: 15px; font-weight: 600; color: var(--c-gray-800); }
.st-duration { margin-left: auto; font-size: 12px; color: var(--c-slate-500); }
.st-duration-label { margin-top: 14px; }
.system-tag { margin-left: 8px; } .system-tag { margin-left: 8px; }
/* 与员工管理页一致:删除区绝对贴底,主内容盖住;左滑才露出 */ /* 与员工管理页一致:删除区绝对贴底,主内容盖住;左滑才露出 */
.st-swipe-wrap { .st-swipe-wrap {

View File

@ -13,7 +13,7 @@
</view> </view>
<view class="store-hero-text"> <view class="store-hero-text">
<text class="store-hero-title">门店资料</text> <text class="store-hero-title">门店资料</text>
<text class="store-hero-desc">在此维护门店名称电话地址与简介并设置营业时间营业时间决定客户线上可预约的时段范围每半小时为一个时段同一时段仅一单</text> <text class="store-hero-desc">在此维护门店资料营业容量时段与并发接待数系统会按服务时长检查每个半小时容量桶</text>
</view> </view>
</view> </view>
@ -68,7 +68,7 @@
<view class="store-card store-card--hours store-card--after-basic"> <view class="store-card store-card--hours store-card--after-basic">
<view class="store-card-head"> <view class="store-card-head">
<text class="store-card-title">营业时间</text> <text class="store-card-title">营业时间</text>
<text class="store-card-sub">客户可预约时段为开始结束之间的半点档每半小时为一档每档最多一单已满或已过不可选</text> <text class="store-card-sub">服务必须在开始至结束之间完成线上预约和到店占用共同消耗并发名额</text>
</view> </view>
<view class="store-field"> <view class="store-field">
<view class="store-field-label"> <view class="store-field-label">
@ -83,12 +83,12 @@
</view> </view>
</picker> </picker>
</view> </view>
<view class="store-field store-field-last"> <view class="store-field">
<view class="store-field-label"> <view class="store-field-label">
<span class="store-ico"><AppIcon name="service" :size="15" :color="SLATE_600" /></span> <span class="store-ico"><AppIcon name="service" :size="15" :color="SLATE_600" /></span>
<text>结束时间</text> <text>结束时间</text>
</view> </view>
<text class="store-field-tip">当日最后一个可预约的半点起始时刻例如 21:30 表示最晚可约 21:0021:30 这一档</text> <text class="store-field-tip">最后一个容量桶的开始时刻例如 21:30 表示营业容量至 22:00</text>
<picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingLastIdx" @change="onBookingLastChange"> <picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingLastIdx" @change="onBookingLastChange">
<view class="app-form-input app-form-picker store-picker-like"> <view class="app-form-input app-form-picker store-picker-like">
<text>{{ form.bookingLastSlotStart || '请选择' }}</text> <text>{{ form.bookingLastSlotStart || '请选择' }}</text>
@ -97,6 +97,19 @@
</picker> </picker>
<view class="booking-hint">结束须不早于开始保存后对新建预约生效</view> <view class="booking-hint">结束须不早于开始保存后对新建预约生效</view>
</view> </view>
<view class="store-field store-field-last">
<view class="store-field-label">
<span class="store-ico"><AppIcon name="profile" :size="15" :color="SLATE_600" /></span>
<text>并发接待数</text>
</view>
<text class="store-field-tip">同一半小时最多可同时服务的顾客数</text>
<picker mode="selector" :range="CAPACITY_OPTIONS" :value="bookingCapacityIdx" @change="onBookingCapacityChange">
<view class="app-form-input app-form-picker store-picker-like">
<text>{{ form.bookingCapacity }} </text>
<text class="app-form-arrow"></text>
</view>
</picker>
</view>
</view> </view>
<view class="store-actions"> <view class="store-actions">
@ -147,6 +160,7 @@ const navSafeStyle = (() => {
})() })()
const saving = ref(false) const saving = ref(false)
const pickingLocation = ref(false) const pickingLocation = ref(false)
const CAPACITY_OPTIONS = Array.from({ length: 10 }, (_, i) => i + 1)
const form = ref({ const form = ref({
name: '', name: '',
phone: '', phone: '',
@ -155,7 +169,8 @@ const form = ref({
latitude: null, latitude: null,
longitude: null, longitude: null,
bookingDayStart: '09:00', bookingDayStart: '09:00',
bookingLastSlotStart: '21:30' bookingLastSlotStart: '21:30',
bookingCapacity: 1
}) })
const toHHmm = (v) => { const toHHmm = (v) => {
@ -169,6 +184,7 @@ const toHHmm = (v) => {
const bookingStartIdx = computed(() => halfHourPickerIndex(form.value.bookingDayStart)) const bookingStartIdx = computed(() => halfHourPickerIndex(form.value.bookingDayStart))
const bookingLastIdx = computed(() => halfHourPickerIndex(form.value.bookingLastSlotStart)) const bookingLastIdx = computed(() => halfHourPickerIndex(form.value.bookingLastSlotStart))
const bookingCapacityIdx = computed(() => Math.max(0, CAPACITY_OPTIONS.indexOf(Number(form.value.bookingCapacity || 1))))
const onBookingStartChange = (e) => { const onBookingStartChange = (e) => {
const i = Number(e.detail.value) const i = Number(e.detail.value)
@ -180,6 +196,10 @@ const onBookingLastChange = (e) => {
form.value.bookingLastSlotStart = HALF_HOUR_TIME_SLOTS[i] || '21:30' form.value.bookingLastSlotStart = HALF_HOUR_TIME_SLOTS[i] || '21:30'
} }
const onBookingCapacityChange = (e) => {
form.value.bookingCapacity = CAPACITY_OPTIONS[Number(e.detail.value)] || 1
}
const hasMapPin = computed(() => form.value.latitude != null && form.value.longitude != null) const hasMapPin = computed(() => form.value.latitude != null && form.value.longitude != null)
const toCoord = (v) => { const toCoord = (v) => {
@ -237,7 +257,8 @@ onMounted(async () => {
latitude: toCoord(storeInfo.latitude), latitude: toCoord(storeInfo.latitude),
longitude: toCoord(storeInfo.longitude), longitude: toCoord(storeInfo.longitude),
bookingDayStart: toHHmm(storeInfo.bookingDayStart) || '09:00', bookingDayStart: toHHmm(storeInfo.bookingDayStart) || '09:00',
bookingLastSlotStart: toHHmm(storeInfo.bookingLastSlotStart) || '21:30' bookingLastSlotStart: toHHmm(storeInfo.bookingLastSlotStart) || '21:30',
bookingCapacity: Number(storeInfo.bookingCapacity || 1)
} }
if (storeInfo.id) { if (storeInfo.id) {
const res = await getStoreDetail(storeInfo.id) const res = await getStoreDetail(storeInfo.id)
@ -245,6 +266,7 @@ onMounted(async () => {
const d = res.data const d = res.data
base.bookingDayStart = toHHmm(d.bookingDayStart) || base.bookingDayStart base.bookingDayStart = toHHmm(d.bookingDayStart) || base.bookingDayStart
base.bookingLastSlotStart = toHHmm(d.bookingLastSlotStart) || base.bookingLastSlotStart base.bookingLastSlotStart = toHHmm(d.bookingLastSlotStart) || base.bookingLastSlotStart
base.bookingCapacity = Number(d.bookingCapacity || base.bookingCapacity)
if (d.name != null) base.name = d.name if (d.name != null) base.name = d.name
if (d.phone != null) base.phone = d.phone if (d.phone != null) base.phone = d.phone
if (d.address != null) base.address = d.address if (d.address != null) base.address = d.address