chuangte_bike_newxcx/page_shanghu/guanli/order_detail.vue
2026-06-01 17:31:04 +08:00

4645 lines
123 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="page-container">
<u-navbar title="订单详情" :border-bottom="false" :background="{ backgroundColor: '#fff' }" title-color='#333'
title-size='34' height='44'></u-navbar>
<!-- 地图区域 -->
<view class="map-wrapper">
<map class="map" id="map" ref="map" :scale="zoomSize" :latitude="latitude" :longitude="longitude"
:show-location="true" :markers="markers" :polyline="polyline" :polygons="polygon" :enable-zoom="true"
:enable-scroll="true">
<cover-view hover-class="app-tap-hover" class="map-ctrl-btn park-btn" @click="toggleIconAndCallout">
<cover-image class="img" src="https://lxnapi.ccttiot.com/bike/img/static/uRiYQZQEb3l2LsltEsyW"></cover-image>
</cover-view>
<cover-view hover-class="app-tap-hover" class="map-ctrl-btn track-btn" @click="toTrack" v-if="orderxqobj.status != 'CANCELED'">
<cover-image class="img" src="https://lxnapi.ccttiot.com/bike/img/static/ufaAAtlirJYs1QwJF25P"></cover-image>
</cover-view>
</map>
</view>
<!-- 订单概览卡片 -->
<view class="info-card overview-card" style="">
<view class="card-header">
<view class="order-no-box" style="display: flex; align-items: center;">
<text class="value">{{orderxqobj.no}}</text>
<zhima-no-deposit-badge v-if="orderxqobj.payChannelApiType === ChannelApiType.ZHIMA_DEPOSIT_ONLY.value" style="margin-left: 12rpx;" />
</view>
<view class="status-tag" :class="{
'processing': orderxqobj.status == 'PROCESSING',
'finished': orderxqobj.status == 'FINISHED',
'canceled': orderxqobj.status == 'CANCELED'
}">
<text v-if="orderxqobj.status == 'PROCESSING'">进行中</text>
<text v-if="orderxqobj.status == 'FINISHED'">已完成</text>
<text v-if="orderxqobj.status == 'CANCELED'">已取消</text>
</view>
</view>
<view class="data-grid">
<view class="data-item">
<view class="data-val price">¥{{orderxqobj.actualReceivedAmount || '0.00'}}</view>
<view class="data-label">实收金额</view>
</view>
<view class="data-item">
<view class="data-val">{{formattedDuration}}</view>
<view class="data-label">骑行时长</view>
</view>
<view class="data-item">
<view class="data-val">{{kmDistance}}<text class="unit">km</text></view>
<view class="data-label">骑行距离</view>
</view>
</view>
<view class="divider"></view>
<view class="detail-list">
<view class="list-item">
<text class="label">开始时间</text>
<text class="value">{{orderxqobj.startTime || '--'}}</text>
</view>
<view class="list-item">
<text class="label">超时状态</text>
<text class="value">{{orderxqobj.timeout ? '有超时' : '无超时'}}</text>
</view>
<view class="list-item">
<text class="label">用户信息</text>
<view class="value-row">
<text>{{orderxqobj.userName}}</text>
<text hover-class="app-tap-hover" class="phone-link" @click="checkbtn(1)">{{orderxqobj.userPhone}}</text>
</view>
</view>
<view class="list-item">
<text class="label">套餐名称</text>
<text class="value highlight">{{orderxqobj.suitName}}</text>
</view>
<view class="list-item">
<text class="label">支付方式</text>
<text class="value">{{orderxqobj.payType == 1 ? '押金抵扣' : '用户支付'}}</text>
</view>
</view>
</view>
<!-- 费用明细卡片 -->
<view class="info-card">
<view class="card-title-bar">
<view class="bar-mark"></view>
<text class="title-text">费用明细</text>
</view>
<view class="fee-list">
<view class="list-item">
<text class="label">骑行费</text>
<text class="value">¥{{orderxqobj.ridingFee}}</text>
</view>
<view class="list-item">
<text class="label">调度费</text>
<text class="value">¥{{orderxqobj.dispatchFee}}</text>
</view>
<view class="list-item">
<text class="label">管理费</text>
<text class="value">¥{{orderxqobj.manageFee}}</text>
</view>
<view class="list-item" v-if="showOrderInsureFee">
<text class="label">应收保险费</text>
<text class="value">¥{{ insureFeeDisplay }}</text>
</view>
<view class="list-item border-dashed">
<text class="label">车损费</text>
<text class="value">¥{{orderxqobj.deductionFee}}</text>
</view>
<view class="damage-display-block" v-if="orderDamageImages.length">
<text class="damage-display-title">车损图片</text>
<view class="damage-display-grid">
<image hover-class="app-tap-hover"
v-for="(dimg, dix) in orderDamageImages"
:key="dix"
:src="dimg"
mode="aspectFill"
class="damage-display-thumb"
@click="previewOrderDamage(dimg)"
></image>
</view>
</view>
<view class="verify-remark-block" v-if="hasVerifyRemark">
<text class="verify-remark-title">还车审核备注</text>
<text class="verify-remark-text">{{ orderxqobj.verifyRemark }}</text>
</view>
<view class="summary-section">
<view class="list-item sm">
<text class="label">结算金额</text>
<text class="value">¥{{orderxqobj.totalFee}}</text>
</view>
<view class="list-item sm">
<text class="label">优惠</text>
<text class="value">¥{{orderxqobj.totalDiscountAmount}}</text>
</view>
<view class="list-item sm">
<text class="label">退款</text>
<text class="value text-red">¥{{orderxqobj.adminRefundAmount}}</text>
</view>
</view>
<view class="total-pay">
<text class="label">实付金额</text>
<text class="price">¥{{orderxqobj.actualReceivedAmount}}</text>
</view>
</view>
</view>
<!-- 车辆信息卡片 -->
<view class="info-card vehicle-card">
<view class="card-title-bar">
<view class="bar-mark"></view>
<text class="title-text">车辆信息</text>
</view>
<view class="vehicle-list">
<view class="vehicle-item" v-for="(item,index) in processedOrderDeviceList" :key="index">
<!-- 车辆头部信息 -->
<view hover-class="app-tap-hover" class="vehicle-header" @click.stop="btntz(item.deviceId)">
<view class="vehicle-header-left">
<view class="vehicle-badge" :class="index==0 ? 'badge-primary' : 'badge-secondary'">
<view class="badge-dot"></view>
<text class="badge-text">{{index == 0 ? '初始车辆' : '更换车辆'}}</text>
</view>
<view class="vehicle-time-info">
<text class="time-label">开始时间</text>
<text class="time-value">{{item.startTime || '--'}}</text>
</view>
</view>
<view class="vehicle-arrow">
<u-icon name="arrow-right" color="#C0C4CC" size="20"></u-icon>
</view>
</view>
<!-- 车辆详细信息 -->
<view hover-class="app-tap-hover" class="vehicle-details" @click.stop="btntz(item.deviceId)">
<view class="detail-grid">
<view class="detail-item">
<text class="detail-label">SN编号</text>
<text class="detail-value">{{item.deviceSn || '--'}}</text>
</view>
<view class="detail-item">
<text class="detail-label">MAC</text>
<text class="detail-value">{{item.deviceMac || '--'}}</text>
</view>
<view class="detail-item">
<text class="detail-label">车牌号</text>
<text class="detail-value highlight">{{item.deviceVehicleNum || '--'}}</text>
</view>
<view class="detail-item">
<text class="detail-label">车型</text>
<text class="detail-value">{{item.deviceModelName || '--'}}</text>
</view>
</view>
<view class="time-info-section">
<view class="time-item">
<view class="time-icon start"></view>
<view class="time-content">
<text class="time-title">开锁时间</text>
<text class="time-desc">{{item.startTime || '--'}}</text>
</view>
</view>
<view class="time-item">
<view class="time-icon end"></view>
<view class="time-content">
<text class="time-title">结束时间</text>
<text class="time-desc">{{item.endTime || '--'}}</text>
</view>
</view>
</view>
</view>
<!-- 检查视频/图片区域 -->
<view class="media-section" v-if="item.checkVideo || item.finishPicture">
<view class="media-item" v-if="item.checkVideo">
<view class="media-header">
<view class="media-icon start-icon"></view>
<text class="media-title">开始前检查</text>
</view>
<view class="media-wrapper">
<video class="media-video" :src="item.checkVideo" controls></video>
</view>
</view>
<view class="media-item" v-if="item.finishPicture">
<view class="media-header">
<view class="media-icon end-icon"></view>
<text class="media-title">结束前检查</text>
</view>
<view class="media-wrapper">
<video class="media-video" :src="item.finishPicture" controls></video>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 最后订单信息 -->
<view class="info-card" v-if="deviceInfos.etOrders && deviceInfos.etOrders[0] && deviceInfos.sn">
<view class="card-title-bar">
<view class="bar-mark"></view>
<text class="title-text">最后订单信息</text>
</view>
<view class="detail-list">
<view class="list-item">
<text class="label">用户姓名</text>
<text class="value">{{deviceInfos.etOrders[0].realName || '--'}}</text>
</view>
<view class="list-item">
<text class="label">用户电话</text>
<text class="value">{{deviceInfos.etOrders[0].phonenumber || '--'}}</text>
</view>
<view class="list-item">
<text class="label">订单编号</text>
<text class="value">{{deviceInfos.etOrders[0].orderNo || '--'}}</text>
</view>
<view class="list-item">
<text class="label">开始时间</text>
<text class="value">{{deviceInfos.etOrders[0].createTime || '--'}}</text>
</view>
<view class="list-item">
<text class="label">订单状态</text>
<text class="value">{{statuss()}}</text>
</view>
</view>
</view>
<view class="footer-spacer" v-if="showFooterBar"></view>
<!-- 底部操作栏(含待审核时固定的「通过审核」) -->
<view class="footer-bar" v-if="showFooterBar">
<view class="btn-group" :class="{ 'btn-group--audit': orderxqobj.status == 'WAIT_VERIFY' }">
<view hover-class="app-tap-hover" class="btn btn-audit" v-if="orderxqobj.status == 'WAIT_VERIFY'" @click="unpass">
<u-icon name="checkmark-circle" color="#fff" size="18" style="margin-right: 8rpx;"></u-icon>
<text>审核</text>
</view>
<view hover-class="app-tap-hover" class="btn btn-outline" v-if="orderxqobj.status == 'FINISHED'" @click="checkbtn(0)">退款</view>
<view hover-class="app-tap-hover" class="btn btn-primary" v-if="orderxqobj.status == 'RIDE_WAIT_PAY'" @click="btngaijia">改价</view>
<view hover-class="app-tap-hover" class="btn btn-primary" v-if="orderxqobj.status == 'RIDE_WAIT_PAY'" @click="btnyajin">押金抵扣</view>
<view hover-class="app-tap-hover" class="btn btn-outline" v-if="orderxqobj.priceChanged == true" @click="btnjilu">改价记录</view>
<view hover-class="app-tap-hover" class="btn btn-primary" @click="btnhuanbike" v-if="orderxqobj.status=='PROCESSING'">辅助换车</view>
<view hover-class="app-tap-hover" class="btn btn-primary" @click="btnfuzhu" v-if="orderxqobj.status=='PROCESSING'">辅助还车</view>
</view>
</view>
<!-- 各种弹窗保留原有逻辑 -->
<u-mask hover-class="app-tap-hover" :show="false" @click="show = false" :z-index='100' />
<u-mask :show="maskloading" :z-index='100' duration='0' />
<view class="maskloadpage" v-if="maskloading">
<view class="maskpage1" v-if="maskepage==4">
<view class="top_info">
<image :src="loadimg" mode=""></image>
<view class="masktxt">
{{buletxt}}
</view>
</view>
<view class="masktips" style="width: 100%;">
请确保与车辆的距离小于3米
</view>
<view class="tipsimg">
<image src="https://lxnapi.ccttiot.com/bike/img/static/ugvqmfB3QYujZ6SnfTia" mode=""></image>
</view>
</view>
<!-- 不允许停车点外还车 -->
<view class="maskpage1" v-if="maskepage==5">
<view class="top_info">
<image src="https://lxnapi.ccttiot.com/bike/img/static/uG3cbPgvPDzwlq6IHHxK" mode=""></image>
<view class="masktxt" v-if="orderinfo.sn">
蓝牙连接失败
</view>
</view>
<view class="masktips" style="width: 100%;">
请确保手机蓝牙已经打开
</view>
<view class="tipsimg">
<image src="https://lxnapi.ccttiot.com/bike/img/static/ugvqmfB3QYujZ6SnfTia" mode=""></image>
</view>
<view class="btn_box">
<view hover-class="app-tap-hover" class="btn4" @click="closemask()">
返回
</view>
<view hover-class="app-tap-hover" class="btn3" @click="Binddevice">
重新连接
</view>
</view>
</view>
</view>
<u-mask hover-class="app-tap-hover" :show="showvehicle" @click="closevehicle()" :z-index='100' />
<view class="tip_box" v-if="showvehicle">
<view class="top" v-if="showvehicle">
<view class="tip">
设备信息修改
</view>
<view class="ipt_box">
<view class="text">
车牌号
</view>
<view class="ipt">
<input type="text" v-model="vehicleNum" placeholder=" " class="input"
placeholder-style="color:#C7CDD3">
</view>
</view>
</view>
<view class="bots">
<view hover-class="app-tap-hover" class="bot_left" @click="closevehicle()">
取消
</view>
<view hover-class="app-tap-hover" class="bot_right" @click="putvehicle()">
确定
</view>
</view>
</view>
<u-mask hover-class="app-tap-hover" :show="showqr" @click="closeQr()" :z-index='100' />
<view class="tip_box" v-if="showqr">
<view class="ewm" style="padding-top: 50rpx;">
<canvas id="qrcode" canvas-id="qrcode" style="width: 350rpx;height:350rpx;margin-left: 164rpx;" />
</view>
<view hover-class="app-tap-hover" class="saveQr" @click="saveQrcode()">
保存二维码
</view>
</view>
<u-mask hover-class="app-tap-hover" :show="showbtntip" @click="closevehicle()" :z-index='100' />
<view class="tip_box" v-if="showbtntip">
<view class="top" v-if="showbtntip">
<view class="tip">
操作提示
</view>
<view class="ipt_box" style="justify-content: center;">
<view class="text" style="width: 80%;text-align: center;">
车辆有正在进行中的订单,是否进行该操作
</view>
</view>
</view>
<view class="bots">
<view hover-class="app-tap-hover" class="bot_left" @click="closeshowtip() ">
取消
</view>
<view hover-class="app-tap-hover" class="bot_right" @click="btn(btnnum)">
确定
</view>
</view>
</view>
<u-select v-model="showModelList" :list="ModelList" title='修改车型' @confirm="confirm"></u-select>
<view class="zengsongone huanche-verify-modal" v-if="huancheflag">
<view class="tops">还车审核</view>
<scroll-view scroll-y class="huanche-verify-scroll">
<view class="huanche-field-label">车损费</view>
<input class="huanche-field-input" type="digit" v-model="price" placeholder="请输入车损费" />
<view class="huanche-field-label">车损图片 <text class="huanche-sub">(最多9张)</text></view>
<view class="verify-damage-upload-wrap">
<view hover-class="app-tap-hover"
class="verify-damage-item"
v-for="(img, vidx) in damageImageList"
:key="'done-' + vidx"
@click="previewVerifyDamage(vidx)"
>
<image :src="img" mode="aspectFill"></image>
<view hover-class="app-tap-hover" class="verify-damage-del" @click.stop="removeVerifyDamage(vidx)">×</view>
</view>
<view
class="verify-damage-item verify-damage-item-uploading"
v-for="item in damageImageUploadingList"
:key="'up-' + item.uid"
>
<image :src="item.tempPath" mode="aspectFill"></image>
<view class="verify-damage-upload-mask">
<u-loading mode="circle" size="28" color="#ffffff"></u-loading>
<text class="verify-damage-upload-mask-text">上传中</text>
</view>
</view>
<view hover-class="app-tap-hover"
class="verify-damage-add"
v-if="damageImageList.length + damageImageUploadingList.length < 9"
@click="chooseVerifyDamageImages"
>
<u-icon name="plus" size="36" color="#999"></u-icon>
<text>{{ damageImageList.length + damageImageUploadingList.length }}/9</text>
</view>
</view>
<view class="huanche-field-label">备注</view>
<input class="huanche-field-input" type="text" v-model="beizhu" placeholder="请输入备注" />
</scroll-view>
<view class="anniu">
<view hover-class="app-tap-hover" class="qx" @click="btnqx">取消</view>
<view hover-class="app-tap-hover"
class="qd"
:class="{ 'qd-disabled': damageImageUploadingList.length > 0 }"
@click="btnqd"
>确定</view>
</view>
</view>
<view class="maskone" v-if="huancheflag"></view>
<view class="zengsong refund-modal" v-if="tkflag">
<view class="refund-modal__header">
<view class="refund-modal__head-text">
<text class="refund-modal__title">订单退款</text>
<text class="refund-modal__desc">在右侧填写退款金额,不得超过左侧实收金额</text>
</view>
</view>
<view class="refund-table__head">
<text class="col-name">费用项目</text>
<text class="col-refund">退款(元)</text>
</view>
<view
class="refund-table__row"
:class="{ 'refund-table__row--disabled': isRefundRowDisabled('riding') }"
>
<text class="col-name">骑行费</text>
<text class="col-original">¥{{ formatRefundMoney(getRefundOriginal('riding')) }}</text>
<input
v-if="!isRefundRowDisabled('riding')"
class="col-refund-input"
type="digit"
v-model="qxfei"
placeholder="填退款"
/>
<text v-else class="col-refund-placeholder">—</text>
</view>
<view
class="refund-table__row"
:class="{ 'refund-table__row--disabled': isRefundRowDisabled('dispatch') }"
>
<text class="col-name">调度费</text>
<text class="col-original">¥{{ formatRefundMoney(getRefundOriginal('dispatch')) }}</text>
<input
v-if="!isRefundRowDisabled('dispatch')"
class="col-refund-input"
type="digit"
v-model="ddfei"
placeholder="填退款"
/>
<text v-else class="col-refund-placeholder">—</text>
</view>
<view
class="refund-table__row"
:class="{ 'refund-table__row--disabled': isRefundRowDisabled('manage') }"
>
<text class="col-name">管理费</text>
<text class="col-original">¥{{ formatRefundMoney(getRefundOriginal('manage')) }}</text>
<input
v-if="!isRefundRowDisabled('manage')"
class="col-refund-input"
type="digit"
v-model="glfei"
placeholder="填退款"
/>
<text v-else class="col-refund-placeholder">—</text>
</view>
<view
class="refund-table__row"
:class="{ 'refund-table__row--disabled': isRefundRowDisabled('deduction') }"
>
<text class="col-name">车损费</text>
<text class="col-original">¥{{ formatRefundMoney(getRefundOriginal('deduction')) }}</text>
<input
v-if="!isRefundRowDisabled('deduction')"
class="col-refund-input"
type="digit"
v-model="csfei"
placeholder="填退款"
/>
<text v-else class="col-refund-placeholder">—</text>
</view>
<view class="refund-reason">
<text class="refund-reason__label">退款原因</text>
<input
class="refund-reason__input"
type="text"
v-model="yuanyin"
placeholder="请填写退款原因(必填)"
/>
</view>
<view class="refund-total">
<text class="refund-total__label">本次退款合计</text>
<text class="refund-total__amount">¥{{ totalRefund }}</text>
</view>
<view class="anniu">
<view hover-class="app-tap-hover" class="qx" @click="qxqx">取消</view>
<view hover-class="app-tap-hover" class="qd refund-modal__confirm" @click="btntuikuan">确认退款</view>
</view>
</view>
<view style="background-color: rgba(0, 0, 0, .5);width: 100%;height: 100vh;position: fixed;top: 0;left: 0;z-index: 11;" v-if="tkflag"></view>
<!-- 退款二次确认 -->
<view class="refund-confirm-mask" v-if="refundConfirmFlag" @click="closeRefundConfirm"></view>
<view class="refund-confirm-dialog" v-if="refundConfirmFlag">
<view class="refund-confirm-dialog__title">确认退款</view>
<view class="refund-confirm-dialog__content">
<view class="refund-confirm-dialog__line1">
<text class="refund-confirm-dialog__text">本次将退款</text>
<text class="refund-confirm-dialog__amount">¥{{ totalRefund }}</text>
</view>
<view class="refund-confirm-dialog__line2">确定要执行退款吗?</view>
</view>
<view class="refund-confirm-dialog__actions">
<view hover-class="app-tap-hover" class="refund-confirm-dialog__btn refund-confirm-dialog__btn--cancel" @click="closeRefundConfirm">取消</view>
<view hover-class="app-tap-hover" class="refund-confirm-dialog__btn refund-confirm-dialog__btn--ok" @click="confirmRefundSubmit">确定</view>
</view>
</view>
<!-- 辅助还车是否收取调度费 -->
<view class="diaodu" v-if="ddflag">
<view hover-class="app-tap-hover" class="top" @click="btncha">
×
</view>
<view class="cen">
该车辆未在停车区还车,是否收取调度费{{fuzhuobj.manageFee > 0 ? fuzhuobj.manageFee : fuzhuobj.dispatchFee > 0 ? fuzhuobj.dispatchFee : ''}}元?
</view>
<view class="anniu">
<view hover-class="app-tap-hover" class="bu" @click="btnfuzhus(false)">
不收取
</view>
<view hover-class="app-tap-hover" class="shou" @click="btnfuzhus(true)">
收取
</view>
</view>
</view>
<view class="mask" v-if="ddflag">
</view>
<!-- 改价弹窗 -->
<view class="zengsong" v-if="zengsongflag">
<view class="tops">
改价
</view>
<view class="gai">
<view class="">
骑行费
</view>
<input type="text" v-model="orderxqobj.ridingFee" :disabled="true" style="background-color: #efefef;"
placeholder="请输入骑行费" /> >
<input type="text" v-model="qxfei" placeholder="请输入骑行费" />
</view>
<view class="gai">
<view class="">
调度费
</view>
<input type="text" v-model="orderxqobj.dispatchFee" :disabled="true" style="background-color: #efefef;"
placeholder="请输入调度费" /> >
<input type="text" v-model="ddfei" placeholder="请输入调度费" />
</view>
<view class="gai">
<view class="">
管理费
</view>
<input type="text" v-model="orderxqobj.manageFee" :disabled="true" style="background-color: #efefef;"
placeholder="请输入管理费" /> >
<input type="text" v-model="glfei" placeholder="请输入管理费" />
</view>
<view class="gai">
<view class="">
改价原因
</view>
<input type="text" style="width:440rpx;" v-model="yuanyin" placeholder="请输入改价原因" />
</view>
<view class="anniu">
<view hover-class="app-tap-hover" class="qx" @click="qxqx">
取消
</view>
<view hover-class="app-tap-hover" class="qd" @click="qdqd">
确定
</view>
</view>
</view>
<view style="background-color: rgba(0, 0, 0, .5);width: 100%;height: 100vh;position: fixed;top: 0;left: 0;z-index: 999;"
v-if="zengsongflag"></view>
<!-- 押金抵扣弹窗 -->
<view class="yajindikou" style="z-index: 999;" v-if="yajinyajin">
<view class="tops">
押金抵扣
</view>
<view class="aaaa">即将进行押金抵扣,请确认</view>
<view class="aaaa">抵扣金额:{{ orderxqobj.totalFee == undefined ? '--' : orderxqobj.totalFee}} 元</view>
<view class="aaaa">剩余可抵扣押金:{{ orderxqobj.depositDeductRemain == undefined ? '--' : orderxqobj.depositDeductRemain}} 元</view>
<view class="aaaa">若押金不足,则收取最大可抵扣金额</view>
<view class="anniu">
<view hover-class="app-tap-hover" class="qx" @click="yjqxqx">
取消
</view>
<view hover-class="app-tap-hover" class="qd" @click="yjqdqd">
确定
</view>
</view>
</view>
<view style="background-color: rgba(0, 0, 0, .5);width: 100%;height: 100vh;position: fixed;top: 0;left: 0;z-index: 998;"
v-if="yajinyajin"></view>
<!-- 辅助换车弹窗 -->
<view class="change-bike-modal" v-if="showChangeBike">
<view class="modal-header">
<view class="modal-title">辅助换车</view>
<view hover-class="app-tap-hover" class="modal-close" @click="closeChangeBike">×</view>
</view>
<view class="modal-content">
<!-- 目标设备信息 -->
<view class="form-section">
<view class="form-item">
<view class="form-label">
<text class="label-text">设备SN</text>
</view>
<view class="input-with-scan">
<view hover-class="app-tap-hover" class="scan-icon" @click="scanDeviceSn">
<u-icon name="scan" color="#4C97E7" size="32"></u-icon>
</view>
<input
class="form-input-with-icon"
type="text"
v-model="selectedDeviceSn"
placeholder="请输入设备SN或点击左侧扫码"
/>
</view>
</view>
</view>
<!-- 换车原因 -->
<view class="form-section">
<view class="section-title">换车原因<text class="required">*</text></view>
<picker
mode="selector"
:range="reasonOptions"
range-key="label"
:value="reasonIndex"
@change="onReasonChange"
>
<view class="picker-item">
<text :class="['picker-text', !changeReason && 'placeholder']">
{{ changeReason ? (changeReason === 'LOW_POWER' ? '电量不足' : '车辆损坏') : '请选择换车原因' }}
</text>
<u-icon name="arrow-right" color="#C0C4CC" size="18"></u-icon>
</view>
</picker>
</view>
<!-- 故障信息(仅当选择车辆损坏时显示) -->
<view class="form-section" v-if="changeReason === 'DEVICE_FAULT'">
<view class="form-item">
<view class="form-label">
<text class="label-text">故障原因</text>
<text class="label-tip">(可选)</text>
</view>
<textarea
class="form-textarea"
v-model="faultDetail"
placeholder="请输入故障原因描述"
maxlength="200"
:auto-height="true"
></textarea>
</view>
<view class="form-item">
<view class="form-label">
<text class="label-text">故障图片</text>
<text class="label-tip">(可选)</text>
</view>
<view class="upload-container">
<view class="upload-preview" v-if="faultPicture">
<image hover-class="app-tap-hover"
class="preview-image"
:src="faultPicture"
mode="aspectFill"
@click="previewFaultImage"
></image>
<view hover-class="app-tap-hover" class="delete-btn" @click="deleteFaultImage">
<u-icon name="close" color="#fff" size="14"></u-icon>
</view>
</view>
<view hover-class="app-tap-hover" class="upload-btn" v-else @click="uploadFaultImage">
<u-icon name="camera" color="#4C97E7" size="32"></u-icon>
<text class="upload-text">上传图片</text>
</view>
</view>
</view>
</view>
</view>
<view class="modal-footer">
<view hover-class="app-tap-hover" class="footer-btn cancel-btn" @click="closeChangeBike">取消</view>
<view hover-class="app-tap-hover" class="footer-btn confirm-btn" @click="confirmChangeBike">确定换车</view>
</view>
</view>
<view style="background-color: rgba(0, 0, 0, .5);width: 100%;height: 100vh;position: fixed;top: 0;left: 0;z-index: 999;"
v-if="showChangeBike"></view>
<qiniu-upload-progress />
</view>
</template>
<script>
const app = getApp()
var xBlufi = require("@/components/blufi/xBlufi.js")
import { ChannelApiType } from '@/common/enums/channel'
import { normalizeLocationLogList, toPolylinePointList } from '@/common/trackMapUtil.js'
import ZhimaNoDepositBadge from '@/components/zhima-no-deposit-badge/zhima-no-deposit-badge.vue'
let _this = null
export default {
components: { ZhimaNoDepositBadge },
data() {
return {
ChannelApiType,
tcindex:1,
yajinyajin:false,
huancheflag:false,
beizhu:'',
qxfei: 0,
ddfei: 0,
glfei: 0,
csfei:0,
yuanyin: '',
zengsongflag: false,
devicesList: [],
searching: false,
btnflag: true,
tishiflag: false,
option: '',
bluthlist: [], //蓝牙数组
statusflag: false,
Bluetoothmac: '',
mac: '',
ishave: false,
ver_data: null,
deviceInfoss: {},
gps: {},
isband: false,
deviceIds: '',
name: '',
orderinfo: {},
dl: 0,
czmoney: true,
iscz: true,
bgc: {
backgroundColor: "#fff",
},
show: true,
showgj: true,
searchKeyword: '11',
latitude: '',
longitude: '',
isMap: false,
zoomSize: 15,
markers: [], // 标记点数组
polyline: [], // 轨迹折线数组
polygon: [], // 用于存储区域多边形
cardId: '001区域',
sn: '',
deviceInfos: {},
carstause: false,
maskloading: false,
toploadtxt: "开锁中0%",
loadimg: 'https://lxnapi.ccttiot.com/bike/img/static/urJQJnOI1DEjWatFqHYh',
tiptxt: '请定点停放,规范用车',
maskepage: 0,
backgps: {},
buletxt: '',
flagdiaodu: false,
buleclose: false,
buleopen: false,
bulering: false,
bulerebort: false,
getnum: 0,
showvehicle: false,
vehicleNum: '',
showbtntip: false,
btnnum: null,
showqr: false,
canvasWidth: 300,
deptId: null,
showModelList: false,
ModelList: [],
jytxt: '开',
orid: '',
orderxqobj: {
distance: 0
},
tkflag: false,
refundConfirmFlag: false,
amount: '',
orderDeviceList: [],
suitSeconds: '',
currentPolyline: [],
trackPoints: [], // 存储轨迹点数据
tkje: true,
ddflag: false,
fuzhuobj: {},
price:0,
youhuiobj:{},
kstime:'',
endtime:'',
// 换车相关
showChangeBike: false,
changeDeviceList: [],
selectedDeviceId: '',
selectedDeviceSn: '',
changeReason: '', // LOW_POWER 或 DEVICE_FAULT
faultDetail: '',
faultPicture: '',
reasonOptions: [
{ label: '电量不足', value: 'LOW_POWER' },
{ label: '车辆损坏', value: 'DEVICE_FAULT' }
],
reasonIndex: -1,
qiniuToken: '', // 七牛云上传token
damageImageList: [], // 还车审核弹窗车损图(已上传完成的 URL
damageImageUploadingList: [], // 上传中:{ tempPath, uid }
/** 任一未完成操作时锁住底部与其它并行按钮;仅在解锁后方可新开链路(防抖多重请求 / 多窗) */
orderActionBusy: false,
verifyAuditBusy: false,
/** 改价确定 / 押金抵扣确定:防双击重复提交 */
priceSubmitting: false,
deductSubmitting: false,
changeBikeConfirming: false,
auxReturnBusy: false
}
},
onLoad(e) {
if(e.id){
this.sn = e.id
}
this.kstime = e.ksitem
this.endtime = e.endTime == null ? this.formatCurrentTime() : e.endTime
console.log(e);
this.orid = e.orid
this.getorderxq()
this.deptId = uni.getStorageSync('deptId')
this.getyouhui()
},
onUnload: function() {
xBlufi.listenDeviceMsgEvent(false, this.funListenDeviceMsgEvent)
xBlufi.notifyStartDiscoverBle({
'isStart': false
})
},
onHide() {
xBlufi.listenDeviceMsgEvent(false, this.funListenDeviceMsgEvent)
xBlufi.notifyStartDiscoverBle({
'isStart': false
})
},
onBeforeUnmount() {
xBlufi.listenDeviceMsgEvent(false, this.funListenDeviceMsgEvent)
xBlufi.notifyStartDiscoverBle({
'isStart': false
})
},
watch: {
},
computed: {
totalRefund() {
return (
Number(this.qxfei || 0) +
Number(this.ddfei || 0) +
Number(this.glfei || 0) +
Number(this.csfei || 0)
).toFixed(2)
},
formattedDuration() {
// 获取开始时间(必须存在)
const startTime = this.orderxqobj?.startTime ? new Date(this.orderxqobj.startTime) : null;
if (!startTime || isNaN(startTime.getTime())) return '00:00:00';
// 获取结束时间若为null则用当前时间
const endTime = this.orderxqobj?.endTime ?
new Date(this.orderxqobj.endTime) :
new Date();
if (isNaN(endTime.getTime())) return '00:00:00';
// 计算时间差(毫秒)
const diffMs = endTime - startTime;
if (diffMs < 0) return '00:00:00'; // 结束时间早于开始时间
// 转换为秒并格式化
const diffSeconds = Math.floor(diffMs / 1000);
return this.formatSecondsToHMS(diffSeconds);
},
// 使用计算属性来进行渲染换车
processedOrderDeviceList() {
return this.orderDeviceList.map(item => ({
...item,
pictures: item.finishPicture ? item.finishPicture.split(',') : []
}))
},
kmDistance() {
return (this.orderxqobj?.distance / 1000).toFixed(2);
},
formattedPayedAmount() {
// 默认值处理
const payedAmount = parseFloat(this.orderxqobj?.payedAmount) || 0;
const payRefunded = parseFloat(this.orderxqobj?.payRefunded) || 0;
const payRefunding = parseFloat(this.orderxqobj?.payRefunding) || 0;
// 计算实际金额
const actualAmount = payedAmount - (payRefunded + payRefunding);
// 确保是合法数字,并格式化为 ¥XX.XX
return `¥${isNaN(actualAmount) ? '0.00' : actualAmount.toFixed(2)}`;
},
showFooterBar() {
const o = this.orderxqobj
if (!o || !o.status) return false
const s = o.status
return (
s === 'WAIT_VERIFY' ||
s === 'FINISHED' ||
s === 'RIDE_WAIT_PAY' ||
s === 'PROCESSING' ||
o.priceChanged === true
)
},
orderDamageImages() {
const urls = this.orderxqobj && this.orderxqobj.damageImageUrls
if (!urls || typeof urls !== 'string') return []
return urls.split(',').map((x) => x.trim()).filter(Boolean)
},
hasVerifyRemark() {
const r = this.orderxqobj && this.orderxqobj.verifyRemark
if (r == null) return false
return String(r).trim().length > 0
},
/** 从订单详情接口解析应收保险费(兼容多字段名与字符串) */
orderInsureFeeParsed() {
const o = this.orderxqobj
if (!o) return null
const keys = [
'insureFee',
'actualInsureFee',
'insuranceFee',
'expectInsureFee',
'insure_fee',
'actual_insure_fee'
]
let raw
for (let i = 0; i < keys.length; i++) {
const v = o[keys[i]]
if (v !== undefined && v !== null && v !== '') {
raw = v
break
}
}
if (raw === undefined) return null
if (typeof raw === 'object' && raw !== null) {
const inner = raw.amount != null ? raw.amount : raw.value
if (inner !== undefined && inner !== null && inner !== '') raw = inner
else return null
}
let s = String(raw).replace(/,/g, '').trim()
s = s.replace(/^¥\s*/, '').replace(/\s*元\s*$/, '')
const n = parseFloat(s)
return Number.isFinite(n) ? n : null
},
/** 有有效金额时展示(非 0 */
showOrderInsureFee() {
const n = this.orderInsureFeeParsed
return n !== null && n !== 0
},
insureFeeDisplay() {
const n = this.orderInsureFeeParsed
return n !== null ? n.toFixed(2) : '0.00'
}
},
methods: {
toastIfOrderBusy() {
if (this.orderActionBusy) {
uni.showToast({ title: '操作进行中,请稍候', icon: 'none', duration: 1200 })
return true
}
return false
},
formatCurrentTimes() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始补0
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
getyouhui(){
let data = {
orderId:this.orid,
}
this.$u.post(`/app/order/calcRideFee`,data).then(res =>{
if(res.code == 200){
this.youhuiobj = res.data
}
})
},
btnqx() {
this.huancheflag = false
this.damageImageList = []
this.damageImageUploadingList = []
this.orderActionBusy = false
this.verifyAuditBusy = false
},
previewOrderDamage(current) {
uni.previewImage({
current,
urls: this.orderDamageImages
})
},
async chooseVerifyDamageImages() {
if (this.damageImageUploadingList.length > 0) {
uni.showToast({ title: '请等待当前图片上传完成', icon: 'none' })
return
}
const remain = 9 - this.damageImageList.length - this.damageImageUploadingList.length
if (remain <= 0) return
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
if (!res.tempFilePaths || !res.tempFilePaths.length) return
const paths = res.tempFilePaths
const uids = paths.map(() => String(this.$u.guid(16)))
paths.forEach((tempPath, i) => {
this.damageImageUploadingList.push({ tempPath, uid: uids[i] })
})
try {
const valid = []
for (let i = 0; i < paths.length; i++) {
const url = await this.uploadVerifyDamageFile(paths[i])
if (url) valid.push(url)
}
this.damageImageUploadingList = this.damageImageUploadingList.filter(
(x) => !uids.includes(x.uid)
)
this.damageImageList = this.damageImageList.concat(valid).slice(0, 9)
if (valid.length) {
uni.showToast({ title: '上传成功', icon: 'success' })
} else if (paths.length) {
uni.showToast({ title: '图片上传失败,请重试', icon: 'none' })
}
} catch (e) {
this.damageImageUploadingList = this.damageImageUploadingList.filter(
(x) => !uids.includes(x.uid)
)
uni.showToast({ title: '部分图片上传失败', icon: 'none' })
}
}
})
},
async uploadVerifyDamageFile(filePath) {
const math = 'static/' + this.$u.guid(20)
try {
return await this.$uploadQiniuFile({
filePath,
key: 'bike/img/' + math,
title: '上传车损图片',
showProgress: true
})
} catch (e) {
return ''
}
},
removeVerifyDamage(index) {
this.damageImageList.splice(index, 1)
},
previewVerifyDamage(index) {
uni.previewImage({
current: this.damageImageList[index],
urls: this.damageImageList
})
},
// 通过审核
btnqd(){
if (this.verifyAuditBusy) {
return
}
if (this.damageImageUploadingList.length > 0) {
uni.showToast({ title: '请等待图片上传完成', icon: 'none' })
return
}
this.verifyAuditBusy = true
uni.showLoading({
title:'加载中...'
})
let data = {
id:this.orderxqobj.id,
deductionFee:this.price,
remark:this.beizhu,
damageImageUrls: this.damageImageList.length ? this.damageImageList.join(',') : ''
}
this.$u.put('/bst/order/verify', data).then((res) => {
if (res.code === 200) {
uni.showToast({
title: '审核成功',
icon: 'success',
duration: 2000
})
this.huancheflag = false
this.price = 0
this.beizhu = ''
this.damageImageList = []
this.damageImageUploadingList = []
setTimeout(()=>{
this.deviceInfo()
this.getorderxq()
},1000)
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(() => {}).finally(() => {
uni.hideLoading()
this.verifyAuditBusy = false
this.orderActionBusy = false
})
},
btntcindex(num){
this.tcindex = num
},
btnjilu(){
if (this.toastIfOrderBusy()) return
uni.navigateTo({
url:'/page_user/gaijiajilu?id=' + this.orid
})
},
// 点击进行押金抵扣
btnyajin(){
if (this.toastIfOrderBusy()) return
this.orderActionBusy = true
this.yajinyajin = true
},
yjqxqx(){
this.yajinyajin = false
this.orderActionBusy = false
this.deductSubmitting = false
},
yjqdqd(){
if (this.deductSubmitting) return
this.deductSubmitting = true
let fee = this.youhuiobj
let data = {
id:this.orid,
fee
}
this.$u.put('/bst/order/deduct', data).then(res=>{
if(res.code == 200) {
uni.showToast({
title: '抵扣成功',
icon: 'success',
duration: 2000
})
this.yajinyajin = false
this.deviceInfo()
this.getorderxq()
}else{
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(() => {}).finally(() => {
this.deductSubmitting = false
this.orderActionBusy = false
})
},
// 点击进行改价
btngaijia() {
if (this.toastIfOrderBusy()) return
this.orderActionBusy = true
this.zengsongflag = true
},
// 点击确定改价
qdqd() {
if (this.priceSubmitting) return
this.priceSubmitting = true
let data = {
id: this.orid,
ridingFee: this.qxfei == '' ? this.orderxqobj.ridingFee : this.qxfei,
dispatchFee: this.ddfei == '' ? this.orderxqobj.dispatchFee : this.ddfei,
manageFee: this.glfei == '' ? this.orderxqobj.manageFee : this.glfei,
reason: this.yuanyin
}
this.$u.put('/bst/order/updatePrice', data).then((res) => {
if (res.code === 200) {
this.zengsongflag = false
uni.showToast({
title: '改价成功',
icon: 'success',
duration: 2000
})
this.qxfei = ''
this.ddfei = ''
this.glfei = ''
this.yuanyin = ''
this.deviceInfo()
this.getorderxq()
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(() => {}).finally(() => {
this.priceSubmitting = false
this.orderActionBusy = false
})
},
qxqx() {
this.zengsongflag = false
this.tkflag = false
this.refundConfirmFlag = false
this.orderActionBusy = false
this.priceSubmitting = false
this.qxfei = ''
this.ddfei = ''
this.glfei = ''
this.csfei = ''
this.yuanyin = ''
},
btntz(id) {
uni.navigateTo({
url: '/page_shanghu/guanli/device_detail?id=' + id
})
},
formatSecondsToHMS(seconds) {
const h = Math.floor(seconds / 3600).toString().padStart(2, '0');
const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
const s = Math.floor(seconds % 60).toString().padStart(2, '0');
return `${h}:${m}:${s}`;
},
formatDatess(startTime, endTime) {
if (!startTime) return "0秒"; // 如果没有开始时间返回0
// 解析时间无效时间则返回0
const start = new Date(startTime);
const ends = endTime == null ? this.formatCurrentTime() : new Date(endTime);
const end = new Date(ends)
console.log(ends, end, '02220');
if (isNaN(start.getTime()) || isNaN(end.getTime())) return "0秒";
// 计算时间差(毫秒)
let diffMs = end - start;
if (diffMs < 0) return "0秒"; // 结束时间早于开始时间
// 计算各时间单位
const diffDays = Math.floor(diffMs / 86400000); // 天
diffMs %= 86400000;
const diffHours = Math.floor(diffMs / 3600000); // 小时
diffMs %= 3600000;
const diffMinutes = Math.floor(diffMs / 60000); // 分钟
diffMs %= 60000;
const diffSeconds = Math.floor(diffMs / 1000); // 秒
// 智能拼接结果忽略0值单位但至少显示"X秒"
const parts = [];
if (diffDays > 0) parts.push(`${diffDays}天`);
if (diffHours > 0) parts.push(`${diffHours}小时`);
if (diffMinutes > 0) parts.push(`${diffMinutes}分钟`);
parts.push(`${diffSeconds}秒`); // 始终显示秒
return parts.join("");
},
formatCurrentTime() {
const now = new Date();
const padZero = num => num < 10 ? `0${num}` : num;
return `${now.getFullYear()}-${padZero(now.getMonth() + 1)}-${padZero(now.getDate())} ${padZero(now.getHours())}:${padZero(now.getMinutes())}:${padZero(now.getSeconds())}`;
},
updateTrackData() {
const formattedStartTime = this.orderxqobj.startTime;
const formattedEndTime = this.orderxqobj.endTime || this.formatCurrentTime();
// this.$u.get('/bst/locationLog/listByTime?sn=' + this.sn + '&startTime=' + formattedStartTime + '&endTime=' + formattedEndTime).then((res) => {
this.$u.get('/bst/locationLog/listAll?orderId=' + this.orid + '&timeRange=' + this.kstime + ',' + this.endtime + '&valid=true').then((res) => {
if (res.code === 200) {
if (!res.data || res.data.length === 0) {
uni.showToast({
title: '该时间段内无轨迹数据',
icon: 'none',
duration: 2000
});
return
}
// 清空之前的轨迹数据
this.polyline = []
this.markers = []
this.trackPoints = normalizeLocationLogList(res.data, (point) => ({
status: point.status,
onlineStatus: point.onlineStatus,
remainingPower: point.bat || 0,
}))
if (!this.trackPoints.length) {
uni.showToast({
title: '该时间段内无有效轨迹数据',
icon: 'none',
duration: 2000,
})
return
}
// 添加起点和终点标记
if (this.trackPoints.length > 0) {
const startPoint = this.trackPoints[0]
const endPoint = this.trackPoints[this.trackPoints.length - 1]
// 添加起点标记
this.markers.push({
id: 'start',
latitude: startPoint.latitude,
longitude: startPoint.longitude,
width: 22,
height: 32,
iconPath: 'https://api.ccttiot.com/smartmeter/img/static/uoORewceRbnD0jcNHhns'
})
// 添加终点标记
this.markers.push({
id: 'end',
latitude: endPoint.latitude,
longitude: endPoint.longitude,
width: 22,
height: 32,
iconPath: 'https://api.ccttiot.com/smartmeter/img/static/u5HZcdTVcyNGkHDUeT4h'
})
// 添加轨迹折线
this.polyline.push({
points: toPolylinePointList(this.trackPoints),
color: '#00AF99',
width: 4,
dottedLine: false,
arrowLine: true,
borderWidth: 2,
borderColor: '#ffffff'
})
// 更新地图显示范围
this.latitude = startPoint.latitude
this.longitude = startPoint.longitude
this.$nextTick(() => {
const ctx = uni.createMapContext('map', this)
if (ctx && typeof ctx.includePoints === 'function') {
ctx.includePoints({
points: toPolylinePointList(this.trackPoints),
padding: [80, 80, 80, 80],
})
}
})
}
}
})
},
btnflagxs() {
this.flagdiaodu = !this.flagdiaodu
},
unpass() {
if (this.toastIfOrderBusy()) return
this.orderActionBusy = true
this.damageImageList = []
this.getQiniuToken()
this.huancheflag = true
},
parsePictures(pictureString) {
console.log(pictureString, '45454545454');
if (!pictureString || typeof pictureString !== 'string') {
return []
}
return pictureString.split(',')
},
preview(img, pic) {
uni.previewImage({
current: img,
urls: this.parsePictures(pic.finishPicture)
})
},
getRefundOriginal(type) {
const keyMap = {
riding: 'actualReceivedRidingFee',
dispatch: 'actualReceivedDispatchFee',
manage: 'actualReceivedManageFee',
deduction: 'actualReceivedDeductionFee'
}
const v = Number(this.orderxqobj[keyMap[type]])
return isNaN(v) ? 0 : v
},
isRefundRowDisabled(type) {
return this.getRefundOriginal(type) <= 0
},
formatRefundMoney(val) {
const n = Number(val)
return (isNaN(n) ? 0 : n).toFixed(2)
},
resetRefundForm() {
this.qxfei = ''
this.ddfei = ''
this.glfei = ''
this.csfei = ''
this.yuanyin = ''
},
validateRefundForm() {
const rows = [
{ type: 'riding', val: this.qxfei, label: '骑行费' },
{ type: 'dispatch', val: this.ddfei, label: '调度费' },
{ type: 'manage', val: this.glfei, label: '管理费' },
{ type: 'deduction', val: this.csfei, label: '车损费' }
]
let hasRefund = false
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
const original = this.getRefundOriginal(row.type)
if (original <= 0) continue
const amount = Number(row.val || 0)
if (isNaN(amount) || amount < 0) {
uni.showToast({ title: row.label + '退款金额不正确', icon: 'none' })
return false
}
if (amount > original) {
uni.showToast({ title: row.label + '退款不能超过实收', icon: 'none' })
return false
}
if (amount > 0) hasRefund = true
}
if (!hasRefund) {
uni.showToast({ title: '请至少填写一项退款金额', icon: 'none' })
return false
}
return true
},
// 点击退款:先校验,再打开自定义确认弹窗
btntuikuan() {
if (!this.tkje) return
if (!this.validateRefundForm()) return
this.refundConfirmFlag = true
},
closeRefundConfirm() {
this.refundConfirmFlag = false
},
confirmRefundSubmit() {
this.refundConfirmFlag = false
this.submitRefundOrder()
},
submitRefundOrder() {
if (!this.tkje) return
this.tkje = false
uni.showLoading({
title: '退款中...',
mask: true
})
const data = {
orderId: this.orid,
ridingRefund: this.isRefundRowDisabled('riding') ? 0 : (this.qxfei === '' ? 0 : this.qxfei),
dispatchRefund: this.isRefundRowDisabled('dispatch') ? 0 : (this.ddfei === '' ? 0 : this.ddfei),
manageRefund: this.isRefundRowDisabled('manage') ? 0 : (this.glfei === '' ? 0 : this.glfei),
deductionRefund: this.isRefundRowDisabled('deduction') ? 0 : (this.csfei === '' ? 0 : this.csfei),
reason: this.yuanyin
}
this.$u.put(`/bst/order/refund`, data).then(res => {
if (res.code == 200) {
uni.showToast({
title: '退款成功',
icon: 'success',
duration: 2000
})
this.resetRefundForm()
this.refundConfirmFlag = false
this.tkflag = false
this.deviceInfo()
this.getorderxq()
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(() => {}).finally(() => {
uni.hideLoading()
this.tkje = true
this.orderActionBusy = false
})
},
btncha() {
this.ddflag = false
this.orderActionBusy = false
},
// 是否收取调度费
btnfuzhus(flag) {
if (this.auxReturnBusy) return
this.auxReturnBusy = true
uni.showLoading({
title: '还车中...',
mask: true
})
let data = {
orderId: this.orid,
requiredIot: false
}
this.$u.put(`/bst/order/finishDevice`, data).then(res =>{
if (res.code != 200) {
uni.showToast({
title: '错误' + res.msg,
icon: 'none',
duration: 2000
})
return Promise.reject(res)
}
let datas = {
orderId: this.orid,
needDispatchFee: flag
}
return this.$u.put(`/bst/order/end`, datas)
}).then((res2) => {
if (!res2) return
if (res2.code == 200) {
uni.showToast({
title: '辅助还车成功',
icon: 'success',
duration: 2000
})
this.ddflag = false
setTimeout(() => {
this.deviceInfo()
this.getorderxq()
}, 1500)
} else {
uni.showToast({
title: res2.msg,
icon: 'none',
duration: 2000
})
}
}).catch(() => {}).finally(() => {
uni.hideLoading()
this.auxReturnBusy = false
this.orderActionBusy = false
})
},
// 点击辅助换车
btnhuanbike() {
if (this.toastIfOrderBusy()) return
this.orderActionBusy = true
uni.showModal({
title: '提示',
content: '确定要进行辅助换车操作吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.doChangeBike()
} else {
this.orderActionBusy = false
}
},
fail: () => {
this.orderActionBusy = false
}
})
},
// 执行换车流程
doChangeBike() {
uni.showLoading({
title: '锁车中...',
mask: true
})
let data = {
orderId:this.orid,
requiredIot:false
}
this.$u.put('/bst/order/finishDevice',data).then((res) => {
if(res.code == 200){
uni.hideLoading()
this.getQiniuToken()
this.showChangeBike = true
}else{
uni.hideLoading()
uni.showToast({
title: '错误' + res.msg,
icon: 'none',
duration: 5000
})
this.orderActionBusy = false
}
}).catch((err) => {
uni.hideLoading()
this.getQiniuToken()
this.showChangeBike = true
})
},
// 获取七牛云上传token
getQiniuToken() {
this.$u.get('/common/qiniuToken').then((res) => {
if (res.code == 200) {
this.qiniuToken = res.data
}
}).catch((err) => {
console.log('获取七牛云token失败', err)
})
},
// 上传故障图片
uploadFaultImage() {
const _this = this
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success(res) {
const tempFilePaths = res.tempFilePaths[0]
const math = 'static/' + _this.$u.guid(20)
// 检查文件大小5MB
uni.getFileInfo({
filePath: tempFilePaths,
async success(fileInfo) {
if (fileInfo.size > 5 * 1024 * 1024) {
uni.showToast({
title: '图片大小不能超过5MB',
icon: 'none',
duration: 2000
})
return
}
try {
const url = await _this.$uploadQiniuFile({
filePath: tempFilePaths,
key: 'bike/img/' + math,
title: '上传故障图片',
showProgress: true
})
_this.faultPicture = url
uni.showToast({
title: '上传成功',
icon: 'success',
duration: 1500
})
} catch (e) {
uni.showToast({
title: '上传失败,请重试',
icon: 'none',
duration: 2000
})
}
}
})
},
fail(err) {
uni.showToast({
title: '未授权访问相册权限,请授权后使用',
icon: 'none',
duration: 2000
})
}
})
},
// 预览故障图片
previewFaultImage() {
if (this.faultPicture) {
uni.previewImage({
current: this.faultPicture,
urls: [this.faultPicture]
})
}
},
// 删除故障图片
deleteFaultImage() {
uni.showModal({
title: '提示',
content: '确定要删除这张图片吗?',
success: (res) => {
if (res.confirm) {
this.faultPicture = ''
}
}
})
},
// 扫码获取设备SN
scanDeviceSn() {
uni.scanCode({
onlyFromCamera: true,
scanType: ['qrCode'],
success: (res) => {
console.log('扫码结果:', res)
// 从二维码中提取SN
function getQueryParam(url, paramName) {
let regex = new RegExp(`[?&]${paramName}=([^&]*)`)
let results = regex.exec(url)
return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null
}
let sceneValue = res.result
let decodedValue = decodeURIComponent(sceneValue)
let sn = getQueryParam(decodedValue, 's') || getQueryParam(decodedValue, 'sn')
// 如果二维码中不包含参数,尝试直接使用结果
if (!sn) {
sn = res.result
}
if (sn) {
this.selectedDeviceSn = sn
uni.showToast({
title: '扫码成功',
icon: 'success',
duration: 1500
})
} else {
uni.showToast({
title: '未识别到设备SN',
icon: 'none',
duration: 2000
})
}
},
fail: (err) => {
console.error('扫码失败:', err)
uni.showToast({
title: '扫码失败,请重试',
icon: 'none',
duration: 2000
})
}
})
},
// 关闭换车弹窗
closeChangeBike() {
this.showChangeBike = false
this.selectedDeviceSn = ''
this.changeReason = ''
this.faultDetail = ''
this.faultPicture = ''
this.reasonIndex = -1
this.changeBikeConfirming = false
this.orderActionBusy = false
},
// 选择换车原因
onReasonChange(e) {
const index = e.detail.value
this.reasonIndex = index
this.changeReason = this.reasonOptions[index].value
},
// 确认换车
confirmChangeBike() {
if (this.changeBikeConfirming) return
if (!this.changeReason) {
uni.showToast({
title: '请选择换车原因',
icon: 'none',
duration: 2000
})
return
}
if (!this.selectedDeviceSn) {
uni.showToast({
title: '请输入目标设备SN',
icon: 'none',
duration: 2000
})
return
}
this.changeBikeConfirming = true
uni.showLoading({
title: '换车中...',
mask: true
})
let data = {
orderId: this.orid,
reason: this.changeReason,
deviceSn: this.selectedDeviceSn
}
if (this.changeReason === 'DEVICE_FAULT') {
if (this.faultDetail) {
data.faultDetail = this.faultDetail
}
if (this.faultPicture) {
data.faultPicture = this.faultPicture
}
}
this.$u.put('/bst/order/changeDevice', data).then((res) => {
if (res.code === 200) {
uni.showToast({
title: '换车成功',
icon: 'success',
duration: 2000
})
this.closeChangeBike()
setTimeout(() => {
this.getorderxq()
}, 1500)
} else {
uni.showToast({
title: res.msg || '换车失败',
icon: 'none',
duration: 2000
})
}
}).catch(() => {}).finally(() => {
uni.hideLoading()
this.changeBikeConfirming = false
this.orderActionBusy = false
})
},
/** 辅助还车:无调度费争议时,锁车 + 结束订单(与弹窗内「确定」逻辑一致) */
_finishAuxReturnNoDispatchDialog() {
uni.showLoading({
title: '锁车中...',
mask: true
})
const data = {
orderId: this.orid,
requiredIot: false
}
this.$u.put('/bst/order/finishDevice', data).then((res) => {
if (res.code != 200) {
uni.showToast({
title: '错误' + res.msg,
icon: 'none',
duration: 5000
})
return Promise.reject(res)
}
return this.$u.put(`/bst/order/end`, { orderId: this.orid })
}).then((res2) => {
if (!res2) return
if (res2.code == 200) {
uni.showToast({
title: '辅助还车成功',
icon: 'success',
duration: 2000
})
setTimeout(() => {
this.deviceInfo()
this.getorderxq()
}, 1500)
} else {
uni.showModal({
title: '提示',
content: res2.msg,
showCancel: false,
confirmText: '知道了',
success: function() {}
})
}
}).catch(() => {}).finally(() => {
uni.hideLoading()
this.orderActionBusy = false
})
},
// 点击辅助还车
btnfuzhu() {
if (this.toastIfOrderBusy()) return
this.orderActionBusy = true
let arr = this.devicesList.map(item => {
const name = item.name || ''
return name.slice(-12)
})
let data = {
orderId: this.orid,
checkLocation: false,
macList:arr
}
this.$u.post(`/app/order/calcFee`, data).then(res => {
if (res.code == 200) {
this.fuzhuobj = res.data
let manageFee = res.data.manageFee == null ? 0 : res.data.manageFee
let dispatchFee = res.data.dispatchFee == null ? 0 : res.data.dispatchFee
if (manageFee > 0 || dispatchFee > 0) {
this.ddflag = true
} else {
let that = this
uni.showModal({
title: '提示',
content: '您确定要进行辅助还车吗?',
showCancel: true,
success(resM) {
if (resM.confirm) {
that._finishAuxReturnNoDispatchDialog()
} else {
that.orderActionBusy = false
}
},
fail() {
that.orderActionBusy = false
}
})
}
} else {
this.orderActionBusy = false
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(() => {
this.orderActionBusy = false
})
},
// 转换成时分秒
formatSecondsToHMS(seconds) {
seconds = seconds || 0 // 处理空值
const h = Math.floor(seconds / 3600).toString().padStart(2, '0')
const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0')
const s = Math.floor(seconds % 60).toString().padStart(2, '0')
return `${h}:${m}:${s}`
},
checkbtn(num) {
if (num == 1) { //1为联系客户
uni.makePhoneCall({
phoneNumber: this.orderxqobj.userPhone,
success: function(res) {
console.log('拨打电话成功', res)
},
fail: function(err) {
console.error('拨打电话失败', err)
}
})
} else { //否则为退款
if (this.toastIfOrderBusy()) return
this.orderActionBusy = true
this.resetRefundForm()
this.tkflag = true
}
},
// 请求订单详情
getorderxq() {
this.$u.get(`/bst/order/${this.orid}?assembleOrderDeviceList=true`).then(res => {
if (res.code == 200) {
this.sn = res.data.deviceSn
this.deviceInfo()
this.orderxqobj = res.data
this.updateTrackData()
this.suitSeconds = res.data.suitSeconds == null ? '--' : Math.ceil(res.data.suitSeconds / 3600)
this.orderDeviceList = res.data.orderDeviceList
console.log(this.orderDeviceList, '12212121211124121')
}else if(res.code == 401){
this.getlogo()
}
})
},
// 静默登录
getlogo(){
let taht = this
wx.login({
success(res) {
if (res.code) {
let data = {
loginCode: res.code,
appId:taht.$store.state.appid
}
taht.$u.post('/wxLogin', data).then(res => {
if (res.code == 200) {
uni.setStorageSync('token', res.token)
taht.getorderxq()
taht.getyouhui()
}
})
}
},
})
},
// 静音
btnjingyin() {
if (this.jytxt == '开') {
let data = {
isSound: 0
}
this.$u.post(`/appVerify/device/mute/${this.sn}`).then(res => {
if (res.code == 200) {
this.jytxt = '关'
uni.showToast({
title: '关闭静音成功',
icon: 'success',
duration: 2000
})
}
})
} else {
let data = {
isSound: 1
}
this.$u.post(`/appVerify/device/mute/${this.sn}`).then(res => {
if (res.code == 200) {
this.jytxt = '开'
uni.showToast({
title: '开启静音成功',
icon: 'success',
duration: 2000
})
}
})
}
},
toTrack() {
let endTime = this.orderxqobj.endTime || this.formatCurrentTime()
console.log(endTime, '020202002');
uni.navigateTo({
url: '/page_shanghu/guanli/bike_track?id=' + this.deviceInfos.id + '&type=1' + '&qufen=123' +
'&startTime=' + this.orderxqobj.startTime + '&endTime=' + endTime + '&orid=' + this.orid
})
},
changeShwoList() {
console.log('diaoyongle')
this.showModelList = true
},
confirm(e) {
let modelId = e[0].value
let data = {
sn: this.sn,
modelId: modelId
}
this.$u.put('/bst/device', data).then((res) => {
if (res.code == 200) {
this.showModelList = false
this.deviceInfo()
uni.showToast({
title: '修改成功',
icon: 'none',
duration: 2000
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
},
getModelList() {
this.$u.get(`/bst/model/list?pageNum=1&pageSize=999`).then((res) => {
if (res.code == 200) {
this.ModelList = res.rows.map(item => ({
value: item.id,
label: item.name
}))
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
},
closeQr() {
this.showqr = false
},
generateQrcode() {
uni.navigateTo({
url: '/page_shanghu/guanli/Qrcode?sn=' + this.deviceInfos.sn
})
},
saveQrcode() {
uni.canvasToTempFilePath({
canvasId: 'qrcode',
x: -10, // 裁剪区域的 x
y: 0, // 裁剪区域的 y
width: 155, // 裁剪区域的宽度,包含边距
height: 157 + 15, // 裁剪区域的高度,包含边距
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
})
this.showqr = false
},
fail: (err) => {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
},
fail: (err) => {
uni.showToast({
title: '生成二维码失败',
icon: 'none'
})
}
})
},
closeshowtip() {
this.showbtntip = false
},
// 确定修改车牌号
putvehicle() {
let data = {
sn: this.sn,
vehicleNum: this.vehicleNum
}
this.$u.put('/bst/device', data).then((res) => {
if (res.code == 200) {
this.deviceInfo()
this.showvehicle = false
uni.showToast({
title: '修改成功',
icon: 'none',
duration: 2000
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
},
closemask() {
this.maskloading = false
},
statuss() {
if (this.deviceInfos.sn) {
if (this.deviceInfos.etOrders[0]) {
if (this.deviceInfos.etOrders[0].status == 0) {
return '预约中'
} else if (this.deviceInfos.etOrders[0].status == 1) {
return '取消预约'
} else if (this.deviceInfos.etOrders[0].status == 2) {
return '骑行中'
} else if (this.deviceInfos.etOrders[0].status == 3) {
return '骑行结束'
} else if (this.deviceInfos.etOrders[0].status == 4) {
return '订单完成'
}
}
}
},
closevehicle() {
this.showvehicle = false
},
bulebtn(num) {
if (num == 1) {
if (this.carstause) {
this.ring()
} else {
this.bulering = true
this.Binddevice()
}
} else if (num == 2) {
if (this.carstause) {
this.open()
} else {
this.buleopen = true
this.Binddevice()
}
} else if (num == 3) {
if (this.carstause) {
this.close()
} else {
this.buleclose = true
this.Binddevice()
}
} else if (num == 4) {
if (this.carstause) {
this.reboot()
} else {
this.bulerebort = true
this.Binddevice()
}
}
},
btn(num) {
this.showbtntip = false
let data = [this.sn]
if (num == 0) {
uni.showLoading({
title: '加载中...'
})
console.log('点击了....1');
this.$u.put('/bst/device/iot/unlock?sn=' + this.sn).then((res) => {
if (res.code == 200) {
// 处理接口返回的数据,将边界数据转换为地图组件需要的折线结构
this.deviceInfo()
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
})
} else {
uni.hideLoading()
this.bulebtn(2)
}
})
} else if (num == 1) {
uni.showLoading({
title: '加载中...'
})
console.log('点击了....2');
this.$u.put('/bst/device/iot/lock?sn=' + this.sn).then((res) => {
if (res.code == 200) {
// 处理接口返回的数据,将边界数据转换为地图组件需要的折线结构
this.deviceInfo()
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
});
} else {
uni.hideLoading()
this.bulebtn(3)
}
})
} else if (num == 2) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/disable', data).then((res) => {
if (res.code == 200) {
uni.showToast({
title: '解禁成功',
icon: 'success',
duration: 2000
});
// 处理接口返回的数据,将边界数据转换为地图组件需要的折线结构
this.deviceInfo()
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
} else if (num == 3) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/enable', data).then((res) => {
if (res.code == 200) {
// 处理接口返回的数据,将边界数据转换为地图组件需要的折线结构
uni.showToast({
title: '解禁成功',
icon: 'success',
duration: 2000
});
this.deviceInfo()
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
} else if (num == 4) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/iot/ring?sn=' + this.sn).then((res) => {
if (res.code == 200) {
uni.hideLoading()
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
});
} else {
uni.hideLoading()
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
});
this.bulebtn(1)
}
})
} else if (num == 5) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/in', data).then((res) => {
if (res.code == 200) {
uni.showToast({
title: '回仓成功',
icon: 'success',
duration: 2000
});
this.deviceInfo()
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
} else if (num == 6) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/out', data).then((res) => {
if (res.code == 200) {
this.deviceInfo()
uni.showToast({
title: '出仓成功',
icon: 'success',
duration: 2000
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
} else if (num == 7) {
this.showvehicle = true
} else if (num == 8) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/iot/reboot?sn=' + this.sn).then((res) => {
if (res.code == 200) {
this.deviceInfo()
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
} else if (num == 9) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/iot/unlockSeat?sn=' + this.sn).then((res) => {
if (res.code == 200) {
this.deviceInfo()
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
} else if (num == 10) {
uni.showLoading({
title: '加载中...'
})
this.$u.put('/bst/device/iot/refresh?id=' + this.sn).then((res) => {
if (res.code == 200) {
this.deviceInfo()
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
}
},
Binddevice() {
uni.getBluetoothAdapterState({
success: function(res) {
console.log('蓝牙状态:', res.available)
},
fail: function(res) {
console.log('获取蓝牙状态失败')
}
})
this.maskloading = true
this.devicesList = []
this.maskepage = 4
this.buletxt = '蓝牙连接中'
console.log("xBlufi", xBlufi.XMQTT_SYSTEM)
xBlufi.listenDeviceMsgEvent(true, this.funListenDeviceMsgEvent)
xBlufi.notifyStartDiscoverBle({
'isStart': true
})
// 两秒后停止蓝牙搜索
setTimeout(() => {
xBlufi.notifyStartDiscoverBle({
'isStart': false
})
setTimeout(() => {
if (this.devicesList.length == 0) {
this.maskepage = 5
} else {
let uniqueDevicesList = Array.from(new Set(this.devicesList))
// 将去重后的数组重新赋值给 this.devicesList
this.devicesList = uniqueDevicesList
console.log(this.devicesList, 'this.devicesListthis.devicesList')
let istrue = false
this.devicesList.forEach(device => {
// 从设备名称中提取 MAC 地址(假设 MAC 地址是设备名称的后6个字符
let macFromName = device.name.substring(device.name.length - 12)
console.log(macFromName, 'macFromNamemacFromName')
// 与 this.mac 进行比较
if (macFromName == this.mac) {
// 如果相同,则将 this.ishave 设置为 true
console.log(device)
this.deviceInfoss = device
istrue = true
this.createBLEConnection(device)
console.log('对比正确1111111111')
} else {
console.log('对比错误')
}
})
setTimeout(() => {
if (!istrue) {
this.buletxt = '蓝牙连接失败'
setTimeout(() => {
this.maskepage = 5
}, 500)
}
}, 500)
}
}, 200)
}, 5000)
},
reboot() {
let vm = this // 将外部的 this 绑定到 vm 上
uni.getNetworkType({
success(res) {
if (res.networkType !== 'none') {
uni.getConnectedBluetoothDevices({
success(res) {
console.log('已连接的蓝牙设备信息:', res)
xBlufi.notifySendCustomData({
customData: "11reboot"
})
vm.maskloading = false
},
fail(err) {
uni.hideLoading()
console.error('获取已连接蓝牙设备信息失败:', err)
}
})
} else {
console.log('手机未连接网络')
}
}
})
},
open() {
let vm = this // 将外部的 this 绑定到 vm 上
uni.getNetworkType({
success(res) {
if (res.networkType !== 'none') {
uni.getConnectedBluetoothDevices({
success(res) {
console.log('已连接的蓝牙设备信息:', res)
xBlufi.notifySendCustomData({
customData: "11open"
})
vm.maskloading = false
vm.deviceInfo()
},
fail(err) {
uni.hideLoading()
console.error('获取已连接蓝牙设备信息失败:', err)
}
})
} else {
console.log('手机未连接网络')
}
}
})
},
close() {
let vm = this // 将外部的 this 绑定到 vm 上
uni.getNetworkType({
success(res) {
if (res.networkType !== 'none') {
uni.getConnectedBluetoothDevices({
success(res) {
console.log('已连接的蓝牙设备信息:', res)
xBlufi.notifySendCustomData({
customData: "11close"
})
vm.maskloading = false
vm.deviceInfo()
},
fail(err) {
uni.hideLoading()
console.error('获取已连接蓝牙设备信息失败:', err)
}
})
} else {
console.log('手机未连接网络')
}
}
})
},
ring() {
let vm = this // 将外部的 this 绑定到 vm 上
uni.getNetworkType({
success(res) {
if (res.networkType !== 'none') {
uni.getConnectedBluetoothDevices({
success(res) {
console.log('已连接的蓝牙设备信息:', res)
xBlufi.notifySendCustomData({
customData: "11play1@"
})
vm.maskloading = false
vm.deviceInfo()
},
fail(err) {
uni.hideLoading()
console.error('获取已连接蓝牙设备信息失败:', err)
}
})
} else {
console.log('手机未连接网络')
}
}
})
},
funListenDeviceMsgEvent: function(options) {
switch (options.type) {
case xBlufi.XBLUFI_TYPE.TYPE_GET_DEVICE_LISTS:
if (options.result) {
let devicesarr = options.data
devicesarr.forEach(device => {
this.devicesList.push(device)
let uniqueDevicesList = Array.from(new Set(this.devicesList))
// 将去重后的数组重新赋值给 this.devicesList
this.devicesList = uniqueDevicesList
})
}
break;
case xBlufi.XBLUFI_TYPE.TYPE_CONNECTED:
console.log("连接回调:" + JSON.stringify(options))
if (options.result) {
xBlufi.notifyInitBleEsp32({
deviceId: options.data.deviceId
})
let systemInfo = uni.getSystemInfoSync()
if (systemInfo.platform === 'android') {
// 当前设备是 Android
} else if (systemInfo.platform === 'ios') {
// 当前设备是 iOS
}
}
if (options.result == false) {
this.carstause = false
if (this.maskepage == 4) {
this.buletxt = '设备连接失败'
setTimeout(() => {
this.maskepage = 5
}, 800)
}
}
break;
case xBlufi.XBLUFI_TYPE.TYPE_RECIEVE_CUSTON_DATA:
if (options.data) {
this.carstause = true
if (this.maskloading && this.maskepage == 4) {
if (this.buleclose == false && this.buleopen == false && this.bulerebort == false &&
this.bulering == false) {
if (this.buletxt == '蓝牙连接中') {
this.buletxt = '蓝牙连接成功!'
}
}
if (this.buleclose) {
this.buleclose = false
this.close()
}
if (this.buleopen) {
this.buleopen = false
this.open()
}
if (this.bulerebort) {
this.buleopen = false
this.reboot()
}
if (this.bulering) {
this.bulering = false
this.ring()
}
}
} else {
this.carstause = false
if (this.maskepage == 4) {
this.buletxt = '设备连接失败'
setTimeout(() => {
this.maskepage = 5
}, 800)
}
}
console.log("1收到设备发来的自定义数据结果", options.data)
break;
case xBlufi.XBLUFI_TYPE.TYPE_STATUS_CONNECTED: {
console.log('状态', options.result)
if (options.result == false) {
this.carstause = false
if (this.maskepage == 4) {
this.buletxt = '设备连接失败'
setTimeout(() => {
this.maskepage = 5
}, 800)
}
}
}
break;
case xBlufi.XBLUFI_TYPE.TYPE_GET_DEVICE_LISTS_STOP:
if (options.result) {
let uniqueDevicesList = Array.from(new Set(this.devicesList))
// 过滤出名称字段的前五个字母为 "CTPOW" 的项
let filteredDevices = uniqueDevicesList.filter(device => device.name.substring(0, 4) ===
"BBLE")
// 将过滤后的数组重新赋值给 this.devicesList
this.devicesList = filteredDevices
}
this.searching = false
break
}
},
btnyc() {
this.titleflag = false
},
// 解析自定义数据
//4、建立连接
createBLEConnection(e) {
console.log('调用了')
xBlufi.notifyStartDiscoverBle({
'isStart': false
});
console.log(e, '蓝牙信息')
const deviceId = e.deviceId
let name = e.name
console.log('点击了蓝牙准备连接的deviceId:' + e.deviceId)
xBlufi.notifyConnectBle({
isStart: true,
deviceId: e.deviceId,
name
})
},
funListenDeviceMsgEvents: function(options) {
let that = this
switch (options.type) {
case xBlufi.XBLUFI_TYPE.TYPE_STATUS_CONNECTED: {
console.log('状态', options.result)
if (options.result == false) {
this.carstause = false
uni.showToast({
title: '设备断开链接,请重新点击蓝牙链接',
icon: 'none'
})
uni.hideLoading()
}
}
break;
case xBlufi.XBLUFI_TYPE.TYPE_RECIEVE_MY_DATA:
this.loadPercent = options.data
this.loadText = '文件读取中'
console.log("文件读取中", options.data)
break;
case xBlufi.XBLUFI_TYPE.TYPE_RECIEVE_CUSTON_DATA:
console.log("1收到设备发来的自定义数据结果", options.data)
if (options.data) {
this.carstause = true
}
break
case xBlufi.XBLUFI_TYPE.TYPE_INIT_ESP32_RESULT:
uni.hideLoading()
if (options.result) {
console.log('初始化成功')
} else {
console.log('初始化失败')
}
break
}
},
// 点击隐藏没有设备提示
btnhd() {
this.tishiflag = false
},
status() {
if (this.deviceInfos.status == 0) {
return '仓库中'
} else if (this.deviceInfos.status == 1) {
return '待租'
} else if (this.deviceInfos.status == 2) {
return '预约中'
} else if (this.deviceInfos.status == 3) {
return '骑行中'
} else if (this.deviceInfos.status == 4) {
return '临时锁车中'
} else if (this.deviceInfos.status == 6) {
return '调度中'
} else if (this.deviceInfos.status == 8) {
return '下线'
}
},
deviceInfo() {
this.markers = [];
this.polygon = []; // 初始化polygon数组
this.$u.get(`/bst/device?sn=${this.sn}&supportLocation=true`).then((res) => {
console.log(res, 'rererer');
if (res.code === 200) {
this.deviceInfos = res.data;
if (res.data.isSound == 0) {
this.jytxt = '关';
} else {
this.jytxt = '开';
}
this.vehicleNum = res.vehicleNum;
this.mac = res.data.mac;
this.getModelList();
this.latitude = this.deviceInfos.latitude;
this.longitude = this.deviceInfos.longitude;
// 确保在获取设备信息后立即获取区域信息
if (this.deviceInfos.areaId) {
this.getArea();
}
if (this.getnum == 0) {
this.getArea()
}
this.getnum = 1
if (this.deviceInfos.status == 0) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
// iconPath: 'https://lxnapi.ccttiot.com/bike/img/static/u6jBvj7S50FPgsHaHXai',
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/uQRng4QNKA38Amk8Wgt5' :
'https://lxnapi.ccttiot.com/bike/img/static/uocjFo8Ar2BJVpzC2G2f',
callout: {
content: this.deviceInfos.remainingPower + '%', // 修改为你想要显示的文字内容
color: '#ffffff', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#000000', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
} else if (this.deviceInfos.status == 1) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/uzhMeExOQJbMcZtrfGUV' :
'https://lxnapi.ccttiot.com/bike/img/static/uheL17wVZn24BwCwEztT',
callout: {
content: this.deviceInfos.remainingPower + '%', // 修改为你想要显示的文字内容
color: '#2679D1', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#D4ECFF', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
} else if (this.deviceInfos.status == 2) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/uR3DQEssiK62ovhh88y8' :
'https://lxnapi.ccttiot.com/bike/img/static/u460R1NKWHEpHbt0U4H7',
callout: {
content: this.deviceInfos.remainingPower + '%', // 修改为你想要显示的文字内容
color: '#2679D1', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#D4ECFF', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
} else if (this.deviceInfos.status == 3) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/uG13E7BpUFF44wVYC9no' :
'https://lxnapi.ccttiot.com/bike/img/static/uHQIdWCTmtUztl49wBKU',
callout: {
content: this.deviceInfos.remainingPower + '%', // 修改为你想要显示的文字内容
color: '#2679D1', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#D4ECFF', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
} else if (this.deviceInfos.status == 4) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
// joinCluster: true,
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/uRod2zf3t9dAOYafWoWt' :
'https://lxnapi.ccttiot.com/bike/img/static/uZpXq3TBtM5gVgJJeImY',
callout: {
content: this.deviceInfos.remainingPower + '%', // 修改为你想要显示的文字内容
color: '#2679D1', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#D4ECFF', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
} else if (this.deviceInfos.status == 6) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/uhZudZM3nEKj0tYKlho2' :
'https://lxnapi.ccttiot.com/bike/img/static/ujur6TezvPf4buFAqPHo',
callout: {
content: '' + this.deviceInfos.remainingPower + '%',
color: '#2679D1', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#D4ECFF', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
} else if (this.deviceInfos.status == 8) {
this.markers.push({
id: parseFloat(this.deviceInfos.sn),
latitude: this.deviceInfos.latitude,
longitude: this.deviceInfos.longitude,
// title: item.deviceName,
width: 40,
height: 47,
iconPath: this.deviceInfos.onlineStatus == 0 ?
'https://lxnapi.ccttiot.com/bike/img/static/ucBKG3ebYRAToVweJihu' :
'https://lxnapi.ccttiot.com/bike/img/static/uyK7Vg4Lu8xb3oNVuG2l',
callout: {
content: this.deviceInfos.remainingPower + '%', // 修改为你想要显示的文字内容
color: '#ffffff', // 修改为文字颜色
fontSize: 10, // 修改为文字大小
borderRadius: 10, // 修改为气泡圆角大小
bgColor: '#000000', // 修改为气泡背景颜色
padding: 2, // 修改为气泡内边距
display: 'ALWAYS', // 修改为气泡的显示策略
}
})
}
this.$forceUpdate()
} else {
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
})
},
convertBoundaryToPolyline(boundary) {
if (!boundary) return null;
try {
const points = JSON.parse(boundary).map(coord => ({
latitude: coord[1],
longitude: coord[0]
}));
return {
points: points,
color: "#22FF00",
width: 2,
dottedLine: false,
arrowLine: false,
borderWidth: 2,
borderColor: "#22FF00"
};
} catch (error) {
console.error("Error converting boundary to polyline:", error);
return null;
}
},
convertBoundaryToPolylines(boundaries, num) {
if (!boundaries || !Array.isArray(boundaries)) return [];
return boundaries.map(boundary => {
if (!boundary) return null;
try {
const coords = JSON.parse(boundary);
if (!Array.isArray(coords)) return null;
const points = coords.map(coord => ({
latitude: coord[1],
longitude: coord[0]
}))
let style = {};
if (num === 1) {
style = {
color: "#3A7EDB",
width: 2,
dottedLine: false,
arrowLine: false,
borderWidth: 2,
borderColor: "#3A7EDB"
};
} else if (num === 2) {
style = {
color: "#FF473E",
width: 2,
dottedLine: false,
arrowLine: false,
borderWidth: 2,
borderColor: "#FF473E"
};
} else if (num === 3) {
style = {
color: "#FFC107",
width: 2,
dottedLine: false,
arrowLine: false,
borderWidth: 2,
borderColor: "#FFC107"
};
}
return {
points: points,
...style
};
} catch (error) {
console.error("Error converting boundary to polylines:", error);
return null;
}
}).filter(polyline => polyline !== null);
},
toggleIconAndCallout() {
this.showIconAndCallout = !this.showIconAndCallout
if (this.showIconAndCallout) {
const newMarkers = []
this.parkingList.forEach(item => {
newMarkers.push({
id: parseFloat(item.id),
latitude: parseFloat(item.latitude),
longitude: parseFloat(item.longitude),
width: 20,
height: 28.95,
iconPath: item.type == 1 ?
'https://lxnapi.ccttiot.com/bike/img/static/up2xXqAgwCX5iER600k3' : item.type == 2 ?
'https://lxnapi.ccttiot.com/bike/img/static/uDNY5Q4zOiZTCBTA2Jdq' :
'https://lxnapi.ccttiot.com/bike/img/static/u53BAQcFIX3vxsCzEZ7t',
callout: {
content: item.name,
color: '#ffffff',
fontSize: 14,
borderRadius: 10,
bgColor: item.type == 1 ? '#3A7EDB' : item.type == 2 ? '#FF473E' : '#FFC107',
padding: 6,
display: 'ALWAYS'
},
isCalloutVisible: true // 添加标记
})
})
this.$set(this, 'markers', [...this.markers, ...newMarkers])
} else {
// 过滤掉所有气泡显示的标记
this.$set(this, 'markers', this.markers.filter(marker => !marker.isCalloutVisible))
}
},
convertBoundaryToPolygon(boundary) {
if (!boundary) return null;
try {
const points = JSON.parse(boundary).map(coord => ({
latitude: coord[1],
longitude: coord[0]
}));
return {
points: points,
fillColor: "#55888840",
strokeColor: "#22FF0080",
strokeWidth: 1,
zIndex: 1
};
} catch (error) {
console.error("Error converting boundary to polygon:", error);
return null;
}
},
convertBoundaryToPolygons(boundaries, num) {
if (!boundaries || !boundaries.length) return [];
const colors = {
1: {
fill: "#3A7EDB40",
stroke: "#3A7EDB"
},
2: {
fill: "#FFF5D640",
stroke: "#FFC107"
},
3: {
fill: "#FFE0E040",
stroke: "#FF473E"
}
};
return boundaries.map(boundary => {
if (!boundary) return null
try {
const coords = JSON.parse(boundary)
if (!Array.isArray(coords)) return null
const points = coords.map(coord => ({
latitude: coord[1],
longitude: coord[0]
}))
return {
points: points,
fillColor: colors[num].fill,
strokeColor: colors[num].stroke,
strokeWidth: 1,
zIndex: 1
}
} catch (error) {
console.error("Error converting boundary to polygons:", error)
return null
}
}).filter(Boolean)
},
getParking() {
let data = {
areaId: this.deviceInfos.areaId
};
this.$u.get('/bst/areaSub/listByAreaId', data).then((res) => {
if (res.code === 200) {
const filteredData = res.data.filter(item => item.status != 1)
const type1Data = []
const type2Data = []
const type3Data = []
filteredData.forEach(row => {
if (row.type == 1) type1Data.push(row)
else if (row.type == 2) type2Data.push(row)
else if (row.type == 3) type3Data.push(row)
})
// 处理 type1 数据
const validBoundaries = type1Data.map(row => row.boundaryStr).filter(boundary =>
typeof boundary === 'string' && boundary.trim() !== '')
const polygons = this.convertBoundaryToPolygons(validBoundaries, 1)
if (polygons && polygons.length > 0) {
this.polygon = this.polygon.concat(polygons)
}
// 处理 type2 数据
const validBoundaries1 = type2Data.map(row => row.boundaryStr).filter(boundary =>
typeof boundary === 'string' && boundary.trim() !== '')
const polygons1 = this.convertBoundaryToPolygons(validBoundaries1, 2)
if (polygons1 && polygons1.length > 0) {
this.polygon = this.polygon.concat(polygons1)
}
// 处理 type3 数据
const validBoundaries2 = type3Data.map(row => row.boundaryStr).filter(boundary =>
typeof boundary === 'string' && boundary.trim() !== '');
const polygons2 = this.convertBoundaryToPolygons(validBoundaries2, 3);
if (polygons2 && polygons2.length > 0) {
this.polygon = this.polygon.concat(polygons2);
}
this.parkingList = filteredData;
this.$forceUpdate();
}
}).catch(error => {
console.error("Error fetching parking data:", error);
});
},
getArea() {
let id = this.deviceInfos.areaId;
if (!id) {
console.error("Area ID is missing");
return;
}
this.$u.get(`/bst/area/${id}`).then((res) => {
if (res.code === 200 && res.data && res.data.boundaryStr) {
const polygon = this.convertBoundaryToPolygon(res.data.boundaryStr);
if (polygon) {
this.polygon = [polygon];
console.log('Area boundary added:', polygon);
this.getParking();
}
}
}).catch(error => {
console.error("Error fetching area data:", error);
});
},
}
}
</script>
<style lang="scss">
.page-container {
background-color: #F6F8FB;
min-height: 100vh;
padding-bottom: 200rpx;
box-sizing: border-box;
}
.diaodu {
position: fixed;
width: 560rpx;
background-color: #fff;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 24rpx;
overflow: hidden;
z-index: 999;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
.top {
width: 100%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 30rpx;
box-sizing: border-box;
font-size: 40rpx;
color: #999;
}
.cen {
font-size: 32rpx;
color: #333;
padding: 0 40rpx 40rpx;
box-sizing: border-box;
text-align: center;
line-height: 1.5;
}
.anniu {
display: flex;
height: 100rpx;
border-top: 1px solid #f5f5f5;
view {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.bu {
color: #666;
background: #f8f8f8;
}
.shou {
color: #fff;
background: #4C97E7;
}
}
}
.map-wrapper {
width: 100%;
height: 400rpx;
position: relative;
.map {
width: 100%;
height: 100%;
}
.map-ctrl-btn {
position: absolute;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
// background: #fff;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
.img {
width: 100%;
height: 100%;
}
&.park-btn {
right: 30rpx;
bottom: 30rpx;
}
&.track-btn {
left: 30rpx;
bottom: 30rpx;
}
}
}
.info-card {
background: #FFFFFF;
border-radius: 20rpx;
margin: 24rpx;
padding: 30rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
position: relative;
overflow: hidden;
&.overview-card {
// z-index: 20;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.order-no-box {
font-size: 26rpx;
color: #666;
.value {
color: #333;
font-weight: 500;
margin-left: 10rpx;
}
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
&.processing { background: rgba(58, 126, 219, 0.1); color: #3A7EDB; }
&.finished { background: rgba(100, 182, 168, 0.1); color: #64B6A8; }
&.canceled { background: #F3F3F3; color: #999; }
}
}
.data-grid {
display: flex;
justify-content: space-around;
text-align: center;
margin-bottom: 30rpx;
.data-item {
display: flex;
flex-direction: column;
align-items: center;
.data-val {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
&.price {
color: #FF7310;
font-size: 36rpx;
}
.unit {
font-size: 24rpx;
font-weight: 400;
margin-left: 4rpx;
}
}
.data-label {
font-size: 24rpx;
color: #999;
}
}
}
.divider {
height: 1rpx;
background: #F3F3F3;
margin: 20rpx 0;
}
.detail-list, .fee-list {
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
font-size: 28rpx;
color: #333;
.label {
color: #909399;
}
.value {
font-weight: 500;
}
.highlight {
color: #3A7EDB;
}
.text-red {
color: #FF4444;
}
&.border-dashed {
border-bottom: 1rpx dashed #eee;
margin-bottom: 10rpx;
}
&.sm {
font-size: 26rpx;
padding: 8rpx 0;
}
.value-row {
display: flex;
align-items: center;
.phone-link {
color: #3A7EDB;
margin-left: 10rpx;
}
}
}
.damage-display-block {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx dashed #eee;
}
.damage-display-title {
display: block;
font-size: 26rpx;
color: #909399;
margin-bottom: 16rpx;
}
.damage-display-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.damage-display-thumb {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #f5f7fa;
}
.verify-remark-block {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx dashed #eee;
}
.verify-remark-title {
display: block;
font-size: 26rpx;
color: #909399;
margin-bottom: 12rpx;
}
.verify-remark-text {
display: block;
font-size: 28rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
}
}
.card-title-bar {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.bar-mark {
width: 6rpx;
height: 30rpx;
background: #4C97E7;
border-radius: 4rpx;
margin-right: 16rpx;
}
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.summary-section {
background: #FAFAFA;
padding: 20rpx;
border-radius: 12rpx;
margin-top: 10rpx;
}
.total-pay {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 24rpx;
font-size: 30rpx;
.label {
margin-right: 16rpx;
}
.price {
color: #FF7310;
font-size: 40rpx;
font-weight: bold;
}
}
.vehicle-card {
.vehicle-list {
.vehicle-item {
background: linear-gradient(135deg, #FFFFFF 0%, #F8FAFC 100%);
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 24rpx;
border: 1rpx solid #F0F2F5;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.02);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.05);
}
&:last-child {
margin-bottom: 0;
}
}
.vehicle-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #F0F2F5;
margin-bottom: 24rpx;
.vehicle-header-left {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20rpx;
}
.vehicle-badge {
display: flex;
align-items: center;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
.badge-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.badge-text {
font-weight: 500;
}
&.badge-primary {
background: linear-gradient(135deg, #E6F3FF 0%, #D4E9FF 100%);
color: #3A7EDB;
.badge-dot {
background: #3A7EDB;
}
}
&.badge-secondary {
background: linear-gradient(135deg, #FFF5E6 0%, #FFEED4 100%);
color: #FF7310;
.badge-dot {
background: #FF7310;
}
}
}
.vehicle-time-info {
display: flex;
flex-direction: column;
.time-label {
font-size: 22rpx;
color: #909399;
margin-bottom: 4rpx;
}
.time-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.vehicle-arrow {
display: flex;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: #F5F7FA;
}
}
.vehicle-details {
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
margin-bottom: 24rpx;
.detail-item {
display: flex;
flex-direction: column;
padding: 16rpx;
background: #FAFBFC;
border-radius: 12rpx;
border: 1rpx solid #F0F2F5;
.detail-label {
font-size: 22rpx;
color: #909399;
margin-bottom: 8rpx;
}
.detail-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
word-break: break-all;
&.highlight {
color: #3A7EDB;
font-weight: 600;
}
}
}
}
.time-info-section {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 20rpx;
background: linear-gradient(135deg, #F8FAFC 0%, #F0F4F8 100%);
border-radius: 12rpx;
.time-item {
display: flex;
align-items: flex-start;
.time-icon {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
&.start {
background: linear-gradient(135deg, #E6F3FF 0%, #D4E9FF 100%);
&::before {
content: '';
width: 0;
height: 0;
border-left: 6rpx solid #3A7EDB;
border-top: 4rpx solid transparent;
border-bottom: 4rpx solid transparent;
}
}
&.end {
background: linear-gradient(135deg, #FFE6E6 0%, #FFD4D4 100%);
&::before {
content: '';
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #FF4444;
}
}
}
.time-content {
flex: 1;
.time-title {
display: block;
font-size: 24rpx;
color: #909399;
margin-bottom: 6rpx;
}
.time-desc {
display: block;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
}
}
.media-section {
margin-top: 24rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
.media-item {
background: #FAFBFC;
border-radius: 12rpx;
padding: 20rpx;
border: 1rpx solid #F0F2F5;
.media-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.media-icon {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
margin-right: 12rpx;
&.start-icon {
background: linear-gradient(135deg, #E6F3FF 0%, #D4E9FF 100%);
&::before {
content: '';
width: 0;
height: 0;
border-left: 5rpx solid #3A7EDB;
border-top: 3rpx solid transparent;
border-bottom: 3rpx solid transparent;
display: block;
margin: 13rpx auto;
}
}
&.end-icon {
background: linear-gradient(135deg, #FFE6E6 0%, #FFD4D4 100%);
&::before {
content: '';
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #FF4444;
display: block;
margin: 11rpx auto;
}
}
}
.media-title {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.media-wrapper {
width: 100%;
border-radius: 8rpx;
overflow: hidden;
background: #000;
.media-video {
width: 100%;
height: 400rpx;
display: block;
}
}
}
}
}
}
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
padding: 20rpx 30rpx 40rpx; // 适配 iPhone X 底部安全区
box-sizing: border-box;
z-index: 100;
.btn-group {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
&.btn-group--audit {
justify-content: stretch;
.btn-audit {
margin-left: 0;
flex: 1;
width: 100%;
}
}
.btn {
margin-left: 20rpx;
margin-bottom: 10rpx;
padding: 0 40rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
text-align: center;
&.btn-primary {
background: linear-gradient(90deg, #4C97E7 0%, #3A7EDB 100%);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(58, 126, 219, 0.3);
}
&.btn-outline {
background: #fff;
color: #666;
border: 1rpx solid #ddd;
}
&.btn-audit {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #4c97e7, #6ab0ff);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(100, 182, 168, 0.3);
}
}
}
}
/* 弹窗样式保留优化 */
.tip_box {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 600rpx;
background: #FFFFFF;
border-radius: 20rpx;
z-index: 110;
overflow: hidden;
.top {
padding: 40rpx;
text-align: center;
.tip { font-size: 34rpx; font-weight: 600; margin-bottom: 30rpx; }
.ipt_box {
display: flex;
align-items: center;
background: #F5F7FA;
padding: 10rpx 20rpx;
border-radius: 10rpx;
.text { width: 120rpx; font-size: 28rpx; }
.ipt { flex: 1; input { width: 100%; } }
}
}
.bots {
display: flex;
border-top: 1rpx solid #eee;
.bot_left, .bot_right {
flex: 1;
height: 90rpx;
line-height: 90rpx;
text-align: center;
font-size: 32rpx;
}
.bot_left { color: #666; background: #fff; }
.bot_right { color: #fff; background: #4C97E7; }
}
}
.zengsongone, .yajindikou, .zengsong {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 650rpx;
background: #fff;
border-radius: 20rpx;
z-index: 200;
padding: 40rpx;
box-sizing: border-box;
z-index: 1000;
.tops { font-size: 36rpx; font-weight: 600; text-align: center; margin-bottom: 40rpx; }
input {
width: 100%;
height: 80rpx;
background: #F5F7FA;
border-radius: 10rpx;
padding: 0 20rpx;
margin-bottom: 20rpx;
}
.gai {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
font-size: 28rpx;
/* 左侧文案退款原因四字勿被压窄换行 */
> view:first-child {
flex-shrink: 0;
white-space: nowrap;
min-width: 140rpx;
padding-right: 12rpx;
box-sizing: border-box;
}
input.gai-full-input {
flex: 1;
min-width: 0;
width: auto !important;
}
input { width: 200rpx; margin-bottom: 0; }
picker {
flex: 1;
.picker-view {
width: 200rpx;
height: 80rpx;
line-height: 80rpx;
background: #F5F7FA;
border-radius: 10rpx;
padding: 0 20rpx;
text-align: right;
font-size: 28rpx;
color: #333;
}
}
}
.anniu {
display: flex;
justify-content: space-between;
margin-top: 40rpx;
.qx, .qd {
width: 45%;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 40rpx;
font-size: 32rpx;
}
.qx { background: #F5F7FA; color: #666; }
.qd { background: #4C97E7; color: #fff; }
}
}
.refund-modal {
width: 680rpx;
padding: 36rpx 32rpx 32rpx;
.tops { display: none; }
input {
width: auto;
height: auto;
background: transparent;
border-radius: 0;
padding: 0;
margin-bottom: 0;
}
.refund-modal__header {
display: flex;
align-items: flex-start;
margin-bottom: 28rpx;
}
.refund-modal__badge {
width: 72rpx;
height: 72rpx;
line-height: 72rpx;
text-align: center;
border-radius: 16rpx;
background: linear-gradient(135deg, #ff6b6b, #ff4444);
color: #fff;
font-size: 36rpx;
font-weight: 700;
flex-shrink: 0;
margin-right: 20rpx;
}
.refund-modal__head-text {
flex: 1;
min-width: 0;
}
.refund-modal__title {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #1a1a1a;
line-height: 1.3;
}
.refund-modal__desc {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #888;
line-height: 1.5;
}
.refund-table__head,
.refund-table__row {
display: flex;
align-items: center;
padding: 0 16rpx;
box-sizing: border-box;
}
.refund-table__head {
height: 56rpx;
background: #f0f4f8;
border-radius: 10rpx;
margin-bottom: 12rpx;
font-size: 22rpx;
color: #666;
font-weight: 600;
}
.refund-table__row {
min-height: 88rpx;
margin-bottom: 12rpx;
border-radius: 12rpx;
background: #f8fafc;
border: 1rpx solid #e8edf2;
}
.refund-table__row--disabled {
background: #ececec;
border-color: #e0e0e0;
.col-name,
.col-original,
.col-refund-placeholder {
color: #aaa;
}
.col-original {
// background: #e4e4e4;
}
}
.col-name {
width: 120rpx;
flex-shrink: 0;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.col-original {
width: 140rpx;
flex-shrink: 0;
height: 64rpx;
line-height: 64rpx;
text-align: center;
font-size: 28rpx;
color: #666;
// background: #eef1f5;
border-radius: 8rpx;
margin-right: 100rpx;
}
.col-refund {
flex: 1;
text-align: right;
}
.col-refund-input {
flex: 1;
min-width: 0;
height: 64rpx;
line-height: 64rpx;
padding: 0 16rpx;
font-size: 30rpx;
font-weight: 600;
color: #ff4444;
background: #fff;
border: 2rpx solid #ffb4b4;
border-radius: 8rpx;
box-sizing: border-box;
}
.col-refund-placeholder {
flex: 1;
text-align: center;
font-size: 32rpx;
color: #bbb;
}
.refund-reason {
margin-top: 20rpx;
padding: 20rpx 16rpx;
background: #f8fafc;
border-radius: 12rpx;
border: 1rpx solid #e8edf2;
}
.refund-reason__label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
}
.refund-reason__input {
width: 100%;
height: 72rpx;
padding: 0 20rpx;
font-size: 28rpx;
background: #fff;
border: 1rpx solid #dde3ea;
border-radius: 8rpx;
box-sizing: border-box;
}
.refund-total {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24rpx;
padding: 20rpx 24rpx;
background: #fff5f5;
border-radius: 12rpx;
border: 1rpx solid #ffd4d4;
}
.refund-total__label {
font-size: 28rpx;
color: #666;
}
.refund-total__amount {
font-size: 40rpx;
font-weight: 700;
color: #ff4444;
}
.refund-modal__confirm {
background: #ff4444 !important;
}
.anniu {
margin-top: 32rpx;
}
}
.refund-confirm-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.55);
z-index: 1100;
}
.refund-confirm-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 580rpx;
background: #fff;
border-radius: 24rpx;
z-index: 1101;
padding: 48rpx 40rpx 36rpx;
box-sizing: border-box;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.12);
}
.refund-confirm-dialog__title {
font-size: 36rpx;
font-weight: 700;
color: #1a1a1a;
text-align: center;
margin-bottom: 36rpx;
}
.refund-confirm-dialog__content {
text-align: center;
margin-bottom: 48rpx;
}
.refund-confirm-dialog__line1 {
display: flex;
align-items: baseline;
justify-content: center;
flex-wrap: wrap;
line-height: 1.5;
}
.refund-confirm-dialog__text {
font-size: 30rpx;
color: #333;
}
.refund-confirm-dialog__amount {
font-size: 44rpx;
font-weight: 700;
color: #ff4444;
margin-left: 8rpx;
}
.refund-confirm-dialog__line2 {
display: block;
margin-top: 20rpx;
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
.refund-confirm-dialog__actions {
display: flex;
justify-content: space-between;
gap: 24rpx;
}
.refund-confirm-dialog__btn {
flex: 1;
height: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 42rpx;
font-size: 32rpx;
}
.refund-confirm-dialog__btn--cancel {
background: #f5f7fa;
color: #666;
}
.refund-confirm-dialog__btn--ok {
background: #ff4444;
color: #fff;
font-weight: 600;
}
.zengsongone.huanche-verify-modal {
max-height: 85vh;
display: flex;
flex-direction: column;
padding: 32rpx;
.tops {
margin-bottom: 24rpx;
flex-shrink: 0;
}
.huanche-verify-scroll {
flex: 1;
min-height: 0;
max-height: 52vh;
}
.huanche-field-label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.huanche-sub {
font-size: 22rpx;
color: #999;
}
.huanche-field-input {
width: 100%;
height: 80rpx;
background: #f5f7fa;
border-radius: 10rpx;
padding: 0 20rpx;
margin-bottom: 24rpx;
box-sizing: border-box;
}
.verify-damage-upload-wrap {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 24rpx;
}
.verify-damage-item,
.verify-damage-add {
width: 140rpx;
height: 140rpx;
border-radius: 10rpx;
overflow: hidden;
position: relative;
}
.verify-damage-item {
image {
width: 100%;
height: 100%;
}
}
.verify-damage-item-uploading {
.verify-damage-upload-mask {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.verify-damage-upload-mask-text {
font-size: 22rpx;
color: #fff;
}
}
.verify-damage-del {
position: absolute;
right: 0;
top: 0;
width: 36rpx;
height: 36rpx;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
border-bottom-left-radius: 8rpx;
}
.verify-damage-add {
background: #f5f7fa;
border: 1rpx dashed #cdd3dd;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
font-size: 22rpx;
}
.anniu {
margin-top: 16rpx;
flex-shrink: 0;
.qd.qd-disabled {
opacity: 0.55;
pointer-events: none;
}
}
}
/* 换车弹窗样式 */
.change-bike-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 680rpx;
max-height: 90vh;
background: #FFFFFF;
border-radius: 24rpx;
z-index: 1000;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
overflow: hidden;
display: flex;
flex-direction: column;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 40rpx;
border-bottom: 1rpx solid #F0F2F5;
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.modal-close {
width: 48rpx;
height: 48rpx;
line-height: 48rpx;
text-align: center;
font-size: 48rpx;
color: #909399;
border-radius: 50%;
transition: all 0.3s;
&:active {
background: #F5F7FA;
color: #606266;
}
}
}
.modal-content {
flex: 1;
padding: 40rpx;
overflow-y: auto;
.form-section {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
display: flex;
align-items: center;
.required {
color: #FF4444;
margin-left: 4rpx;
}
}
.form-item {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.label-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.label-tip {
font-size: 24rpx;
color: #909399;
margin-left: 8rpx;
}
}
.form-input {
width: 100%;
height: 88rpx;
background: #F5F7FA;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
&::placeholder {
color: #C0C4CC;
}
}
.input-with-scan {
display: flex;
align-items: center;
background: #F5F7FA;
border-radius: 12rpx;
padding: 0 20rpx;
box-sizing: border-box;
.scan-icon {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-right: 12rpx;
border-radius: 8rpx;
transition: all 0.3s;
&:active {
background: rgba(76, 151, 231, 0.1);
}
}
.form-input-with-icon {
flex: 1;
height: 88rpx;
font-size: 28rpx;
color: #333;
border: none;
background: transparent;
&::placeholder {
color: #C0C4CC;
}
}
}
.form-textarea {
width: 100%;
min-height: 160rpx;
background: #F5F7FA;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
line-height: 1.6;
&::placeholder {
color: #C0C4CC;
}
}
.picker-item {
display: flex;
justify-content: space-between;
align-items: center;
height: 88rpx;
background: #F5F7FA;
border-radius: 12rpx;
padding: 0 24rpx;
.picker-text {
font-size: 28rpx;
color: #333;
&.placeholder {
color: #C0C4CC;
}
}
}
.upload-container {
.upload-preview {
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
.preview-image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
.upload-btn {
width: 200rpx;
height: 200rpx;
background: #F5F7FA;
border: 2rpx dashed #C0C4CC;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
&:active {
background: #EBEDF0;
border-color: #4C97E7;
}
.upload-text {
margin-top: 12rpx;
font-size: 24rpx;
color: #4C97E7;
}
}
}
}
.form-divider {
text-align: center;
color: #909399;
font-size: 24rpx;
margin: 16rpx 0;
}
}
}
.modal-footer {
display: flex;
padding: 24rpx 40rpx 40rpx;
gap: 24rpx;
border-top: 1rpx solid #F0F2F5;
.footer-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
transition: all 0.3s;
&.cancel-btn {
background: #F5F7FA;
color: #666;
&:active {
background: #EBEDF0;
}
}
&.confirm-btn {
background: linear-gradient(90deg, #4C97E7 0%, #3A7EDB 100%);
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(58, 126, 219, 0.3);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(58, 126, 219, 0.2);
}
}
}
}
}
.maskone, .mask {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
z-index: 99;
}
.maskloadpage {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
border-radius: 40rpx 40rpx 0 0;
padding: 50rpx;
z-index: 200;
box-sizing: border-box;
.top_info {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30rpx;
image { width: 50rpx; height: 50rpx; margin-right: 20rpx; }
.masktxt { font-size: 40rpx; font-weight: 600; }
}
.tipsimg {
display: flex; justify-content: center; margin: 50rpx 0;
image { width: 400rpx; height: 200rpx; }
}
.btn_box {
display: flex; justify-content: space-between;
.btn3, .btn4 {
width: 45%; height: 90rpx; border-radius: 45rpx;
display: flex; align-items: center; justify-content: center; font-size: 32rpx;
}
.btn3 { background: #4C97E7; color: #fff; }
.btn4 { border: 1rpx solid #ccc; color: #666; }
}
}
</style>