1747 lines
48 KiB
Plaintext
1747 lines
48 KiB
Plaintext
<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> |