chuangte_bike_newxcx/page_shanghu/guanli/agent_order.vue
2026-06-02 16:35:58 +08:00

861 lines
24 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">
<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>