完善服务模块缺少付款页的bug
This commit is contained in:
@@ -37,7 +37,8 @@
|
||||
</view>
|
||||
</ServicePanel>
|
||||
|
||||
<ServicePanel title="Step3 上门时间" subtitle="可直接选择推荐时段,也支持手动输入。">
|
||||
<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
|
||||
@@ -48,6 +49,7 @@
|
||||
>
|
||||
<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>
|
||||
@@ -61,9 +63,18 @@
|
||||
<text :class="['booking-slot-label', selectedSlotId == item.id ? 'booking-slot-label-active' : '']">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item form-item-last">
|
||||
<text class="label">期望时间</text>
|
||||
<input v-model="form.preferredTime" class="input" placeholder="例如 2026-05-14 上午" />
|
||||
<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>
|
||||
|
||||
@@ -112,45 +123,88 @@ 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 } from '@/services/homeServiceService.uts'
|
||||
import { createHomeServiceApplication, fetchHomeServiceCatalog, fetchHomeServicePackages } from '@/services/homeServiceService.uts'
|
||||
import { HomeServiceApplicationDraftType, HomeServiceCatalogType } from '@/types/home-service.uts'
|
||||
import { BookingDayOptionType, BookingTimeSlotType, getBookingDayOptions, getBookingTimeSlots } from '@/utils/homeServiceUiMock.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 selectedDayId = ref('day-1')
|
||||
const selectedSlotId = ref('slot-1')
|
||||
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: '2026-05-14 上午',
|
||||
preferredTime: '',
|
||||
appointmentStartAt: 0,
|
||||
appointmentEndAt: 0,
|
||||
appointmentDate: '',
|
||||
slotSource: '',
|
||||
minAdvanceMinutesSnapshot: 30,
|
||||
demandSummary: '老人需要基础照护、血压监测和跌倒风险提醒。'
|
||||
} as HomeServiceApplicationDraftType)
|
||||
|
||||
const selectedPackagePrice = ref('0')
|
||||
|
||||
const selectedPrice = computed((): string => {
|
||||
for (let i = 0; i < services.value.length; i++) {
|
||||
if (services.value[i].id == selectedServiceId.value) {
|
||||
return services.value[i].price.toString()
|
||||
}
|
||||
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() {
|
||||
bookingDays.value = getBookingDayOptions()
|
||||
bookingSlots.value = getBookingTimeSlots()
|
||||
const now = new Date()
|
||||
bookingDays.value = buildBookingDays(now)
|
||||
initDefaultDaySelection(now)
|
||||
services.value = await fetchHomeServiceCatalog()
|
||||
if (services.value.length > 0) {
|
||||
selectService(services.value[0].id, services.value[0].name)
|
||||
await selectService(services.value[0].id, services.value[0].name)
|
||||
return
|
||||
}
|
||||
selectedServiceId.value = ''
|
||||
@@ -158,43 +212,113 @@ async function loadData() {
|
||||
form.serviceName = ''
|
||||
}
|
||||
|
||||
function selectService(serviceId: string, serviceName: string) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
function syncPreferredTime() {
|
||||
let selectedDay = ''
|
||||
const selectedDay = computed((): BookingDayOptionType | null => {
|
||||
for (let i = 0; i < bookingDays.value.length; i++) {
|
||||
if (bookingDays.value[i].id == selectedDayId.value) {
|
||||
selectedDay = bookingDays.value[i].label + ' ' + bookingDays.value[i].dateText
|
||||
break
|
||||
return bookingDays.value[i]
|
||||
}
|
||||
}
|
||||
let selectedSlot = ''
|
||||
return null
|
||||
})
|
||||
|
||||
const selectedSlot = computed((): BookingTimeSlotType | null => {
|
||||
for (let i = 0; i < bookingSlots.value.length; i++) {
|
||||
if (bookingSlots.value[i].id == selectedSlotId.value) {
|
||||
selectedSlot = bookingSlots.value[i].label
|
||||
break
|
||||
return bookingSlots.value[i]
|
||||
}
|
||||
}
|
||||
if (selectedDay != '' && selectedSlot != '') {
|
||||
form.preferredTime = selectedDay + ' ' + selectedSlot
|
||||
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' })
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -203,10 +327,29 @@ async function submitApplication() {
|
||||
uni.showToast({ title: '当前没有可预约的服务项目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (form.applicantName == '' || form.elderName == '' || form.phone == '' || form.address == '' || form.preferredTime == '') {
|
||||
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
|
||||
@@ -460,4 +603,39 @@ onLoad(() => {
|
||||
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>
|
||||
@@ -36,6 +36,9 @@
|
||||
<text class="summary-meta-value">{{ detail.staffPhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="detail.statusText == '派单未成功' && detail.summary != ''" class="dispatch-fail-banner">
|
||||
<text class="dispatch-fail-text">{{ detail.summary }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<ServicePanel title="预约信息" subtitle="围绕联系人、地址和服务对象展示当前预约信息。">
|
||||
@@ -73,9 +76,22 @@
|
||||
<ServiceTimeline :items="detail.timeline"></ServiceTimeline>
|
||||
</ServicePanel>
|
||||
|
||||
<view v-if="consumerViewState.showExceptionPanel" class="exception-panel">
|
||||
<text class="exception-title">{{ consumerViewState.exceptionTitle }}</text>
|
||||
<text class="exception-desc">{{ consumerViewState.exceptionDesc }}</text>
|
||||
<view v-if="consumerViewState.exceptionReason != ''" class="exception-reason">
|
||||
<text class="exception-reason-label">异常原因:</text>
|
||||
<text class="exception-reason-value">{{ consumerViewState.exceptionReason }}</text>
|
||||
</view>
|
||||
<text class="exception-update-time">状态更新时间:{{ consumerViewState.statusUpdatedAt }}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<view class="secondary-btn" @click="bookAgain">再次预约</view>
|
||||
<view v-if="detail.statusText == '派单未成功'" class="secondary-btn" @click="bookAgain">再次预约</view>
|
||||
<view v-else class="secondary-btn" @click="bookAgain">再次预约</view>
|
||||
<view v-if="detail.status == 'pending_acceptance'" class="primary-btn" @click="goFeedback">去验收反馈</view>
|
||||
<view v-else-if="consumerViewState.showRescheduleBtn" class="primary-btn" @click="bookAgain">重新选择时间</view>
|
||||
<view v-else-if="detail.statusText == '派单未成功'" class="primary-btn" @click="retryDispatch">重新派单</view>
|
||||
<view v-else class="primary-btn" @click="contactService">联系客服</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -83,14 +99,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import ServicePageScaffold from '@/components/homeService/ServicePageScaffold.uvue'
|
||||
import ServiceInfoList from '@/components/homeService/ServiceInfoList.uvue'
|
||||
import ServicePanel from '@/components/homeService/ServicePanel.uvue'
|
||||
import ServiceStatusTag from '@/components/homeService/ServiceStatusTag.uvue'
|
||||
import ServiceTimeline from '@/components/homeService/ServiceTimeline.uvue'
|
||||
import { fetchConsumerHomeServiceCaseDetail } from '@/services/homeServiceService.uts'
|
||||
import { dispatchPaidHomecareOrder, getHomecareOrderDisplayStatus, HOMECARE_DISPATCH_STATUS_FAILED, showHomecareDispatchFailureModal } from '@/services/serviceOrderService.uts'
|
||||
import { HomeServiceCaseType } from '@/types/home-service.uts'
|
||||
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
|
||||
import { goToLogin } from '@/utils/utils.uts'
|
||||
@@ -130,6 +147,33 @@ function goFeedback() {
|
||||
})
|
||||
}
|
||||
|
||||
async function goPayment(): Promise<void> {
|
||||
if (caseId.value == '') {
|
||||
uni.showToast({ title: '订单信息异常', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (detail.value == null) {
|
||||
uni.showToast({ title: '订单信息加载失败,请稍后重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 刷新一次订单状态
|
||||
await loadData()
|
||||
if (detail.value == null) {
|
||||
uni.showToast({ title: '订单信息加载失败,请稍后重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!canPayServiceOrder.value) {
|
||||
uni.showToast({ title: '当前订单已不可支付,请刷新查看最新状态', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/consumer/payment'
|
||||
+ '?orderId=' + encodeURIComponent(detail.value.id)
|
||||
+ '&source=service'
|
||||
+ '&bizType=service'
|
||||
})
|
||||
}
|
||||
|
||||
function bookAgain() {
|
||||
if (detail.value == null) {
|
||||
return
|
||||
@@ -148,16 +192,172 @@ function contactService() {
|
||||
uni.showToast({ title: '即将接入专属客服入口', icon: 'none' })
|
||||
}
|
||||
|
||||
let isRetryDispatching = false
|
||||
|
||||
function retryDispatch() {
|
||||
if (isRetryDispatching || detail.value == null) {
|
||||
return
|
||||
}
|
||||
const currentId = detail.value.id
|
||||
isRetryDispatching = true
|
||||
uni.showLoading({ title: '正在重新派单', mask: true })
|
||||
dispatchPaidHomecareOrder(currentId).then((result) => {
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.showToast({ title: '派单成功', icon: 'success' })
|
||||
loadData()
|
||||
return
|
||||
}
|
||||
showHomecareDispatchFailureModal(currentId, result, (id: string) => {
|
||||
retryDispatch()
|
||||
})
|
||||
}).catch((e) => {
|
||||
uni.hideLoading()
|
||||
console.error('[retryDispatch] 重新派单异常:', e)
|
||||
uni.showModal({
|
||||
title: '派单服务异常',
|
||||
content: '派单服务暂时异常,请稍后重试',
|
||||
showCancel: true,
|
||||
cancelText: '稍后再试',
|
||||
confirmText: '重新派单',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
retryDispatch()
|
||||
}
|
||||
}
|
||||
})
|
||||
}).finally(() => {
|
||||
isRetryDispatching = false
|
||||
})
|
||||
}
|
||||
|
||||
function getLatestTimelineRemark(caseDetail: HomeServiceCaseType): string {
|
||||
if (caseDetail.timeline.length > 0) {
|
||||
return caseDetail.timeline[0].description
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function isTerminalStatus(status: string): boolean {
|
||||
return status == 'accepted_by_user' || status == 'reviewed' || status == 'settled' || status == 'cancelled' || status == 'exception'
|
||||
}
|
||||
|
||||
const consumerViewState = computed(() => {
|
||||
const defaultState = {
|
||||
showExceptionPanel: false,
|
||||
exceptionTitle: '',
|
||||
exceptionDesc: '',
|
||||
exceptionReason: '',
|
||||
statusUpdatedAt: '',
|
||||
showRescheduleBtn: false,
|
||||
showCancelBtn: false,
|
||||
showRefundBtn: false
|
||||
}
|
||||
if (detail.value == null) {
|
||||
return defaultState
|
||||
}
|
||||
const status = detail.value.status
|
||||
const remark = getLatestTimelineRemark(detail.value)
|
||||
const result = { ...defaultState }
|
||||
|
||||
if (detail.value.statusText == '派单未成功') {
|
||||
result.showExceptionPanel = true
|
||||
result.exceptionTitle = '派单未成功'
|
||||
result.exceptionDesc = detail.value.summary != '' ? detail.value.summary : '当前暂无匹配的服务人员,请稍后重试或联系客服。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'created' || status == 'assigned') {
|
||||
result.exceptionTitle = '正在安排服务人员'
|
||||
result.exceptionDesc = '您的预约申请已提交,平台正在为您匹配可上门的服务人员,请耐心等待。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'accepted') {
|
||||
result.exceptionTitle = '服务人员已接单'
|
||||
result.exceptionDesc = '服务人员已确认接单,正在准备上门,请保持电话畅通。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'departed') {
|
||||
result.exceptionTitle = '服务人员正在前往'
|
||||
result.exceptionDesc = '服务人员已出发,正在前往服务地点,请做好接待准备。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'arrived' || status == 'in_service') {
|
||||
result.exceptionTitle = '服务人员已到达'
|
||||
result.exceptionDesc = '服务人员已到达服务地点,服务正在进行中。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'rejected') {
|
||||
result.showExceptionPanel = true
|
||||
result.exceptionTitle = '当前预约暂未安排到服务人员'
|
||||
result.exceptionDesc = '很抱歉,服务人员未接受该工单。您可以重新选择服务时间,或取消本次服务申请。'
|
||||
result.exceptionReason = remark != '' ? remark : '服务人员未接单'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
result.showRescheduleBtn = true
|
||||
} else if (status == 'exception') {
|
||||
result.showExceptionPanel = true
|
||||
result.exceptionTitle = '当前预约暂未安排到服务人员'
|
||||
result.exceptionDesc = '很抱歉,当前所选预约时间暂未匹配到可服务人员。您可以重新选择服务时间,或取消本次服务申请。'
|
||||
result.exceptionReason = remark != '' ? remark : '履约异常,请稍后重试或联系客服'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
result.showRescheduleBtn = true
|
||||
} else if (status == 'cancelled') {
|
||||
result.showExceptionPanel = true
|
||||
result.exceptionTitle = '服务申请已取消'
|
||||
result.exceptionDesc = '该服务申请已被取消。如有疑问,请联系客服了解详情。'
|
||||
result.exceptionReason = remark != '' ? remark : '已取消'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'pending_acceptance') {
|
||||
result.exceptionTitle = '服务已完成,等待验收'
|
||||
result.exceptionDesc = '服务人员已提交服务记录,请您确认服务结果并进行评价。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
} else if (status == 'completed' || status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') {
|
||||
result.exceptionTitle = '服务已完成'
|
||||
result.exceptionDesc = '本次服务已结束,感谢您的使用。'
|
||||
result.statusUpdatedAt = detail.value.serviceTime
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const canPayServiceOrder = computed<boolean>(() => {
|
||||
if (detail.value == null) {
|
||||
return false
|
||||
}
|
||||
return detail.value.paymentStatus == 1
|
||||
&& detail.value.status == 'created'
|
||||
})
|
||||
|
||||
let detailRefreshTimerId: number = 0
|
||||
|
||||
function startDetailRefreshTimer(): void {
|
||||
stopDetailRefreshTimer()
|
||||
if (detail.value != null && !isTerminalStatus(detail.value.status)) {
|
||||
detailRefreshTimerId = setInterval(() => {
|
||||
loadData()
|
||||
}, 15000)
|
||||
}
|
||||
}
|
||||
|
||||
function stopDetailRefreshTimer(): void {
|
||||
if (detailRefreshTimerId > 0) {
|
||||
clearInterval(detailRefreshTimerId)
|
||||
detailRefreshTimerId = 0
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
const id = options['id']
|
||||
if (id != null) {
|
||||
caseId.value = id as string
|
||||
loadData()
|
||||
loadData().then(() => {
|
||||
startDetailRefreshTimer()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadData()
|
||||
loadData().then(() => {
|
||||
startDetailRefreshTimer()
|
||||
})
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
stopDetailRefreshTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -291,4 +491,66 @@ onShow(() => {
|
||||
padding: 120rpx 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.exception-panel {
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.exception-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #b45309;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.exception-desc {
|
||||
font-size: 24rpx;
|
||||
color: #66788a;
|
||||
line-height: 36rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.exception-reason {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.exception-reason-label {
|
||||
font-size: 24rpx;
|
||||
color: #66788a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.exception-reason-value {
|
||||
font-size: 24rpx;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.exception-update-time {
|
||||
font-size: 22rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dispatch-fail-banner {
|
||||
margin-top: 18rpx;
|
||||
padding: 18rpx 24rpx;
|
||||
background: #fff7ed;
|
||||
border-radius: 16rpx;
|
||||
border-width: 1rpx;
|
||||
border-style: solid;
|
||||
border-color: #fed7aa;
|
||||
}
|
||||
|
||||
.dispatch-fail-text {
|
||||
font-size: 26rpx;
|
||||
color: #c2410c;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -85,7 +85,8 @@
|
||||
</view>
|
||||
</ServicePanel>
|
||||
|
||||
<ServicePanel title="Step3 上门时间" subtitle="先选日期和时间段,再提交预约。">
|
||||
<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
|
||||
@@ -110,6 +111,19 @@
|
||||
<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="补全被照护人情况,便于机构判断服务方案。">
|
||||
@@ -221,7 +235,10 @@
|
||||
@click="selectServicePackage(item.id)"
|
||||
>
|
||||
<view class="package-top-row">
|
||||
<text :class="['package-name', item.selected ? 'package-name-active' : '']">{{ item.name }}</text>
|
||||
<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>
|
||||
@@ -271,7 +288,7 @@
|
||||
<text class="booking-bottom-time">{{ selectedTimeText }}</text>
|
||||
</view>
|
||||
<view class="booking-submit-btn" @click="submitBooking">
|
||||
<text class="booking-submit-btn-text">立即预约</text>
|
||||
<text class="booking-submit-btn-text">提交预约</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -279,25 +296,33 @@
|
||||
|
||||
<script setup lang="uts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
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 } from '@/services/homeServiceService.uts'
|
||||
import { HomeServiceApplicationDraftType, HomeServiceCatalogType, HomeServiceSelectedAddressType } from '@/types/home-service.uts'
|
||||
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 { goToLogin } from '@/utils/utils.uts'
|
||||
import {
|
||||
BookingDayOptionType,
|
||||
BookingTimeSlotType,
|
||||
HomeServiceAgencyType,
|
||||
HomeServiceGuaranteeItemType,
|
||||
getBookingDayOptions,
|
||||
getBookingTimeSlots,
|
||||
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
|
||||
@@ -337,6 +362,7 @@ type ServicePackageOptionType = {
|
||||
desc: string
|
||||
price: number
|
||||
duration: string
|
||||
sourceTag: string
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
@@ -397,9 +423,13 @@ const defaultCareAddress: CareAddressInfoType = {
|
||||
const careNeedOptions = ref<Array<CareNeedOptionType>>([])
|
||||
const careRiskOptions = ref<Array<CareRiskOptionType>>([])
|
||||
const servicePackages = ref<Array<ServicePackageOptionType>>([])
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
|
||||
const selectedDayId = ref('day-1')
|
||||
const selectedSlotId = ref('slot-1')
|
||||
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)
|
||||
@@ -486,27 +516,20 @@ function buildDefaultRiskOptions(currentServiceId: string): Array<CareRiskOption
|
||||
]
|
||||
}
|
||||
|
||||
function buildDefaultPackages(currentServiceId: string, basePrice: number, durationText: string): Array<ServicePackageOptionType> {
|
||||
// TODO: 后续接入真实服务套餐与机构报价接口,当前先使用页面内 mock 方案。
|
||||
if (currentServiceId == 'svc-002') {
|
||||
return [
|
||||
{ id: 'pkg_rehab_assess', name: '康复基础评估', desc: '动作评估、居家训练建议、康复重点说明', price: Math.max(basePrice - 30, 128), duration: '约 1.5 小时', selected: true },
|
||||
{ id: 'pkg_rehab_plus', name: '上门康复训练', desc: '动作陪练、训练纠正、阶段性康复记录', price: basePrice, duration: durationText, selected: false },
|
||||
{ id: 'pkg_rehab_follow', name: '连续康复计划', desc: '适合术后恢复阶段,支持持续跟踪与复盘', price: basePrice + 58, duration: '约 2.5 小时', selected: false }
|
||||
]
|
||||
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
|
||||
})
|
||||
}
|
||||
if (currentServiceId == 'svc-003') {
|
||||
return [
|
||||
{ id: 'pkg_chronic_basic', name: '慢病基础随访', desc: '血压血糖记录、用药核对、风险提醒', price: Math.max(basePrice - 20, 98), duration: '约 1 小时', selected: true },
|
||||
{ id: 'pkg_chronic_plus', name: '慢病加强随访', desc: '增加生活方式建议和家属宣教内容', price: basePrice, duration: durationText, selected: false },
|
||||
{ id: 'pkg_chronic_family', name: '家庭联合随访', desc: '适合需要家属共同参与的连续照护场景', price: basePrice + 36, duration: '约 1.5 小时', selected: false }
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ id: 'pkg_basic_home', name: '基础照护', desc: '生命体征观察、协助起居、用药提醒', price: Math.max(basePrice - 20, 148), duration: '约 1.5 小时', selected: true },
|
||||
{ id: 'pkg_basic_nursing', name: '标准照护', desc: '适合日常陪护与护理观察,支持家属沟通', price: basePrice, duration: durationText, selected: false },
|
||||
{ id: 'pkg_basic_plus', name: '加强照护', desc: '增加翻身协助、环境提醒和风险沟通', price: basePrice + 48, duration: '约 2.5 小时', selected: false }
|
||||
]
|
||||
return result
|
||||
}
|
||||
|
||||
const addressLineText = computed((): string => {
|
||||
@@ -560,25 +583,60 @@ function loadCachedSelectedAddress(): void {
|
||||
applySelectedAddress(null)
|
||||
}
|
||||
|
||||
const selectedTimeText = computed((): string => {
|
||||
let selectedDayLabel = ''
|
||||
const selectedDay = computed((): BookingDayOptionType | null => {
|
||||
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
|
||||
return bookingDays.value[i]
|
||||
}
|
||||
}
|
||||
let selectedSlotLabel = ''
|
||||
return null
|
||||
})
|
||||
|
||||
const selectedSlot = computed((): BookingTimeSlotType | null => {
|
||||
for (let i = 0; i < bookingSlots.value.length; i++) {
|
||||
if (bookingSlots.value[i].id == selectedSlotId.value) {
|
||||
selectedSlotLabel = bookingSlots.value[i].label
|
||||
break
|
||||
return bookingSlots.value[i]
|
||||
}
|
||||
}
|
||||
if (selectedDayLabel == '' || selectedSlotLabel == '') {
|
||||
return '请选择上门时间'
|
||||
for (let i = 0; i < customSlots.value.length; i++) {
|
||||
if (customSlots.value[i].id == selectedSlotId.value) {
|
||||
return customSlots.value[i]
|
||||
}
|
||||
}
|
||||
return selectedDayLabel + ' ' + selectedSlotLabel
|
||||
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 => {
|
||||
@@ -674,23 +732,45 @@ function buildDemandSummary(): string {
|
||||
|
||||
function setUnavailableServiceState() {
|
||||
serviceTitle.value = '服务暂未配置'
|
||||
serviceSubtitle.value = '当前服务目录未找到该项目,请稍后再试。'
|
||||
serviceSubtitle.value = '当前服务目录或套餐未配置,请稍后再试。'
|
||||
servicePrice.value = 0
|
||||
serviceDuration.value = '待配置'
|
||||
serviceSuitableFor.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() {
|
||||
bookingDays.value = getBookingDayOptions()
|
||||
bookingSlots.value = getBookingTimeSlots()
|
||||
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)
|
||||
servicePackages.value = buildDefaultPackages(serviceId.value, servicePrice.value, serviceDuration.value)
|
||||
|
||||
const catalog = await fetchHomeServiceCatalog()
|
||||
let matchedService: HomeServiceCatalogType | null = null
|
||||
@@ -706,14 +786,21 @@ async function loadData() {
|
||||
}
|
||||
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
|
||||
}
|
||||
servicePackages.value = buildDefaultPackages(serviceId.value, servicePrice.value, serviceDuration.value)
|
||||
const packages = await fetchHomeServicePackages(serviceId.value)
|
||||
servicePackages.value = mapServicePackages(packages)
|
||||
if (servicePackages.value.length == 0) {
|
||||
setUnavailableServiceState()
|
||||
return
|
||||
}
|
||||
const firstPackage = servicePackages.value[0]
|
||||
servicePrice.value = firstPackage.price
|
||||
serviceDuration.value = firstPackage.duration
|
||||
}
|
||||
|
||||
async function ensureLogin(): Promise<boolean> {
|
||||
@@ -748,14 +835,44 @@ function handleAgencyNavigate() {
|
||||
|
||||
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' })
|
||||
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) {
|
||||
@@ -808,9 +925,33 @@ 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
|
||||
}
|
||||
@@ -839,32 +980,79 @@ async function submitBooking() {
|
||||
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: selectedTimeText.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
|
||||
}
|
||||
|
||||
const created = await createHomeServiceApplication(draft)
|
||||
if (created == null) {
|
||||
uni.showToast({ title: '预约提交失败,请稍后重试', icon: 'none' })
|
||||
return
|
||||
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
|
||||
}
|
||||
uni.showToast({ title: '预约已提交', icon: 'success' })
|
||||
uni.navigateTo({ url: '/pages/mall/consumer/home-service/order-detail?id=' + created.id })
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
@@ -879,8 +1067,62 @@ onLoad((options) => {
|
||||
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()
|
||||
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>
|
||||
|
||||
@@ -1011,6 +1253,7 @@ onShow(() => {
|
||||
.booking-slot-grid,
|
||||
.agency-top-row,
|
||||
.action-row,
|
||||
.package-name-row,
|
||||
.package-top-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -1023,6 +1266,14 @@ onShow(() => {
|
||||
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;
|
||||
@@ -1085,6 +1336,16 @@ onShow(() => {
|
||||
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;
|
||||
@@ -1390,4 +1651,39 @@ onShow(() => {
|
||||
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>
|
||||
@@ -175,6 +175,7 @@ import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
import { goToLogin } from '@/utils/utils.uts'
|
||||
import { formatCountdownHM, getRemainingSeconds, getUnifiedDisplayState, isOrderPayExpired, isPendingPayOrder, ORDER_STATUS_CANCELLED, ORDER_STATUS_PENDING, ORDER_STATUS_TIMEOUT_LEGACY, ORDER_TIMEOUT_CANCEL_REASON, PAYMENT_STATUS_TIMEOUT, PAYMENT_STATUS_UNPAID, type OrderStatusSource } from '@/utils/orderStatus.uts'
|
||||
import { dispatchPaidHomecareOrder, showHomecareDispatchFailureModal } from '@/services/serviceOrderService.uts'
|
||||
|
||||
type BusinessTabItem = {
|
||||
id: string,
|
||||
@@ -223,7 +224,9 @@ type OrderItem = {
|
||||
merchant_id: string,
|
||||
shop_name: string,
|
||||
products: OrderProduct[],
|
||||
service_info: UnifiedServiceInfo | null
|
||||
service_info: UnifiedServiceInfo | null,
|
||||
dispatch_status: string,
|
||||
dispatch_error_message: string
|
||||
}
|
||||
|
||||
type UnifiedOrdersCursorResult = {
|
||||
@@ -713,7 +716,9 @@ const mapRawOrdersToViewModels = (rawOrders: UTSJSONObject[]): OrderItem[] => {
|
||||
merchant_id: merchantId ?? '',
|
||||
shop_name: shopName,
|
||||
products: productsList,
|
||||
service_info: serviceInfo
|
||||
service_info: serviceInfo,
|
||||
dispatch_status: orderObj.getString('dispatch_status') ?? '',
|
||||
dispatch_error_message: orderObj.getString('dispatch_error_message') ?? ''
|
||||
})
|
||||
}
|
||||
return mappedOrders
|
||||
@@ -1333,7 +1338,11 @@ const getOrderStatusText = (order: OrderItem): string => {
|
||||
if (displayState == 'cancelled' || displayState == 'expired') return '已取消'
|
||||
|
||||
if (order.biz_type === 'service') {
|
||||
if (order.status == 1) return '待付款'
|
||||
if (order.payment_status == 1 && order.status == 1) return '待付款'
|
||||
if (order.payment_status == 2 && (order.dispatch_status == 'pending' || order.dispatch_status == '')) return '待派单'
|
||||
if (order.payment_status == 2 && order.dispatch_status == 'dispatching') return '正在派单'
|
||||
if (order.payment_status == 2 && order.dispatch_status == 'failed') return '派单未成功'
|
||||
if (order.payment_status == 2 && order.dispatch_status == 'assigned') return '已派单'
|
||||
if (order.status == 2) return '待接单'
|
||||
if (order.status == 3) return '待服务'
|
||||
if (order.status == 4) return '服务中'
|
||||
@@ -1648,6 +1657,50 @@ const rebookService = (order: OrderItem) => {
|
||||
uni.showToast({ title: '暂无可预约服务', icon: 'none' })
|
||||
}
|
||||
|
||||
let isRedispatching = false
|
||||
|
||||
const redispatchServiceOrder = async (order: OrderItem) => {
|
||||
if (isRedispatching) {
|
||||
return
|
||||
}
|
||||
isRedispatching = true
|
||||
uni.showLoading({ title: '正在重新派单', mask: true })
|
||||
try {
|
||||
const result = await dispatchPaidHomecareOrder(order.id)
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.showToast({ title: '派单成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: `/pages/mall/consumer/home-service/order-detail?id=${order.id}` })
|
||||
}, 800)
|
||||
return
|
||||
}
|
||||
showHomecareDispatchFailureModal(order.id, result, (id: string) => {
|
||||
const targetOrder = getLoadedOrderById(id)
|
||||
if (targetOrder != null) {
|
||||
redispatchServiceOrder(targetOrder)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[redispatchServiceOrder] 重新派单异常:', e)
|
||||
uni.showModal({
|
||||
title: '派单服务异常',
|
||||
content: '派单服务暂时异常,请稍后重试',
|
||||
showCancel: true,
|
||||
cancelText: '稍后再试',
|
||||
confirmText: '重新派单',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
redispatchServiceOrder(order)
|
||||
}
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
isRedispatching = false
|
||||
}
|
||||
}
|
||||
|
||||
const viewSimilarService = (order: OrderItem) => {
|
||||
const serviceName = order.service_info != null ? order.service_info.service_name : ''
|
||||
const url = serviceName != ''
|
||||
@@ -1727,6 +1780,11 @@ const getOrderActions = (order: OrderItem): OrderActionItem[] => {
|
||||
}
|
||||
|
||||
if (order.biz_type === 'service') {
|
||||
if (order.payment_status == 2 && order.dispatch_status == 'failed') {
|
||||
actions.push({ id: 'view-detail', name: '查看详情', kind: 'secondary' })
|
||||
actions.push({ id: 'redispatch', name: '重新派单', kind: 'primary' })
|
||||
return actions
|
||||
}
|
||||
if (order.status === 2) {
|
||||
actions.push({ id: 'cancel', name: '取消预约', kind: 'secondary' })
|
||||
actions.push({ id: 'contact-service', name: '联系客服', kind: 'secondary' })
|
||||
@@ -1797,6 +1855,8 @@ const onOrderActionTap = (order: OrderItem, actionId: string) => {
|
||||
uni.showToast({ title: '服务评价功能待接入', icon: 'none' })
|
||||
} else if (actionId === 'rebook') {
|
||||
rebookService(order)
|
||||
} else if (actionId === 'redispatch') {
|
||||
redispatchServiceOrder(order)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ import { supabaseService } from '@/utils/supabaseService.uts'
|
||||
import { goToLogin } from '@/utils/utils.uts'
|
||||
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
|
||||
import { formatCountdownHMS, getUnifiedDisplayState, ORDER_PAY_TIMEOUT_SECONDS, ORDER_STATUS_CANCELLED, ORDER_STATUS_PAID_OR_SHIPPING, ORDER_STATUS_PENDING, ORDER_STATUS_TIMEOUT_LEGACY, ORDER_TIMEOUT_CANCEL_REASON, PAYMENT_STATUS_PAID, PAYMENT_STATUS_TIMEOUT, PAYMENT_STATUS_UNPAID, type OrderStatusSource } from '@/utils/orderStatus.uts'
|
||||
import { dispatchPaidHomecareOrder, showHomecareDispatchFailureModal } from '@/services/serviceOrderService.uts'
|
||||
|
||||
type PaymentMethodType = {
|
||||
id: string
|
||||
@@ -430,6 +431,7 @@ const paymentMethods = ref<Array<PaymentMethodType>>([])
|
||||
const selectedMethod = ref<string>('wechat')
|
||||
const userBalance = ref<number>(0)
|
||||
const isPaying = ref<boolean>(false)
|
||||
const isDispatchSubmitting = ref<boolean>(false)
|
||||
const showPassword = ref<boolean>(false)
|
||||
const password = ref<string>('')
|
||||
const addressInfo = ref<AddressInfo>(createEmptyAddress())
|
||||
@@ -1469,6 +1471,42 @@ const confirmPayment = async () => {
|
||||
|
||||
uni.$emit('orderUpdated', { orderId: orderId.value, status: ORDER_STATUS_PAID_OR_SHIPPING, paymentStatus: PAYMENT_STATUS_PAID })
|
||||
|
||||
if (source.value == 'service' || bizType.value == 'service') {
|
||||
isDispatchSubmitting.value = true
|
||||
uni.showLoading({ title: '正在安排服务', mask: true })
|
||||
try {
|
||||
const dispatchResult = await dispatchPaidHomecareOrder(orderId.value)
|
||||
uni.hideLoading()
|
||||
if (dispatchResult.success) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/mall/consumer/home-service/order-detail?id=' + orderId.value
|
||||
})
|
||||
return
|
||||
}
|
||||
showHomecareDispatchFailureModal(orderId.value, dispatchResult, (id: string) => {
|
||||
retryHomecareDispatch(id)
|
||||
})
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[confirmPayment] 派单异常:', e)
|
||||
uni.showModal({
|
||||
title: '派单服务异常',
|
||||
content: '派单服务暂时异常,请稍后重试',
|
||||
showCancel: true,
|
||||
cancelText: '稍后再试',
|
||||
confirmText: '重新派单',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
retryHomecareDispatch(orderId.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
isDispatchSubmitting.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/mall/consumer/payment-success?orderId=' + orderId.value
|
||||
@@ -1545,6 +1583,44 @@ const forgotPassword = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const retryHomecareDispatch = async (targetOrderId: string) => {
|
||||
if (isDispatchSubmitting.value) {
|
||||
return
|
||||
}
|
||||
isDispatchSubmitting.value = true
|
||||
uni.showLoading({ title: '正在重新派单', mask: true })
|
||||
try {
|
||||
const result = await dispatchPaidHomecareOrder(targetOrderId)
|
||||
uni.hideLoading()
|
||||
if (result.success) {
|
||||
uni.redirectTo({
|
||||
url: '/pages/mall/consumer/home-service/order-detail?id=' + targetOrderId
|
||||
})
|
||||
return
|
||||
}
|
||||
showHomecareDispatchFailureModal(targetOrderId, result, (id: string) => {
|
||||
retryHomecareDispatch(id)
|
||||
})
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.error('[retryHomecareDispatch] 重新派单异常:', e)
|
||||
uni.showModal({
|
||||
title: '派单服务异常',
|
||||
content: '派单服务暂时异常,请稍后重试',
|
||||
showCancel: true,
|
||||
cancelText: '稍后再试',
|
||||
confirmText: '重新派单',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
retryHomecareDispatch(targetOrderId)
|
||||
}
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
isDispatchSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goToOrderList = () => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/mall/consumer/orders'
|
||||
|
||||
Reference in New Issue
Block a user