fix: 修复预约及页面展示
This commit is contained in:
parent
2497204de1
commit
e91f140bac
@ -64,7 +64,7 @@
|
||||
</picker>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="app-form-label">预约时间</text>
|
||||
<text class="app-form-label">预约时段</text>
|
||||
<view class="c-time-row">
|
||||
<picker mode="date" :value="selectedDateStr" @change="onAppointmentDateChange" class="c-time-half">
|
||||
<view class="app-form-input app-form-picker">
|
||||
@ -73,14 +73,14 @@
|
||||
</picker>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="availableTimes"
|
||||
:range="availableSlotLabels"
|
||||
:value="slotPickerIndex"
|
||||
:disabled="!availableTimes.length"
|
||||
@change="onAppointmentTimeOnlyChange"
|
||||
class="c-time-half"
|
||||
class="c-time-half c-time-half--slot"
|
||||
>
|
||||
<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>
|
||||
</picker>
|
||||
</view>
|
||||
@ -108,7 +108,7 @@ import {
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
import { getUserSession, getStoreSession, setStoreSession } from '../../utils/session.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 storeInfo = getStoreSession()
|
||||
@ -240,7 +240,7 @@ const creatingAppt = ref(false)
|
||||
const selectedDateStr = ref(ymdLocal())
|
||||
const availableTimes = ref([])
|
||||
const loadingSlots = ref(false)
|
||||
const bookingWindowHint = ref('半小时为一号源,同一时段仅接待一单;已满或已过期不可选')
|
||||
const bookingWindowHint = ref('请选择半小时时间段;同一时段仅接待一单,已满或已过期不可选')
|
||||
|
||||
const normalizeText = (s) => (s || '').toString().trim().toLowerCase()
|
||||
|
||||
@ -304,6 +304,15 @@ const slotPickerIndex = computed(() => {
|
||||
return i >= 0 ? i : 0
|
||||
})
|
||||
|
||||
/** 滚轮选项展示为「09:00–09:30」 */
|
||||
const availableSlotLabels = computed(() =>
|
||||
availableTimes.value.map((t) => formatHalfHourSlotRange(t))
|
||||
)
|
||||
|
||||
const selectedSlotRangeLabel = computed(() =>
|
||||
appointmentTime.value ? formatHalfHourSlotRange(appointmentTime.value) : ''
|
||||
)
|
||||
|
||||
const loadAvailableSlots = async () => {
|
||||
const row = orderedStores.value[selectedStoreIndex.value]
|
||||
const storeId = row?.id
|
||||
@ -326,7 +335,7 @@ const loadAvailableSlots = async () => {
|
||||
if (ds != null && le != null) {
|
||||
const a = String(ds).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)
|
||||
if (!availableTimes.value.length) {
|
||||
@ -537,7 +546,11 @@ onMounted(async () => {
|
||||
}
|
||||
.c-field { margin-bottom: 16px; }
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -142,7 +142,7 @@
|
||||
</picker>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="app-form-label">预约时间</text>
|
||||
<text class="app-form-label">预约时段</text>
|
||||
<view class="c-time-row">
|
||||
<picker mode="date" :value="staffApptDateStr" @change="onStaffApptDateChange" class="c-time-half">
|
||||
<view class="app-form-input app-form-picker">
|
||||
@ -151,18 +151,18 @@
|
||||
</picker>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="staffAvailableTimes"
|
||||
:range="staffAvailableSlotLabels"
|
||||
:value="staffSlotPickerIndex"
|
||||
:disabled="!staffAvailableTimes.length"
|
||||
@change="onStaffApptTimeChange"
|
||||
class="c-time-half"
|
||||
class="c-time-half c-time-half--slot"
|
||||
>
|
||||
<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>
|
||||
</picker>
|
||||
</view>
|
||||
<text class="c-store-hint home-slot-hint">半小时一号源,约满不可选</text>
|
||||
<text class="c-store-hint home-slot-hint">每行半小时时段,约满不可选</text>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="app-form-label">备注(可选)</text>
|
||||
@ -199,7 +199,7 @@ import { getUserSession, getStoreSession } from '../../utils/session.js'
|
||||
import { useNavigator } from '../../composables/useNavigator.js'
|
||||
import { formatDateTimeCN } from '../../utils/datetime.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 storeInfo = getStoreSession()
|
||||
@ -254,6 +254,14 @@ const staffSlotPickerIndex = computed(() => {
|
||||
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 => {
|
||||
if (currentStatus.value === 'new') return o.status === 'new'
|
||||
if (currentStatus.value === 'doing') return o.status === 'doing'
|
||||
@ -540,7 +548,11 @@ onShow(() => {
|
||||
line-height: 1.4;
|
||||
}
|
||||
.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; }
|
||||
|
||||
/* B端 Tab */
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺管理 -->
|
||||
<!-- 店铺管理(老板) -->
|
||||
<view v-if="userInfo.role === 'boss'" class="menu-group">
|
||||
<text class="menu-label">店铺管理</text>
|
||||
<view class="menu-card">
|
||||
@ -37,6 +37,18 @@
|
||||
</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">
|
||||
<text class="menu-label">个人中心</text>
|
||||
|
||||
@ -1,103 +1,106 @@
|
||||
<template>
|
||||
<div class="page-shell store-page">
|
||||
<view class="page-shell store-page">
|
||||
<view class="store-nav nav-gradient" :style="navSafeStyle">
|
||||
<view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view>
|
||||
<text class="nav-title">店铺设置</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<div class="store-body">
|
||||
<!-- 说明条:填充顶部视觉,交代用途 -->
|
||||
<div class="store-hero">
|
||||
<view class="store-body">
|
||||
<view class="store-hero">
|
||||
<view class="store-hero-icon" aria-hidden="true">
|
||||
<AppIcon name="store" :size="22" color="#15803d" />
|
||||
</view>
|
||||
<div class="store-hero-text">
|
||||
<div class="store-hero-title">门店资料</div>
|
||||
<div class="store-hero-desc">以下信息会用于预约展示与客户联系,建议保持准确、易读。</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="store-hero-text">
|
||||
<text class="store-hero-title">门店资料</text>
|
||||
<text class="store-hero-desc">请先设置「营业时间」,再完善名称、电话与地址。营业时间即客户线上预约可选的时段范围(半小时一号)。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<div class="store-card">
|
||||
<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="store" :size="15" color="#475569" /></span>
|
||||
<text>店铺名称</text>
|
||||
</div>
|
||||
<input v-model="form.name" class="van-field store-input" placeholder="请输入店铺名称" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- 营业时间置顶,避免被误认为没有该配置 -->
|
||||
<view class="store-card store-card--hours">
|
||||
<view class="store-card-head">
|
||||
<text class="store-card-title">营业时间</text>
|
||||
<text class="store-card-sub">客户可预约时段:从「开始」到「结束」之间的半点档均可放号(与挂号类似)</text>
|
||||
</view>
|
||||
<view class="store-field">
|
||||
<view class="store-field-label">
|
||||
<span class="store-ico"><AppIcon name="service" :size="15" color="#475569" /></span>
|
||||
<text>开始时间</text>
|
||||
</view>
|
||||
<text class="store-field-tip">当日第一个可预约的半点(例如 09:00)</text>
|
||||
<picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingStartIdx" @change="onBookingStartChange">
|
||||
<view class="app-form-input app-form-picker store-picker-like">
|
||||
<text>{{ form.bookingDayStart || '请选择' }}</text>
|
||||
<text class="app-form-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</div>
|
||||
<div class="store-field store-field-last">
|
||||
<div class="store-field-label">
|
||||
<span class="store-ico"><AppIcon name="pin" :size="15" color="#475569" /></span>
|
||||
<text>每日最后一个号源</text>
|
||||
</div>
|
||||
</view>
|
||||
<view class="store-field store-field-last">
|
||||
<view class="store-field-label">
|
||||
<span class="store-ico"><AppIcon name="service" :size="15" color="#475569" /></span>
|
||||
<text>结束时间</text>
|
||||
</view>
|
||||
<text class="store-field-tip">当日最后一个可预约的半点起始时刻(例如 21:30 表示最晚可约 21:00–21:30 这一档)</text>
|
||||
<picker mode="selector" :range="HALF_HOUR_TIME_SLOTS" :value="bookingLastIdx" @change="onBookingLastChange">
|
||||
<view class="app-form-input app-form-picker store-picker-like">
|
||||
<text>{{ form.bookingLastSlotStart || '请选择' }}</text>
|
||||
<text class="app-form-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
<div class="booking-hint">半小时一档;末号须不早于首号。保存后对新建预约生效。</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="booking-hint">结束须不早于开始。保存后对新建预约生效。</view>
|
||||
</view>
|
||||
</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
|
||||
class="van-button van-button--primary van-button--block store-save-btn"
|
||||
:loading="saving"
|
||||
@ -105,12 +108,12 @@
|
||||
>
|
||||
保存设置
|
||||
</button>
|
||||
<div class="store-actions-hint">保存后立即生效,并写入本地缓存。</div>
|
||||
</div>
|
||||
<view class="store-actions-hint">保存后立即生效,并写入本地缓存。</view>
|
||||
</view>
|
||||
|
||||
<div class="store-footer-tip">宠伴生活馆 · 商家版</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="store-footer-tip">宠伴生活馆 · 商家版</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -210,7 +213,7 @@ const saveStore = async () => {
|
||||
const i0 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingDayStart)
|
||||
const i1 = HALF_HOUR_TIME_SLOTS.indexOf(form.value.bookingLastSlotStart)
|
||||
if (i0 < 0 || i1 < 0 || i1 < i0) {
|
||||
uni.showToast({ title: '末号须不早于首号', icon: 'none' })
|
||||
uni.showToast({ title: '结束时间须不早于开始时间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
@ -309,13 +312,19 @@ onMounted(async () => {
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.store-hero-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.store-hero-title {
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #14532d;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.store-hero-desc {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
@ -330,6 +339,10 @@ onMounted(async () => {
|
||||
padding: 4px 4px 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.store-card--hours {
|
||||
border-color: #86efac;
|
||||
box-shadow: 0 8px 22px rgba(34, 197, 94, 0.12);
|
||||
}
|
||||
.store-card-head {
|
||||
padding: 14px 14px 6px;
|
||||
}
|
||||
@ -339,6 +352,20 @@ onMounted(async () => {
|
||||
color: #94a3b8;
|
||||
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 {
|
||||
padding: 12px 12px 4px;
|
||||
@ -350,11 +377,18 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
.store-field-tip {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.store-ico {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
@ -42,6 +42,30 @@ export function halfHourPickerIndex(hhmm) {
|
||||
|
||||
const pad2 = (n) => String(n).padStart(2, '0')
|
||||
|
||||
/**
|
||||
* 半小时挂号:展示为「开始–结束」时间段(如 09:00–09:30)。
|
||||
* 末档 23:30 结束展示为 23:30–24: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 */
|
||||
export function ymdLocal(d = new Date()) {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user