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

641 lines
16 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="apply-page">
<ServicePageScaffold title="提交服务申请" fallback-url="/pages/mall/consumer/home-service/index">
<view class="summary-card">
<text class="summary-title">快速预约上门服务</text>
<text class="summary-desc">保留现有申请逻辑,把旧入口调整为预约下单流程页体验。</text>
<view class="summary-tips-row">
<text class="summary-tip">平台认证</text>
<text class="summary-tip">可预约</text>
<text class="summary-tip">服务保障</text>
</view>
</view>
<ServicePanel title="Step1 选择服务" subtitle="先确认预约的居家服务项目。">
<view class="choice-wrap">
<view
v-for="item in services"
:key="item.id"
class="choice-card"
:class="selectedServiceId == item.id ? 'choice-active' : ''"
@click="selectService(item.id, item.name)"
>
<view class="choice-header-row">
<text class="choice-title">{{ item.name }}</text>
<text class="choice-price">¥{{ item.price }}</text>
</view>
<text class="choice-desc">{{ item.durationText }} · {{ item.summary }}</text>
<text class="choice-suitable">适用对象:{{ item.suitableFor }}</text>
</view>
</view>
</ServicePanel>
<ServicePanel title="Step2 服务地址" subtitle="保持原有地址字段,先用预约卡形式承载。">
<view class="form-item">
<text class="label">服务地址</text>
<textarea v-model="form.address" class="textarea" placeholder="请输入详细上门地址"></textarea>
</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-date-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-item">
<text class="label">申请人</text>
<input v-model="form.applicantName" class="input" placeholder="请输入申请人姓名" />
</view>
<view class="form-item">
<text class="label">服务对象</text>
<input v-model="form.elderName" class="input" placeholder="请输入老人姓名" />
</view>
<view class="form-item">
<text class="label">年龄</text>
<input v-model="ageText" class="input" type="number" placeholder="请输入老人年龄" />
</view>
<view class="form-item">
<text class="label">联系电话</text>
<input v-model="form.phone" class="input" type="number" placeholder="请输入联系电话" />
</view>
<view class="form-item form-item-last">
<text class="label">需求说明</text>
<textarea v-model="form.demandSummary" class="textarea" placeholder="简要描述照护需求、病情重点、注意事项"></textarea>
</view>
</ServicePanel>
<view class="page-bottom-space"></view>
</ServicePageScaffold>
<view class="bottom-bar">
<view>
<text class="bottom-bar-label">预计金额</text>
<view class="bottom-bar-price-row">
<text class="bottom-bar-price-prefix">¥</text>
<text class="bottom-bar-price">{{ selectedPrice }}</text>
<text class="bottom-bar-price-unit">起</text>
</view>
</view>
<view class="submit-btn" @click="submitApplication">提交申请</view>
</view>
</view>
</template>
<script setup lang="uts">
import { computed, reactive, 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, fetchHomeServicePackages } from '@/services/homeServiceService.uts'
import { HomeServiceApplicationDraftType, HomeServiceCatalogType } from '@/types/home-service.uts'
import {
BookingDayOptionType,
BookingTimeSlotType,
buildBookingDays,
buildCustomSlots,
buildPresetSlots,
formatStandardAppointmentTime,
hasAnyAvailableSlots,
validateSelectedAppointmentTime
} from '@/utils/homeServiceBookingTime.uts'
const services = ref<Array<HomeServiceCatalogType>>([])
const selectedServiceId = ref('svc-001')
const ageText = ref('78')
const bookingDays = ref<Array<BookingDayOptionType>>([])
const bookingSlots = ref<Array<BookingTimeSlotType>>([])
const customSlots = ref<Array<BookingTimeSlotType>>([])
const selectedDayId = ref('')
const selectedSlotId = ref('')
const selectedCustomSlotId = ref('')
const customSlotIndex = ref(-1)
const form = reactive({
serviceId: 'svc-001',
serviceName: '基础上门护理',
selectedPackageId: '',
selectedPackageName: '',
applicantName: '李晓兰',
elderName: '李奶奶',
age: 78,
gender: '女',
phone: '13800138000',
address: '梅州市梅江区学海路 18 号 2 栋 602',
preferredTime: '',
appointmentStartAt: 0,
appointmentEndAt: 0,
appointmentDate: '',
slotSource: '',
minAdvanceMinutesSnapshot: 30,
demandSummary: '老人需要基础照护、血压监测和跌倒风险提醒。'
} as HomeServiceApplicationDraftType)
const selectedPackagePrice = ref('0')
const selectedPrice = computed((): string => {
if (selectedPackagePrice.value != '0') {
return selectedPackagePrice.value
}
return '0'
})
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)
services.value = await fetchHomeServiceCatalog()
if (services.value.length > 0) {
await selectService(services.value[0].id, services.value[0].name)
return
}
selectedServiceId.value = ''
form.serviceId = ''
form.serviceName = ''
}
async function selectService(serviceId: string, serviceName: string) {
selectedServiceId.value = serviceId
form.serviceId = serviceId
form.serviceName = serviceName
form.selectedPackageId = ''
form.selectedPackageName = ''
selectedPackagePrice.value = '0'
const packages = await fetchHomeServicePackages(serviceId)
if (packages.length > 0) {
form.selectedPackageId = packages[0].id
form.selectedPackageName = packages[0].packageName
selectedPackagePrice.value = packages[0].price.toString()
}
}
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]
}
}
return null
})
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 '请选择其他时间段'
})
function syncPreferredTime() {
const day = selectedDay.value
const slot = selectedSlot.value
if (day != null && slot != null) {
form.preferredTime = day.label + ' ' + day.dateText + ' ' + day.weekday + ' ' + slot.label
form.appointmentStartAt = slot.startAt
form.appointmentEndAt = slot.endAt
form.appointmentDate = day.dateKey
form.slotSource = slot.source
}
}
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)
}
syncPreferredTime()
}
function selectSlot(slotId: string, available: boolean) {
if (!available) {
uni.showToast({ title: '该时间段已无法预约,请选择稍晚时间', icon: 'none' })
return
}
selectedSlotId.value = slotId
selectedCustomSlotId.value = ''
customSlotIndex.value = -1
syncPreferredTime()
}
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
syncPreferredTime()
}
async function submitApplication() {
if (form.serviceId == '' || form.serviceName == '') {
uni.showToast({ title: '当前没有可预约的服务项目', icon: 'none' })
return
}
if (form.applicantName == '' || form.elderName == '' || form.phone == '' || form.address == '') {
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
}
const standardTime = formatStandardAppointmentTime(day, slot)
if (standardTime != '') {
form.preferredTime = standardTime
}
const parsedAge = parseInt(ageText.value)
form.age = isNaN(parsedAge) ? 0 : parsedAge
const created = await createHomeServiceApplication(form)
if (created == null) {
uni.showToast({ title: '申请提交失败,请检查登录和预约信息', icon: 'none' })
return
}
uni.showToast({ title: '申请已提交', icon: 'success' })
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
}
onLoad(() => {
loadData()
})
</script>
<style scoped>
.apply-page {
background: #f4f8fb;
}
.summary-card {
background: linear-gradient(180deg, #eafbf7 0%, #eff6ff 100%);
border-radius: 32rpx;
padding: 28rpx;
margin-bottom: 24rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 118, 110, 0.08);
}
.summary-title {
font-size: 38rpx;
font-weight: 700;
color: #16324f;
}
.summary-desc,
.choice-desc,
.choice-suitable,
.bottom-bar-label {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #64748b;
}
.summary-tips-row,
.choice-header-row,
.bottom-bar,
.bottom-bar-price-row,
.booking-day-row {
flex-direction: row;
align-items: center;
}
.summary-tips-row {
flex-wrap: wrap;
margin-top: 20rpx;
}
.summary-tip {
padding: 10rpx 16rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.78);
font-size: 22rpx;
color: #476072;
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-item-last {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
font-weight: 700;
color: #16324f;
margin-bottom: 12rpx;
}
.input,
.textarea {
width: 100%;
background: #f8fbfc;
border-radius: 18rpx;
font-size: 28rpx;
color: #23384d;
box-sizing: border-box;
}
.input {
height: 84rpx;
padding: 0 24rpx;
}
.textarea {
padding: 22rpx 24rpx;
height: 160rpx;
}
.choice-wrap {
margin-top: 4rpx;
}
.choice-card {
padding: 22rpx;
background: #f8fbfc;
border-radius: 24rpx;
margin-bottom: 16rpx;
border-width: 2rpx;
border-style: solid;
border-color: transparent;
}
.choice-active {
border-color: #0f766e;
background: #effcf8;
}
.choice-header-row {
justify-content: space-between;
}
.choice-title {
font-size: 30rpx;
font-weight: 700;
color: #16324f;
}
.choice-price,
.bottom-bar-price-prefix,
.bottom-bar-price {
font-size: 34rpx;
font-weight: 700;
color: #0f766e;
}
.choice-desc {
margin-top: 8rpx;
}
.booking-day-scroll {
height: 132rpx;
margin-bottom: 18rpx;
}
.booking-day-row {
padding-right: 20rpx;
}
.booking-day-card {
width: 150rpx;
padding: 18rpx;
border-radius: 22rpx;
background: #f8fbfd;
margin-right: 16rpx;
box-sizing: border-box;
}
.booking-day-card-active {
background: #0f766e;
}
.booking-day-label,
.booking-day-date,
.booking-slot-label {
font-size: 24rpx;
line-height: 34rpx;
color: #476072;
}
.booking-day-date {
margin-top: 6rpx;
}
.booking-day-label-active,
.booking-day-date-active,
.booking-slot-label-active {
color: #ffffff;
font-weight: 700;
}
.booking-slot-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 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: #0f766e;
border-color: #0f766e;
}
.booking-slot-card-disabled {
background: #f1f5f9;
opacity: 0.55;
}
.page-bottom-space {
height: 150rpx;
}
.bottom-bar {
position: fixed;
left: 24rpx;
right: 24rpx;
bottom: 28rpx;
justify-content: space-between;
background: #ffffff;
border-radius: 30rpx;
padding: 20rpx 22rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.12);
}
.bottom-bar-price-unit {
font-size: 22rpx;
color: #64748b;
margin-left: 8rpx;
margin-bottom: 4rpx;
}
.submit-btn {
height: 84rpx;
padding: 0 36rpx;
border-radius: 999rpx;
background: #0f766e;
align-items: center;
justify-content: center;
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>