Files
uni-fans-score/pages/order/detail.vue
T

1870 lines
49 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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 class="header-desc">{{ getStatusDesc() }}</view>
</view>
<view class="header-right" v-if="orderInfo.orderStatus === 'in_used'">
<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">{{ $t('unit.yuan') }}</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-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>
<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="">
<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>
</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="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>
<!-- 只有支持快递归还时才显示倒计时和快递归还按钮 -->
<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') }}
</view>
<view v-if="showExpressAction" class="action-btn primary" @click="quickReturn">
{{ $t('order.quickReturn') }}
</view>
</template>
<!-- 不支持快递归还时只显示快速归还按钮 -->
<view v-else class="action-btn primary" @click="quickReturn">
{{ $t('order.quickReturn') }}
</view>
<!-- 不想还了转为自用按钮 -->
<view class="action-btn convert-btn" @click="handleConvertToOwnedWithMaxFee">
{{ $t('order.convertToOwnWithMaxFee') }}
</view>
</template>
<!-- 已完成状态 -->
<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="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>
</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 class="return-map-popup" v-if="showReturnMapPopup" @click.self="closeReturnMapPopup">
<view class="popup-mask"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ $t('order.returnLocationMap') }}</text>
<view class="close-btn" @click="closeReturnMapPopup">
<text class="close-icon">×</text>
</view>
</view>
<view class="popup-body">
<image :src="returnMapImageUrl" mode="widthFix" class="map-image" lazy-load="true"></image>
</view>
<view class="popup-footer">
<view class="save-btn" @click="saveReturnMapImage">
{{ $t('common.saveImage') }}
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted,
onUnmounted,
getCurrentInstance
} from 'vue'
import {
onLoad,
onShow,
onHide,
onUnload
} from '@dcloudio/uni-app'
import {
queryById,
cancelOrder,
reportDeviceNoEject,
convertToOwned,
closeWithMaxFee,
getInUseOrder
} 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
} 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: ''
})
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 showReturnMapPopup = ref(false)
const returnMapImageUrl = ref('')
// 计算属性:是否显示优惠券/会员卡可用提示
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'
}
// 获取订单状态文字
const getOrderStatusText = () => {
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
return `${orderInfo.value.unitPrice}${t('unit.yuan')}/${orderTypeText}`
}
// 格式化倒计时(显示为 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) {
// 有值则弹窗显示图片
returnMapImageUrl.value = orderInfo.value.returnMapImage
showReturnMapPopup.value = true
} else {
// 没有值则继续执行跳转流程
uni.navigateTo({
url: '/pages/search/index'
})
}
}
// 关闭归还地图弹窗
const closeReturnMapPopup = () => {
showReturnMapPopup.value = false
}
// 保存归还地图图片
const saveReturnMapImage = () => {
uni.showLoading({
title: t('common.saving')
})
uni.downloadFile({
url: returnMapImageUrl.value,
success: (res) => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading()
uni.showToast({
title: t('common.saveSuccess'),
icon: 'success'
})
},
fail: (err) => {
uni.hideLoading()
console.error('保存图片失败:', err)
uni.showToast({
title: t('common.saveFailed'),
icon: 'none'
})
}
})
} else {
uni.hideLoading()
uni.showToast({
title: t('common.downloadFailed'),
icon: 'none'
})
}
},
fail: (err) => {
uni.hideLoading()
console.error('下载图片失败:', err)
uni.showToast({
title: t('common.downloadFailed'),
icon: 'none'
})
}
})
}
// 再次租借
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 getUsedTimeLabel = () => {
// 使用中状态显示"已使用",已完成状态显示"使用时长"
return orderInfo.value.orderStatus === 'in_used' ? t('order.used') : t('order.duration')
}
// 获取订单费用(不含单位)
const getOrderFee = () => {
const fee = orderInfo.value.originalFee || orderInfo.value.originalFee || '0'
// 移除可能的"元"字符
return 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
}
// 清除倒计时
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()
const elapsedSeconds = Math.max(0, Math.floor((nowMs - startMs) / 1000))
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||''
// 保存快递归还开始时间(小时为单位)
orderInfo.value.expressReturnStart = orderData.expressReturnStart || null
// 保存是否支持快递归还
orderInfo.value.isSupportExpressReturn = orderData.isSupportExpressReturn || 'yes'
// 如果有有效的 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'
})
}
}
}
// 获取订单详情
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)
}
}
}
} 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 = () => {
uni.showModal({
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 = () => {
uni.navigateTo({
url: '/pages/feedback/index?selectedType=0'
})
}
// 处理"归还提醒" - 授权微信通知
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()
}
})
onHide(() => {
console.log('订单详情页隐藏,清理资源')
isPageActive.value = false
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
removeFromOrderMonitor()
})
onUnload(() => {
console.log('订单详情页卸载,清理所有资源')
isPageActive.value = false
clearTimer()
clearStatusCheckTimer()
clearExpressCountdown()
removeFromOrderMonitor()
uni.$off('orderCompleted', handleOrderCompleted)
})
</script>
<style lang="scss" scoped>
.order-detail-container {
min-height: 100vh;
background: #f7f8fa;
padding: 30rpx;
padding-bottom: 180rpx;
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-title {
font-size: 48rpx;
font-weight: bold;
color: #333;
}
.header-desc {
font-size: 28rpx;
color: #999;
}
.header-right {
display: flex;
align-items: center;
height: 100%;
text-align: center;
}
.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: 120rpx;
.device-no-eject-icon {
width: 68rpx;
height: 68rpx;
margin-bottom: 4rpx;
}
.device-no-eject-text {
font-size: 26rpx;
color: #07c160;
font-weight: 500;
}
&: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;
display: flex;
// justify-content: space-between;
align-items: center;
.info-col {
width: 200rpx;
// 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: 0;
.return-reminder-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 12rpx 24rpx;
background: #3EAB64;
border-radius: 50rpx;
border: 2rpx solid #3EAB64;
.return-reminder-text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
&:active {
opacity: 0.8;
}
}
}
}
.fee-rule {
background: #E8F5E9;
border-radius: 8rpx;
padding: 16rpx;
font-size: 24rpx;
color: #4CAF50;
line-height: 1.6;
}
.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-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
}
.rent-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-of-type {
border-bottom: none;
}
.rent-label {
font-size: 28rpx;
color: #999;
}
.rent-value {
font-size: 28rpx;
color: #333;
text-align: right;
max-width: 400rpx;
&.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: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20rpx;
.bottom-icon-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 100rpx;
.icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 8rpx;
}
text {
font-size: 24rpx;
color: #666;
}
&:active {
opacity: 0.7;
}
}
.countdown-btn {
flex: 1;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #07c160;
background: #E8F5E9;
border-radius: 44rpx;
border: 2rpx solid #07c160;
}
.action-btn {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
border-radius: 44rpx;
padding: 0 40rpx;
white-space: nowrap;
&.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;
}
}
&.convert-btn {
background: #fff;
color: #07c160;
border: 2rpx solid #07c160;
flex: 1;
&: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;
}
}
}
}
}
/* 归还地图弹窗样式 */
.return-map-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
}
.popup-content {
position: relative;
width: 90%;
max-width: 600rpx;
background: #ffffff;
border-radius: 24rpx;
overflow: hidden;
z-index: 10000;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f5f5f5;
.close-icon {
font-size: 40rpx;
color: #666;
line-height: 1;
}
&:active {
opacity: 0.7;
}
}
}
.popup-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
.map-image {
width: 100%;
display: block;
border-radius: 12rpx;
}
}
.popup-footer {
padding: 20rpx 30rpx 30rpx;
.save-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #07c160, #10d673);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
color: #fff;
&:active {
opacity: 0.8;
}
}
}
}
}
</style>