628 lines
17 KiB
Plaintext
628 lines
17 KiB
Plaintext
<template>
|
||
<view class="booking-detail-page">
|
||
<ServicePageScaffold title="预约服务" fallback-url="/pages/mall/consumer/home-service/index">
|
||
<view class="detail-service-summary">
|
||
<view class="detail-summary-cover">
|
||
<text class="detail-summary-cover-text">{{ serviceImageText }}</text>
|
||
</view>
|
||
<view class="detail-summary-main">
|
||
<text class="detail-summary-title">{{ serviceTitle }}</text>
|
||
<text class="detail-summary-desc">{{ serviceSubtitle }}</text>
|
||
<text class="detail-summary-duration">服务时长:{{ serviceDuration }}</text>
|
||
<view class="detail-summary-tags">
|
||
<text v-for="tag in guaranteeTags" :key="tag.id" class="detail-summary-tag">{{ tag.label }}</text>
|
||
</view>
|
||
<view class="detail-summary-price-row">
|
||
<text class="detail-summary-price-prefix">¥</text>
|
||
<text class="detail-summary-price">{{ servicePrice }}</text>
|
||
<text class="detail-summary-price-unit">起 / 次</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<ServicePanel title="服务说明卡" subtitle="围绕服务内容、适用人群和注意事项建立预约预期。">
|
||
<view class="detail-info-list">
|
||
<view class="detail-info-item">
|
||
<text class="detail-info-label">服务内容</text>
|
||
<text class="detail-info-value">{{ serviceSubtitle }}</text>
|
||
</view>
|
||
<view class="detail-info-item">
|
||
<text class="detail-info-label">适用人群</text>
|
||
<text class="detail-info-value">{{ serviceSuitableFor }}</text>
|
||
</view>
|
||
<view class="detail-info-item">
|
||
<text class="detail-info-label">服务时长</text>
|
||
<text class="detail-info-value">{{ serviceDuration }}</text>
|
||
</view>
|
||
<view class="detail-info-item">
|
||
<text class="detail-info-label">注意事项</text>
|
||
<text class="detail-info-value">预约成功后请保持电话畅通,首次上门建议家属在场。</text>
|
||
</view>
|
||
<view class="detail-info-item">
|
||
<text class="detail-info-label">不包含项目</text>
|
||
<text class="detail-info-value">{{ serviceExcludeText }}</text>
|
||
</view>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="Step1 服务地址" subtitle="没有选过地址时先提示用户补全上门地址。">
|
||
<view class="booking-step-card">
|
||
<text class="step-card-title">上门服务地址</text>
|
||
<text v-if="addressText == ''" class="step-card-placeholder">请选择上门服务地址</text>
|
||
<text v-else class="step-card-value">{{ addressText }}</text>
|
||
<view class="step-card-action" @click="selectAddress">选择地址</view>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="Step2 服务机构 / 服务人员" subtitle="当前先展示静态推荐机构,后续可接入真实人员选择。">
|
||
<view class="booking-step-card agency-card">
|
||
<view class="agency-card-top">
|
||
<text class="step-card-title">{{ agency.name }}</text>
|
||
<text class="agency-card-distance">{{ agency.distance }}</text>
|
||
</view>
|
||
<text class="agency-card-meta">评分 {{ agency.rating }}</text>
|
||
<text class="step-card-value">{{ agency.summary }}</text>
|
||
<text class="agency-card-todo">TODO:后续接入真实服务机构 / 服务人员选择接口。</text>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="Step3 上门时间" subtitle="先完成日期和时间段选择,再提交预约申请。">
|
||
<scroll-view class="booking-day-scroll" direction="horizontal" :show-scrollbar="false">
|
||
<view class="booking-day-row">
|
||
<view
|
||
v-for="item in bookingDays"
|
||
:key="item.id"
|
||
:class="['booking-day-card', selectedDayId == item.id ? 'booking-day-card-active' : '']"
|
||
@click="selectDay(item.id)"
|
||
>
|
||
<text :class="['booking-day-label', selectedDayId == item.id ? 'booking-day-label-active' : '']">{{ item.label }}</text>
|
||
<text :class="['booking-day-date', selectedDayId == item.id ? 'booking-day-date-active' : '']">{{ item.dateText }}</text>
|
||
<text :class="['booking-day-weekday', selectedDayId == item.id ? 'booking-day-weekday-active' : '']">{{ item.weekday }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="booking-slot-grid">
|
||
<view
|
||
v-for="item in bookingSlots"
|
||
:key="item.id"
|
||
:class="['booking-slot-card', selectedSlotId == item.id ? 'booking-slot-card-active' : '', item.available ? '' : 'booking-slot-card-disabled']"
|
||
@click="selectSlot(item.id, item.available)"
|
||
>
|
||
<text :class="['booking-slot-label', selectedSlotId == item.id ? 'booking-slot-label-active' : '']">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="Step4 联系人信息" subtitle="使用现有 input 组件完成预约联系人填写。">
|
||
<view class="contact-form-item">
|
||
<text class="contact-form-label">联系人姓名</text>
|
||
<input v-model="contactName" class="contact-form-input" placeholder="请输入联系人姓名" />
|
||
</view>
|
||
<view class="contact-form-item">
|
||
<text class="contact-form-label">联系电话</text>
|
||
<input v-model="contactPhone" class="contact-form-input" type="number" placeholder="请输入联系电话" />
|
||
</view>
|
||
<view class="contact-form-item">
|
||
<text class="contact-form-label">性别</text>
|
||
<view class="gender-row">
|
||
<view :class="['gender-pill', contactGender == '先生' ? 'gender-pill-active' : '']" @click="contactGender = '先生'">先生</view>
|
||
<view :class="['gender-pill', contactGender == '女士' ? 'gender-pill-active' : '']" @click="contactGender = '女士'">女士</view>
|
||
</view>
|
||
</view>
|
||
<view class="contact-form-item">
|
||
<text class="contact-form-label">备注</text>
|
||
<textarea v-model="remarkText" class="contact-form-textarea" placeholder="如行动不便、术后照护重点、上门注意事项"></textarea>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<ServicePanel title="用户保障卡" subtitle="让预约页更像服务平台而不是普通商品页。">
|
||
<view class="guarantee-grid">
|
||
<view v-for="item in guaranteeTags" :key="item.id" class="guarantee-grid-item">
|
||
<text class="guarantee-grid-text">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</ServicePanel>
|
||
|
||
<view class="detail-page-bottom-space"></view>
|
||
</ServicePageScaffold>
|
||
|
||
<view class="booking-bottom-bar">
|
||
<view class="booking-bottom-main">
|
||
<view>
|
||
<text class="booking-bottom-price-prefix">¥</text>
|
||
<text class="booking-bottom-price">{{ servicePrice }}</text>
|
||
<text class="booking-bottom-unit">起</text>
|
||
</view>
|
||
<text class="booking-bottom-time">{{ selectedTimeText }}</text>
|
||
</view>
|
||
<view class="booking-submit-btn" @click="submitBooking">立即预约</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { computed, ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
|
||
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
|
||
import { createHomeServiceApplication, fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
|
||
import { HomeServiceApplicationDraftType, HomeServiceCatalogType } from '@/types/home-service.uts'
|
||
import {
|
||
BookingDayOptionType,
|
||
BookingTimeSlotType,
|
||
HomeServiceAgencyType,
|
||
HomeServiceGuaranteeItemType,
|
||
getBookingDayOptions,
|
||
getBookingTimeSlots,
|
||
getHomeServiceItems,
|
||
getRecommendedAgency,
|
||
getServiceExcludes,
|
||
getServiceGuarantees
|
||
} from '@/utils/homeServiceUiMock.uts'
|
||
|
||
const serviceId = ref('svc-001')
|
||
const serviceTitle = ref('基础上门护理')
|
||
const serviceSubtitle = ref('覆盖生命体征监测、基础照护、风险提醒。')
|
||
const servicePrice = ref(168)
|
||
const serviceDuration = ref('约 2 小时')
|
||
const serviceSuitableFor = ref('行动不便、术后恢复、慢病随访老人')
|
||
const serviceImageText = ref('照护')
|
||
const serviceExcludeText = ref('高风险处置、住院陪护、急诊陪诊')
|
||
|
||
const bookingDays = ref<Array<BookingDayOptionType>>([])
|
||
const bookingSlots = ref<Array<BookingTimeSlotType>>([])
|
||
const guaranteeTags = ref<Array<HomeServiceGuaranteeItemType>>([])
|
||
const agency = ref<HomeServiceAgencyType>({
|
||
id: 'agency-001',
|
||
name: '梅江居家护理服务中心',
|
||
distance: '距您 1.2km',
|
||
rating: '4.9',
|
||
summary: '提供基础照护、上门护理和长者陪伴服务。'
|
||
})
|
||
|
||
const addressText = ref('')
|
||
const selectedDayId = ref('day-1')
|
||
const selectedSlotId = ref('slot-1')
|
||
const contactName = ref('李晓兰')
|
||
const contactPhone = ref('13800138000')
|
||
const contactGender = ref('女士')
|
||
const remarkText = ref('老人需要基础照护与血压监测。')
|
||
|
||
const selectedTimeText = computed((): string => {
|
||
let selectedDayLabel = ''
|
||
for (let i = 0; i < bookingDays.value.length; i++) {
|
||
if (bookingDays.value[i].id == selectedDayId.value) {
|
||
selectedDayLabel = bookingDays.value[i].label + ' ' + bookingDays.value[i].dateText
|
||
break
|
||
}
|
||
}
|
||
let selectedSlotLabel = ''
|
||
for (let i = 0; i < bookingSlots.value.length; i++) {
|
||
if (bookingSlots.value[i].id == selectedSlotId.value) {
|
||
selectedSlotLabel = bookingSlots.value[i].label
|
||
break
|
||
}
|
||
}
|
||
if (selectedDayLabel == '' || selectedSlotLabel == '') {
|
||
return '请选择上门时间'
|
||
}
|
||
return selectedDayLabel + ' ' + selectedSlotLabel
|
||
})
|
||
|
||
async function loadData() {
|
||
bookingDays.value = getBookingDayOptions()
|
||
bookingSlots.value = getBookingTimeSlots()
|
||
guaranteeTags.value = getServiceGuarantees()
|
||
agency.value = getRecommendedAgency(serviceId.value)
|
||
serviceExcludeText.value = getServiceExcludes(serviceId.value).join(';')
|
||
|
||
const catalog = await fetchHomeServiceCatalog()
|
||
let matchedService: HomeServiceCatalogType | null = null
|
||
for (let i = 0; i < catalog.length; i++) {
|
||
if (catalog[i].id == serviceId.value) {
|
||
matchedService = catalog[i]
|
||
break
|
||
}
|
||
}
|
||
if (matchedService == null) {
|
||
const fallbackItems = getHomeServiceItems(catalog)
|
||
if (fallbackItems.length > 0) {
|
||
serviceTitle.value = fallbackItems[0].title
|
||
serviceSubtitle.value = fallbackItems[0].subtitle
|
||
servicePrice.value = fallbackItems[0].price
|
||
serviceSuitableFor.value = fallbackItems[0].suitableFor
|
||
serviceImageText.value = fallbackItems[0].imageText
|
||
}
|
||
return
|
||
}
|
||
serviceTitle.value = matchedService.name
|
||
serviceSubtitle.value = matchedService.summary
|
||
servicePrice.value = matchedService.price
|
||
serviceDuration.value = matchedService.durationText
|
||
serviceSuitableFor.value = matchedService.suitableFor
|
||
const mappedItems = getHomeServiceItems([matchedService])
|
||
if (mappedItems.length > 0) {
|
||
serviceImageText.value = mappedItems[0].imageText
|
||
}
|
||
}
|
||
|
||
function selectAddress() {
|
||
if (addressText.value == '') {
|
||
addressText.value = '梅州市梅江区学海路 18 号 2 栋 602'
|
||
uni.showToast({ title: '已填入示例地址', icon: 'none' })
|
||
return
|
||
}
|
||
uni.navigateTo({ url: '/pages/mall/consumer/address-list' })
|
||
}
|
||
|
||
function selectDay(dayId: string) {
|
||
selectedDayId.value = dayId
|
||
}
|
||
|
||
function selectSlot(slotId: string, available: boolean) {
|
||
if (!available) {
|
||
uni.showToast({ title: '该时段暂不可约', icon: 'none' })
|
||
return
|
||
}
|
||
selectedSlotId.value = slotId
|
||
}
|
||
|
||
async function submitBooking() {
|
||
if (addressText.value == '') {
|
||
uni.showToast({ title: '请选择上门服务地址', icon: 'none' })
|
||
return
|
||
}
|
||
if (contactName.value == '' || contactPhone.value == '') {
|
||
uni.showToast({ title: '请补全联系人信息', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const draft: HomeServiceApplicationDraftType = {
|
||
serviceId: serviceId.value,
|
||
serviceName: serviceTitle.value,
|
||
applicantName: contactName.value,
|
||
elderName: contactName.value,
|
||
age: 78,
|
||
phone: contactPhone.value,
|
||
address: addressText.value,
|
||
preferredTime: selectedTimeText.value,
|
||
demandSummary: remarkText.value == '' ? serviceSubtitle.value : remarkText.value
|
||
}
|
||
|
||
const created = await createHomeServiceApplication(draft)
|
||
uni.showToast({ title: '预约已提交', icon: 'success' })
|
||
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
|
||
}
|
||
|
||
onLoad((options) => {
|
||
const id = options['id']
|
||
if (id != null) {
|
||
serviceId.value = id as string
|
||
}
|
||
const mode = options['mode']
|
||
if (mode != null && mode == 'booking') {
|
||
addressText.value = '梅州市梅江区学海路 18 号 2 栋 602'
|
||
}
|
||
loadData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.booking-detail-page {
|
||
background: #f4f8fb;
|
||
}
|
||
|
||
.detail-service-summary {
|
||
background: #ffffff;
|
||
border-radius: 32rpx;
|
||
padding: 28rpx;
|
||
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
|
||
flex-direction: row;
|
||
align-items: flex-start;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.detail-summary-cover {
|
||
width: 172rpx;
|
||
height: 172rpx;
|
||
border-radius: 32rpx;
|
||
background: linear-gradient(180deg, #eafbf7 0%, #eff6ff 100%);
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.detail-summary-cover-text {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.detail-summary-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-left: 24rpx;
|
||
}
|
||
|
||
.detail-summary-title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #16324f;
|
||
}
|
||
|
||
.detail-summary-desc,
|
||
.detail-summary-duration,
|
||
.detail-info-value,
|
||
.step-card-value,
|
||
.step-card-placeholder,
|
||
.agency-card-meta,
|
||
.agency-card-todo,
|
||
.booking-bottom-time {
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
.detail-summary-tags,
|
||
.gender-row,
|
||
.guarantee-grid,
|
||
.agency-card-top,
|
||
.booking-bottom-bar,
|
||
.booking-bottom-main {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.detail-summary-tags {
|
||
flex-wrap: wrap;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.detail-summary-tag {
|
||
padding: 10rpx 16rpx;
|
||
border-radius: 999rpx;
|
||
background: #eff6ff;
|
||
font-size: 22rpx;
|
||
color: #476072;
|
||
margin-right: 10rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.detail-summary-price-row {
|
||
margin-top: 14rpx;
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.detail-summary-price-prefix,
|
||
.detail-summary-price,
|
||
.booking-bottom-price-prefix,
|
||
.booking-bottom-price {
|
||
font-size: 38rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.detail-summary-price-unit,
|
||
.booking-bottom-unit {
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
margin-left: 8rpx;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.detail-info-item,
|
||
.contact-form-item {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.detail-info-label,
|
||
.contact-form-label,
|
||
.step-card-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #16324f;
|
||
}
|
||
|
||
.booking-step-card {
|
||
padding: 24rpx;
|
||
border-radius: 24rpx;
|
||
background: #f8fbfd;
|
||
}
|
||
|
||
.step-card-placeholder {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.step-card-action {
|
||
margin-top: 20rpx;
|
||
height: 68rpx;
|
||
border-radius: 999rpx;
|
||
background: #16a085;
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.agency-card-distance {
|
||
font-size: 22rpx;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.agency-card-todo {
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.booking-day-scroll {
|
||
height: 156rpx;
|
||
}
|
||
|
||
.booking-day-row {
|
||
flex-direction: row;
|
||
padding-right: 20rpx;
|
||
}
|
||
|
||
.booking-day-card {
|
||
width: 160rpx;
|
||
padding: 20rpx;
|
||
border-radius: 24rpx;
|
||
background: #f8fbfd;
|
||
margin-right: 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.booking-day-card-active {
|
||
background: #0f766e;
|
||
}
|
||
|
||
.booking-day-label,
|
||
.booking-day-date,
|
||
.booking-day-weekday {
|
||
font-size: 24rpx;
|
||
line-height: 34rpx;
|
||
color: #476072;
|
||
}
|
||
|
||
.booking-day-date,
|
||
.booking-day-weekday {
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.booking-day-label-active,
|
||
.booking-day-date-active,
|
||
.booking-day-weekday-active {
|
||
color: #ffffff;
|
||
}
|
||
|
||
.booking-slot-grid {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.booking-slot-card {
|
||
width: 48.5%;
|
||
height: 82rpx;
|
||
border-radius: 22rpx;
|
||
background: #f8fbfd;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 16rpx;
|
||
border-width: 2rpx;
|
||
border-style: solid;
|
||
border-color: transparent;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.booking-slot-card-active {
|
||
background: #eafbf7;
|
||
border-color: #16a085;
|
||
}
|
||
|
||
.booking-slot-card-disabled {
|
||
background: #f1f5f9;
|
||
opacity: 0.55;
|
||
}
|
||
|
||
.booking-slot-label {
|
||
font-size: 24rpx;
|
||
color: #476072;
|
||
}
|
||
|
||
.booking-slot-label-active {
|
||
color: #0f766e;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.contact-form-input,
|
||
.contact-form-textarea {
|
||
width: 100%;
|
||
background: #f8fbfd;
|
||
border-radius: 22rpx;
|
||
font-size: 26rpx;
|
||
color: #16324f;
|
||
box-sizing: border-box;
|
||
margin-top: 14rpx;
|
||
}
|
||
|
||
.contact-form-input {
|
||
height: 84rpx;
|
||
padding: 0 24rpx;
|
||
}
|
||
|
||
.contact-form-textarea {
|
||
height: 160rpx;
|
||
padding: 20rpx 24rpx;
|
||
}
|
||
|
||
.gender-pill {
|
||
height: 64rpx;
|
||
padding: 0 28rpx;
|
||
border-radius: 999rpx;
|
||
background: #f1f5f9;
|
||
font-size: 24rpx;
|
||
color: #476072;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 16rpx;
|
||
margin-top: 14rpx;
|
||
}
|
||
|
||
.gender-pill-active {
|
||
background: #eafbf7;
|
||
color: #0f766e;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.guarantee-grid {
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.guarantee-grid-item {
|
||
width: 48%;
|
||
height: 72rpx;
|
||
border-radius: 20rpx;
|
||
background: #f8fbfd;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.guarantee-grid-text {
|
||
font-size: 24rpx;
|
||
color: #476072;
|
||
}
|
||
|
||
.detail-page-bottom-space {
|
||
height: 160rpx;
|
||
}
|
||
|
||
.booking-bottom-bar {
|
||
position: fixed;
|
||
left: 24rpx;
|
||
right: 24rpx;
|
||
bottom: 28rpx;
|
||
background: #ffffff;
|
||
border-radius: 30rpx;
|
||
padding: 20rpx 22rpx;
|
||
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.12);
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.booking-submit-btn {
|
||
height: 84rpx;
|
||
padding: 0 34rpx;
|
||
border-radius: 999rpx;
|
||
background: #16a085;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #ffffff;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
</style> |