新增暂停时长

This commit is contained in:
2026-04-08 18:01:56 +08:00
parent 9ca377907b
commit 09be26e2aa
9 changed files with 451 additions and 110 deletions
+1 -1
View File
@@ -444,7 +444,7 @@
const order = inUseRes.data
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
uni.redirectTo({
url: `/subPackages/service/return/index?deviceId=${deviceId.value}`
url: `/pages/order/detail?orderId=${order.orderId}`
})
return
}
+361 -89
View File
@@ -11,9 +11,17 @@
{{ $t('order.canUsePromotion') }}
</view>
</view>
<view class="header-desc">{{ getStatusDesc() }}</view>
<!-- 已暂停副标题展示暂停持续时长 + 快递归还时间限制倒计时暂停期间不累计 -->
<view v-if="orderInfo.orderStatus === 'in_used' && orderInfo.pauseTime" class="header-desc-block">
<text class="header-desc">{{ pauseDurationLine }}</text>
<text v-if="orderInfo.isSupportExpressReturn !== 'no' && !showExpressAction && countdownRemaining > 0"
class="header-desc header-desc-secondary">{{ formatCountdown(countdownRemaining) }}{{ $t('order.canExpressReturn') }}</text>
<text v-else-if="orderInfo.isSupportExpressReturn !== 'no' && showExpressAction"
class="header-desc header-desc-secondary">{{ $t('order.pausedExpressAvailable') }}</text>
</view>
<view v-else class="header-desc">{{ getStatusDesc() }}</view>
</view>
<view class="header-right" v-if="orderInfo.orderStatus === 'in_used'">
<view class="header-right" v-if="orderInfo.orderStatus === 'in_used' && !orderInfo.pauseTime">
<view class="device-no-eject-btn" @click="handleDeviceNoEject">
<image src="/static/power_no_popout.png" class="device-no-eject-icon" mode="aspectFit" lazy-load="true"></image>
<text class="device-no-eject-text">{{ $t('order.deviceNoEject') }}</text>
@@ -54,64 +62,68 @@
</view>
</view>
<!-- 租借信息卡片 -->
<!-- 租借信息卡片可折叠 -->
<view class="rent-card">
<view class="rent-title">{{ $t('order.rentInfo') }}</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.orderNo') }}</view>
<view class="rent-value">{{ orderInfo.orderNo || '-' }}</view>
<view class="rent-header" @click="toggleRentInfo">
<text class="rent-title">{{ $t('order.rentInfo') }}</text>
<text class="rent-collapse-label">{{ rentInfoExpanded ? $t('order.rentInfoCollapse') : $t('order.rentInfoExpand') }}</text>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.fanNo') }}</view>
<view class="rent-value">{{ deviceId || '-' }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.rentTime') }}</view>
<view class="rent-value">{{ orderInfo.startTime || '-' }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.rentLocation') }}</view>
<view class="rent-value">{{ orderInfo.positionName || '-' }}</view>
</view>
<view class="rent-item" v-if="orderInfo.freeRentTime&&orderInfo.freeRentTime!=='0'">
<view class="rent-label">{{ $t('order.freeRentTime') }}</view>
<view class="rent-value">{{ getFreeRentTimeDisplay() }}</view>
</view>
<view class="rent-item" v-if="orderInfo.unitPrice && orderInfo.orderType">
<view class="rent-label">{{ $t('order.pricingRule') }}</view>
<view class="rent-value">{{ getPricingRuleDisplay() }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.paymentMethod') }}</view>
<view class="rent-value">{{ getPayWayText() }}</view>
</view>
<!-- 优惠信息显示 - 订单完成且使用了优惠 -->
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.discountTypeName">
<view class="rent-label">{{ $t('order.usedPromotion') }}</view>
<view class="rent-value promotion-value">
<image src="/static/promotion-icon.png" class="promotion-icon" mode="aspectFit" lazy-load="true"></image>
{{ orderInfo.discountTypeName }}<text
v-if="orderInfo.discountAmount">{{'-'+orderInfo.discountAmount||''}}</text>
<view class="rent-body" :class="{ open: rentInfoExpanded }">
<view class="rent-item">
<view class="rent-label">{{ $t('order.orderNo') }}</view>
<view class="rent-value">{{ orderInfo.orderNo || '-' }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.fanNo') }}</view>
<view class="rent-value">{{ deviceId || '-' }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.rentTime') }}</view>
<view class="rent-value">{{ orderInfo.startTime || '-' }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.rentLocation') }}</view>
<view class="rent-value">{{ orderInfo.positionName || '-' }}</view>
</view>
<view class="rent-item" v-if="orderInfo.freeRentTime&&orderInfo.freeRentTime!=='0'">
<view class="rent-label">{{ $t('order.freeRentTime') }}</view>
<view class="rent-value">{{ getFreeRentTimeDisplay() }}</view>
</view>
<view class="rent-item" v-if="orderInfo.unitPrice && orderInfo.orderType">
<view class="rent-label">{{ $t('order.pricingRule') }}</view>
<view class="rent-value">{{ getPricingRuleDisplay() }}</view>
</view>
<view class="rent-item">
<view class="rent-label">{{ $t('order.paymentMethod') }}</view>
<view class="rent-value">{{ getPayWayText() }}</view>
</view>
<!-- 优惠信息显示 - 订单完成且使用了优惠 -->
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.discountTypeName">
<view class="rent-label">{{ $t('order.usedPromotion') }}</view>
<view class="rent-value promotion-value">
<image src="/static/promotion-icon.png" class="promotion-icon" mode="aspectFit" lazy-load="true"></image>
{{ orderInfo.discountTypeName }}<text
v-if="orderInfo.discountAmount">{{'-'+orderInfo.discountAmount||''}}</text>
</view>
</view>
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.endTime">
<view class="rent-label">{{ $t('order.returnTime') }}</view>
<view class="rent-value">{{ orderInfo.endTime }}</view>
</view>
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.returnPosition">
<view class="rent-label">{{ $t('order.returnLocation') }}</view>
<view class="rent-value">{{ orderInfo.returnPosition || '-' }}</view>
</view>
<view class="rent-paid" v-if="isOrderCompleted()">
<text class="paid-label">{{ $t('order.paid') }}</text>
<text class="paid-value">{{ orderInfo.currentFee || orderInfo.payAmount || '10' }}</text>
<text class="paid-unit">{{ $t('unit.yuan') }}</text>
</view>
</view>
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.endTime">
<view class="rent-label">{{ $t('order.returnTime') }}</view>
<view class="rent-value">{{ orderInfo.endTime }}</view>
</view>
<view class="rent-item" v-if="isOrderCompleted() && orderInfo.returnPosition">
<view class="rent-label">{{ $t('order.returnLocation') }}</view>
<view class="rent-value">{{ orderInfo.returnPosition || '-' }}</view>
</view>
<view class="rent-paid" v-if="isOrderCompleted()">
<text class="paid-label">{{ $t('order.paid') }}</text>
<text class="paid-value">{{ orderInfo.currentFee || orderInfo.payAmount || '10' }}</text>
<text class="paid-unit">{{ $t('unit.yuan') }}</text>
</view>
</view>
<view class="">
<view class="" style="font-size: 24rpx;text-align: center;">
{{ $t('order.returnProblemTip') }}<text @click="contactService"
style="color:#07c160 ;">{{ $t('user.customerService') }}</text>{{ $t('order.contactStaff') }}
<view class="rent-service-tip">
<text>{{ $t('order.returnProblemTip') }}</text>
<text class="rent-service-link" @click="contactService">{{ $t('user.customerService') }}</text>
<text>{{ $t('order.contactStaff') }}</text>
</view>
</view>
@@ -124,43 +136,44 @@
<view class="bottom-bar">
<!-- 支付成功状态 -->
<template v-if="orderInfo.orderStatus === 'payment_successful'">
<view class="bottom-icon-btn" @click="contactService">
<image src="/static/customer-service.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('user.customerService') }}</text>
</view>
<view class="bottom-icon-btn" @click="handleDeviceNoEject">
<image src="/static/complaint.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('order.deviceNoEject') }}</text>
</view>
</template>
<!-- 使用中状态 -->
<template v-if="orderInfo.orderStatus === 'in_used'">
<view class="bottom-icon-btn" @click="contactService">
<image src="/static/customer-service.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('user.customerService') }}</text>
</view>
<!-- 使用中状态独立一行 flex 容器避免 convert-btn 换行后 flex:1 撑满整行 -->
<view v-if="orderInfo.orderStatus === 'in_used'" class="bottom-bar-in-use">
<!-- 只有支持快递归还时才显示倒计时和快递归还按钮 -->
<template v-if="orderInfo.isSupportExpressReturn !== 'no'">
<view v-if="!showExpressAction" class="countdown-btn">
{{ formatCountdown(countdownRemaining) }}{{ $t('order.canExpressReturn') }}
</view>
<view v-if="showExpressAction" class="action-btn secondary" @click="expressRetrunOrder">
{{ $t('order.pauseBilling') }}
{{ $t('express.title') }}
</view>
<view v-if="showExpressAction" class="action-btn primary" @click="quickReturn">
{{ $t('order.quickReturn') }}
<view v-if="showExpressAction" class="bottom-icon-btn" @click="quickReturn">
<image src="/static/map.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('order.quickReturn') }}</text>
</view>
</template>
<!-- 不支持快递归还时只显示快速归还按钮 -->
<view v-else class="action-btn primary" @click="quickReturn">
{{ $t('order.quickReturn') }}
<!-- 不支持快递归还时只显示快速归还图标+小字 -->
<view v-else class="bottom-icon-btn" @click="quickReturn">
<image src="/static/map.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('order.quickReturn') }}</text>
</view>
<view
v-if="showPauseBillingButton"
class="action-btn secondary pause-billing-btn"
:class="{ 'is-disabled': !isPauseBillingClickable }"
@click="onPauseBillingTap"
>
{{ $t('order.pauseBilling') }}
</view>
<!-- 不想还了转为自用按钮 -->
<view class="action-btn convert-btn" @click="handleConvertToOwnedWithMaxFee">
{{ $t('order.convertToOwnWithMaxFee') }}
</view>
</template>
</view>
<!-- 已完成状态 -->
<template v-if="isOrderCompleted()">
@@ -169,10 +182,6 @@
<image src="/static/suggess.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('order.feeAppeal') }}</text>
</view>
<view class="bottom-icon-btn" @click="contactService">
<image src="/static/customer-service.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('user.customerService') }}</text>
</view>
<view class="action-btn primary" @click="rentAgain">
{{ $t('order.rentAgain') }}
</view>
@@ -218,6 +227,7 @@
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
getCurrentInstance
@@ -235,7 +245,9 @@
convertToOwned,
closeWithMaxFee,
getInUseOrder,
deviceRentByOrderNo
deviceRentByOrderNo,
getPauseBillingEligible,
requestPauseBilling
} from '@/config/api/order.js'
import {
withdrawDeposit
@@ -292,7 +304,9 @@
discountTypeName: '',
originalFee:'',
discountAmount:'',
returnMapImage: ''
returnMapImage: '',
/** 有值表示当前处于暂停计费中(接口 pauseTime) */
pauseTime: ''
})
const timer = ref(null)
const statusCheckTimer = ref(null)
@@ -308,6 +322,102 @@
const feeRuleText = ref('')
const convertToOwnPopup = ref(null)
const lastDeviceEjectTime = ref(0) // 上次点击"宝未弹出"的时间戳
/** 是否允许点击暂停:null 拉取中,true 可暂停,false 不可暂停(按钮仍展示为禁用) */
const pauseBillingEligible = ref(null)
const pauseBillingLoading = ref(false)
const hasOrderPauseTime = computed(() => {
const pt = orderInfo.value.pauseTime
return pt != null && String(pt).trim() !== ''
})
/** 使用中且未处于暂停计费态时展示「暂停计费」按钮 */
const showPauseBillingButton = computed(() => {
return orderInfo.value.orderStatus === 'in_used' && !hasOrderPauseTime.value
})
const isPauseBillingClickable = computed(() => {
return pauseBillingEligible.value === true && !pauseBillingLoading.value
})
/** 租借信息区折叠态:默认展开,仅由用户点击标题切换;换订单时重置为展开 */
const rentInfoExpanded = ref(true)
watch(
() => orderInfo.value.orderId,
(id, prev) => {
if (prev !== undefined && prev !== '' && id !== prev) {
rentInfoExpanded.value = true
pauseBillingEligible.value = null
}
}
)
const isApiOk = (res) => res && (res.code === 200 || res.code === 0)
const toggleRentInfo = () => {
rentInfoExpanded.value = !rentInfoExpanded.value
}
/** 刷新是否可暂停计费(仅决定按钮是否可点,不决定显隐) */
const refreshPauseBillingEligible = async () => {
pauseBillingEligible.value = null
if (orderInfo.value.orderStatus !== 'in_used' || !orderInfo.value.orderId) {
return
}
if (hasOrderPauseTime.value) {
return
}
try {
const res = await getPauseBillingEligible(orderInfo.value.orderId)
if (!isApiOk(res)) {
pauseBillingEligible.value = false
return
}
const payload = res.data || {}
pauseBillingEligible.value = payload.eligible === true
} catch (e) {
console.warn('refreshPauseBillingEligible', e)
pauseBillingEligible.value = false
}
}
const onPauseBillingTap = () => {
if (!isPauseBillingClickable.value) return
handlePauseBilling()
}
const handlePauseBilling = async () => {
if (!orderInfo.value.orderId || pauseBillingLoading.value) return
pauseBillingLoading.value = true
try {
uni.showLoading({
title: t('common.loading'),
mask: true
})
const pauseRes = await requestPauseBilling(orderInfo.value.orderId)
if (isApiOk(pauseRes)) {
uni.showToast({
title: pauseRes.msg || t('order.pauseBillingSuccess'),
icon: 'success'
})
await getOrderDetails()
} else {
uni.showToast({
title: pauseRes?.msg || t('order.pauseBillingFailed'),
icon: 'none'
})
}
} catch (e) {
console.error('handlePauseBilling', e)
uni.showToast({
title: t('order.pauseBillingFailed'),
icon: 'none'
})
} finally {
uni.hideLoading()
pauseBillingLoading.value = false
}
}
// 计算属性:是否显示优惠券/会员卡可用提示
const canUsePromotionTag = computed(() => {
@@ -334,8 +444,11 @@
orderInfo.value.orderStatus === 'used_down'
}
// 获取订单状态文字
// 获取订单状态文字(使用中且存在 pauseTime 时优先展示已暂停计费)
const getOrderStatusText = () => {
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.pauseTime) {
return t('order.orderStatusBillingPaused')
}
const statusMap = {
'waiting_for_payment': t('order.waitingForPayment'),
'payment_in_progress': t('order.paymentInProgress'),
@@ -558,6 +671,40 @@
return NaN
}
/** 暂停计费已持续时长(与 pauseTime 联动,每秒刷新) */
const pauseDurationLine = ref('')
let pauseDurationTimer = null
const syncPauseDurationLine = () => {
if (orderInfo.value.orderStatus !== 'in_used' || !orderInfo.value.pauseTime) {
pauseDurationLine.value = ''
return
}
const start = parseStartTimeToMs(orderInfo.value.pauseTime)
if (isNaN(start)) {
pauseDurationLine.value = ''
return
}
const secs = Math.max(0, Math.floor((Date.now() - start) / 1000))
const timeStr = formatCountdown(secs)
pauseDurationLine.value = `${t('order.billingPausedDurationLabel')} ${timeStr}`
}
const stopPauseDurationTicker = () => {
if (pauseDurationTimer != null) {
clearInterval(pauseDurationTimer)
pauseDurationTimer = null
}
}
const startPauseDurationTicker = () => {
stopPauseDurationTicker()
syncPauseDurationLine()
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.pauseTime) {
pauseDurationTimer = setInterval(syncPauseDurationLine, 1000)
}
}
// 清除倒计时
const clearExpressCountdown = () => {
if (countdownTimer.value) {
@@ -587,7 +734,15 @@
return
}
const nowMs = Date.now()
const elapsedSeconds = Math.max(0, Math.floor((nowMs - startMs) / 1000))
let elapsedSeconds = Math.max(0, Math.floor((nowMs - startMs) / 1000))
// 暂停计费期间:墙钟仍在走,但有效租借计时不应包含「当前这一段暂停」,否则快递归还等时间限制会异常变少
if (orderInfo.value.pauseTime) {
const pauseMs = parseStartTimeToMs(orderInfo.value.pauseTime)
if (!isNaN(pauseMs)) {
const pauseOngoingSec = Math.max(0, Math.floor((nowMs - pauseMs) / 1000))
elapsedSeconds = Math.max(0, elapsedSeconds - pauseOngoingSec)
}
}
const remaining = expressThresholdSeconds.value - elapsedSeconds
if (remaining <= 0) {
countdownRemaining.value = 0
@@ -821,6 +976,9 @@
// 保存是否支持快递归还
orderInfo.value.isSupportExpressReturn = orderData.isSupportExpressReturn || 'yes'
const pt = orderData.pauseTime
orderInfo.value.pauseTime = (pt !== undefined && pt !== null && String(pt).trim() !== '') ? pt : ''
// 如果有有效的 expressReturnStart,立即更新倒计时阈值(小时转秒)
if (orderInfo.value.expressReturnStart && typeof orderInfo.value.expressReturnStart === 'number' && orderInfo
.value.expressReturnStart > 0) {
@@ -854,6 +1012,13 @@
})
}
}
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.pauseTime) {
startPauseDurationTicker()
} else {
stopPauseDurationTicker()
syncPauseDurationLine()
}
}
// 获取订单详情
@@ -922,6 +1087,7 @@
}
}
}
await refreshPauseBillingEligible()
} else {
throw new Error(result.msg || t('order.getOrderFailed'))
}
@@ -1272,6 +1438,12 @@
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.isSupportExpressReturn !== 'no') {
startExpressCountdown()
}
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.orderId) {
refreshPauseBillingEligible()
}
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.pauseTime) {
startPauseDurationTicker()
}
})
onHide(() => {
@@ -1280,6 +1452,7 @@
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
stopPauseDurationTicker()
removeFromOrderMonitor()
})
@@ -1289,6 +1462,7 @@
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
stopPauseDurationTicker()
removeFromOrderMonitor()
uni.$off('orderCompleted', handleOrderCompleted)
})
@@ -1323,6 +1497,24 @@
margin-bottom: 12rpx;
}
.header-desc-block {
margin-bottom: 4rpx;
.header-desc {
display: block;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
}
.header-desc-secondary {
font-size: 26rpx;
color: #07c160;
}
}
.header-title {
font-size: 48rpx;
font-weight: bold;
@@ -1511,11 +1703,55 @@
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.rent-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
.rent-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
&:active {
opacity: 0.92;
}
.rent-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.rent-collapse-label {
font-size: 26rpx;
color: #07c160;
margin-left: 16rpx;
flex-shrink: 0;
}
}
.rent-body {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
&.open {
max-height: 3200rpx;
transition: max-height 0.35s ease-in;
padding-top: 8rpx;
}
}
.rent-service-tip {
font-size: 24rpx;
color: #666;
text-align: center;
line-height: 1.6;
padding-top: 20rpx;
.rent-service-link {
color: #07c160;
}
}
.rent-item {
@@ -1596,10 +1832,23 @@
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 20rpx;
.bottom-bar-in-use {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
align-items: center;
justify-content: space-between;
// gap: 20rpx;
box-sizing: border-box;
}
.bottom-icon-btn {
display: flex;
flex-direction: column;
@@ -1624,7 +1873,8 @@
}
.countdown-btn {
flex: 1;
flex: 1 1 auto;
min-width: 200rpx;
height: 88rpx;
display: flex;
align-items: center;
@@ -1668,13 +1918,35 @@
&:active {
opacity: 0.8;
}
&.is-disabled {
opacity: 0.45;
color: #999;
background: #ebebeb;
border-color: #ddd;
&:active {
opacity: 0.45;
}
}
&:not(.is-disabled):active {
opacity: 0.8;
}
}
&.secondary.pause-billing-btn:not(.is-disabled) {
border-color: #07c160;
color: #07c160;
background: rgba(7, 193, 96, 0.08);
}
&.convert-btn {
background: #fff;
color: #07c160;
border: 2rpx solid #07c160;
flex: 1;
flex: 0 1 auto;
max-width: 100%;
&:active {
opacity: 0.8;