Files
medical-mall/pages/mall/consumer/home-service/service-detail.uvue
2026-06-04 18:32:08 +08:00

1747 lines
48 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="booking-page-shell">
<view class="detail-fixed-header">
<ServicePageHeader title="康养预约" fallback-url="/pages/mall/consumer/home-service/index"></ServicePageHeader>
</view>
<view class="detail-header-placeholder" :style="{ height: headerHeight + 'px' }"></view>
<scroll-view class="detail-scroll" scroll-y="true">
<view class="summary-card">
<view class="summary-top-row">
<view class="summary-cover">
<text class="summary-cover-text">{{ serviceImageText }}</text>
</view>
<view class="summary-main">
<text class="summary-title">{{ serviceTitle }}</text>
<text class="summary-desc">{{ serviceSubtitle }}</text>
<text class="summary-meta">服务时长:{{ serviceDuration }}</text>
<text class="summary-meta">有效期:{{ serviceValidity }}</text>
</view>
</view>
<view class="summary-tag-row">
<text v-for="tag in summaryGuaranteeTags" :key="tag.id" class="summary-tag">{{ tag.label }}</text>
</view>
<view class="summary-price-row">
<view class="summary-price-main">
<text class="summary-price-prefix">¥</text>
<text class="summary-price">{{ minPriceText }}</text>
<text class="summary-price-unit">起</text>
</view>
<view class="summary-favorite-btn" @click="toggleServiceFavorite">
<text class="summary-favorite-btn-text">{{ isFavorite ? '已收藏' : '收藏服务' }}</text>
</view>
</view>
</view>
<ServicePanel title="Step1 服务地址" subtitle="确认上门地址、楼层与入户条件。">
<view class="service-address-card" @click="selectAddress">
<view class="service-address-main">
<text class="service-address-title">服务地址</text>
<view v-if="selectedAddress != null" class="service-address-detail">
<view class="service-address-contact-row">
<text class="service-address-contact">{{ selectedAddress.contactName }}</text>
<text class="service-address-phone">{{ getSelectedAddressPhone() }}</text>
</view>
<text class="service-address-full">{{ selectedAddress.fullAddress }}</text>
<text v-if="selectedAddress.remark != ''" class="service-address-remark">备注:{{ selectedAddress.remark }}</text>
</view>
<text v-else class="service-address-placeholder">请选择服务地址</text>
</view>
<text class="service-address-arrow">></text>
</view>
<view class="form-grid">
<view class="form-grid-item">
<text class="label">所在区域</text>
<input v-model="careArea" class="input" placeholder="请输入区域" />
</view>
<view class="form-grid-item">
<text class="label">楼层</text>
<input v-model="careFloor" class="input" placeholder="如 6 楼" />
</view>
</view>
<view class="form-item form-item-last">
<text class="label">是否有电梯</text>
<view class="pill-row">
<view :class="['pill', careHasElevator ? 'pill-active' : '']" @click="setElevator(true)">
<text :class="['pill-text', careHasElevator ? 'pill-text-active' : '']">有电梯</text>
</view>
<view :class="['pill', !careHasElevator ? 'pill-active' : '']" @click="setElevator(false)">
<text :class="['pill-text', !careHasElevator ? 'pill-text-active' : '']">无电梯</text>
</view>
</view>
</view>
</ServicePanel>
<ServicePanel title="Step2 服务机构 / 服务人员" subtitle="当前优先展示平台推荐机构,后续可接入真实选择接口。">
<view class="agency-card">
<view class="agency-top-row">
<view>
<text class="agency-name">{{ agency.name }}</text>
<text class="agency-meta">{{ agency.distance }} 评分 {{ agency.rating }}</text>
</view>
<text class="agency-badge">平台认证</text>
</view>
<text class="agency-desc">{{ agency.summary }}</text>
<text class="agency-desc">服务资质:护理员上门、服务留痕、预约前电话确认</text>
<view class="action-row">
<view class="ghost-btn" @click="handleAgencyReplace"><text class="ghost-btn-text">更换机构</text></view>
<view class="ghost-btn" @click="handleAgencyContact"><text class="ghost-btn-text">立即联系</text></view>
<view class="ghost-btn" @click="handleAgencyNavigate"><text class="ghost-btn-text">立即导航</text></view>
</view>
</view>
</ServicePanel>
<ServicePanel title="Step3 预约上门时间" subtitle="请选择服务人员预计到达的日期和时间段,提交前系统将重新校验可预约状态。">
<text class="booking-hint">请至少提前30分钟预约以便安排服务人员到达现场。</text>
<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>
<view class="custom-slot-section">
<text class="custom-slot-title">其他上门时间</text>
<picker
mode="selector"
:range="customSlotLabels"
:value="customSlotIndex"
@change="handleCustomSlotChange"
>
<view class="custom-slot-picker">
<text class="custom-slot-picker-text">{{ customSlotDisplayText }}</text>
</view>
</picker>
</view>
</ServicePanel>
<ServicePanel title="Step4 服务对象信息" subtitle="补全被照护人情况,便于机构判断服务方案。">
<view class="form-grid">
<view class="form-grid-item">
<text class="label">服务对象姓名</text>
<input v-model="recipientName" class="input" placeholder="请输入姓名" />
</view>
<view class="form-grid-item">
<text class="label">年龄</text>
<input v-model="recipientAgeText" class="input" type="number" placeholder="请输入年龄" />
</view>
</view>
<view class="form-grid">
<view class="form-grid-item">
<text class="label">性别</text>
<view class="pill-row">
<view :class="['pill', recipientGender == '男' ? 'pill-active' : '']" @click="setRecipientGender('男')">
<text :class="['pill-text', recipientGender == '男' ? 'pill-text-active' : '']">男</text>
</view>
<view :class="['pill', recipientGender == '女' ? 'pill-active' : '']" @click="setRecipientGender('女')">
<text :class="['pill-text', recipientGender == '女' ? 'pill-text-active' : '']">女</text>
</view>
</view>
</view>
<view class="form-grid-item">
<text class="label">与下单人关系</text>
<input v-model="recipientRelation" class="input" placeholder="如 母亲 / 父亲 / 本人" />
</view>
</view>
<view class="form-item">
<text class="label">身体情况</text>
<textarea v-model="recipientHealthStatus" class="textarea" placeholder="请输入身体情况,如术后恢复、慢病随访、行动不便"></textarea>
</view>
<view class="form-item">
<text class="label">行动能力</text>
<input v-model="recipientMobilityLevel" class="input" placeholder="如 需要搀扶 / 轮椅出行 / 可自理" />
</view>
<view class="form-item">
<text class="label">风险标签</text>
<view class="tag-row">
<view
v-for="item in careRiskOptions"
:key="item.id"
:class="['tag-chip', item.selected ? 'tag-chip-active' : '']"
@click="toggleRiskOption(item.id)"
>
<text :class="['tag-chip-text', item.selected ? 'tag-chip-text-active' : '']">{{ item.label }}</text>
</view>
</view>
</view>
<view class="form-item form-item-last">
<text class="label">家属陪同</text>
<view class="pill-row">
<view :class="['pill', recipientNeedFamilyPresent ? 'pill-active' : '']" @click="setFamilyPresent(true)">
<text :class="['pill-text', recipientNeedFamilyPresent ? 'pill-text-active' : '']">需要家属在场</text>
</view>
<view :class="['pill', !recipientNeedFamilyPresent ? 'pill-active' : '']" @click="setFamilyPresent(false)">
<text :class="['pill-text', !recipientNeedFamilyPresent ? 'pill-text-active' : '']">可独立服务</text>
</view>
</view>
</view>
</ServicePanel>
<ServicePanel title="Step5 联系人信息" subtitle="保留现有提交流程所需联系人字段。">
<view class="form-grid">
<view class="form-grid-item">
<text class="label">联系人姓名</text>
<input v-model="contactName" class="input" placeholder="请输入联系人姓名" />
</view>
<view class="form-grid-item">
<text class="label">联系电话</text>
<input v-model="contactPhone" class="input" type="number" placeholder="请输入联系电话" />
</view>
</view>
<view class="form-item">
<text class="label">性别</text>
<view class="pill-row">
<view :class="['pill', contactGender == '先生' ? 'pill-active' : '']" @click="setContactGender('先生')">
<text :class="['pill-text', contactGender == '先生' ? 'pill-text-active' : '']">先生</text>
</view>
<view :class="['pill', contactGender == '女士' ? 'pill-active' : '']" @click="setContactGender('女士')">
<text :class="['pill-text', contactGender == '女士' ? 'pill-text-active' : '']">女士</text>
</view>
</view>
</view>
<view class="form-grid">
<view class="form-grid-item">
<text class="label">紧急联系人</text>
<input v-model="emergencyName" class="input" placeholder="请输入紧急联系人姓名" />
</view>
<view class="form-grid-item">
<text class="label">紧急联系人电话</text>
<input v-model="emergencyPhone" class="input" type="number" placeholder="请输入紧急联系人电话" />
</view>
</view>
<view class="form-item form-item-last">
<text class="label">补充备注</text>
<textarea v-model="remarkText" class="textarea" placeholder="如老人作息、注意事项、上门前沟通重点"></textarea>
</view>
</ServicePanel>
<ServicePanel title="Step6 服务需求" subtitle="选择服务方案和重点需求,仍然兼容现有申请接口。">
<view class="package-list">
<view
v-for="item in servicePackages"
:key="item.id"
:class="['package-card', item.selected ? 'package-card-active' : '']"
@click="selectServicePackage(item.id)"
>
<view class="package-top-row">
<view class="package-name-row">
<text :class="['package-name', item.selected ? 'package-name-active' : '']">{{ item.name }}</text>
<text v-if="item.sourceTag != ''" class="package-source-tag">{{ item.sourceTag }}</text>
</view>
<text class="package-price">¥{{ formatPrice(item.price) }}</text>
</view>
<text class="package-desc">{{ item.desc }}</text>
<text class="package-duration">{{ item.duration }}</text>
</view>
</view>
<view class="form-item">
<text class="label">需求标签</text>
<view class="tag-row">
<view
v-for="item in careNeedOptions"
:key="item.id"
:class="['tag-chip', item.selected ? 'tag-chip-active' : '']"
@click="toggleCareNeedOption(item.id)"
>
<text :class="['tag-chip-text', item.selected ? 'tag-chip-text-active' : '']">{{ item.label }}</text>
</view>
</view>
</view>
<view class="form-item form-item-last">
<text class="label">详细需求</text>
<textarea v-model="demandDetailText" class="textarea" placeholder="请输入更详细的服务需求,如血压监测频次、陪诊时间、康复重点"></textarea>
</view>
</ServicePanel>
<ServicePanel title="服务说明与保障" subtitle="展示适用人群、免责项与平台保障信息。">
<view class="info-item">
<text class="info-label">适用人群</text>
<text class="info-value">{{ serviceSuitableFor }}</text>
</view>
<view class="info-item">
<text class="info-label">不包含项目</text>
<text class="info-value">{{ serviceExcludeText }}</text>
</view>
<view class="tag-row tag-row-last">
<text v-for="item in guaranteeTags" :key="item.id" class="summary-tag">{{ item.label }}</text>
<text class="summary-tag">上门前确认</text>
</view>
</ServicePanel>
<view class="detail-page-bottom-space"></view>
</scroll-view>
<view class="booking-bottom-bar">
<view>
<text class="booking-bottom-price">¥{{ minPriceText }} 起</text>
<text class="booking-bottom-time">{{ selectedTimeText }}</text>
</view>
<view class="booking-submit-btn" @click="submitBooking">
<text class="booking-submit-btn-text">提交预约</text>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { computed, ref } from 'vue'
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import ServicePageHeader from '@/components/homeService/ServicePageHeader.uvue'
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
import { createHomeServiceApplication, fetchHomeServiceCatalog, fetchHomeServicePackages } from '@/services/homeServiceService.uts'
import { shouldUseCareTaskPath } from '@/services/serviceOrderService.uts'
import { HomeServiceApplicationDraftType, HomeServiceCatalogType, HomeServicePackageType, HomeServiceSelectedAddressType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
import {
HomeServiceAgencyType,
HomeServiceGuaranteeItemType,
getHomeServiceItems,
getRecommendedAgency,
getServiceExcludes,
getServiceGuarantees
} from '@/utils/homeServiceUiMock.uts'
import {
BookingDayOptionType,
BookingTimeSlotType,
buildBookingDays,
buildCustomSlots,
buildPresetSlots,
formatAppointmentDisplayText,
formatStandardAppointmentTime,
hasAnyAvailableSlots,
validateSelectedAppointmentTime
} from '@/utils/homeServiceBookingTime.uts'
type CareRecipientInfoType = {
name: string
age: number
gender: string
relation: string
healthStatus: string
mobilityLevel: string
needFamilyPresent: boolean
}
type CareContactInfoType = {
contactName: string
contactPhone: string
contactGender: string
emergencyName: string
emergencyPhone: string
}
type CareAddressInfoType = {
address: string
detailAddress: string
area: string
floor: string
hasElevator: boolean
}
type CareNeedOptionType = {
id: string
label: string
selected: boolean
}
type ServicePackageOptionType = {
id: string
name: string
desc: string
price: number
duration: string
sourceTag: string
selected: boolean
}
type CareRiskOptionType = {
id: string
label: string
selected: boolean
}
const serviceId = ref('svc-001')
const headerHeight = ref(116)
const serviceTitle = ref('基础上门护理')
const serviceSubtitle = ref('覆盖生命体征监测、基础照护、风险提醒。')
const servicePrice = ref(168)
const serviceDuration = ref('约 2 小时')
const serviceValidity = ref('预约后 30 天内可服务')
const serviceSuitableFor = ref('行动不便、术后恢复、慢病随访老人')
const serviceImageText = ref('照护')
const serviceExcludeText = ref('高风险处置、住院陪护、急诊陪诊')
const isFavorite = ref(false)
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 defaultCareRecipient: CareRecipientInfoType = {
name: '李奶奶',
age: 78,
gender: '女',
relation: '母亲',
healthStatus: '行动不便,需基础照护与血压监测',
mobilityLevel: '需要搀扶',
needFamilyPresent: true
}
const defaultCareContact: CareContactInfoType = {
contactName: '李晓兰',
contactPhone: '13800138000',
contactGender: '女士',
emergencyName: '李先生',
emergencyPhone: '13900139000'
}
const defaultCareAddress: CareAddressInfoType = {
address: '梅州市梅江区学海路 18 号',
detailAddress: '2 栋 602',
area: '梅江区',
floor: '6 楼',
hasElevator: true
}
const careNeedOptions = ref<Array<CareNeedOptionType>>([])
const careRiskOptions = ref<Array<CareRiskOptionType>>([])
const servicePackages = ref<Array<ServicePackageOptionType>>([])
const isSubmitting = ref<boolean>(false)
const selectedDayId = ref('')
const selectedSlotId = ref('')
const selectedCustomSlotId = ref('')
const customSlots = ref<Array<BookingTimeSlotType>>([])
const customSlotIndex = ref(-1)
const selectedAddress = ref<HomeServiceSelectedAddressType | null>(null)
const careAddress = ref(defaultCareAddress.address)
const careDetailAddress = ref(defaultCareAddress.detailAddress)
const careArea = ref(defaultCareAddress.area)
const careFloor = ref(defaultCareAddress.floor)
const careHasElevator = ref(defaultCareAddress.hasElevator)
const recipientName = ref(defaultCareRecipient.name)
const recipientAgeText = ref(defaultCareRecipient.age.toString())
const recipientGender = ref(defaultCareRecipient.gender)
const recipientRelation = ref(defaultCareRecipient.relation)
const recipientHealthStatus = ref(defaultCareRecipient.healthStatus)
const recipientMobilityLevel = ref(defaultCareRecipient.mobilityLevel)
const recipientNeedFamilyPresent = ref(defaultCareRecipient.needFamilyPresent)
const contactName = ref(defaultCareContact.contactName)
const contactPhone = ref(defaultCareContact.contactPhone)
const contactGender = ref(defaultCareContact.contactGender)
const emergencyName = ref(defaultCareContact.emergencyName)
const emergencyPhone = ref(defaultCareContact.emergencyPhone)
const remarkText = ref('老人需要基础照护与血压监测。')
const demandDetailText = ref('请优先安排上午上门,家属希望同步了解老人近期血压情况。')
try {
const windowInfo = uni.getWindowInfo()
const statusBar = windowInfo.statusBarHeight
const unit = windowInfo.windowWidth / 750
headerHeight.value = statusBar + Math.round(124 * unit)
} catch (error) {
headerHeight.value = 116
}
function buildDefaultNeedOptions(currentServiceId: string): Array<CareNeedOptionType> {
if (currentServiceId == 'svc-002') {
return [
{ id: 'rehab', label: '康复指导', selected: true },
{ id: 'basic_care', label: '基础照护', selected: false },
{ id: 'blood_pressure', label: '血压监测', selected: false },
{ id: 'medicine', label: '用药提醒', selected: false },
{ id: 'escort', label: '陪诊陪护', selected: false },
{ id: 'chronic', label: '慢病随访', selected: true }
]
}
if (currentServiceId == 'svc-003') {
return [
{ id: 'basic_care', label: '基础照护', selected: false },
{ id: 'blood_pressure', label: '血压监测', selected: true },
{ id: 'medicine', label: '用药提醒', selected: true },
{ id: 'rehab', label: '康复指导', selected: false },
{ id: 'escort', label: '陪诊陪护', selected: false },
{ id: 'chronic', label: '慢病随访', selected: true }
]
}
return [
{ id: 'basic_care', label: '基础照护', selected: true },
{ id: 'blood_pressure', label: '血压监测', selected: true },
{ id: 'medicine', label: '用药提醒', selected: false },
{ id: 'rehab', label: '康复指导', selected: false },
{ id: 'escort', label: '陪诊陪护', selected: false },
{ id: 'chronic', label: '慢病随访', selected: true }
]
}
function buildDefaultRiskOptions(currentServiceId: string): Array<CareRiskOptionType> {
if (currentServiceId == 'svc-002') {
return [
{ id: 'post_op', label: '术后恢复', selected: true },
{ id: 'fall', label: '跌倒风险', selected: true },
{ id: 'chronic', label: '慢病管理', selected: false }
]
}
if (currentServiceId == 'svc-003') {
return [
{ id: 'post_op', label: '术后恢复', selected: false },
{ id: 'fall', label: '跌倒风险', selected: false },
{ id: 'chronic', label: '慢病管理', selected: true }
]
}
return [
{ id: 'post_op', label: '术后恢复', selected: false },
{ id: 'fall', label: '跌倒风险', selected: true },
{ id: 'chronic', label: '慢病管理', selected: true }
]
}
function mapServicePackages(packages: Array<HomeServicePackageType>): Array<ServicePackageOptionType> {
const result = [] as Array<ServicePackageOptionType>
for (let i = 0; i < packages.length; i++) {
result.push({
id: packages[i].id,
name: packages[i].packageName,
desc: packages[i].packageDesc,
price: packages[i].price,
duration: packages[i].durationText != '' ? packages[i].durationText : '待配置',
sourceTag: packages[i].dataSource == 'dev_seed' ? '测试数据' : '',
selected: i == 0
})
}
return result
}
const addressLineText = computed((): string => {
if (selectedAddress.value != null && selectedAddress.value.fullAddress != '') {
return selectedAddress.value.fullAddress
}
const fullAddress = careAddress.value + ' ' + careDetailAddress.value
return fullAddress.trim() != '' ? fullAddress : '请选择服务地址'
})
function applySelectedAddress(address: HomeServiceSelectedAddressType | null): void {
selectedAddress.value = address
if (address == null) {
careAddress.value = ''
careDetailAddress.value = ''
return
}
const locationAddress = address.locationAddress != null && address.locationAddress != '' ? address.locationAddress : address.addressDetail
const doorNo = address.doorNo != null && address.doorNo != '' ? address.doorNo : address.houseNumber
careAddress.value = locationAddress
careDetailAddress.value = doorNo
if (contactName.value == '') {
contactName.value = address.contactName
}
const phoneText = address.phone != null && address.phone != '' ? address.phone : address.contactPhone
if (contactPhone.value == '') {
contactPhone.value = phoneText
}
}
function getSelectedAddressPhone(): string {
if (selectedAddress.value == null) {
return ''
}
if (selectedAddress.value.phone != null && selectedAddress.value.phone != '') {
return selectedAddress.value.phone
}
return selectedAddress.value.contactPhone
}
function loadCachedSelectedAddress(): void {
try {
const cachedAddress = uni.getStorageSync('hss_selected_service_address') as HomeServiceSelectedAddressType | null
if (cachedAddress != null && cachedAddress.fullAddress != '') {
applySelectedAddress(cachedAddress)
return
}
} catch (error) {
console.error('读取服务地址缓存失败', error)
}
applySelectedAddress(null)
}
const selectedDay = computed((): BookingDayOptionType | null => {
for (let i = 0; i < bookingDays.value.length; i++) {
if (bookingDays.value[i].id == selectedDayId.value) {
return bookingDays.value[i]
}
}
return null
})
const selectedSlot = computed((): BookingTimeSlotType | null => {
for (let i = 0; i < bookingSlots.value.length; i++) {
if (bookingSlots.value[i].id == selectedSlotId.value) {
return bookingSlots.value[i]
}
}
for (let i = 0; i < customSlots.value.length; i++) {
if (customSlots.value[i].id == selectedSlotId.value) {
return customSlots.value[i]
}
}
return null
})
const selectedPackage = computed((): ServicePackageOptionType | null => {
for (let i = 0; i < servicePackages.value.length; i++) {
if (servicePackages.value[i].selected) {
return servicePackages.value[i]
}
}
return null
})
const selectedTimeText = computed((): string => {
return formatAppointmentDisplayText(selectedDay.value, selectedSlot.value)
})
const customSlotLabels = computed((): Array<string> => {
const labels: Array<string> = []
for (let i = 0; i < customSlots.value.length; i++) {
labels.push(customSlots.value[i].label)
}
return labels
})
const customSlotDisplayText = computed((): string => {
if (selectedCustomSlotId.value == '') {
return '请选择其他时间段'
}
for (let i = 0; i < customSlots.value.length; i++) {
if (customSlots.value[i].id == selectedCustomSlotId.value) {
return customSlots.value[i].label
}
}
return '请选择其他时间段'
})
const minPriceValue = computed((): number => {
let minPrice = servicePrice.value
for (let i = 0; i < servicePackages.value.length; i++) {
if (i == 0 || servicePackages.value[i].price < minPrice) {
minPrice = servicePackages.value[i].price
}
}
return minPrice
})
const minPriceText = computed((): string => {
return formatPrice(minPriceValue.value)
})
const summaryGuaranteeTags = computed((): Array<HomeServiceGuaranteeItemType> => {
const result: Array<HomeServiceGuaranteeItemType> = []
for (let i = 0; i < guaranteeTags.value.length && result.length < 3; i++) {
result.push(guaranteeTags.value[i])
}
return result
})
function formatPrice(price: number): string {
const rounded = Math.round(price)
if (Math.abs(price - rounded) < 0.001) {
return rounded.toString()
}
return price.toFixed(2)
}
function getSelectedPackageName(): string {
for (let i = 0; i < servicePackages.value.length; i++) {
if (servicePackages.value[i].selected) {
return servicePackages.value[i].name
}
}
return ''
}
function getSelectedRiskText(): string {
let text = ''
for (let i = 0; i < careRiskOptions.value.length; i++) {
if (careRiskOptions.value[i].selected) {
if (text != '') {
text = text + '、'
}
text = text + careRiskOptions.value[i].label
}
}
return text
}
function getSelectedNeedText(): string {
let text = ''
for (let i = 0; i < careNeedOptions.value.length; i++) {
if (careNeedOptions.value[i].selected) {
if (text != '') {
text = text + '、'
}
text = text + careNeedOptions.value[i].label
}
}
return text
}
function buildDemandSummary(): string {
const selectedNeeds = getSelectedNeedText()
const selectedRisks = getSelectedRiskText()
const packageName = getSelectedPackageName()
const familyPresentText = recipientNeedFamilyPresent.value ? '需要家属在场' : '可独立服务'
const elevatorText = careHasElevator.value ? '有电梯' : '无电梯'
return '服务对象:' + recipientName.value
+ ',年龄:' + recipientAgeText.value
+ ',性别:' + recipientGender.value
+ ',关系:' + recipientRelation.value
+ ',身体情况:' + recipientHealthStatus.value
+ ',行动能力:' + recipientMobilityLevel.value
+ ',风险标签:' + selectedRisks
+ ',是否需家属在场:' + familyPresentText
+ ',服务地址:' + addressLineText.value
+ ',区域:' + careArea.value
+ ',楼层:' + careFloor.value
+ ',电梯:' + elevatorText
+ ',套餐:' + packageName
+ ',服务需求:' + selectedNeeds
+ ',详细需求:' + demandDetailText.value
+ ',紧急联系人:' + emergencyName.value + ' ' + emergencyPhone.value
+ ',服务地址备注:' + (selectedAddress.value != null ? selectedAddress.value.remark : '')
+ ',备注:' + remarkText.value
}
function setUnavailableServiceState() {
serviceTitle.value = '服务暂未配置'
serviceSubtitle.value = '当前服务目录或套餐未配置,请稍后再试。'
servicePrice.value = 0
serviceDuration.value = '待配置'
serviceSuitableFor.value = '请联系管理员初始化服务目录与正式套餐。'
serviceImageText.value = '服务'
servicePackages.value = [] as Array<ServicePackageOptionType>
}
function initDefaultDaySelection(now: Date) {
let firstAvailableIndex = -1
for (let i = 0; i < bookingDays.value.length; i++) {
if (hasAnyAvailableSlots(bookingDays.value[i].dateKey, now)) {
firstAvailableIndex = i
break
}
}
if (firstAvailableIndex >= 0) {
selectedDayId.value = bookingDays.value[firstAvailableIndex].id
bookingSlots.value = buildPresetSlots(bookingDays.value[firstAvailableIndex].dateKey, now)
customSlots.value = buildCustomSlots(bookingDays.value[firstAvailableIndex].dateKey, now)
} else {
selectedDayId.value = ''
bookingSlots.value = [] as Array<BookingTimeSlotType>
customSlots.value = [] as Array<BookingTimeSlotType>
}
selectedSlotId.value = ''
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
}
async function loadData() {
const now = new Date()
bookingDays.value = buildBookingDays(now)
initDefaultDaySelection(now)
guaranteeTags.value = getServiceGuarantees()
agency.value = getRecommendedAgency(serviceId.value)
serviceExcludeText.value = getServiceExcludes(serviceId.value).join('')
careNeedOptions.value = buildDefaultNeedOptions(serviceId.value)
careRiskOptions.value = buildDefaultRiskOptions(serviceId.value)
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) {
setUnavailableServiceState()
await syncFavoriteState()
return
}
serviceTitle.value = matchedService.name
serviceSubtitle.value = matchedService.summary
serviceDuration.value = matchedService.durationText
serviceSuitableFor.value = matchedService.suitableFor
const mappedItems = getHomeServiceItems([matchedService])
if (mappedItems.length > 0) {
serviceImageText.value = mappedItems[0].imageText
}
const packages = await fetchHomeServicePackages(serviceId.value)
servicePackages.value = mapServicePackages(packages)
if (servicePackages.value.length == 0) {
setUnavailableServiceState()
await syncFavoriteState()
return
}
const firstPackage = servicePackages.value[0]
servicePrice.value = firstPackage.price
serviceDuration.value = firstPackage.duration
await syncFavoriteState()
}
async function syncFavoriteState(): Promise<void> {
const userId = getCurrentUserId()
if (userId == '') {
isFavorite.value = false
return
}
isFavorite.value = await supabaseService.checkFavorite(serviceId.value, 'service')
}
async function toggleServiceFavorite(): Promise<void> {
if (!(await ensureLogin())) {
return
}
const nextState = await supabaseService.toggleFavorite(serviceId.value, 'service')
isFavorite.value = nextState
uni.showToast({
title: nextState ? '已收藏服务' : '已取消收藏',
icon: nextState ? 'success' : 'none'
})
}
async function ensureLogin(): Promise<boolean> {
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
goToLogin('/pages/mall/consumer/home-service/service-detail?id=' + serviceId.value + '&mode=booking')
return false
}
return true
}
function selectAddress() {
ensureLogin().then((ok) => {
if (!ok) {
return
}
uni.navigateTo({ url: '/pages/mall/consumer/address-list?selectMode=true' })
})
}
function handleAgencyReplace() {
uni.showToast({ title: '服务机构选择建设中', icon: 'none' })
}
function handleAgencyContact() {
uni.showToast({ title: '已为你联系服务机构', icon: 'none' })
}
function handleAgencyNavigate() {
uni.showToast({ title: '导航功能建设中', icon: 'none' })
}
function selectDay(dayId: string) {
selectedDayId.value = dayId
selectedSlotId.value = ''
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
const day = selectedDay.value
if (day != null) {
const now = new Date()
bookingSlots.value = buildPresetSlots(day.dateKey, now)
customSlots.value = buildCustomSlots(day.dateKey, now)
}
}
function selectSlot(slotId: string, available: boolean) {
if (!available) {
uni.showToast({ title: '该时间段已无法预约,请选择稍晚时间', icon: 'none' })
return
}
selectedSlotId.value = slotId
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
}
function handleCustomSlotChange(e: any) {
const detail = e.detail
const index = typeof detail.value == 'number' ? detail.value : parseInt(detail.value)
if (isNaN(index) || index < 0 || index >= customSlots.value.length) {
return
}
const slot = customSlots.value[index]
if (slot == null) {
return
}
if (!slot.available) {
uni.showToast({ title: '该时间段已无法预约,请选择稍晚时间', icon: 'none' })
return
}
selectedCustomSlotId.value = slot.id
selectedSlotId.value = slot.id
customSlotIndex.value = index
}
function setRecipientGender(gender: string) {
recipientGender.value = gender
}
function setContactGender(gender: string) {
contactGender.value = gender
}
function setFamilyPresent(needFamilyPresent: boolean) {
recipientNeedFamilyPresent.value = needFamilyPresent
}
function setElevator(hasElevator: boolean) {
careHasElevator.value = hasElevator
}
function toggleRiskOption(optionId: string) {
for (let i = 0; i < careRiskOptions.value.length; i++) {
if (careRiskOptions.value[i].id == optionId) {
careRiskOptions.value[i].selected = !careRiskOptions.value[i].selected
break
}
}
}
function toggleCareNeedOption(optionId: string) {
let selectedCount = 0
let targetIndex = -1
for (let i = 0; i < careNeedOptions.value.length; i++) {
if (careNeedOptions.value[i].selected) {
selectedCount++
}
if (careNeedOptions.value[i].id == optionId) {
targetIndex = i
}
}
if (targetIndex == -1) {
return
}
if (careNeedOptions.value[targetIndex].selected && selectedCount <= 1) {
uni.showToast({ title: '请至少保留一个服务需求', icon: 'none' })
return
}
careNeedOptions.value[targetIndex].selected = !careNeedOptions.value[targetIndex].selected
}
function selectServicePackage(packageId: string) {
for (let i = 0; i < servicePackages.value.length; i++) {
servicePackages.value[i].selected = servicePackages.value[i].id == packageId
}
const current = selectedPackage.value
if (current != null) {
servicePrice.value = current.price
serviceDuration.value = current.duration
}
}
function goToServicePayment(orderId: string): void {
if (orderId == '') {
uni.showToast({
title: '订单创建成功,但订单编号异常,请从订单详情继续付款',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/mall/consumer/payment'
+ '?orderId=' + encodeURIComponent(orderId)
+ '&source=service'
+ '&bizType=service'
})
}
async function submitBooking() {
if (isSubmitting.value) {
return
}
if (!(await ensureLogin())) {
return
}
if (servicePackages.value.length == 0 || servicePrice.value <= 0) {
uni.showToast({ title: '当前服务暂不可预约', icon: 'none' })
return
}
if (selectedAddress.value == null) {
uni.showToast({ title: '请选择服务地址', icon: 'none' })
return
}
if (recipientName.value == '') {
uni.showToast({ title: '请填写服务对象姓名', icon: 'none' })
return
}
if (contactPhone.value == '' || contactName.value == '') {
uni.showToast({ title: '请补全联系人信息', icon: 'none' })
return
}
if (emergencyName.value == '' || emergencyPhone.value == '') {
uni.showToast({ title: '请补全紧急联系人信息', icon: 'none' })
return
}
if (selectedTimeText.value == '请选择上门时间') {
uni.showToast({ title: '请选择上门时间', icon: 'none' })
return
}
const now = new Date()
const day = selectedDay.value
const slot = selectedSlot.value
const validation = validateSelectedAppointmentTime(day, slot, now)
if (!validation.valid) {
uni.showToast({ title: validation.message, icon: 'none' })
if (day != null && slot != null && !slot.available) {
bookingSlots.value = buildPresetSlots(day.dateKey, now)
customSlots.value = buildCustomSlots(day.dateKey, now)
selectedSlotId.value = ''
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
}
return
}
let parsedAge = parseInt(recipientAgeText.value)
if (isNaN(parsedAge)) {
parsedAge = 0
}
const standardTime = formatStandardAppointmentTime(day, slot)
const draft: HomeServiceApplicationDraftType = {
serviceId: serviceId.value,
serviceName: serviceTitle.value,
selectedPackageId: selectedPackage.value != null ? selectedPackage.value.id : '',
selectedPackageName: selectedPackage.value != null ? selectedPackage.value.name : '',
applicantName: contactName.value,
elderName: recipientName.value,
age: parsedAge,
gender: recipientGender.value,
phone: contactPhone.value,
address: addressLineText.value,
preferredTime: standardTime,
appointmentStartAt: slot != null ? slot.startAt : 0,
appointmentEndAt: slot != null ? slot.endAt : 0,
appointmentDate: day != null ? day.dateKey : '',
slotSource: slot != null ? slot.source : '',
minAdvanceMinutesSnapshot: 30,
demandSummary: buildDemandSummary(),
serviceAddressSnapshot: selectedAddress.value
}
isSubmitting.value = true
try {
const created = await createHomeServiceApplication(draft)
if (created == null) {
uni.showToast({ title: '预约提交失败,请稍后重试', icon: 'none' })
return
}
// TODO(SERVICE_PAYMENT_ROUTE_UNIFY):
// 当前仅因 ec_care_tasks 新链尚未接入支付能力,临时使用订单链路类型区分。
// 长期应依据服务订单真实 payment_status / payment_required / payment_order_id
// 判断是否进入支付,而不是仅依据 ID 是否为 UUID。
const isCareTaskPath = shouldUseCareTaskPath(created.id)
if (!isCareTaskPath) {
// hss_service_orders 套餐订单,进入 payment
uni.showToast({
title: '订单已创建,请完成付款',
icon: 'success'
})
goToServicePayment(created.id)
return
}
// 当前兼容处理:对于后端明确没有支付能力的新链任务,只保持原有详情跳转。
// 不得把这种兼容逻辑扩展为所有服务均无需付款。
uni.showToast({ title: '预约已提交', icon: 'success' })
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
} finally {
isSubmitting.value = false
}
}
onLoad((options) => {
const id = options['id']
if (id != null) {
serviceId.value = id as string
}
const mode = options['mode']
if (mode != null && mode == 'booking') {
careArea.value = defaultCareAddress.area
}
loadData()
})
let refreshTimerId: number = 0
function startRefreshTimer(): void {
stopRefreshTimer()
refreshTimerId = setInterval(() => {
refreshBookingSlots()
}, 60000)
}
function stopRefreshTimer(): void {
if (refreshTimerId > 0) {
clearInterval(refreshTimerId)
refreshTimerId = 0
}
}
function refreshBookingSlots(): void {
const now = new Date()
const day = selectedDay.value
if (day == null) {
return
}
const todayKey = buildBookingDays(now)[0].dateKey
if (day.dateKey == todayKey) {
bookingSlots.value = buildPresetSlots(day.dateKey, now)
customSlots.value = buildCustomSlots(day.dateKey, now)
const slot = selectedSlot.value
if (slot != null && !slot.available) {
selectedSlotId.value = ''
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
uni.showToast({ title: '当前选择的上门时间已失效,请重新选择稍晚的时间段', icon: 'none' })
}
}
}
onShow(() => {
loadCachedSelectedAddress()
syncFavoriteState()
const now = new Date()
const day = selectedDay.value
if (day != null) {
bookingSlots.value = buildPresetSlots(day.dateKey, now)
customSlots.value = buildCustomSlots(day.dateKey, now)
const slot = selectedSlot.value
if (slot != null && !slot.available) {
selectedSlotId.value = ''
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
uni.showToast({ title: '当前选择的上门时间已失效,请重新选择稍晚的时间段', icon: 'none' })
}
}
startRefreshTimer()
})
onUnload(() => {
stopRefreshTimer()
})
</script>
<style scoped>
.booking-page-shell {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
background: #f7f8fb;
overflow: hidden;
}
.detail-fixed-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: #f3f7f9;
}
.detail-header-placeholder {
width: 100%;
flex-shrink: 0;
}
.detail-scroll {
flex: 1;
min-height: 0;
width: 100%;
}
.summary-card {
background: linear-gradient(180deg, #fff0f4 0%, #fff7f9 100%);
border-radius: 28rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
}
.service-address-card {
background: #ffffff;
border-radius: 24rpx;
padding: 24rpx;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1rpx solid rgba(255, 125, 151, 0.16);
margin-bottom: 24rpx;
box-shadow: 0 10rpx 20rpx rgba(15, 23, 42, 0.04);
}
.service-address-main {
flex: 1;
margin-right: 20rpx;
}
.service-address-title {
font-size: 28rpx;
font-weight: 600;
color: #1f2937;
margin-bottom: 12rpx;
}
.service-address-detail {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.service-address-contact-row {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 16rpx;
}
.service-address-contact,
.service-address-phone,
.service-address-full,
.service-address-remark,
.service-address-placeholder,
.service-address-arrow {
font-size: 24rpx;
}
.service-address-contact {
font-weight: 600;
color: #111827;
}
.service-address-phone {
color: #4b5563;
}
.service-address-full {
color: #374151;
line-height: 1.5;
}
.service-address-remark {
color: #6b7280;
line-height: 1.5;
}
.service-address-placeholder {
color: #9ca3af;
line-height: 1.5;
}
.service-address-arrow {
color: #c18b95;
font-weight: 600;
}
.summary-top-row,
.summary-tag-row,
.summary-price-row,
.booking-bottom-bar,
.booking-day-row,
.form-grid,
.pill-row,
.tag-row,
.booking-slot-grid,
.agency-top-row,
.action-row,
.package-name-row,
.package-top-row {
flex-direction: row;
align-items: center;
}
.summary-top-row,
.agency-top-row,
.booking-bottom-bar,
.package-top-row {
justify-content: space-between;
}
.package-name-row {
flex: 1;
min-width: 0;
align-items: center;
flex-wrap: wrap;
margin-right: 16rpx;
}
.summary-cover {
width: 144rpx;
height: 144rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, #dff6f1 0%, #ecf3ff 100%);
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.summary-cover-text {
font-size: 36rpx;
font-weight: 700;
color: #0f766e;
}
.summary-main {
flex: 1;
min-width: 0;
margin-left: 22rpx;
}
.summary-title {
font-size: 34rpx;
font-weight: 700;
color: #16324f;
line-height: 44rpx;
}
.summary-desc,
.summary-meta,
.agency-meta,
.agency-desc,
.booking-bottom-time,
.package-desc,
.package-duration,
.info-value,
.value-card-text {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #64748b;
}
.summary-tag-row,
.pill-row,
.tag-row {
flex-wrap: wrap;
margin-top: 18rpx;
}
.summary-tag,
.agency-badge,
.tag-chip {
padding: 10rpx 16rpx;
border-radius: 999rpx;
font-size: 22rpx;
margin-right: 12rpx;
margin-bottom: 12rpx;
box-sizing: border-box;
}
.package-source-tag {
padding: 6rpx 12rpx;
border-radius: 999rpx;
font-size: 20rpx;
color: #b45309;
background: #fff7ed;
margin-left: 12rpx;
margin-bottom: 12rpx;
}
.summary-tag {
background: #ffffff;
color: #ff5a7a;
}
.agency-badge {
background: #eafbf7;
color: #0f766e;
}
.summary-price-row {
margin-top: 12rpx;
justify-content: space-between;
align-items: flex-end;
}
.summary-price-main {
flex-direction: row;
align-items: flex-end;
}
.summary-price-prefix,
.summary-price,
.booking-bottom-price,
.package-price {
font-size: 40rpx;
font-weight: 700;
color: #ff5a7a;
}
.summary-price-unit {
font-size: 22rpx;
color: #64748b;
margin-left: 8rpx;
margin-bottom: 6rpx;
}
.summary-favorite-btn {
min-width: 132rpx;
height: 56rpx;
padding: 0 22rpx;
border-radius: 999rpx;
background: #fff3ee;
border-width: 1rpx;
border-style: solid;
border-color: #ffd0c2;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.summary-favorite-btn-text {
font-size: 22rpx;
color: #d85b34;
font-weight: 600;
}
.form-item {
margin-bottom: 24rpx;
}
.form-item-last,
.tag-row-last {
margin-bottom: 0;
}
.label,
.agency-name,
.info-label,
.package-name {
font-size: 28rpx;
font-weight: 700;
color: #16324f;
margin-bottom: 12rpx;
}
.value-card,
.agency-card,
.package-card {
background: #f8fbfc;
border-radius: 22rpx;
padding: 22rpx;
box-sizing: border-box;
}
.value-card {
border-width: 1rpx;
border-style: solid;
border-color: #eef2f6;
}
.value-card-action {
margin-top: 14rpx;
font-size: 22rpx;
color: #ff5a7a;
}
.form-grid {
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
}
.form-grid-item {
width: 48.5%;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.input,
.textarea {
width: 100%;
background: #f8fbfc;
border-radius: 18rpx;
font-size: 28rpx;
color: #23384d;
box-sizing: border-box;
border-width: 1rpx;
border-style: solid;
border-color: #eef2f6;
}
.input {
height: 84rpx;
padding: 0 24rpx;
}
.textarea {
padding: 22rpx 24rpx;
height: 160rpx;
}
.pill {
height: 64rpx;
padding: 0 24rpx;
border-radius: 999rpx;
background: #f3f6f8;
align-items: center;
justify-content: center;
margin-right: 12rpx;
margin-bottom: 12rpx;
box-sizing: border-box;
}
.pill-active {
background: #fff0f4;
border-width: 1rpx;
border-style: solid;
border-color: #ffc7d3;
}
.pill-text,
.tag-chip-text,
.booking-slot-label,
.booking-day-label,
.booking-day-date,
.booking-day-weekday,
.ghost-btn-text {
font-size: 24rpx;
color: #476072;
}
.pill-text-active,
.tag-chip-text-active,
.booking-slot-label-active,
.booking-day-label-active,
.booking-day-date-active,
.booking-day-weekday-active {
color: #ff5a7a;
font-weight: 700;
}
.tag-chip {
background: #f3f6f8;
padding-left: 20rpx;
padding-right: 20rpx;
}
.tag-chip-active {
background: #fff0f4;
border-width: 1rpx;
border-style: solid;
border-color: #ffc7d3;
}
.action-row {
margin-top: 20rpx;
flex-wrap: wrap;
}
.ghost-btn {
padding: 0 22rpx;
height: 68rpx;
border-radius: 999rpx;
background: #ffffff;
border-width: 1rpx;
border-style: solid;
border-color: #ffd5de;
align-items: center;
justify-content: center;
margin-right: 12rpx;
margin-bottom: 12rpx;
box-sizing: border-box;
}
.ghost-btn-text {
color: #ff5a7a;
font-weight: 700;
}
.booking-day-scroll {
height: 156rpx;
}
.booking-day-row {
padding-right: 20rpx;
}
.booking-day-card {
width: 160rpx;
padding: 20rpx;
border-radius: 24rpx;
background: #f8fbfc;
margin-right: 16rpx;
box-sizing: border-box;
border-width: 1rpx;
border-style: solid;
border-color: #eef2f6;
}
.booking-day-card-active {
background: #fff0f4;
border-color: #ffc7d3;
}
.booking-day-date,
.booking-day-weekday {
display: block;
margin-top: 6rpx;
}
.booking-slot-grid {
flex-wrap: wrap;
justify-content: space-between;
margin-top: 20rpx;
}
.booking-slot-card {
width: 48.5%;
height: 82rpx;
border-radius: 22rpx;
background: #f8fbfc;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
border-width: 1rpx;
border-style: solid;
border-color: #eef2f6;
box-sizing: border-box;
}
.booking-slot-card-active {
background: #fff0f4;
border-color: #ffc7d3;
}
.booking-slot-card-disabled {
opacity: 0.55;
background: #f1f5f9;
}
.package-list {
margin-bottom: 22rpx;
}
.package-card {
margin-bottom: 16rpx;
border-width: 1rpx;
border-style: solid;
border-color: #eef2f6;
}
.package-card-active {
background: #fff8fa;
border-color: #ffc7d3;
}
.package-name-active {
color: #ff5a7a;
}
.info-item {
margin-bottom: 22rpx;
}
.detail-page-bottom-space {
height: 180rpx;
}
.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);
box-sizing: border-box;
}
.booking-bottom-time {
display: block;
}
.booking-submit-btn {
height: 84rpx;
padding: 0 34rpx;
border-radius: 999rpx;
background: #16a085;
align-items: center;
justify-content: center;
box-sizing: border-box;
flex-shrink: 0;
}
.booking-submit-btn-text {
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
.booking-hint {
font-size: 24rpx;
color: #64748b;
margin-bottom: 18rpx;
line-height: 34rpx;
}
.custom-slot-section {
margin-top: 10rpx;
}
.custom-slot-title {
font-size: 26rpx;
font-weight: 600;
color: #16324f;
margin-bottom: 14rpx;
}
.custom-slot-picker {
height: 82rpx;
border-radius: 22rpx;
background: #f8fbfc;
border-width: 1rpx;
border-style: solid;
border-color: #eef2f6;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.custom-slot-picker-text {
font-size: 24rpx;
color: #476072;
}
</style>