Files
medical-mall/pages/mall/consumer/home-service/service-detail.uvue

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