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

604 lines
18 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>
<ServicePageScaffold title="服务单详情" fallback-url="/pages/mall/consumer/home-service/index">
<view v-if="detail == null" class="empty-box">
<text class="empty-text">未找到对应服务单</text>
</view>
<view v-else>
<view class="summary-card">
<view class="summary-top-row">
<view>
<text class="summary-title">{{ detail.serviceName }}</text>
<text class="summary-case-no">服务单号:{{ detail.caseNo }}</text>
</view>
<ServiceStatusTag :text="detail.statusText" :tone="detail.statusTone"></ServiceStatusTag>
</view>
<text class="summary-desc">{{ detail.summary }}</text>
<view class="summary-price-row">
<text class="summary-price-prefix">¥</text>
<text class="summary-price">{{ detail.amount }}</text>
<text class="summary-price-unit">服务金额</text>
</view>
<view class="summary-meta-grid">
<view class="summary-meta-item">
<text class="summary-meta-label">上门时间</text>
<text class="summary-meta-value">{{ detail.serviceTime }}</text>
</view>
<view class="summary-meta-item">
<text class="summary-meta-label">当前进度</text>
<text class="summary-meta-value">第 {{ detail.currentStep }} / {{ detail.totalSteps }} 步</text>
</view>
<view class="summary-meta-item">
<text class="summary-meta-label">服务机构 / 人员</text>
<text class="summary-meta-value">{{ detail.staffName }}</text>
</view>
<view class="summary-meta-item">
<text class="summary-meta-label">联系电话</text>
<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="围绕联系人、地址和服务对象展示当前预约信息。">
<ServiceInfoList
:items="[
{ label: '联系人:', value: detail.applicantName },
{ label: '服务对象:', value: detail.elderName + '' + detail.age + ' 岁' },
{ label: '联系电话:', value: detail.phone },
{ label: '服务地址:', value: detail.address },
{ label: '预约备注:', value: detail.summary }
]"
></ServiceInfoList>
</ServicePanel>
<ServicePanel title="服务保障" subtitle="用户在预约后仍可看到平台保障与追溯承诺。">
<view class="guarantee-row">
<text class="guarantee-chip">平台认证</text>
<text class="guarantee-chip">明码标价</text>
<text class="guarantee-chip">服务可追溯</text>
<text class="guarantee-chip">异常可申诉</text>
</view>
</ServicePanel>
<ServicePanel title="服务过程" subtitle="基于真实状态日志展示预约受理、派单、上门与验收进度。">
<ServiceInfoList
:items="[
{ label: '签到时间:', value: detail.checkinTime != '' ? detail.checkinTime : '暂未签到' },
{ label: '签到地点:', value: detail.checkinAddress != '' ? detail.checkinAddress : '暂未记录' },
{ label: '开始服务:', value: detail.serviceStartedAt != '' ? detail.serviceStartedAt : '暂未开始' },
{ label: '完成服务:', value: detail.serviceFinishedAt != '' ? detail.serviceFinishedAt : '暂未完成' },
{ label: '执行摘要:', value: detail.executionSummary != '' ? detail.executionSummary : '服务人员暂未提交执行摘要' },
{ label: '证据数量:', value: detail.evidenceCount > 0 ? String(detail.evidenceCount) + ' 份' : '暂未上传' }
]"
></ServiceInfoList>
<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 v-if="isServicePaymentExpired" class="action-row">
<view class="secondary-btn" @click="goHome">返回首页</view>
<view class="primary-btn" @click="bookAgain">再次预约</view>
</view>
<view v-else class="action-row">
<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>
</ServicePageScaffold>
</template>
<script setup lang="uts">
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, 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'
const caseId = ref('')
const detail = ref<HomeServiceCaseType | null>(null)
async function ensureLogin(): Promise<boolean> {
const user = await getCurrentUser()
if (user == null || getCurrentUserId() == '') {
goToLogin('/pages/mall/consumer/home-service/order-detail?id=' + caseId.value)
return false
}
return true
}
async function loadData() {
if (caseId.value == '') {
return
}
if (!(await ensureLogin())) {
detail.value = null
return
}
detail.value = await fetchConsumerHomeServiceCaseDetail(caseId.value)
}
function goFeedback() {
if (caseId.value == '') {
return
}
ensureLogin().then((ok) => {
if (!ok) {
return
}
uni.navigateTo({ url: '/pages/mall/consumer/home-service/feedback?id=' + caseId.value })
})
}
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 (isServicePaymentExpired.value) {
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
}
let serviceTargetId = 'svc-001'
if (detail.value.serviceName == '康复训练指导') {
serviceTargetId = 'svc-002'
}
if (detail.value.serviceName == '慢病健康随访') {
serviceTargetId = 'svc-003'
}
uni.navigateTo({ url: '/pages/mall/consumer/home-service/service-detail?id=' + serviceTargetId + '&mode=booking' })
}
function contactService() {
uni.showToast({ title: '即将接入专属客服入口', icon: 'none' })
}
function goHome() {
uni.navigateTo({ url: '/pages/mall/consumer/home-service/index' })
}
function getPayExpireMs(caseDetail: HomeServiceCaseType): number {
if (caseDetail.payExpireAt == null || caseDetail.payExpireAt == '') {
return 0
}
const parsed = Date.parse(caseDetail.payExpireAt)
return isNaN(parsed) ? 0 : parsed
}
function isPaymentTimeExpired(caseDetail: HomeServiceCaseType): boolean {
if (caseDetail.paymentStatus != 1 || caseDetail.status != 'created') {
return false
}
const expireMs = getPayExpireMs(caseDetail)
if (expireMs <= 0) {
return false
}
return expireMs <= Date.now()
}
const isServicePaymentExpired = computed<boolean>(() => {
if (detail.value == null) {
return false
}
return isPaymentTimeExpired(detail.value)
})
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 (isPaymentTimeExpired(detail.value)) {
result.showExceptionPanel = true
result.exceptionTitle = '订单已超时未支付'
result.exceptionDesc = '支付时间已结束,请返回首页重新预约或刷新查看最新状态。'
result.statusUpdatedAt = detail.value.payExpireAt != null && detail.value.payExpireAt != '' ? detail.value.payExpireAt : detail.value.serviceTime
return result
}
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'
&& !isPaymentTimeExpired(detail.value)
})
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().then(() => {
startDetailRefreshTimer()
})
}
})
onShow(() => {
loadData().then(() => {
startDetailRefreshTimer()
})
})
onUnload(() => {
stopDetailRefreshTimer()
})
</script>
<style scoped>
.summary-card {
background: #ffffff;
border-radius: 32rpx;
padding: 28rpx;
box-shadow: 0 12rpx 24rpx rgba(15, 23, 42, 0.06);
margin-bottom: 24rpx;
}
.summary-top-row,
.summary-price-row,
.action-row,
.guarantee-row {
flex-direction: row;
align-items: center;
}
.summary-top-row,
.action-row {
justify-content: space-between;
}
.summary-title {
font-size: 34rpx;
font-weight: 700;
color: #16324f;
}
.summary-case-no,
.summary-desc,
.summary-meta-label,
.summary-meta-value,
.empty-text {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #66788a;
}
.summary-desc {
margin-top: 18rpx;
}
.summary-price-row {
margin-top: 18rpx;
align-items: flex-end;
}
.summary-price-prefix,
.summary-price {
font-size: 40rpx;
font-weight: 700;
color: #0f766e;
}
.summary-price-unit {
font-size: 22rpx;
color: #64748b;
margin-left: 10rpx;
margin-bottom: 6rpx;
}
.summary-meta-grid {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 22rpx;
}
.summary-meta-item {
width: 48%;
padding: 22rpx;
border-radius: 24rpx;
background: #f8fbfd;
box-sizing: border-box;
margin-bottom: 16rpx;
}
.guarantee-row {
flex-wrap: wrap;
}
.guarantee-chip {
padding: 12rpx 18rpx;
border-radius: 999rpx;
background: #eef6ff;
font-size: 22rpx;
color: #476072;
margin-right: 12rpx;
margin-bottom: 12rpx;
}
.action-row {
margin-top: 10rpx;
margin-bottom: 12rpx;
}
.secondary-btn,
.primary-btn {
width: 48%;
height: 78rpx;
border-radius: 999rpx;
font-size: 26rpx;
font-weight: 700;
align-items: center;
justify-content: center;
}
.secondary-btn {
background: #ffffff;
border-width: 2rpx;
border-style: solid;
border-color: #cbd5e1;
color: #476072;
}
.primary-btn {
background: #16a085;
color: #ffffff;
}
.empty-text {
margin-top: 10rpx;
}
.empty-box {
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>