861 lines
24 KiB
Vue
861 lines
24 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="page">
|
|||
|
|
<u-navbar title="代客下单" :border-bottom="false" :background="bgc" title-color="#2E4975" title-size="36"
|
|||
|
|
back-icon-color="#2E4975" height="45"></u-navbar>
|
|||
|
|
|
|||
|
|
<scroll-view scroll-y class="scroll-body">
|
|||
|
|
<view class="scroll-inner">
|
|||
|
|
<!-- 车辆信息 -->
|
|||
|
|
<view class="vehicle-wrap" v-if="deviceInfo" hover-class="app-tap-hover" @click="openDeviceSelector">
|
|||
|
|
<view class="change-device-icon">
|
|||
|
|
<u-icon name="reload" color="#4C97E7" size="28"></u-icon>
|
|||
|
|
</view>
|
|||
|
|
<VehicleCard :bikeobj="deviceInfo" :iconobj="iconobj" />
|
|||
|
|
</view>
|
|||
|
|
<view class="device-loading" v-else-if="deviceLoading">
|
|||
|
|
<u-loading mode="circle" size="40" color="#4C97E7"></u-loading>
|
|||
|
|
<text>车辆信息加载中...</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<CustomerSearchCard
|
|||
|
|
:phone-number="phoneNumber"
|
|||
|
|
:target-user="targetUser"
|
|||
|
|
@update:phoneNumber="phoneNumber = $event"
|
|||
|
|
@search="searchUser"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<block v-if="targetUser">
|
|||
|
|
<!-- 套餐选择(与用户端一致) -->
|
|||
|
|
<view class="package-section">
|
|||
|
|
<view class="section-head">
|
|||
|
|
<text class="section-title">选择套餐</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="package-wrap" v-if="suitList.length > 0">
|
|||
|
|
<PackageSelector
|
|||
|
|
:taocanlist="suitList"
|
|||
|
|
:fanganindex="fanganindex"
|
|||
|
|
:bikeobj="packageBikeobj"
|
|||
|
|
:actiobj="packageActiobj"
|
|||
|
|
:instructions="packageInstructions"
|
|||
|
|
@select="onPackageSelect"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
<view class="empty-tip" v-else>暂无可用套餐</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<PayChannelSelector
|
|||
|
|
:channel-list="channelList"
|
|||
|
|
:selected-channel-id="selectedChannelId"
|
|||
|
|
@select="selectChannel"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<InsuranceSelector
|
|||
|
|
:insurance-device-current="insuranceDeviceCurrent"
|
|||
|
|
:selected-insurance-id="selectedInsuranceId"
|
|||
|
|
@toggle="toggleInsurance"
|
|||
|
|
@skip="skipInsurance"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<view class="scroll-bottom-spacer"></view>
|
|||
|
|
</block>
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
|
|||
|
|
<AgentOrderFooter
|
|||
|
|
v-if="targetUser"
|
|||
|
|
:display-deposit="displayDeposit"
|
|||
|
|
:selected-suit-id="selectedSuitId"
|
|||
|
|
:submitting="submitting"
|
|||
|
|
@open-detail="openPriceDetail"
|
|||
|
|
@submit="submitOrder"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<DeviceSelectorPopup
|
|||
|
|
:visible="deviceSelectorShow"
|
|||
|
|
:area-id="areaId"
|
|||
|
|
:current-device-id="deviceId"
|
|||
|
|
@update:visible="deviceSelectorShow = $event"
|
|||
|
|
@select="switchDevice"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<AgentOrderConfirmModal
|
|||
|
|
:visible="showConfirmModal"
|
|||
|
|
:target-user="targetUser"
|
|||
|
|
:selected-suit="selectedSuit"
|
|||
|
|
:display-deposit="displayDeposit"
|
|||
|
|
:selected-channel-obj="selectedChannelObj"
|
|||
|
|
:price-info="priceInfo"
|
|||
|
|
@close="closeConfirmModal"
|
|||
|
|
@confirm="confirmScanOrder"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<PriceDetailPopup
|
|||
|
|
:visible="priceDetailShow"
|
|||
|
|
:price-info="priceInfo"
|
|||
|
|
:display-deposit="displayDeposit"
|
|||
|
|
@update:visible="priceDetailShow = $event"
|
|||
|
|
/>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { ChannelApiType } from '@/common/enums/channel';
|
|||
|
|
import PackageSelector from '@/components/order/PackageSelector.vue';
|
|||
|
|
import VehicleCard from '@/components/order/VehicleCard.vue';
|
|||
|
|
import AgentOrderConfirmModal from './components/AgentOrderConfirmModal.vue';
|
|||
|
|
import AgentOrderFooter from './components/AgentOrderFooter.vue';
|
|||
|
|
import CustomerSearchCard from './components/CustomerSearchCard.vue';
|
|||
|
|
import DeviceSelectorPopup from './components/DeviceSelectorPopup.vue';
|
|||
|
|
import InsuranceSelector from './components/InsuranceSelector.vue';
|
|||
|
|
import PayChannelSelector from './components/PayChannelSelector.vue';
|
|||
|
|
import PriceDetailPopup from './components/PriceDetailPopup.vue';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
components: {
|
|||
|
|
PackageSelector,
|
|||
|
|
VehicleCard,
|
|||
|
|
AgentOrderConfirmModal,
|
|||
|
|
AgentOrderFooter,
|
|||
|
|
CustomerSearchCard,
|
|||
|
|
DeviceSelectorPopup,
|
|||
|
|
InsuranceSelector,
|
|||
|
|
PayChannelSelector,
|
|||
|
|
PriceDetailPopup
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
bgc: { backgroundColor: '#EEF3FA' },
|
|||
|
|
deviceId: '',
|
|||
|
|
areaId: '',
|
|||
|
|
deviceInfo: null,
|
|||
|
|
deviceLoading: false,
|
|||
|
|
iconobj: {},
|
|||
|
|
phoneNumber: '',
|
|||
|
|
targetUser: null,
|
|||
|
|
suitList: [],
|
|||
|
|
fanganindex: 0,
|
|||
|
|
channelList: [],
|
|||
|
|
/** /app/insuranceDevice/current 有 data 时非空,与用户端一致 */
|
|||
|
|
insuranceDeviceCurrent: null,
|
|||
|
|
selectedSuitId: null,
|
|||
|
|
selectedSuit: null,
|
|||
|
|
selectedChannelId: null,
|
|||
|
|
selectedInsuranceId: null,
|
|||
|
|
priceInfo: null,
|
|||
|
|
submitting: false,
|
|||
|
|
priceDetailShow: false,
|
|||
|
|
showConfirmModal: false,
|
|||
|
|
deviceSelectorShow: false,
|
|||
|
|
deviceLoadSeq: 0,
|
|||
|
|
priceCalcSeq: 0,
|
|||
|
|
offlinePayNo: '',
|
|||
|
|
pendingOfflineAudit: false,
|
|||
|
|
offlineAuditHandled: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
selectedChannelObj() {
|
|||
|
|
if (!this.selectedChannelId) return null
|
|||
|
|
return this.channelList.find(c => c.id === this.selectedChannelId) || null
|
|||
|
|
},
|
|||
|
|
displayDeposit() {
|
|||
|
|
if (this.priceInfo && this.priceInfo.depositPrice != null) {
|
|||
|
|
let total = Number(this.priceInfo.depositPrice) || 0
|
|||
|
|
if (this.priceInfo.insureFee) {
|
|||
|
|
total += Number(this.priceInfo.insureFee) || 0
|
|||
|
|
}
|
|||
|
|
return total
|
|||
|
|
}
|
|||
|
|
return '--'
|
|||
|
|
},
|
|||
|
|
packageBikeobj() {
|
|||
|
|
return this.deviceInfo || { areaId: this.areaId }
|
|||
|
|
},
|
|||
|
|
packageActiobj() {
|
|||
|
|
const suit = this.selectedSuit
|
|||
|
|
return {
|
|||
|
|
freeRideTime: suit ? suit.freeRideTime : null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
packageInstructions() {
|
|||
|
|
return this.selectedSuit && this.selectedSuit.instructions ? this.selectedSuit.instructions : ''
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
onLoad(e) {
|
|||
|
|
this.deviceId = e.deviceId || ''
|
|||
|
|
this.areaId = e.areaId || ''
|
|||
|
|
this.iconobj = this.$store.state.iconobj || {}
|
|||
|
|
this.loadDevice()
|
|||
|
|
},
|
|||
|
|
onShow() {
|
|||
|
|
this.handleOfflineAuditReturn()
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
readOfflineAuditResult() {
|
|||
|
|
let result = uni.getStorageSync('agentOrderOfflineResult')
|
|||
|
|
if (!result) return null
|
|||
|
|
if (typeof result === 'string') {
|
|||
|
|
try {
|
|||
|
|
result = JSON.parse(result)
|
|||
|
|
} catch (e) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return result && typeof result === 'object' ? result : null
|
|||
|
|
},
|
|||
|
|
handleOfflineAuditReturn() {
|
|||
|
|
if (!this.pendingOfflineAudit) {
|
|||
|
|
uni.removeStorageSync('agentOrderOfflineResult')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const result = this.readOfflineAuditResult()
|
|||
|
|
if (!result) return
|
|||
|
|
this.processOfflineAuditResult(result)
|
|||
|
|
},
|
|||
|
|
processOfflineAuditResult(result) {
|
|||
|
|
if (!result || this.offlineAuditHandled) return
|
|||
|
|
this.offlineAuditHandled = true
|
|||
|
|
this.pendingOfflineAudit = false
|
|||
|
|
uni.removeStorageSync('agentOrderOfflineResult')
|
|||
|
|
if (result.pass) {
|
|||
|
|
const payNo = result.payNo || this.offlinePayNo
|
|||
|
|
this.offlinePayNo = payNo || this.offlinePayNo
|
|||
|
|
if (payNo) {
|
|||
|
|
this.submitting = true
|
|||
|
|
uni.showLoading({ title: '确认支付结果...', mask: true })
|
|||
|
|
this.pollPayResult(payNo)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: '下单成功', icon: 'success', duration: 1500 })
|
|||
|
|
setTimeout(() => { uni.navigateBack() }, 1500)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.submitting = false
|
|||
|
|
this.offlinePayNo = ''
|
|||
|
|
if (result.rejected) {
|
|||
|
|
uni.showToast({ title: '线下支付审核已驳回', icon: 'none', duration: 2500 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (result.canceled) {
|
|||
|
|
uni.showToast({ title: '已取消线下支付审核', icon: 'none', duration: 2000 })
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
// 加载车辆信息
|
|||
|
|
loadDevice() {
|
|||
|
|
if (!this.deviceId) return
|
|||
|
|
const seq = ++this.deviceLoadSeq
|
|||
|
|
this.deviceLoading = true
|
|||
|
|
this.deviceInfo = null
|
|||
|
|
this.resetDeviceRelatedState()
|
|||
|
|
this.$u.get(`/bst/device?id=${this.deviceId}`).then(res => {
|
|||
|
|
if (seq !== this.deviceLoadSeq) return
|
|||
|
|
this.deviceLoading = false
|
|||
|
|
if (res.code === 200 && res.data) {
|
|||
|
|
this.deviceInfo = res.data
|
|||
|
|
if (res.data.areaId) {
|
|||
|
|
this.areaId = res.data.areaId
|
|||
|
|
}
|
|||
|
|
this.loadSuits()
|
|||
|
|
this.fetchInsuranceDeviceCurrent()
|
|||
|
|
this.loadChannels()
|
|||
|
|
}
|
|||
|
|
}).catch(() => {
|
|||
|
|
if (seq !== this.deviceLoadSeq) return
|
|||
|
|
this.deviceLoading = false
|
|||
|
|
uni.showToast({ title: '车辆信息加载失败', icon: 'none', duration: 2000 })
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
resetDeviceRelatedState() {
|
|||
|
|
this.suitList = []
|
|||
|
|
this.fanganindex = 0
|
|||
|
|
this.channelList = []
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedSuitId = null
|
|||
|
|
this.selectedSuit = null
|
|||
|
|
this.selectedChannelId = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
this.priceInfo = null
|
|||
|
|
this.priceCalcSeq++
|
|||
|
|
this.priceDetailShow = false
|
|||
|
|
this.showConfirmModal = false
|
|||
|
|
},
|
|||
|
|
openDeviceSelector() {
|
|||
|
|
if (this.submitting) return
|
|||
|
|
this.deviceSelectorShow = true
|
|||
|
|
},
|
|||
|
|
switchDevice(device) {
|
|||
|
|
const id = device && (device.id != null ? device.id : device.deviceId)
|
|||
|
|
if (id === undefined || id === null || String(id).trim() === '') {
|
|||
|
|
uni.showToast({ title: '车辆信息异常', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (String(id) === String(this.deviceId)) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.deviceId = id
|
|||
|
|
if (device.areaId) {
|
|||
|
|
this.areaId = device.areaId
|
|||
|
|
}
|
|||
|
|
this.loadDevice()
|
|||
|
|
},
|
|||
|
|
// 按车型加载套餐(与用户端一致)
|
|||
|
|
loadSuits() {
|
|||
|
|
const seq = this.deviceLoadSeq
|
|||
|
|
const modelId = this.deviceInfo && this.deviceInfo.modelId
|
|||
|
|
if (!modelId) {
|
|||
|
|
this.suitList = []
|
|||
|
|
this.selectedSuitId = null
|
|||
|
|
this.selectedSuit = null
|
|||
|
|
this.priceInfo = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.$u.get(`/app/suit/listByModel?temp=false&modelId=${modelId}`).then(res => {
|
|||
|
|
if (seq !== this.deviceLoadSeq) return
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
const list = res.data || []
|
|||
|
|
this.suitList = this.normalizeTaocanList(list)
|
|||
|
|
if (this.suitList.length > 0) {
|
|||
|
|
this.fanganindex = 0
|
|||
|
|
this.selectSuit(this.suitList[0])
|
|||
|
|
} else {
|
|||
|
|
this.selectedSuitId = null
|
|||
|
|
this.selectedSuit = null
|
|||
|
|
this.priceInfo = null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 加载支付渠道列表(scene=MCH 商户场景)
|
|||
|
|
loadChannels() {
|
|||
|
|
const seq = this.deviceLoadSeq
|
|||
|
|
const appId = this.$store.state.appid || ''
|
|||
|
|
let url = `/app/channel/list?scene=MCH&bstType=1&appType=1&appId=${appId}`
|
|||
|
|
if (this.areaId) url += `&areaId=${this.areaId}`
|
|||
|
|
this.$u.get(url).then(res => {
|
|||
|
|
if (seq !== this.deviceLoadSeq) return
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
this.channelList = res.data || []
|
|||
|
|
if (this.channelList.length > 0) {
|
|||
|
|
this.selectChannel(this.channelList[0])
|
|||
|
|
} else {
|
|||
|
|
this.selectedChannelId = null
|
|||
|
|
this.priceInfo = null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 加载当前设备保险(与用户端一致)
|
|||
|
|
fetchInsuranceDeviceCurrent() {
|
|||
|
|
const seq = this.deviceLoadSeq
|
|||
|
|
const raw = this.deviceInfo
|
|||
|
|
if (!raw || typeof raw !== 'object') {
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const did = raw.id != null && raw.id !== '' ? raw.id : raw.deviceId
|
|||
|
|
if (did === undefined || did === null || String(did).trim() === '') {
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
this.$u
|
|||
|
|
.get(`/app/insuranceDevice/current?deviceId=${encodeURIComponent(String(did))}`)
|
|||
|
|
.then((res) => {
|
|||
|
|
if (seq !== this.deviceLoadSeq) return
|
|||
|
|
if (res.code != 200) {
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!this._insuranceCurrentDataHasValue(res.data)) {
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.insuranceDeviceCurrent = res.data
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
})
|
|||
|
|
.catch(() => {
|
|||
|
|
if (seq !== this.deviceLoadSeq) return
|
|||
|
|
this.insuranceDeviceCurrent = null
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
_insuranceCurrentDataHasValue(d) {
|
|||
|
|
if (d === null || d === undefined) return false
|
|||
|
|
if (typeof d === 'string') return d.trim() !== ''
|
|||
|
|
if (typeof d === 'number' || typeof d === 'boolean') return true
|
|||
|
|
if (Array.isArray(d)) return d.length > 0
|
|||
|
|
if (typeof d === 'object') return Object.keys(d).length > 0
|
|||
|
|
return false
|
|||
|
|
},
|
|||
|
|
// 预计算价格(套餐/渠道/保险变更后调用)
|
|||
|
|
calculatePrice() {
|
|||
|
|
if (!this.selectedSuitId || !this.selectedChannelId) {
|
|||
|
|
this.priceInfo = null
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const seq = ++this.priceCalcSeq
|
|||
|
|
const appId = this.$store.state.appid || ''
|
|||
|
|
const data = {
|
|||
|
|
suitId: this.selectedSuitId,
|
|||
|
|
channelId: this.selectedChannelId,
|
|||
|
|
appId
|
|||
|
|
}
|
|||
|
|
if (this.selectedInsuranceId) {
|
|||
|
|
data.insuranceDeviceId = this.selectedInsuranceId
|
|||
|
|
}
|
|||
|
|
this.$u.post('/app/order/calculatePrice', data).then(res => {
|
|||
|
|
if (seq !== this.priceCalcSeq) return
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
this.priceInfo = res.data
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 查询客户信息
|
|||
|
|
searchUser() {
|
|||
|
|
if (!this.phoneNumber || this.phoneNumber.length !== 11) {
|
|||
|
|
uni.showToast({ title: '请输入正确的11位手机号', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
uni.showLoading({ title: '查询中...', mask: true })
|
|||
|
|
this.$u.get(`/app/user/getByUserName?userName=${this.phoneNumber}`).then(res => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
if (res.code === 200 && res.data) {
|
|||
|
|
this.targetUser = res.data
|
|||
|
|
uni.showToast({ title: '查询成功', icon: 'success', duration: 1500 })
|
|||
|
|
} else {
|
|||
|
|
this.targetUser = null
|
|||
|
|
uni.showToast({ title: '未找到该手机号对应用户', icon: 'none', duration: 2000 })
|
|||
|
|
}
|
|||
|
|
}).catch(() => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
uni.showToast({ title: '查询失败,请重试', icon: 'none', duration: 2000 })
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
parseRuleValue(ruleValue) {
|
|||
|
|
if (!ruleValue) return null
|
|||
|
|
if (typeof ruleValue === 'object') return ruleValue
|
|||
|
|
if (typeof ruleValue !== 'string') return null
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(ruleValue)
|
|||
|
|
} catch (e) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
normalizeTaocanList(list) {
|
|||
|
|
return (list || []).map((item) => {
|
|||
|
|
const ridingRule = Number(item.ridingRule || 1)
|
|||
|
|
const parsedRule = this.parseRuleValue(item.rule)
|
|||
|
|
const normalized = { ...item, ridingRule }
|
|||
|
|
if (ridingRule === 1) {
|
|||
|
|
normalized.startRule = parsedRule && !Array.isArray(parsedRule)
|
|||
|
|
? parsedRule
|
|||
|
|
: (this.parseRuleValue(item.startRule) || item.startRule || {})
|
|||
|
|
normalized.intervalRule = []
|
|||
|
|
normalized.deadlineRule = null
|
|||
|
|
} else if (ridingRule === 2) {
|
|||
|
|
normalized.intervalRule = Array.isArray(parsedRule)
|
|||
|
|
? parsedRule
|
|||
|
|
: (this.parseRuleValue(item.intervalRule) || item.intervalRule || [])
|
|||
|
|
normalized.startRule = {}
|
|||
|
|
normalized.deadlineRule = null
|
|||
|
|
} else if (ridingRule === 3) {
|
|||
|
|
normalized.deadlineRule = parsedRule && !Array.isArray(parsedRule)
|
|||
|
|
? parsedRule
|
|||
|
|
: {}
|
|||
|
|
normalized.startRule = {}
|
|||
|
|
normalized.intervalRule = []
|
|||
|
|
} else {
|
|||
|
|
normalized.startRule = {}
|
|||
|
|
normalized.intervalRule = []
|
|||
|
|
normalized.deadlineRule = null
|
|||
|
|
}
|
|||
|
|
return normalized
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
onPackageSelect(index, item) {
|
|||
|
|
this.fanganindex = index
|
|||
|
|
this.selectSuit(item)
|
|||
|
|
},
|
|||
|
|
openPriceDetail() {
|
|||
|
|
if (!this.selectedSuitId) {
|
|||
|
|
uni.showToast({ title: '请先选择套餐', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this.priceDetailShow = true
|
|||
|
|
},
|
|||
|
|
// 选择套餐
|
|||
|
|
selectSuit(item) {
|
|||
|
|
this.selectedSuitId = item.id
|
|||
|
|
this.selectedSuit = item
|
|||
|
|
const idx = this.suitList.findIndex(s => s.id === item.id)
|
|||
|
|
if (idx >= 0) {
|
|||
|
|
this.fanganindex = idx
|
|||
|
|
}
|
|||
|
|
this.calculatePrice()
|
|||
|
|
},
|
|||
|
|
// 选择支付渠道
|
|||
|
|
selectChannel(item) {
|
|||
|
|
this.selectedChannelId = item.id
|
|||
|
|
this.calculatePrice()
|
|||
|
|
},
|
|||
|
|
// 切换保险选中状态
|
|||
|
|
toggleInsurance() {
|
|||
|
|
if (!this.insuranceDeviceCurrent) return
|
|||
|
|
if (this.selectedInsuranceId === this.insuranceDeviceCurrent.id) {
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
} else {
|
|||
|
|
this.selectedInsuranceId = this.insuranceDeviceCurrent.id
|
|||
|
|
}
|
|||
|
|
this.calculatePrice()
|
|||
|
|
},
|
|||
|
|
// 不投保
|
|||
|
|
skipInsurance() {
|
|||
|
|
this.selectedInsuranceId = null
|
|||
|
|
this.calculatePrice()
|
|||
|
|
},
|
|||
|
|
// 扫客户付款码,返回 Promise<authCode>
|
|||
|
|
scanPayCode() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
uni.scanCode({
|
|||
|
|
scanType: ['barCode', 'qrCode'],
|
|||
|
|
onlyFromCamera: false,
|
|||
|
|
success: (res) => {
|
|||
|
|
const code = (res && res.result || '').trim()
|
|||
|
|
if (code) {
|
|||
|
|
resolve(code)
|
|||
|
|
} else {
|
|||
|
|
reject(new Error('未识别到付款码'))
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
fail: () => {
|
|||
|
|
reject(new Error('已取消扫码'))
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 轮询支付结果,最长约 30 秒
|
|||
|
|
pollPayResult(no) {
|
|||
|
|
const maxTimes = 15
|
|||
|
|
const interval = 2000
|
|||
|
|
let times = 0
|
|||
|
|
const queryDetail = () => {
|
|||
|
|
this.$u.get(`/app/pay/detail?no=${no}`).then(res => {
|
|||
|
|
const status = res && res.data ? res.data.status : ''
|
|||
|
|
if (status === 'PAYED') {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: '收款成功', icon: 'success', duration: 1500 })
|
|||
|
|
setTimeout(() => { uni.navigateBack() }, 1500)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (status === 'CANCELED' || status === 'REFUNDED') {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: '支付未完成,订单已取消', icon: 'none', duration: 2500 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (times >= maxTimes) {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '支付处理中',
|
|||
|
|
content: '支付结果尚未确认,请稍后在订单列表中查看。',
|
|||
|
|
showCancel: false,
|
|||
|
|
success: () => { uni.navigateBack() }
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
setTimeout(checkOnce, interval)
|
|||
|
|
}).catch(() => {
|
|||
|
|
if (times >= maxTimes) {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: '支付结果查询失败,请稍后在订单中查看', icon: 'none', duration: 2500 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
setTimeout(checkOnce, interval)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
const checkOnce = () => {
|
|||
|
|
times++
|
|||
|
|
this.$u.put(`/app/pay/refreshPayResult?no=${no}`).then(queryDetail).catch(queryDetail)
|
|||
|
|
}
|
|||
|
|
setTimeout(checkOnce, interval)
|
|||
|
|
},
|
|||
|
|
// 提交代客下单
|
|||
|
|
submitOrder() {
|
|||
|
|
if (this.submitting) return
|
|||
|
|
if (!this.targetUser) {
|
|||
|
|
uni.showToast({ title: '请先查询并确认客户信息', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!this.selectedSuitId) {
|
|||
|
|
uni.showToast({ title: '请选择套餐', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (!this.selectedChannelId) {
|
|||
|
|
uni.showToast({ title: '请选择支付渠道', icon: 'none', duration: 2000 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const apiType = this.selectedChannelObj && this.selectedChannelObj.apiType
|
|||
|
|
if (this.isScanPayChannel(apiType)) {
|
|||
|
|
this.showConfirmModal = true
|
|||
|
|
} else if (this.isOfflinePayChannel(apiType)) {
|
|||
|
|
this.doAgentCreate()
|
|||
|
|
} else {
|
|||
|
|
uni.showToast({ title: '当前支付方式暂不支持', icon: 'none', duration: 2000 })
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
isScanPayChannel(apiType) {
|
|||
|
|
return apiType === ChannelApiType.TM_MICRO.value
|
|||
|
|
},
|
|||
|
|
isOfflinePayChannel(apiType) {
|
|||
|
|
return apiType === ChannelApiType.OFFLINE.value
|
|||
|
|
},
|
|||
|
|
// 关闭确认弹窗
|
|||
|
|
closeConfirmModal() {
|
|||
|
|
this.showConfirmModal = false
|
|||
|
|
},
|
|||
|
|
// 确认扫码下单
|
|||
|
|
confirmScanOrder() {
|
|||
|
|
this.showConfirmModal = false
|
|||
|
|
this.scanPayCode().then(payAuthCode => {
|
|||
|
|
this.doAgentCreate(payAuthCode)
|
|||
|
|
}).catch(err => {
|
|||
|
|
uni.showToast({ title: (err && err.message) || '扫码失败', icon: 'none', duration: 2000 })
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
// 携带付款码创建订单并发起收款
|
|||
|
|
doAgentCreate(payAuthCode) {
|
|||
|
|
const apiType = this.selectedChannelObj && this.selectedChannelObj.apiType
|
|||
|
|
const isOfflinePay = this.isOfflinePayChannel(apiType)
|
|||
|
|
this.submitting = true
|
|||
|
|
uni.showLoading({ title: isOfflinePay ? '创建审核中...' : '收款中...', mask: true })
|
|||
|
|
const appId = this.$store.state.appid || ''
|
|||
|
|
const areaPromotionId = uni.getStorageSync('areaPromotionId') || ''
|
|||
|
|
const data = {
|
|||
|
|
deviceId: this.deviceId,
|
|||
|
|
suitId: this.selectedSuitId,
|
|||
|
|
channelId: this.selectedChannelId,
|
|||
|
|
userId: this.targetUser.userId,
|
|||
|
|
appId,
|
|||
|
|
appType: 1,
|
|||
|
|
areaPromotionId: areaPromotionId || undefined,
|
|||
|
|
price: this.priceInfo || undefined
|
|||
|
|
}
|
|||
|
|
if (payAuthCode) {
|
|||
|
|
data.payAuthCode = payAuthCode
|
|||
|
|
}
|
|||
|
|
if (this.selectedInsuranceId) {
|
|||
|
|
data.insuranceDeviceId = this.selectedInsuranceId
|
|||
|
|
}
|
|||
|
|
this.$u.post('/bst/order/agentCreate', data).then(res => {
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
if (isOfflinePay) {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
this.handleOfflinePay(res.data || {})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const payNo = res.data && res.data.pay ? res.data.pay.no : ''
|
|||
|
|
if (payNo) {
|
|||
|
|
uni.showLoading({ title: '等待支付结果...', mask: true })
|
|||
|
|
this.pollPayResult(payNo)
|
|||
|
|
} else {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: '下单成功', icon: 'success', duration: 1500 })
|
|||
|
|
setTimeout(() => { uni.navigateBack() }, 1500)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: res.msg || '下单失败', icon: 'none', duration: 3000 })
|
|||
|
|
}
|
|||
|
|
}).catch(() => {
|
|||
|
|
uni.hideLoading()
|
|||
|
|
this.submitting = false
|
|||
|
|
uni.showToast({ title: '网络异常,请重试', icon: 'none', duration: 2000 })
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
handleOfflinePay(data) {
|
|||
|
|
const applyNo = data.payParams && data.payParams.no ? String(data.payParams.no) : ''
|
|||
|
|
const payNo = data.pay && data.pay.no ? String(data.pay.no) : ''
|
|||
|
|
if (!applyNo) {
|
|||
|
|
uni.showToast({ title: '线下支付申请单创建失败', icon: 'none', duration: 2500 })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const query = `no=${encodeURIComponent(applyNo)}&payNo=${encodeURIComponent(payNo)}`
|
|||
|
|
this.offlinePayNo = payNo
|
|||
|
|
this.pendingOfflineAudit = true
|
|||
|
|
this.offlineAuditHandled = false
|
|||
|
|
uni.navigateTo({
|
|||
|
|
url: `/page_shanghu/offlinePay/audit?${query}`,
|
|||
|
|
events: {
|
|||
|
|
offlineAuditResult: (payload) => {
|
|||
|
|
this.processOfflineAuditResult(payload)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
@import './components/agent-order-theme.scss';
|
|||
|
|
|
|||
|
|
page {
|
|||
|
|
background-color: $ao-bg-page;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page {
|
|||
|
|
height: 100vh;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background-color: $ao-bg-page;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.scroll-body {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.scroll-inner {
|
|||
|
|
padding: 24rpx 32rpx 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.scroll-bottom-spacer {
|
|||
|
|
height: 24rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.vehicle-wrap {
|
|||
|
|
@include ao-section;
|
|||
|
|
position: relative;
|
|||
|
|
|
|||
|
|
::v-deep .jieshao {
|
|||
|
|
width: 100%;
|
|||
|
|
margin: 0;
|
|||
|
|
margin-top: 0;
|
|||
|
|
border: 1rpx solid rgba(76, 151, 231, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .vehicle-info {
|
|||
|
|
padding-left: 48rpx;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.change-device-icon {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 24rpx;
|
|||
|
|
top: 24rpx;
|
|||
|
|
z-index: 2;
|
|||
|
|
width: 44rpx;
|
|||
|
|
height: 44rpx;
|
|||
|
|
border-radius: 999rpx;
|
|||
|
|
background: #fff;
|
|||
|
|
border: 1rpx solid $ao-border;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
box-shadow: 0 4rpx 12rpx rgba(46, 73, 117, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.device-loading {
|
|||
|
|
@include ao-section;
|
|||
|
|
@include ao-empty-tip;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
padding: 48rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-head {
|
|||
|
|
@include ao-section-head;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-title {
|
|||
|
|
@include ao-section-title;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.package-section {
|
|||
|
|
@include ao-section;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.package-wrap {
|
|||
|
|
margin: 0;
|
|||
|
|
|
|||
|
|
::v-deep .zcfangan {
|
|||
|
|
width: 100%;
|
|||
|
|
margin: 0;
|
|||
|
|
margin-top: 0;
|
|||
|
|
padding-right: 0 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .zcfangan > .name {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .package-info-box {
|
|||
|
|
width: 100%;
|
|||
|
|
margin-left: 0;
|
|||
|
|
margin-right: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .package-header .package-buttons {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .package-content .info-item {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
padding: 8rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .package-content .info-icon-wrap {
|
|||
|
|
width: 40rpx;
|
|||
|
|
height: 40rpx;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-right: 8rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .package-content .info-text {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
line-height: 40rpx;
|
|||
|
|
margin-left: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .fanganlist_item.gaoliang {
|
|||
|
|
background: #fff !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
::v-deep .package-info-box,
|
|||
|
|
::v-deep .fanganlist_item {
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-tip {
|
|||
|
|
@include ao-empty-tip;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
</style>
|