修复订单显示bug

This commit is contained in:
2026-06-10 20:20:47 +08:00
parent de62513987
commit 9fbc6f8cd1
45 changed files with 7514 additions and 2025 deletions

View File

@@ -86,6 +86,17 @@
<text class="exception-update-time">状态更新时间:{{ consumerViewState.statusUpdatedAt }}</text>
</view>
<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>
@@ -105,6 +116,8 @@
<script setup lang="uts">
import { computed, ref } from 'vue'
import { onLoad, onShow, onUnload } 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'
@@ -200,75 +213,6 @@ 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
@@ -298,20 +242,7 @@ const consumerViewState = computed(() => {
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') {
if (status == 'created' || status == 'assigned') {
result.exceptionTitle = '正在安排服务人员'
result.exceptionDesc = '您的预约申请已提交,平台正在为您匹配可上门的服务人员,请耐心等待。'
result.statusUpdatedAt = detail.value.serviceTime
@@ -360,15 +291,6 @@ const consumerViewState = computed(() => {
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 {
@@ -584,20 +506,4 @@ onUnload(() => {
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>

View File

@@ -1,10 +1,33 @@
<template>
<ServicePageScaffold title="到达签到" fallback-url="/pages/mall/delivery/orders/route">
<ServicePanel title="居家服务认证" subtitle="使用居家服务系统账号登录">
<view v-if="!isHomecareLoggedIn">
<text class="info-text">状态:未登录</text>
<text class="info-text warning-text">居家服务认证服务暂不可用,请跳过此步骤继续操作</text>
</view>
<view v-else>
<text class="info-text">已登录:{{ homecareUserEmail }}</text>
<text class="info-text success-text">居家服务认证通过</text>
</view>
</ServicePanel>
<ServicePanel title="签到要求" subtitle="定位签到或扫码签到,要求在 50 米范围内并上传现场图片。">
<text v-if="order != null" class="info-text">服务地址:{{ order.fullAddress }}</text>
<text class="info-text">当前定位:{{ locationText }}</text>
<text class="info-text">定位精度:{{ accuracyText }}</text>
<text class="info-text">现场图片:{{ photos.length }} 张</text>
</ServicePanel>
<ServicePanel title="距离预校验" subtitle="获取定位后校验是否在允许签到范围内">
<text class="info-text">距离服务点:{{ distanceText }}</text>
<text class="info-text">允许半径:{{ allowedRadiusText }}</text>
<text class="info-text">校验状态:{{ precheckStatusText }}</text>
<text v-if="reasonText !== ''" class="info-text warning-text">{{ reasonText }}</text>
<view class="button-stack">
<button class="secondary-btn" :disabled="prechecking" @click="handlePrecheck">{{ prechecking ? '预校验中...' : '距离预校验' }}</button>
</view>
</ServicePanel>
<ServicePanel title="签到操作" subtitle="定位失败时要给出清晰提示。">
<view class="button-stack">
<button class="secondary-btn" @click="getCurrentLocation">获取 GPS 定位</button>
@@ -25,15 +48,63 @@ import type { DeliveryLocationType, DeliveryOrderType } from '@/types/delivery.u
import { checkinOrder, getDeliveryOrderDetail } from '@/services/deliveryService.uts'
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts'
import {
emailLogin,
checkinPrecheck,
getHomecareToken,
getHomecareUser,
getReasonText
} from '@/utils/homecareAuth.uts'
const orderId = ref('')
const order = ref<DeliveryOrderType | null>(null)
const currentLocation = ref<DeliveryLocationType | null>(null)
const locationText = ref('未获取')
const accuracyText = ref('未知')
const photos = ref([] as Array<string>)
const note = ref('')
const submitting = ref(false)
// 居家服务认证状态
const isHomecareLoggedIn = ref(false)
const homecareUserEmail = ref('')
// 距离预校验状态
const prechecking = ref(false)
const canCheckin = ref(false)
const distanceText = ref('未校验')
const allowedRadiusText = ref('未校验')
const precheckStatusText = ref('未校验')
const reasonText = ref('')
function updateHomecareLoginStatus(): void {
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: called')
const token = getHomecareToken()
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: token length:', token.length)
if (token !== '') {
isHomecareLoggedIn.value = true
const user = getHomecareUser()
if (user != null) {
const email = user.getString('email')
homecareUserEmail.value = email != null ? email : ''
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: logged in as', homecareUserEmail.value)
}
} else {
isHomecareLoggedIn.value = false
homecareUserEmail.value = ''
console.warn('[CHECKIN DEBUG] updateHomecareLoginStatus: not logged in')
}
}
async function loginHomecare(): Promise<void> {
const token = getHomecareToken()
if (token !== '') {
uni.showToast({ title: '已登录,无需重复登录', icon: 'success' })
return
}
uni.showToast({ title: '居家服务认证服务暂不可用', icon: 'none' })
}
function toRadians(value: number): number {
return value * 3.1415926 / 180
}
@@ -48,13 +119,73 @@ function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: numbe
}
function wrapLocation(): Promise<DeliveryLocationType> {
console.warn('[CHECKIN DEBUG] wrapLocation: starting...')
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
enableHighAccuracy: true,
success: (res) => {
resolve({ latitude: res.latitude, longitude: res.longitude, address: '当前位置', time: new Date().toISOString().replace('T', ' ').substring(0, 19) })
console.warn('[CHECKIN DEBUG] wrapLocation SUCCESS:', JSON.stringify({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
speed: res.speed,
altitude: res.altitude,
altitudeAccuracy: res.altitudeAccuracy,
heading: res.heading,
timestamp: res.timestamp
}))
resolve({
latitude: res.latitude,
longitude: res.longitude,
address: '当前位置',
time: new Date().toISOString().replace('T', ' ').substring(0, 19)
})
},
fail: () => {
fail: (err) => {
console.warn('[CHECKIN DEBUG] wrapLocation FAIL:', JSON.stringify(err))
reject(new Error('定位失败'))
}
})
})
}
type LocationFullResult = {
location: DeliveryLocationType
accuracy: number
}
function wrapLocationFull(): Promise<LocationFullResult> {
console.warn('[CHECKIN DEBUG] wrapLocationFull: starting...')
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
isHighAccuracy: true,
enableHighAccuracy: true,
success: (res) => {
console.warn('[CHECKIN DEBUG] wrapLocationFull SUCCESS:', JSON.stringify({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
speed: res.speed,
altitude: res.altitude,
altitudeAccuracy: res.altitudeAccuracy,
heading: res.heading,
timestamp: res.timestamp
}))
const loc: DeliveryLocationType = {
latitude: res.latitude,
longitude: res.longitude,
address: '当前位置',
time: new Date().toISOString().replace('T', ' ').substring(0, 19)
}
const acc = res.accuracy != null ? res.accuracy : 0
console.warn('[CHECKIN DEBUG] wrapLocationFull resolved with accuracy:', acc)
resolve({ location: loc, accuracy: acc })
},
fail: (err) => {
console.warn('[CHECKIN DEBUG] wrapLocationFull FAIL:', JSON.stringify(err))
reject(new Error('定位失败'))
}
})
@@ -77,18 +208,32 @@ function wrapChooseImage(): Promise<Array<string>> {
}
async function loadData() {
console.warn('[CHECKIN DEBUG] loadData: called, orderId:', orderId.value)
const authResult = await requireDeliveryAuth({ redirectOnFail: true, toastOnFail: true })
if (!authResult.ok || orderId.value == '') {
console.warn('[CHECKIN DEBUG] loadData: auth failed or orderId empty')
return
}
console.warn('[CHECKIN DEBUG] loadData: fetching order detail')
order.value = await getDeliveryOrderDetail(orderId.value)
console.warn('[CHECKIN DEBUG] loadData: order fetched:', JSON.stringify({
id: order.value?.id,
fullAddress: order.value?.fullAddress,
latitude: order.value?.latitude,
longitude: order.value?.longitude,
allowCheckinRadiusMeters: order.value?.allowCheckinRadiusMeters
}))
updateHomecareLoginStatus()
}
async function getCurrentLocation() {
console.warn('[CHECKIN DEBUG] getCurrentLocation: called')
try {
currentLocation.value = await wrapLocation()
locationText.value = '纬度 ' + String(currentLocation.value.latitude) + ' / 经度 ' + String(currentLocation.value.longitude)
console.warn('[CHECKIN DEBUG] getCurrentLocation completed, location:', JSON.stringify(currentLocation.value))
} catch (error) {
console.warn('[CHECKIN DEBUG] getCurrentLocation error:', error)
uni.showToast({ title: '签到定位失败,请检查定位权限', icon: 'none' })
}
}
@@ -102,22 +247,123 @@ async function choosePhoto() {
}
}
async function handlePrecheck(): Promise<void> {
console.warn('[CHECKIN PAGE] ========== handlePrecheck START ==========')
console.warn('[CHECKIN PAGE] 当前 orderId:', orderId.value)
if (prechecking.value) {
console.warn('[CHECKIN PAGE] 已经在预校验中,直接返回')
return
}
if (orderId.value === '') {
console.warn('[CHECKIN PAGE] orderId 为空')
uni.showToast({ title: '工单信息缺失', icon: 'none' })
return
}
console.warn('[CHECKIN PAGE] 开始预校验流程')
prechecking.value = true
canCheckin.value = false
distanceText.value = '定位中...'
allowedRadiusText.value = '定位中...'
precheckStatusText.value = '定位中...'
reasonText.value = ''
try {
console.warn('[CHECKIN PAGE] 步骤 1: 调用 wrapLocationFull 获取位置')
const fullResult = await wrapLocationFull()
const location = fullResult.location
const accuracy = fullResult.accuracy
console.warn('[CHECKIN PAGE] 位置信息: lat=', location.latitude, ' lng=', location.longitude, ' accuracy=', accuracy)
currentLocation.value = location
locationText.value = '纬度 ' + String(location.latitude) + ' / 经度 ' + String(location.longitude)
accuracyText.value = accuracy > 0 ? String(accuracy) + ' 米' : '未知'
// 调用 RPC 预校验(现在直接走 Supabase不需要本地后端
console.warn('[CHECKIN PAGE] 步骤 2: 调用 checkinPrecheck RPC')
const result = await checkinPrecheck(
orderId.value,
location.latitude,
location.longitude,
accuracy
)
console.warn('[CHECKIN PAGE] RPC 返回结果: distance=', result.distanceMeters, ' radius=', result.allowedRadiusMeters, ' canCheckin=', result.canCheckin, ' reason=', result.reasonCode)
if (result.distanceMeters != null) {
distanceText.value = String(result.distanceMeters) + ' 米'
} else {
distanceText.value = '未知'
}
allowedRadiusText.value = String(result.allowedRadiusMeters) + ' 米'
if (result.canCheckin) {
canCheckin.value = true
precheckStatusText.value = '可以签到 / 下一步拍照'
reasonText.value = ''
console.warn('[CHECKIN PAGE] 可以签到')
} else {
canCheckin.value = false
precheckStatusText.value = '不可签到'
reasonText.value = getReasonText(result.reasonCode)
console.warn('[CHECKIN PAGE] 不可签到,原因:', result.reasonCode, getReasonText(result.reasonCode))
}
} catch (error) {
console.warn('[CHECKIN PAGE] ========== 捕获异常 ==========')
console.warn('[CHECKIN PAGE] 异常类型:', error instanceof Error ? 'Error' : typeof error)
console.warn('[CHECKIN PAGE] 异常信息:', error)
console.warn('[CHECKIN PAGE] ========== handlePrecheck END (ERROR) ==========')
uni.showToast({ title: '预校验失败,请重试', icon: 'none' })
precheckStatusText.value = '预校验失败'
} finally {
prechecking.value = false
console.warn('[CHECKIN PAGE] ========== handlePrecheck END (FINISHED) ==========')
}
}
async function submitCheckin() {
if (submitting.value) return
if (order.value == null) return
console.warn('[CHECKIN DEBUG] submitCheckin: called')
if (submitting.value) {
console.warn('[CHECKIN DEBUG] submitCheckin: already submitting, returning')
return
}
if (order.value == null) {
console.warn('[CHECKIN DEBUG] submitCheckin: order is null')
uni.showToast({ title: '订单信息缺失', icon: 'none' })
return
}
if (currentLocation.value == null) {
console.warn('[CHECKIN DEBUG] submitCheckin: currentLocation is null, calling getCurrentLocation')
await getCurrentLocation()
if (currentLocation.value == null) return
if (currentLocation.value == null) {
console.warn('[CHECKIN DEBUG] submitCheckin: still null after getCurrentLocation')
return
}
}
if (photos.value.length == 0) {
console.warn('[CHECKIN DEBUG] submitCheckin: no photos uploaded')
uni.showToast({ title: '请至少上传一张现场图片', icon: 'none' })
return
}
const distance = calculateDistance(currentLocation.value.latitude, currentLocation.value.longitude, order.value.latitude, order.value.longitude)
if (distance > order.value.allowCheckinRadiusMeters) {
uni.showToast({ title: '距离服务地址超出允许范围,不能签到', icon: 'none' })
// RPC 预校验已经做了距离判断,这里直接提交
// 保留坐标检查作为兜底,防止跳过预校验直接提交
if (order.value.latitude == 0 && order.value.longitude == 0) {
console.warn('[CHECKIN DEBUG] submitCheckin: order has no valid coordinates (lat=0, lng=0)')
uni.showToast({ title: '订单缺少服务地址坐标', icon: 'none' })
return
}
const distance = calculateDistance(currentLocation.value.latitude, currentLocation.value.longitude, order.value.latitude, order.value.longitude)
console.warn('[CHECKIN DEBUG] submitCheckin: calculated distance:', distance, 'meters, allowedRadius:', order.value.allowCheckinRadiusMeters)
console.warn('[CHECKIN DEBUG] submitCheckin: proceeding with checkin, photos count:', photos.value.length)
doCheckin()
}
async function doCheckin() {
submitting.value = true
try {
await checkinOrder(orderId.value, {
@@ -126,17 +372,24 @@ async function submitCheckin() {
photos: photos.value,
checkinMode: 'gps'
})
console.warn('[CHECKIN DEBUG] submitCheckin: checkinOrder succeeded')
uni.showToast({ title: '签到成功', icon: 'success' })
uni.redirectTo({ url: '/pages/mall/delivery/service-record/index?id=' + orderId.value })
} catch (error) {
console.warn('[CHECKIN DEBUG] submitCheckin error:', error)
uni.showToast({ title: '签到失败,请重试', icon: 'none' })
} finally {
submitting.value = false
}
}
onLoad((options) => {
console.warn('[CHECKIN DEBUG] onLoad: called, options:', JSON.stringify(options))
if (options != null) {
orderId.value = getDeliveryRouteParam(options as UTSJSONObject, 'id')
console.warn('[CHECKIN DEBUG] onLoad: extracted orderId:', orderId.value)
}
console.warn('[CHECKIN DEBUG] onLoad: calling loadData')
loadData()
})
</script>
@@ -150,6 +403,15 @@ onLoad((options) => {
color: #16324f;
}
.success-text {
color: #0f766e;
font-weight: bold;
}
.warning-text {
color: #dc2626;
}
.button-stack {
flex-direction: column;
}
@@ -166,6 +428,10 @@ onLoad((options) => {
color: #ffffff;
}
.primary-btn[disabled] {
background: #9ca3af;
}
.secondary-btn {
background: #eaf2f0;
color: #0f766e;
@@ -179,4 +445,4 @@ onLoad((options) => {
background: #f2f7f6;
font-size: 28rpx;
}
</style>
</style>

View File

@@ -12,7 +12,7 @@
<view class="timeline-box">
<view v-for="item in order.timeline" :key="item.id" class="timeline-item">
<text class="timeline-title">{{ item.title }}</text>
<text class="timeline-meta">{{ item.time }}</text>
<text class="timeline-meta">{{ formatDateTime(item.time) }}</text>
<text class="timeline-desc">{{ item.description }}</text>
</view>
</view>
@@ -22,7 +22,7 @@
<text class="section-title">服务信息</text>
<text class="row-text">服务名称:{{ order.serviceName }}</text>
<text class="row-text">服务类型:{{ order.serviceType }}</text>
<text class="row-text">预约时间:{{ order.appointmentTime }}</text>
<text class="row-text">预约时间:{{ formatDateTime(order.appointmentTime) }}</text>
<text class="row-text">预计时长:{{ order.duration }} 分钟</text>
<text class="row-text">服务价格:¥{{ order.price }}</text>
<text class="row-text">预计收入:¥{{ order.staffIncome }}</text>
@@ -71,7 +71,7 @@
<text class="section-title">异常记录</text>
<text class="row-text">异常类型:{{ order.abnormalReport!.type }}</text>
<text class="row-text">异常说明:{{ order.abnormalReport!.description }}</text>
<text class="row-text">发生时间:{{ order.abnormalReport!.occurredAt }}</text>
<text class="row-text">发生时间:{{ formatDateTime(order.abnormalReport!.occurredAt) }}</text>
</view>
<view class="action-card">
@@ -98,6 +98,7 @@ import {
import { getNextStepText, getPrimaryActionText } from '@/utils/deliveryCareUi.uts'
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts'
import { formatDateTime } from '@/utils/utils.uts'
const orderId = ref('')
const order = ref<DeliveryOrderType | null>(null)
@@ -112,6 +113,7 @@ async function loadData() {
return
}
order.value = await getServiceOrderDetail(orderId.value)
console.warn('[orders/detail] order detail:', JSON.stringify(order.value))
}
function joinTags(tags: Array<string>): string {
@@ -316,4 +318,4 @@ onLoad((options) => {
font-size: 26rpx;
color: #ffffff;
}
</style>
</style>

View File

@@ -16,7 +16,7 @@
<view class="order-top" @click="goDetail(item.id)">
<view class="order-main">
<text class="order-title">{{ item.serviceName }}</text>
<text class="order-subtitle">{{ item.elderName }} · {{ item.appointmentTime }}</text>
<text class="order-subtitle">{{ item.elderName }} · {{ formatDateTime(item.appointmentTime) }}</text>
</view>
<text class="order-status">{{ item.statusText }}</text>
</view>
@@ -50,6 +50,7 @@ import {
} from '@/services/deliveryService.uts'
import { getDeliveryOrderTabs, getPrimaryActionText } from '@/utils/deliveryCareUi.uts'
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
import { formatDateTime } from '@/utils/utils.uts'
const tabs = getDeliveryOrderTabs()
const currentTab = ref('pending')
@@ -68,13 +69,16 @@ async function loadData() {
}
if (currentTab.value == 'pending') {
orders.value = await getPendingServiceOrders()
console.warn('[orders/index] pending orders:', JSON.stringify(orders.value))
return
}
if (currentTab.value == 'history') {
orders.value = await getHistoryServiceOrders()
console.warn('[orders/index] history orders:', JSON.stringify(orders.value))
return
}
orders.value = await getTodayServiceOrders()
console.warn('[orders/index] today orders:', JSON.stringify(orders.value))
}
function consumeStoredTab(): void {
@@ -342,4 +346,4 @@ onShow(() => {
.empty-box {
padding: 24rpx 0;
}
</style>
</style>

View File

@@ -2,7 +2,7 @@
<ServicePageScaffold title="出发与导航" fallback-url="/pages/mall/delivery/orders/detail">
<ServicePanel title="路线信息" subtitle="支持出发状态回写、地图导航和位置上报。">
<text v-if="order != null" class="info-text">服务地址:{{ order.fullAddress }}</text>
<text v-if="order != null" class="info-text">预约时间:{{ order.appointmentStartTime }}</text>
<text v-if="order != null" class="info-text">预约时间:{{ formatDateTime(order.appointmentStartTime) }}</text>
<text class="info-text">当前位置:{{ currentLocationText }}</text>
</ServicePanel>
<ServicePanel title="执行动作" subtitle="先获取位置,再执行出发或到达。">
@@ -25,6 +25,7 @@ import type { DeliveryLocationType, DeliveryOrderType } from '@/types/delivery.u
import { arriveOrder, getDeliveryOrderDetail, startDepart } from '@/services/deliveryService.uts'
import { requireDeliveryAuth } from '@/utils/deliveryAuth.uts'
import { getDeliveryRouteParam } from '@/utils/deliveryRoute.uts'
import { formatDateTime } from '@/utils/utils.uts'
const orderId = ref('')
const order = ref<DeliveryOrderType | null>(null)

View File

@@ -86,6 +86,43 @@ const photoCount = ref(0)
const photosText = computed((): string => '已添加占位照片 ' + String(photoCount.value) + ' 张')
function formatDisplayTime(isoStr: string): string {
if (isoStr == '') {
return ''
}
// 如果已经是 YYYY-MM-DD HH:mm 格式,直接返回
if (isoStr.indexOf('T') == -1 && isoStr.length >= 16) {
return isoStr.substring(0, 16)
}
// 处理 ISO 8601: 2026-05-19T03:00:00+08:00
const parts = isoStr.split('T')
if (parts.length < 2) {
return isoStr
}
const datePart = parts[0]
let timePart = parts[1]
// 去掉时区后缀
const plusIdx = timePart.indexOf('+')
if (plusIdx != -1) {
timePart = timePart.substring(0, plusIdx)
}
const zIdx = timePart.indexOf('Z')
if (zIdx != -1) {
timePart = timePart.substring(0, zIdx)
}
// 去掉毫秒
const dotIdx = timePart.indexOf('.')
if (dotIdx != -1) {
timePart = timePart.substring(0, dotIdx)
}
// 取 HH:mm
const timeSegments = timePart.split(':')
if (timeSegments.length >= 2) {
return datePart + ' ' + timeSegments[0] + ':' + timeSegments[1]
}
return datePart + ' ' + timePart
}
function toggleItem(itemId: string, event: any) {
for (let i = 0; i < serviceItems.value.length; i++) {
if (serviceItems.value[i].id == itemId) {
@@ -111,8 +148,8 @@ async function loadData() {
order.value = await getServiceOrderDetail(orderId.value)
if (order.value != null) {
serviceItems.value = order.value.serviceItems
startTime.value = order.value.startServiceTime != null && order.value.startServiceTime != '' ? order.value.startServiceTime : order.value.appointmentTime
endTime.value = order.value.finishTime
startTime.value = formatDisplayTime(order.value.startServiceTime != null && order.value.startServiceTime != '' ? order.value.startServiceTime : order.value.appointmentTime)
endTime.value = formatDisplayTime(order.value.finishTime)
processNote.value = order.value.serviceSummary
staffRemark.value = order.value.progressNote
if (order.value.serviceRecord != null) {

View File

@@ -173,10 +173,14 @@ const loginType = ref<number>(0)
// 默认账号密码(唯一来源,修改只改这里)
// 必须在 account/password ref 之前声明,否则 ref 初始化时无法引用
// ─────────────────────────────────────────────
const CONSUMER_TEST_ACCOUNT = 'test@mall.com'
const CONSUMER_TEST_PASSWORD = 'Hf2152111'
const MERCHANT_TEST_ACCOUNT = 'test19@163.com'
const MERCHANT_TEST_PASSWORD = 'huang123456'
// const TEST_ACCOUNT = 'test@mall.com' // ← 旧账号(已停用)
// const TEST_PASSWORD = 'Hf2152111' // ← 旧密码(已停用)
const TEST_ACCOUNT = 'test20@163.com'
const TEST_PASSWORD = 'huang123456'
// delivery 端默认账号(居家服务员)
const DELIVERY_TEST_ACCOUNT = 'homecare_worker@test.com'
const DELIVERY_TEST_PASSWORD = 'Homecare123!'
// ✅ account/password 直接以常量作初始值,上线/刷新立即生效,不再依赖 onMounted 延迟赋值
const account = ref<string>(CONSUMER_TEST_ACCOUNT)
@@ -401,8 +405,8 @@ onMounted(() => {
})
if (isDeliveryMode()) {
account.value = ''
password.value = ''
account.value = DELIVERY_TEST_ACCOUNT
password.value = DELIVERY_TEST_PASSWORD
} else if (isMerchantMode()) {
account.value = MERCHANT_TEST_ACCOUNT
password.value = MERCHANT_TEST_PASSWORD