Files
uni-fans-score/pages/order/detail.vue
2026-06-12 16:08:00 +08:00

2156 lines
58 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="order-detail-container">
<!-- 顶部标题 -->
<view class="page-header">
<view class="header-top">
<view class="header-left">
<view class="header-title-row">
<text class="header-title">{{ getOrderStatusText() }}</text>
<!-- 优惠券/会员卡可用提示标签 -->
<view v-if="orderInfo.orderStatus === 'in_used' && canUsePromotionTag" class="promotion-tag">
{{ $t('order.canUsePromotion') }}
</view>
</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' && !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>
</view>
</view>
</view>
</view>
<!-- 订单信息卡片 -->
<view class="info-card">
<view class="info-row">
<view class="info-left">
<view class="info-col">
<view class="info-value-wrapper">
<text class="info-value-large">{{ getUsedTimeDisplay().number }}</text>
<text class="info-value-unit">{{ getUsedTimeDisplay().unit }}</text>
</view>
<view class="info-label">{{ getUsedTimeLabel() }}</view>
</view>
<view class="info-col">
<view class="info-value-wrapper">
<text class="info-value-large">{{ getOrderFee() }}</text>
<text class="info-value-unit">{{ getCurrencyUnitText() }}</text>
</view>
<view class="info-label">{{ $t('order.totalAmount') }}</view>
</view>
</view>
<view class="info-right" v-if="orderInfo.orderStatus === 'in_used'">
<view class="return-reminder-btn" @click="handleReturnReminderSubscribe">
<uv-icon name="bell" size="20" color="#FFF"></uv-icon>
<text class="return-reminder-text">{{ $t('order.returnReminder') }}</text>
</view>
</view>
</view>
<!-- 计费规则图片 -->
<view class="fee-rule-image">
<image :src="getFeeRuleImageUrl()" mode="widthFix" class="rule-image" lazy-load="true"></image>
</view>
</view>
<!-- 租借信息卡片可折叠 -->
<view class="rent-card">
<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-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">{{ formatAmountDisplay(orderInfo.currentFee || orderInfo.payAmount || '10') }}</text>
<text class="paid-unit">{{ getCurrencyUnitText() }}</text>
</view>
</view>
<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>
<!-- 转为自用提示 - 在使用中状态时显示 -->
<!-- <view v-if="orderInfo.orderStatus === 'in_used'" class="convert-tip" @click="handleConvertToOwned">
{{ $t('order.convertToOwn') }}
</view> -->
<!-- 底部操作栏 -->
<view class="bottom-bar">
<!-- 支付成功状态 -->
<template v-if="orderInfo.orderStatus === 'payment_successful'">
<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>
<!-- 使用中状态独立一行 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('express.title') }}
</view>
<!-- <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="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>
</view>
<!-- 已完成状态 -->
<template v-if="isOrderCompleted()">
<view class="bottom-icon-btn" @click="handleWithdraw"
v-if="!orderInfo.isWithdrawn && orderInfo.refundAmount > 0">
<image src="/static/suggess.png" class="icon" mode="aspectFit" lazy-load="true"></image>
<text>{{ $t('order.feeAppeal') }}</text>
</view>
<view class="action-btn primary" @click="rentAgain">
{{ $t('order.rentAgain') }}
</view>
</template>
<!-- 待支付状态 -->
<template v-if="orderInfo.orderStatus === 'waiting_for_payment'">
<view class="action-btn secondary" @click="handleCancelOrder">{{ $t('order.cancelOrder') }}</view>
<view class="action-btn primary" @click="handlePayment">{{ $t('order.payNow') }}</view>
</template>
<!-- 已取消状态 -->
<template v-if="orderInfo.orderStatus === 'order_cancelled'">
<view class="action-btn primary full-width" @click="goToHome">{{ $t('order.backToHome') }}</view>
</template>
</view>
<!-- 转为自用确认弹窗 -->
<uv-popup ref="convertToOwnPopup" mode="center" round="20" :overlay="true" :closeOnClickOverlay="false"
:safeAreaInsetBottom="false">
<view class="convert-to-own-popup">
<view class="popup-header">
<text class="popup-title">{{ $t('order.convertToOwnWithMaxFeeTitle') }}</text>
</view>
<view class="popup-content">
<text class="popup-text">{{ $t('order.convertToOwnWithMaxFeeConfirm') }}</text>
</view>
<view class="popup-actions">
<view class="popup-btn cancel-btn" @click="closeConvertToOwnPopup">
{{ $t('order.convertToOwnCancelBtn') }}
</view>
<view class="popup-btn confirm-btn" @click="confirmConvertToOwn">
{{ $t('order.convertToOwnConfirmBtn') }}
</view>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
getCurrentInstance
} from 'vue'
import {
onLoad,
onShow,
onHide,
onUnload
} from '@dcloudio/uni-app'
import {
queryById,
cancelOrder,
reportDeviceNoEject,
convertToOwned,
closeWithMaxFee,
getInUseOrder,
deviceRentByOrderNo,
getPauseBillingEligible,
requestPauseBilling
} from '@/config/api/order.js'
import {
withdrawDeposit
} from '@/config/api/user.js'
import {
addUserFeedback
} from '@/config/api/feedback.js'
import {
getSystemConfig
} from '@/config/api/system.js'
import {
URL
} from "@/config/url.js"
import {
useI18n,
showModalI18n
} from '@/utils/i18n.js'
const {
t
} = useI18n()
const instance = getCurrentInstance()
const $orderMonitor = instance?.proxy?.$orderMonitor || null
// 响应式数据
const deviceId = ref('')
const orderInfo = ref({
orderId: '',
orderNo: '',
startTime: '',
endTime: '',
usedTime: '0分钟',
currentFee: '0.00',
orderStatus: '',
payWay: '',
depositAmount: '',
packageTime: '',
packagePrice: '',
payAmount: '0.00',
phone: '',
refundAmount: '0.00',
withdrawStatus: 'waiting',
isWithdrawn: false,
positionName: '',
returnPosition: '',
expressReturnStart: null,
isSupportExpressReturn: 'yes',
freeRentTime: '',
unitPrice: '',
orderType: '',
canUseMember: false,
canUseCoupon: false,
userMemberCardId: '',
userPurchaseId: '',
discountTypeName: '',
originalFee:'',
discountAmount:'',
returnMapImage: '',
/** 有值表示当前处于暂停计费中(接口 pauseTime) */
pauseTime: ''
})
const timer = ref(null)
const statusCheckTimer = ref(null)
const maxStatusChecks = ref(60)
const currentStatusChecks = ref(0)
const statusCheckInterval = ref(3000)
const isPageActive = ref(false)
const isLoadingOrder = ref(false)
const expressThresholdSeconds = ref(180)
const countdownRemaining = ref(0)
const showExpressAction = ref(false)
const countdownTimer = ref(null)
const feeRuleText = ref('')
const convertToOwnPopup = ref(null)
const lastDeviceEjectTime = ref(0) // 上次点击"宝未弹出"的时间戳
const currencyCode = ref('CNY')
const resolveCurrencyCode = (orderData = {}) => {
const explicitCurrency = String(
orderData.currency || orderData.positionCurrency || orderData.position?.currency || ''
).toUpperCase()
if (explicitCurrency) return explicitCurrency
const payWay = String(orderData.payWay || '').toLowerCase()
const phone = String(orderData.phone || '')
const depositAmount = Number(orderData.depositAmount || 0)
const unitPrice = Number(orderData.unitPrice || 0)
// 海外 H5 订单接口偶发不返回 currency,按业务特征兜底识别 IDR
if (payWay.includes('antom') || phone.startsWith('+62') || depositAmount >= 1000 || unitPrice >= 1000) {
return 'IDR'
}
return 'CNY'
}
/** 是否允许点击暂停: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(() => {
return orderInfo.value.canUseMember === true || orderInfo.value.canUseCoupon === true
})
// 计算属性:是否使用了优惠
const hasUsedPromotion = computed(() => {
return !!(orderInfo.value.userMemberCardId || orderInfo.value.userPurchaseId)
})
// 获取已使用优惠的文本
const getUsedPromotionText = () => {
if (orderInfo.value.userMemberCardId) {
return t('user.myCards')
} else if (orderInfo.value.userPurchaseId) {
return t('user.myCoupons')
}
return '-'
}
// 判断订单是否已完成
const isOrderCompleted = () => {
return orderInfo.value.orderStatus === 'used_done' ||
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'),
'payment_successful': t('order.paymentSuccess'),
'in_used': t('order.inUse'),
'payment_failed': t('order.paymentFailed'),
'order_cancelled': t('order.cancelled'),
'used_done': t('order.finished'),
'used_down': t('order.finished')
}
return statusMap[orderInfo.value.orderStatus] || t('order.orderDetail')
}
// 获取状态描述
const getStatusDesc = () => {
const descMap = {
'waiting_for_payment': t('order.pleasePaySoon'),
'in_used': t('order.pleaseReturnInTime'),
'used_done': t('order.returnedThankYou'),
'used_down': t('order.returnedThankYou'),
'order_cancelled': t('order.orderCancelled'),
'payment_failed': t('order.paymentFailedRetry')
}
return descMap[orderInfo.value.orderStatus] || ''
}
// 获取支付方式文本
const getPayWayText = () => {
const payWayMap = {
'wx_score_pay': t('order.depositFree'),
'wx_member_pay': t('order.memberOrder'),
'wx_pay': t('order.depositPay')
}
return payWayMap[orderInfo.value.payWay] || t('order.depositFree')
}
// 获取免费时长显示
const getFreeRentTimeDisplay = () => {
if (!orderInfo.value.freeRentTime) return '-'
return orderInfo.value.freeRentTime
}
// 获取计费规则显示
const getPricingRuleDisplay = () => {
if (!orderInfo.value.unitPrice || !orderInfo.value.orderType) return '-'
const orderTypeMap = {
'hours': t('time.hours'),
'minutes': t('time.minutes'),
'halfhours': t('time.halfHours')
}
const orderTypeText = orderTypeMap[orderInfo.value.orderType] || orderInfo.value.orderType
if (currencyCode.value === 'IDR') {
return `Rp${formatAmountDisplay(orderInfo.value.unitPrice)}/${orderTypeText}`
}
return `${formatAmountDisplay(orderInfo.value.unitPrice)}${getCurrencyUnitText()}/${orderTypeText}`
}
const getCurrencyUnitText = () => {
if (currencyCode.value === 'IDR') return 'Rp'
return t('unit.yuan')
}
// 格式化倒计时(显示为 HH:MM:SS 格式)
const formatCountdown = (totalSeconds) => {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) return '00:00:00'
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const pad = (n) => n.toString().padStart(2, '0')
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
}
// 联系客服
const contactService = () => {
uni.navigateTo({
url: '/subPackages/service/help/index'
})
}
// 获取计费规则图片URL
const getFeeRuleImageUrl = () => {
const locale = instance?.proxy?.$i18n?.locale || 'en_US'
// 如果是中文环境,显示中文图片,否则显示英文图片
if (locale === 'zh_CN' || locale === 'zh-CN' || locale === 'zh') {
return 'https://static.fdzpower.com/order_notice/notice_CN.png'
}
return 'https://static.fdzpower.com/order_notice/notice_EN.png'
}
// 快速归还
const quickReturn = () => {
// 检查是否有 returnMapImage 字段
console.log(orderInfo.value.returnMapImage)
if (orderInfo.value.returnMapImage) {
// 有值则跳转到新的地图页查看大图(支持放大)
const encodedUrl = encodeURIComponent(orderInfo.value.returnMapImage)
uni.navigateTo({
url: `/subPackages/order/return-map?imageUrl=${encodedUrl}`
})
} else {
// 没有值则继续执行原有跳转流程
uni.navigateTo({
url: '/pages/search/index'
})
}
}
// 再次租借
const rentAgain = () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
// 获取使用时长显示信息
const getUsedTimeDisplay = () => {
let usedTime = orderInfo.value.usedTime
// 如果 usedTime 为空,通过开始时间和结束时间计算
if (usedTime === '0分钟' && orderInfo.value.startTime && orderInfo.value.endTime) {
const startMs = parseStartTimeToMs(orderInfo.value.startTime)
const endMs = parseStartTimeToMs(orderInfo.value.endTime)
if (!isNaN(startMs) && !isNaN(endMs)) {
const diffMs = endMs - startMs
const totalMinutes = Math.floor(diffMs / (1000 * 60))
// 格式化为 "X小时X分钟" 或 "X分钟"
if (totalMinutes >= 60) {
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
usedTime = minutes > 0 ? `${hours}${t('time.hour')}${minutes}${t('time.minute')}` :
`${hours}${t('time.hour')}`
} else {
usedTime = `${totalMinutes}${t('time.minute')}`
}
}
}
// 如果还是没有值,使用默认值
if (!usedTime) {
usedTime = `0${t('time.minute')}`
}
// 解析时长字符串,例如 "1小时5分钟" 或 "5分钟"
const hourMatch = usedTime.match(new RegExp(`(\\d+)${t('time.hour')}`))
const minuteMatch = usedTime.match(new RegExp(`(\\d+)${t('time.minute')}`))
let displayNumber = ''
let displayUnit = ''
if (hourMatch && minuteMatch) {
// 有小时也有分钟,如 "1小时5分钟"
displayNumber = `${hourMatch[1]}`
displayUnit = `${t('time.hour')}${minuteMatch[1]}${t('time.minute')}`
} else if (hourMatch) {
// 只有小时,如 "1小时"
displayNumber = hourMatch[1]
displayUnit = t('time.hour')
} else if (minuteMatch) {
// 只有分钟,如 "5分钟"
displayNumber = minuteMatch[1]
displayUnit = t('time.minute')
} else {
// 默认情况
displayNumber = '0'
displayUnit = t('time.minute')
}
return {
number: displayNumber,
unit: displayUnit
}
}
const formatAmountDisplay = (amount) => {
if (amount === null || amount === undefined || amount === '') return '0'
const normalized = String(amount).replace(/[^\d.-]/g, '')
const num = Number(normalized)
if (Number.isFinite(num)) {
return String(Math.trunc(num))
}
return String(amount).split('.')[0]
}
// 获取使用时长标签文本
const getUsedTimeLabel = () => {
// 使用中状态显示"已使用",已完成状态显示"使用时长"
return orderInfo.value.orderStatus === 'in_used' ? t('order.used') : t('order.duration')
}
// 获取订单费用(不含单位)
const getOrderFee = () => {
let fee
if (orderInfo.value.originalFee) {
fee = orderInfo.value.originalFee || '0'
} else if (orderInfo.value.payAmount) {
fee = orderInfo.value.payAmount
} else {
fee = orderInfo.value.currentFee
}
// 移除小数位,仅显示整数金额
return formatAmountDisplay(String(fee).replace(/[元¥]/g, ''))
}
// 解析开始时间
const parseStartTimeToMs = (timeStr) => {
if (!timeStr) return NaN
if (typeof timeStr === 'number') {
return timeStr < 1e12 ? timeStr * 1000 : timeStr
}
let normalized = String(timeStr).trim()
normalized = normalized.replace('T', ' ').replace(/\.\d+Z?$/, '')
const candidates = [
normalized,
normalized.replace(/-/g, '/')
]
for (let i = 0; i < candidates.length; i++) {
const ts = Date.parse(candidates[i])
if (!isNaN(ts)) return ts
}
const m = normalized.match(/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})\s+(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/)
if (m) {
const y = parseInt(m[1])
const mon = parseInt(m[2]) - 1
const d = parseInt(m[3])
const h = parseInt(m[4])
const min = parseInt(m[5])
const s = m[6] ? parseInt(m[6]) : 0
return new Date(y, mon, d, h, min, s).getTime()
}
const num = Number(normalized)
if (!isNaN(num)) {
return num < 1e12 ? num * 1000 : num
}
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) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
}
// 重新计算倒计时
const recomputeExpressCountdownFromStartTime = () => {
// 如果不支持快递归还,直接返回
if (orderInfo.value.isSupportExpressReturn === 'no') {
showExpressAction.value = false
countdownRemaining.value = 0
return
}
if (orderInfo.value.orderStatus !== 'in_used') {
showExpressAction.value = false
countdownRemaining.value = 0
return
}
const startMs = parseStartTimeToMs(orderInfo.value.startTime)
if (isNaN(startMs)) {
showExpressAction.value = false
countdownRemaining.value = 0
return
}
const nowMs = Date.now()
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
showExpressAction.value = true
} else {
countdownRemaining.value = remaining
showExpressAction.value = false
}
}
// 启动快递归还倒计时
const startExpressCountdown = () => {
// 如果不支持快递归还,直接返回
if (orderInfo.value.isSupportExpressReturn === 'no') {
console.log('订单不支持快递归还,不启动倒计时')
showExpressAction.value = false
countdownRemaining.value = 0
return
}
clearExpressCountdown()
recomputeExpressCountdownFromStartTime()
if (showExpressAction.value) return
countdownTimer.value = setInterval(() => {
if (!isPageActive.value) {
clearExpressCountdown()
return
}
if (orderInfo.value.orderStatus !== 'in_used') {
clearExpressCountdown()
return
}
// 再次检查是否支持快递归还
if (orderInfo.value.isSupportExpressReturn === 'no') {
clearExpressCountdown()
showExpressAction.value = false
countdownRemaining.value = 0
return
}
recomputeExpressCountdownFromStartTime()
if (showExpressAction.value) {
clearExpressCountdown()
}
}, 1000)
}
// 加载系统配置
const loadSystemConfig = async () => {
try {
// 优先使用订单数据中的 expressReturnStart(小时转秒)
if (orderInfo.value.expressReturnStart && typeof orderInfo.value.expressReturnStart === 'number' &&
orderInfo.value.expressReturnStart > 0) {
expressThresholdSeconds.value = orderInfo.value.expressReturnStart * 3600
console.log('使用订单配置的快递归还阈值:', orderInfo.value.expressReturnStart, '小时 =>', expressThresholdSeconds
.value, '秒')
} else {
// 如果订单数据中没有,则使用系统配置
const res = await getSystemConfig()
if (res && res.code === 200 && res.data && typeof res.data.expressReturnCountdownSeconds ===
'number') {
const seconds = res.data.expressReturnCountdownSeconds
if (seconds > 0) {
expressThresholdSeconds.value = seconds
console.log('使用系统配置的快递归还阈值:', seconds, '秒')
}
}
}
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.startTime) {
recomputeExpressCountdownFromStartTime()
}
} catch (e) {
console.log('加载系统配置失败:', e)
}
}
// 从订单监控中移除订单
const removeFromOrderMonitor = () => {
if (orderInfo.value.orderId && $orderMonitor) {
try {
$orderMonitor.removeOrder({
orderId: orderInfo.value.orderId
})
console.log('订单已从监控队列移除:', orderInfo.value.orderId)
} catch (error) {
console.error('从监控队列移除订单失败:', error)
}
}
// 清除本地缓存的 activeOrderId
try {
uni.removeStorageSync('activeOrderId')
console.log('已清除本地缓存的 activeOrderId')
} catch (error) {
console.error('清除 activeOrderId 缓存失败:', error)
}
}
// 处理订单完成事件
const handleOrderCompleted = (orderData) => {
console.log('收到订单完成事件:', orderData)
if (orderData.orderId === orderInfo.value.orderId || orderData.orderNo === orderInfo.value.orderNo) {
// 刷新订单详情
getOrderDetails()
}
}
// 清除定时器
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
console.log('已清除使用时长更新计时器')
}
}
// 清除状态检查定时器
const clearStatusCheckTimer = () => {
if (statusCheckTimer.value) {
clearInterval(statusCheckTimer.value)
statusCheckTimer.value = null
console.log('已清除归还状态检查计时器')
}
}
// 更新使用时长的定时器(每30秒更新一次)
const startTimer = () => {
clearTimer()
timer.value = setInterval(() => {
if (!isPageActive.value) {
console.log('页面已不活跃,停止计时器')
clearTimer()
return
}
if (orderInfo.value.orderStatus !== 'in_used') {
console.log('订单状态已变更,停止计时器')
clearTimer()
return
}
console.log('执行定时更新订单信息')
getOrderDetails()
}, 30000) // 改为30秒更新一次
console.log('已启动使用时长更新计时器(30秒间隔)')
}
// 开始状态检查定时器(优化版)
const startStatusCheckTimer = () => {
currentStatusChecks.value = 0
clearStatusCheckTimer()
statusCheckTimer.value = setInterval(() => {
if (!isPageActive.value) {
console.log('页面已不活跃,停止状态检查计时器')
clearStatusCheckTimer()
return
}
if (orderInfo.value.orderStatus !== 'in_used') {
console.log('订单状态已变更,停止状态检查计时器')
clearStatusCheckTimer()
return
}
currentStatusChecks.value++
console.log(`执行归还状态检查 (${currentStatusChecks.value}/${maxStatusChecks.value})`)
checkReturnStatus()
if (currentStatusChecks.value >= maxStatusChecks.value) {
clearStatusCheckTimer()
uni.showToast({
title: t('order.pleaseRefreshManually'),
icon: 'none',
duration: 3000
})
}
}, statusCheckInterval.value)
console.log('已启动归还状态检查计时器(3秒间隔)')
}
// 检查归还状态
const checkReturnStatus = async () => {
try {
if (isPageActive.value) {
await getOrderDetails()
}
} catch (error) {
console.error('检查归还状态失败:', error)
}
}
// 更新订单信息
const updateOrderInfo = (orderData) => {
const oldStatus = orderInfo.value.orderStatus
orderInfo.value.usedTime = orderData.usedTime || '0分钟'
orderInfo.value.currentFee = orderData.currentFee || orderData.actualDeviceAmount || orderData.payAmount ||
'0.00'
orderInfo.value.orderStatus = orderData.orderStatus || ''
orderInfo.value.payWay = orderData.payWay || ''
orderInfo.value.startTime = orderData.startTime || orderData.createTime || ''
orderInfo.value.endTime = orderData.endTime || ''
orderInfo.value.depositAmount = orderData.depositAmount || ''
orderInfo.value.packageTime = orderData.packageTime || ''
orderInfo.value.packagePrice = orderData.packagePrice || ''
orderInfo.value.payAmount = orderData.payAmount || '0.00'
orderInfo.value.phone = orderData.phone || ''
orderInfo.value.orderNo = orderData.orderNo || ''
orderInfo.value.refundAmount = orderData.residueAmount || '0.00'
orderInfo.value.withdrawStatus = orderData.withdrawStatus || 'waiting'
orderInfo.value.isWithdrawn = orderData.withdrawStatus === 'success'
orderInfo.value.positionName = orderData.positionName || orderData.positionLocation || ''
orderInfo.value.returnPosition = orderData.returnPosition || orderData.positionName || orderData
.positionLocation || ''
orderInfo.value.freeRentTime = orderData.freeRentTime || ''
orderInfo.value.unitPrice = orderData.unitPrice || ''
orderInfo.value.orderType = orderData.orderType || ''
orderInfo.value.discountAmount = orderData.discountAmount||''
// 保存优惠券/会员卡相关信息
orderInfo.value.canUseMember = orderData.canUseMember === true
orderInfo.value.canUseCoupon = orderData.canUseCoupon === true
orderInfo.value.userMemberCardId = orderData.userMemberCardId || ''
orderInfo.value.userPurchaseId = orderData.userPurchaseId || ''
orderInfo.value.discountTypeName = orderData.discountTypeName || ''
orderInfo.value.originalFee = orderData.originalFee||''
orderInfo.value.returnMapImage = orderData.returnMapImage||''
currencyCode.value = resolveCurrencyCode(orderData)
// 保存快递归还开始时间(小时为单位)
orderInfo.value.expressReturnStart = orderData.expressReturnStart || null
// 保存是否支持快递归还
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) {
expressThresholdSeconds.value = orderInfo.value.expressReturnStart * 3600
console.log('从订单数据更新快递归还阈值:', orderInfo.value.expressReturnStart, '小时 =>', expressThresholdSeconds.value,
'秒')
}
if (orderData.deviceNo && !deviceId.value) {
deviceId.value = orderData.deviceNo
}
// 如果订单已完成,从监控中移除
if (isOrderCompleted()) {
removeFromOrderMonitor()
console.log('订单已完成,已从监控队列移除')
}
// 如果订单状态从 'in_used' 变为其他状态,清理所有定时器
if (oldStatus === 'in_used' && orderInfo.value.orderStatus !== 'in_used') {
console.log('订单状态已从使用中变为:', orderInfo.value.orderStatus, ',清理所有定时器')
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
// 显示订单完成提示
if (orderInfo.value.orderStatus === 'used_done' || orderInfo.value.orderStatus === 'used_down') {
uni.showToast({
title: t('order.orderCompleted'),
icon: 'success'
})
}
}
if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.pauseTime) {
startPauseDurationTicker()
} else {
stopPauseDurationTicker()
syncPauseDurationLine()
}
}
// 获取订单详情
const getOrderDetails = async () => {
if (!isPageActive.value) {
console.log('页面已不活跃,跳过订单详情请求')
return
}
// 防止并发请求
if (isLoadingOrder.value) {
console.log('正在加载订单,跳过重复请求')
return
}
try {
if (!orderInfo.value.orderId) {
throw new Error(t('order.orderIdRequired'))
}
isLoadingOrder.value = true
const result = await queryById(orderInfo.value.orderId)
if (result.code === 200 && result.data) {
const orderData = result.data
console.log('订单数据:', orderData)
// 更新订单信息
updateOrderInfo(orderData)
// 只有使用中的订单才启动本页的计时器逻辑
if (orderInfo.value.orderStatus === 'in_used') {
startTimer()
startStatusCheckTimer()
// 只有支持快递归还时才启动倒计时
if (orderInfo.value.isSupportExpressReturn !== 'no') {
startExpressCountdown()
}
}
// 根据订单状态决定是否添加到监控队列
if (orderInfo.value.orderId) {
// 如果订单已完成,从监控中移除
if (isOrderCompleted()) {
removeFromOrderMonitor()
console.log('订单已完成,已从监控队列移除:', orderInfo.value.orderId, '当前状态:', orderInfo.value
.orderStatus)
} else {
// 订单未完成,添加到监控队列
uni.setStorageSync('activeOrderId', orderInfo.value.orderId)
try {
if ($orderMonitor) {
$orderMonitor.removeOrder({
orderId: orderInfo.value.orderId
})
$orderMonitor.addOrder({
orderId: orderInfo.value.orderId
}, 'detail')
console.log('订单已添加到监控队列:', orderInfo.value.orderId, '当前状态:', orderInfo.value
.orderStatus)
} else {
console.warn('$orderMonitor 未定义,无法添加订单到监控队列')
}
} catch (error) {
console.error('添加订单到监控队列失败:', error)
}
}
}
await refreshPauseBillingEligible()
} else {
throw new Error(result.msg || t('order.getOrderFailed'))
}
} catch (error) {
console.error('获取订单详情错误:', error)
uni.showToast({
title: error.message || t('order.getOrderFailed'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
} finally {
isLoadingOrder.value = false
uni.hideLoading()
}
}
// 通过设备号查询使用中的订单
const getOrderByDevice = async () => {
try {
if (!deviceId.value) {
throw new Error(t('device.deviceNoRequired'))
}
const inUseRes = await getInUseOrder()
console.log('通过设备号查询订单结果:', JSON.stringify(inUseRes))
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const inUseOrder = inUseRes.data
console.log('使用中的订单:', inUseOrder)
orderInfo.value.orderId = inUseOrder.orderId
orderInfo.value.orderStatus = inUseOrder.orderStatus || ''
orderInfo.value.payWay = inUseOrder.payWay || ''
orderInfo.value.startTime = inUseOrder.startTime || ''
getOrderDetails()
} else {
throw new Error(t('order.noOrderInUse'))
}
} catch (error) {
console.error('通过设备号查询订单失败:', error)
uni.showToast({
title: error.message || t('order.getOrderFailed'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
} finally {
uni.hideLoading()
}
}
// 快递归还
const expressRetrunOrder = () => {
uni.navigateTo({
url: `/pages/expressReturn/addExpressReturn?orderId=${orderInfo.value.orderId}`
})
}
// 取消订单
const handleCancelOrder = () => {
showModalI18n({
title: t('order.confirmCancel'),
content: t('order.confirmCancelContent'),
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: t('common.processing')
})
const result = await cancelOrder({
orderId: orderInfo.value.orderId
})
if (result.code === 200) {
uni.hideLoading()
uni.showToast({
title: t('order.cancelSuccess'),
icon: 'success'
})
await getOrderDetails()
} else {
throw new Error(result.msg || t('order.cancelFailed'))
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || t('order.cancelFailed'),
icon: 'none'
})
}
}
}
})
}
// 立即支付
const handlePayment = () => {
uni.navigateTo({
url: `/pages/order/payment?orderId=${orderInfo.value.orderId}`
})
}
// 申请退款
const handleWithdraw = async () => {
uni.navigateTo({
url:'/subPackages/service/feedback/index'
})
// try {
// uni.showLoading({
// title: t('common.processing')
// })
// const res = await withdrawDeposit(orderInfo.value.orderNo)
// if (res && res.code === 200) {
// uni.showToast({
// title: t('order.refundSuccess'),
// icon: 'success'
// })
// orderInfo.value.withdrawStatus = 'processing'
// orderInfo.value.isWithdrawn = true
// setTimeout(() => {
// getOrderDetails()
// }, 1500)
// } else {
// throw new Error(res?.msg || t('order.refundFailed'))
// }
// } catch (error) {
// console.error('退款申请失败:', error)
// uni.showToast({
// title: error.message || t('order.refundFailed'),
// icon: 'none'
// })
// } finally {
// uni.hideLoading()
// }
}
// 返回首页
const goToHome = () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
// 处理"宝未弹出" - 跳转到反馈页面
const handleDeviceNoEject = async () => {
// 检查是否在30秒间隔内
// const now = Date.now()
// const timeDiff = now - lastDeviceEjectTime.value
// const requiredInterval = 30000 // 30秒
// if (timeDiff < requiredInterval) {
// const remainingSeconds = Math.ceil((requiredInterval - timeDiff) / 1000)
// uni.showToast({
// title: `${t('order.deviceEjectWait')}${remainingSeconds}${t('order.deviceEjectRetry')}`,
// icon: 'none',
// duration: 2000
// })
// return
// }
// // 更新最后点击时间
// lastDeviceEjectTime.value = now
//原有逻辑跳转到投诉反馈页面
// uni.navigateTo({
// url: '/subPackages/service/feedback/index?selectedType=0'
// })
/*
现逻辑使用最新接口二次尝试弹出设备
*/
try {
uni.showLoading({
title: t('order.deviceEjectProcessing')
})
const res = await deviceRentByOrderNo(orderInfo.value.orderNo)
uni.hideLoading()
if (res.code == 200) {
uni.showToast({
title: t('order.deviceEjectSuccess'),
icon: 'success',
duration: 2000
})
} else {
uni.showToast({
title: res.msg || t('order.deviceEjectFailed'),
icon: 'none',
duration: 2000
})
}
} catch (error) {
uni.hideLoading()
console.error('二次弹出设备失败:', error)
uni.showToast({
title: t('order.deviceEjectError'),
icon: 'none',
duration: 2000
})
}
}
// 处理"归还提醒" - 授权微信通知
const handleReturnReminderSubscribe = async () => {
try {
// 订阅微信通知
await new Promise((resolve, reject) => {
uni.requestSubscribeMessage({
tmplIds: ['euJmQA2FjRFPZVSM4Z6U96i2eLRiGTLy3E3qMjIoYYY'],
success: (subscribeRes) => {
console.log('订阅消息success回调', subscribeRes)
resolve(subscribeRes)
},
fail: (subscribeErr) => {
console.log('订阅消息fail回调', subscribeErr)
// 订阅失败不影响主流程
resolve(subscribeErr)
}
})
})
uni.showToast({
title: t('payment.subscriptionSuccess'),
icon: 'success',
duration: 2000
})
} catch (error) {
console.log('订阅消息异常', error)
uni.showToast({
title: t('payment.subscriptionFailed'),
icon: 'none',
duration: 2000
})
}
}
// 处理"不想还了转为自用"(按最高费用)
const handleConvertToOwnedWithMaxFee = () => {
try {
convertToOwnPopup.value && typeof convertToOwnPopup.value.open === 'function' && convertToOwnPopup.value
.open()
} catch (e) {
console.error('打开弹窗失败', e)
}
}
// 关闭转为自用弹窗
const closeConvertToOwnPopup = () => {
try {
convertToOwnPopup.value && typeof convertToOwnPopup.value.close === 'function' && convertToOwnPopup.value
.close()
} catch (e) {
console.error('关闭弹窗失败', e)
}
}
// 确认转为自用
const confirmConvertToOwn = async () => {
try {
closeConvertToOwnPopup()
uni.showLoading({
title: t('common.processing')
})
const result = await closeWithMaxFee(orderInfo.value.orderNo)
if (result.code === 200) {
uni.hideLoading()
uni.showToast({
title: t('order.convertToOwnWithMaxFeeSuccess'),
icon: 'success',
duration: 2000
})
// 刷新订单详情
setTimeout(() => {
getOrderDetails()
}, 2000)
} else {
throw new Error(result.msg || t('order.convertToOwnWithMaxFeeFailed'))
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || t('order.convertToOwnWithMaxFeeFailed'),
icon: 'none',
duration: 2000
})
}
}
// 生命周期钩子
onLoad((options) => {
console.log('订单详情页加载,参数:', JSON.stringify(options))
// 设置页面标题
uni.setNavigationBarTitle({
title: t('order.orderDetail')
})
isPageActive.value = true
// 从缓存读取通知内容(计费规则)
try {
const cachedNotice = uni.getStorageSync('noticeContent')
if (cachedNotice) {
console.log('从缓存读取计费规则:', cachedNotice)
feeRuleText.value = cachedNotice
}
} catch (e) {
console.warn('读取缓存通知内容失败:', e)
}
orderInfo.value.orderId = options.orderId || ''
deviceId.value = options.deviceNo || options.deviceId || ''
if (!orderInfo.value.orderId && deviceId.value) {
getOrderByDevice()
} else if (orderInfo.value.orderId) {
getOrderDetails()
} else {
uni.showToast({
title: t('order.orderInfoMissing'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
}
// 注册全局订单完成事件监听器
uni.$on('orderCompleted', handleOrderCompleted)
})
onShow(() => {
isPageActive.value = true
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(() => {
console.log('订单详情页隐藏,清理资源')
isPageActive.value = false
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
stopPauseDurationTicker()
removeFromOrderMonitor()
})
onUnload(() => {
console.log('订单详情页卸载,清理所有资源')
isPageActive.value = false
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
stopPauseDurationTicker()
removeFromOrderMonitor()
uni.$off('orderCompleted', handleOrderCompleted)
})
</script>
<style lang="scss" scoped>
.order-detail-container {
min-height: 100vh;
background: #f7f8fa;
padding: 30rpx;
/* 底部操作栏为多行时,预留更大的安全滚动空间,避免遮挡详情内容 */
padding-bottom: calc(320rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
// 顶部标题
.page-header {
margin-bottom: 30rpx;
.header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.header-left {
flex: 1;
}
.header-title-row {
display: flex;
align-items: center;
gap: 12rpx;
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;
color: #333;
}
.header-desc {
font-size: 28rpx;
color: #999;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
line-height: 1.5;
}
.header-right {
display: flex;
align-items: center;
height: 100%;
text-align: center;
min-width: 0;
flex-shrink: 1;
}
.device-no-eject-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// padding: 12rpx 20rpx;
// background: #E8F5E9;
// border-radius: 12rpx;
// border: 2rpx solid #07c160;
min-width: 0;
max-width: 180rpx;
width: 100%;
.device-no-eject-icon {
width: 68rpx;
height: 68rpx;
margin-bottom: 4rpx;
}
.device-no-eject-text {
font-size: 26rpx;
color: #07c160;
font-weight: 500;
display: block;
width: 100%;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
line-height: 1.3;
text-align: center;
}
&:active {
opacity: 0.8;
}
}
}
.promotion-tag {
padding: 6rpx 16rpx;
background: linear-gradient(135deg, #FFF4E6 0%, #FFE9CC 100%);
border-radius: 10rpx;
border: 1rpx solid #FFB84D;
font-size: 22rpx;
color: #FF8C00;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
// 订单信息卡片
.info-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.info-row {
display: flex;
// justify-content: space-between;
align-items: center;
// gap: 20rpx;
.info-left {
flex: 1;
min-width: 0;
display: flex;
// justify-content: space-between;
align-items: center;
.info-col {
width: auto;
flex: 1;
min-width: 0;
// flex: 1;
// text-align: center;
.info-value-wrapper {
display: flex;
align-items: baseline;
// justify-content: center;
margin-bottom: 8rpx;
.info-value-large {
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 1;
}
.info-value-unit {
font-size: 24rpx;
color: #666;
margin-left: 4rpx;
}
}
.info-label {
font-size: 24rpx;
color: #999;
}
}
}
.info-right {
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 1;
min-width: 0;
max-width: 48%;
margin-left: 12rpx;
.return-reminder-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 12rpx 24rpx;
background: #3EAB64;
border-radius: 50rpx;
border: 2rpx solid #3EAB64;
width: 100%;
max-width: 100%;
flex-shrink: 1;
gap: 8rpx;
overflow: hidden;
box-sizing: border-box;
.return-reminder-text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
display: block;
flex: 1;
min-width: 0;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
line-height: 1.3;
text-align: center;
}
&:active {
opacity: 0.8;
}
}
}
}
.fee-rule {
background: #E8F5E9;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #4CAF50;
line-height: 1.6;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.fee-rule-image {
margin-top: 20rpx;
width: 100%;
.rule-image {
// height: 100%;
width: 100%;
display: block;
border-radius: 12rpx;
}
}
}
// 转为自用提示
.convert-tip {
position: fixed;
left: 30rpx;
right: 30rpx;
bottom: calc(120rpx + env(safe-area-inset-bottom));
color: #000;
text-align: center;
padding: 20rpx 30rpx;
border-radius: 12rpx;
font-size: 26rpx;
z-index: 9;
&:active {
opacity: 0.85;
}
}
// 租借信息卡片
.rent-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.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;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
.rent-service-link {
color: #07c160;
}
}
.rent-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
gap: 16rpx;
&:last-of-type {
border-bottom: none;
}
.rent-label {
font-size: 28rpx;
color: #999;
flex-shrink: 0;
max-width: 42%;
line-height: 1.5;
}
.rent-value {
font-size: 28rpx;
color: #333;
text-align: right;
flex: 1;
min-width: 0;
max-width: none;
line-height: 1.5;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
&.promotion-value {
display: flex;
align-items: center;
justify-content: flex-end;
color: #FF8C00;
font-weight: 500;
.promotion-icon {
width: 32rpx;
height: 32rpx;
margin-right: 8rpx;
}
}
}
}
.rent-paid {
display: flex;
justify-content: flex-end;
align-items: baseline;
padding-top: 20rpx;
margin-top: 10rpx;
// border-top: 1rpx solid #f0f0f0;
.paid-label {
font-size: 28rpx;
color: #999;
margin-right: 8rpx;
}
.paid-value {
font-size: 48rpx;
font-weight: bold;
color: #ff6b6b;
}
.paid-unit {
font-size: 28rpx;
color: #ff6b6b;
margin-left: 4rpx;
}
}
}
// 底部操作栏
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 18rpx 24rpx;
padding-bottom: calc(18rpx + env(safe-area-inset-bottom));
background: #fff;
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -6rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 10;
display: flex;
flex-wrap: wrap;
align-items: stretch;
gap: 12rpx;
.bottom-bar-in-use {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
align-items: stretch;
gap: 12rpx;
box-sizing: border-box;
> .countdown-btn,
> .action-btn.secondary,
> .action-btn.pause-billing-btn {
flex: 1 1 0;
min-width: 0;
}
> .bottom-icon-btn {
flex: 0 0 122rpx;
}
}
.bottom-icon-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 108rpx;
min-height: 88rpx;
padding: 8rpx 8rpx;
box-sizing: border-box;
background: #f7f8fa;
border-radius: 20rpx;
.icon {
width: 42rpx;
height: 42rpx;
margin-bottom: 6rpx;
}
text {
font-size: 22rpx;
color: #666;
line-height: 1.25;
text-align: center;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
&:active {
opacity: 0.7;
}
}
.countdown-btn {
flex: 1 1 auto;
min-width: 200rpx;
min-height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #07c160;
background: #E8F5E9;
border-radius: 20rpx;
border: 2rpx solid #07c160;
padding: 0 18rpx;
box-sizing: border-box;
text-align: center;
line-height: 1.3;
}
.action-btn {
min-height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 27rpx;
border-radius: 20rpx;
padding: 12rpx 20rpx;
white-space: normal;
text-align: center;
line-height: 1.35;
&.full-width {
flex: 1;
}
&.primary {
background: #07c160;
color: #fff;
flex: 1;
&:active {
opacity: 0.8;
}
}
&.secondary {
background: #f5f5f5;
color: #333;
border: 1rpx solid #e0e0e0;
&:active {
opacity: 0.8;
}
&.is-disabled {
opacity: 1;
color: #b8b8b8;
background: #f3f3f3;
border-color: #e6e6e6;
&: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 1 100%;
width: 100%;
min-width: 0;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
&:active {
opacity: 0.8;
}
}
}
}
}
/* 转为自用确认弹窗样式 */
.convert-to-own-popup {
width: 640rpx;
max-width: 86vw;
background: #ffffff;
border-radius: 24rpx;
padding: 40rpx 40rpx 0;
box-sizing: border-box;
overflow: hidden;
.popup-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
.popup-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.popup-content {
padding: 0 0 40rpx;
box-sizing: border-box;
text-align: left;
.popup-text {
font-size: 28rpx;
color: #666;
line-height: 1.8;
text-align: left;
display: block;
width: 100%;
}
}
.popup-actions {
display: flex;
border-top: 1rpx solid #f0f0f0;
margin-left: -40rpx;
margin-right: -40rpx;
.popup-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
box-sizing: border-box;
&.cancel-btn {
color: #666;
border-right: 1rpx solid #f0f0f0;
&:active {
background-color: #f5f5f5;
}
}
&.confirm-btn {
color: #07c160;
font-weight: 600;
&:active {
background-color: #f0f9f4;
}
}
}
}
}
</style>