fix: 修复预约及页面展示

This commit is contained in:
MaDaLei 2026-04-17 11:28:44 +08:00
parent 2497204de1
commit e91f140bac
5 changed files with 194 additions and 99 deletions

View File

@ -64,7 +64,7 @@
</picker> </picker>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">预约时</text> <text class="app-form-label">预约时</text>
<view class="c-time-row"> <view class="c-time-row">
<picker mode="date" :value="selectedDateStr" @change="onAppointmentDateChange" class="c-time-half"> <picker mode="date" :value="selectedDateStr" @change="onAppointmentDateChange" class="c-time-half">
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
@ -73,14 +73,14 @@
</picker> </picker>
<picker <picker
mode="selector" mode="selector"
:range="availableTimes" :range="availableSlotLabels"
:value="slotPickerIndex" :value="slotPickerIndex"
:disabled="!availableTimes.length" :disabled="!availableTimes.length"
@change="onAppointmentTimeOnlyChange" @change="onAppointmentTimeOnlyChange"
class="c-time-half" class="c-time-half c-time-half--slot"
> >
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'app-form-placeholder': !appointmentTime && !loadingSlots }">{{ loadingSlots ? '加载中…' : (appointmentTime || '时间') }}</text> <text class="slot-range-text" :class="{ 'app-form-placeholder': !appointmentTime && !loadingSlots }">{{ loadingSlots ? '加载中…' : (selectedSlotRangeLabel || '选择时段') }}</text>
</view> </view>
</picker> </picker>
</view> </view>
@ -108,7 +108,7 @@ import {
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 { normalizeToHalfHour, ymdLocal } from '../../utils/halfHourTime.js' import { normalizeToHalfHour, ymdLocal, formatHalfHourSlotRange } from '../../utils/halfHourTime.js'
const userInfo = getUserSession() const userInfo = getUserSession()
const storeInfo = getStoreSession() const storeInfo = getStoreSession()
@ -240,7 +240,7 @@ const creatingAppt = ref(false)
const selectedDateStr = ref(ymdLocal()) const selectedDateStr = ref(ymdLocal())
const availableTimes = ref([]) const availableTimes = 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()
@ -304,6 +304,15 @@ const slotPickerIndex = computed(() => {
return i >= 0 ? i : 0 return i >= 0 ? i : 0
}) })
/** 滚轮选项展示为「09:0009:30」 */
const availableSlotLabels = computed(() =>
availableTimes.value.map((t) => formatHalfHourSlotRange(t))
)
const selectedSlotRangeLabel = computed(() =>
appointmentTime.value ? formatHalfHourSlotRange(appointmentTime.value) : ''
)
const loadAvailableSlots = async () => { const loadAvailableSlots = async () => {
const row = orderedStores.value[selectedStoreIndex.value] const row = orderedStores.value[selectedStoreIndex.value]
const storeId = row?.id const storeId = row?.id
@ -326,7 +335,7 @@ const loadAvailableSlots = async () => {
if (ds != null && le != null) { if (ds != null && le != 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(le).slice(0, 5)
bookingWindowHint.value = `本店放号 ${a}${b},半小时一号源;同一时段仅一单,已满或已过不可选` bookingWindowHint.value = `本店可约 ${a}${b};以下为半小时时段,同一时段仅一单,已满或已过不可选`
} }
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) {
@ -537,7 +546,11 @@ onMounted(async () => {
} }
.c-field { margin-bottom: 16px; } .c-field { margin-bottom: 16px; }
.c-time-row { display: flex; gap: 10px; } .c-time-row { display: flex; gap: 10px; }
.c-time-half { flex: 1; } .c-time-half { flex: 1; min-width: 0; }
.c-time-half--slot .slot-range-text {
font-size: 12px;
letter-spacing: -0.02em;
}
.pet-name-row { .pet-name-row {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -142,7 +142,7 @@
</picker> </picker>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="app-form-label">预约时</text> <text class="app-form-label">预约时</text>
<view class="c-time-row"> <view class="c-time-row">
<picker mode="date" :value="staffApptDateStr" @change="onStaffApptDateChange" class="c-time-half"> <picker mode="date" :value="staffApptDateStr" @change="onStaffApptDateChange" class="c-time-half">
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
@ -151,18 +151,18 @@
</picker> </picker>
<picker <picker
mode="selector" mode="selector"
:range="staffAvailableTimes" :range="staffAvailableSlotLabels"
:value="staffSlotPickerIndex" :value="staffSlotPickerIndex"
:disabled="!staffAvailableTimes.length" :disabled="!staffAvailableTimes.length"
@change="onStaffApptTimeChange" @change="onStaffApptTimeChange"
class="c-time-half" class="c-time-half c-time-half--slot"
> >
<view class="app-form-input app-form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'app-form-placeholder': !appointmentTime && !staffLoadingSlots }">{{ staffLoadingSlots ? '加载中…' : (appointmentTime || '时间') }}</text> <text class="home-slot-range-text" :class="{ 'app-form-placeholder': !appointmentTime && !staffLoadingSlots }">{{ staffLoadingSlots ? '加载中…' : (staffSelectedSlotRangeLabel || '选择时段') }}</text>
</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>
@ -199,7 +199,7 @@ 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 { normalizeToHalfHour, ymdLocal } from '../../utils/halfHourTime.js' import { normalizeToHalfHour, ymdLocal, formatHalfHourSlotRange } from '../../utils/halfHourTime.js'
const userInfo = getUserSession() const userInfo = getUserSession()
const storeInfo = getStoreSession() const storeInfo = getStoreSession()
@ -254,6 +254,14 @@ const staffSlotPickerIndex = computed(() => {
return i >= 0 ? i : 0 return i >= 0 ? i : 0
}) })
const staffAvailableSlotLabels = computed(() =>
staffAvailableTimes.value.map((t) => formatHalfHourSlotRange(t))
)
const staffSelectedSlotRangeLabel = computed(() =>
appointmentTime.value ? formatHalfHourSlotRange(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'
@ -540,7 +548,11 @@ onShow(() => {
line-height: 1.4; line-height: 1.4;
} }
.c-time-row { display: flex; gap: 10px; } .c-time-row { display: flex; gap: 10px; }
.c-time-half { flex: 1; } .c-time-half { flex: 1; min-width: 0; }
.c-time-half--slot .home-slot-range-text {
font-size: 12px;
letter-spacing: -0.02em;
}
.home-remark-textarea { min-height: 80px; line-height: 1.5; } .home-remark-textarea { min-height: 80px; line-height: 1.5; }
/* B端 Tab */ /* B端 Tab */

View File

@ -15,7 +15,7 @@
</view> </view>
</view> </view>
<!-- 店铺管理 --> <!-- 店铺管理老板 -->
<view v-if="userInfo.role === 'boss'" class="menu-group"> <view v-if="userInfo.role === 'boss'" class="menu-group">
<text class="menu-label">店铺管理</text> <text class="menu-label">店铺管理</text>
<view class="menu-card"> <view class="menu-card">
@ -37,6 +37,18 @@
</view> </view>
</view> </view>
<!-- 员工仅门店设置含营业时间 -->
<view v-else-if="userInfo.role === 'staff'" class="menu-group">
<text class="menu-label">门店</text>
<view class="menu-card">
<view class="menu-item" @click="goStorePage">
<view class="menu-icon-wrap"><AppIcon name="store" :size="16" color="#666660" /></view>
<text class="menu-text">店铺设置</text>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<!-- 个人 --> <!-- 个人 -->
<view class="menu-group"> <view class="menu-group">
<text class="menu-label">个人中心</text> <text class="menu-label">个人中心</text>

View File

@ -1,103 +1,106 @@
<template> <template>
<div class="page-shell store-page"> <view class="page-shell store-page">
<view class="store-nav nav-gradient" :style="navSafeStyle"> <view class="store-nav nav-gradient" :style="navSafeStyle">
<view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view> <view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view>
<text class="nav-title">店铺设置</text> <text class="nav-title">店铺设置</text>
<view class="nav-placeholder"></view> <view class="nav-placeholder"></view>
</view> </view>
<div class="store-body"> <view class="store-body">
<!-- 说明条填充顶部视觉交代用途 --> <view class="store-hero">
<div class="store-hero">
<view class="store-hero-icon" aria-hidden="true"> <view class="store-hero-icon" aria-hidden="true">
<AppIcon name="store" :size="22" color="#15803d" /> <AppIcon name="store" :size="22" color="#15803d" />
</view> </view>
<div class="store-hero-text"> <view class="store-hero-text">
<div class="store-hero-title">门店资料</div> <text class="store-hero-title">门店资料</text>
<div class="store-hero-desc">以下信息会用于预约展示与客户联系建议保持准确易读</div> <text class="store-hero-desc">请先设置营业时间再完善名称电话与地址营业时间即客户线上预约可选的时段范围半小时一号</text>
</div> </view>
</div> </view>
<div class="store-card"> <!-- 营业时间置顶避免被误认为没有该配置 -->
<div class="store-card-head"> <view class="store-card store-card--hours">
<span class="store-card-kicker">基础信息</span> <view class="store-card-head">
</div> <text class="store-card-title">营业时间</text>
<text class="store-card-sub">客户可预约时段开始结束之间的半点档均可放号与挂号类似</text>
<div class="store-field"> </view>
<div class="store-field-label"> <view class="store-field">
<span class="store-ico"><AppIcon name="store" :size="15" color="#475569" /></span> <view class="store-field-label">
<text>店铺名称</text> <span class="store-ico"><AppIcon name="service" :size="15" color="#475569" /></span>
</div> <text>开始时间</text>
<input v-model="form.name" class="van-field store-input" placeholder="请输入店铺名称" /> </view>
</div> <text class="store-field-tip">当日第一个可预约的半点例如 09:00</text>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="phone" :size="15" color="#475569" /></span>
<text>联系电话</text>
</div>
<input v-model="form.phone" type="tel" class="van-field store-input" placeholder="请输入联系电话" maxlength="20" />
</div>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
<text>地址</text>
</div>
<div class="address-row">
<input v-model="form.address" class="van-field store-input address-input" placeholder="输入地址或点右侧选点" />
<button type="button" class="pick-location-btn" :disabled="pickingLocation" @click="pickAddress">
地图选点
</button>
</div>
<div v-if="hasMapPin" class="geo-hint">已选地图位置可再微调上方文字后保存</div>
</div>
<div class="store-field store-field-last">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="report" :size="15" color="#475569" /></span>
<text>简介</text>
</div>
<textarea
v-model="form.intro"
class="van-field store-input intro-textarea"
placeholder="一句话介绍门店特色、服务亮点等"
/>
</div>
</div>
<div class="store-card" style="margin-top: 14px">
<div class="store-card-head">
<span class="store-card-kicker">预约放号</span>
</div>
<div class="store-field">
<div class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
<text>每日首个号源</text>
</div>
<picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingStartIdx" @change="onBookingStartChange"> <picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingStartIdx" @change="onBookingStartChange">
<view class="app-form-input app-form-picker store-picker-like"> <view class="app-form-input app-form-picker store-picker-like">
<text>{{ form.bookingDayStart || '请选择' }}</text> <text>{{ form.bookingDayStart || '请选择' }}</text>
<text class="app-form-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
</div> </view>
<div class="store-field store-field-last"> <view class="store-field store-field-last">
<div class="store-field-label"> <view class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span> <span class="store-ico"><AppIcon name="service" :size="15" color="#475569" /></span>
<text>每日最后一个号源</text> <text>结束时间</text>
</div> </view>
<text class="store-field-tip">当日最后一个可预约的半点起始时刻例如 21:30 表示最晚可约 21:0021:30 这一档</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>
<text class="app-form-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
<div class="booking-hint">半小时一档末号须不早于首号保存后对新建预约生效</div> <view class="booking-hint">结束须不早于开始保存后对新建预约生效</view>
</div> </view>
</div> </view>
<div class="store-actions"> <view class="store-card" style="margin-top: 14px">
<view class="store-card-head">
<text class="store-card-kicker">基础信息</text>
</view>
<view class="store-field">
<view class="store-field-label">
<span class="store-ico"><AppIcon name="store" :size="15" color="#475569" /></span>
<text>店铺名称</text>
</view>
<input v-model="form.name" class="van-field store-input" placeholder="请输入店铺名称" />
</view>
<view class="store-field">
<view class="store-field-label">
<span class="store-ico"><AppIcon name="phone" :size="15" color="#475569" /></span>
<text>联系电话</text>
</view>
<input v-model="form.phone" type="tel" class="van-field store-input" placeholder="请输入联系电话" maxlength="20" />
</view>
<view class="store-field">
<view class="store-field-label">
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
<text>地址</text>
</view>
<view class="address-row">
<input v-model="form.address" class="van-field store-input address-input" placeholder="输入地址或点右侧选点" />
<button type="button" class="pick-location-btn" :disabled="pickingLocation" @click="pickAddress">
地图选点
</button>
</view>
<view v-if="hasMapPin" class="geo-hint">已选地图位置可再微调上方文字后保存</view>
</view>
<view class="store-field store-field-last">
<view class="store-field-label">
<span class="store-ico"><AppIcon name="report" :size="15" color="#475569" /></span>
<text>简介</text>
</view>
<textarea
v-model="form.intro"
class="van-field store-input intro-textarea"
placeholder="一句话介绍门店特色、服务亮点等"
/>
</view>
</view>
<view class="store-actions">
<button <button
class="van-button van-button--primary van-button--block store-save-btn" class="van-button van-button--primary van-button--block store-save-btn"
:loading="saving" :loading="saving"
@ -105,12 +108,12 @@
> >
保存设置 保存设置
</button> </button>
<div class="store-actions-hint">保存后立即生效并写入本地缓存</div> <view class="store-actions-hint">保存后立即生效并写入本地缓存</view>
</div> </view>
<div class="store-footer-tip">宠伴生活馆 · 商家版</div> <view class="store-footer-tip">宠伴生活馆 · 商家版</view>
</div> </view>
</div> </view>
</template> </template>
<script setup> <script setup>
@ -210,7 +213,7 @@ const saveStore = async () => {
const i0 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingDayStart) const i0 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingDayStart)
const i1 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingLastSlotStart) const i1 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingLastSlotStart)
if (i0 < 0 || i1 < 0 || i1 < i0) { if (i0 < 0 || i1 < 0 || i1 < i0) {
uni.showToast({ title: '末号须不早于首号', icon: 'none' }) uni.showToast({ title: '结束时间须不早于开始时间', icon: 'none' })
return return
} }
saving.value = true saving.value = true
@ -309,13 +312,19 @@ onMounted(async () => {
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
} }
.store-hero-text {
flex: 1;
min-width: 0;
}
.store-hero-title { .store-hero-title {
display: block;
font-size: 17px; font-size: 17px;
font-weight: 700; font-weight: 700;
color: #14532d; color: #14532d;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.store-hero-desc { .store-hero-desc {
display: block;
margin-top: 6px; margin-top: 6px;
font-size: 13px; font-size: 13px;
line-height: 1.55; line-height: 1.55;
@ -330,6 +339,10 @@ onMounted(async () => {
padding: 4px 4px 8px; padding: 4px 4px 8px;
overflow: hidden; overflow: hidden;
} }
.store-card--hours {
border-color: #86efac;
box-shadow: 0 8px 22px rgba(34, 197, 94, 0.12);
}
.store-card-head { .store-card-head {
padding: 14px 14px 6px; padding: 14px 14px 6px;
} }
@ -339,6 +352,20 @@ onMounted(async () => {
color: #94a3b8; color: #94a3b8;
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.store-card-title {
display: block;
font-size: 17px;
font-weight: 800;
color: #14532d;
letter-spacing: 0.02em;
}
.store-card-sub {
display: block;
margin-top: 6px;
font-size: 12px;
line-height: 1.5;
color: #64748b;
}
.store-field { .store-field {
padding: 12px 12px 4px; padding: 12px 12px 4px;
@ -350,11 +377,18 @@ onMounted(async () => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 6px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #374151; color: #374151;
} }
.store-field-tip {
display: block;
margin-bottom: 8px;
font-size: 11px;
line-height: 1.45;
color: #94a3b8;
}
.store-ico { .store-ico {
width: 28px; width: 28px;
height: 28px; height: 28px;

View File

@ -42,6 +42,30 @@ export function halfHourPickerIndex(hhmm) {
const pad2 = (n) => String(n).padStart(2, '0') const pad2 = (n) => String(n).padStart(2, '0')
/**
* 半小时挂号展示为开始结束时间段 09:0009:30
* 末档 23:30 结束展示为 23:3024:00
*/
export function formatHalfHourSlotRange(startHHmm) {
if (startHHmm == null || startHHmm === '') return ''
const m = String(startHHmm).trim().match(/^(\d{1,2}):(\d{2})$/)
if (!m) return String(startHHmm)
let h = parseInt(m[1], 10)
let mi = parseInt(m[2], 10)
if (Number.isNaN(h) || Number.isNaN(mi)) return String(startHHmm)
h = Math.min(23, Math.max(0, h))
mi = Math.min(59, Math.max(0, mi))
const startM = h * 60 + mi
const endM = startM + 30
const startStr = `${pad2(h)}:${pad2(mi)}`
if (endM >= 24 * 60) {
return `${startStr}24:00`
}
const endH = Math.floor(endM / 60)
const endMi = endM % 60
return `${startStr}${pad2(endH)}:${pad2(endMi)}`
}
/** 本地日历 YYYY-MM-DD */ /** 本地日历 YYYY-MM-DD */
export function ymdLocal(d = new Date()) { export function ymdLocal(d = new Date()) {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`