chuangte_bike_newxcx/components/order/PackageSelector.vue

604 lines
23 KiB
Vue
Raw Normal View History

2026-06-02 16:35:58 +08:00
<template>
<view class="zcfangan">
<view class="name" style="margin-bottom: 24rpx;font-size:36rpx;font-weight: 700;">
选择套餐
</view>
<view class="fanganlist">
<view hover-class="app-tap-hover"
v-for="(item,index) in taocanlist" :key="index"
:class="['fanganlist_item', fanganindex == index ? 'gaoliang' : '', item.ridingRule == 2 ? 'interval-billing' : '', item.ridingRule == 3 ? 'deadline-billing' : '']"
@click="onSelect(index, item)">
<view class="check-icon" v-if="fanganindex == index">
<text class="check-mark"></text>
</view>
<template v-if="item.ridingRule == 1">
<scroll-view
class="package-name-wrapper"
:id="'package-name-' + index"
:ref="'packageName' + index"
scroll-x="true"
:scroll-left="packageNameScrollLeft[index] || 0"
:show-scrollbar="false"
:enable-back-to-top="false"
:scroll-with-animation="false">
<view class="package-name-content" :style="{ justifyContent: packageNeedScroll[index] ? 'flex-start' : 'center' }">
<view class="package-name" :class="{ 'selected': fanganindex == index }">{{item.name}}</view>
<view v-if="packageNeedScroll[index]" class="package-name package-name-duplicate" style="margin-left: 40rpx;" :class="{ 'selected': fanganindex == index }">{{item.name}}</view>
</view>
</scroll-view>
<view class="package-price">
<text class="price-value">{{item.startRule.startingPrice}}</text>
<text class="price-unit"></text>
</view>
<view class="package-duration">{{getPackageDuration(item)}}</view>
</template>
<template v-if="item.ridingRule == 2">
<scroll-view
class="package-name-wrapper"
:id="'package-name-' + index"
:ref="'packageName' + index"
scroll-x="true"
:scroll-left="packageNameScrollLeft[index] || 0"
:show-scrollbar="false"
:enable-back-to-top="false"
:scroll-with-animation="false">
<view class="package-name-content" :style="{ justifyContent: packageNeedScroll[index] ? 'flex-start' : 'center' }">
<view class="package-name" :class="{ 'selected': fanganindex == index }">{{item.name}}</view>
<view v-if="packageNeedScroll[index]" class="package-name package-name-duplicate" style="margin-left: 40rpx;" :class="{ 'selected': fanganindex == index }">{{item.name}}</view>
</view>
</scroll-view>
<view class="interval-list">
<view class="interval-item" v-for="(rule, ruleIndex) in item.intervalRule" :key="ruleIndex">
<view class="interval-content">
<view class="interval-left">
<text class="interval-range">
<text v-if="rule.end == null || rule.end === undefined">
{{rule.start == null ? '0' : rule.start}}
<text v-if="item.rentalUnit == 'hours'"></text>
<text v-if="item.rentalUnit == 'minutes'"></text>
<text v-if="item.rentalUnit == 'day'"></text>
以上
</text>
<text v-else>
{{rule.start == null ? '0' : rule.start}}-{{rule.end}}
<text v-if="item.rentalUnit == 'hours'"></text>
<text v-if="item.rentalUnit == 'minutes'"></text>
<text v-if="item.rentalUnit == 'day'"></text>
</text>
</text>
</view>
<view class="interval-right">
<text class="interval-price">{{rule.fee}}</text>
<text class="interval-unit">/{{rule.eachUnit}}
<text v-if="item.rentalUnit == 'hours'"></text>
<text v-if="item.rentalUnit == 'minutes'"></text>
<text v-if="item.rentalUnit == 'day'"></text>
</text>
</view>
</view>
<view class="interval-divider" v-if="ruleIndex < item.intervalRule.length - 1"></view>
</view>
</view>
</template>
<template v-if="item.ridingRule == 3">
<scroll-view
class="package-name-wrapper"
:id="'package-name-' + index"
:ref="'packageName' + index"
scroll-x="true"
:scroll-left="packageNameScrollLeft[index] || 0"
:show-scrollbar="false"
:enable-back-to-top="false"
:scroll-with-animation="false">
<view class="package-name-content" :style="{ justifyContent: packageNeedScroll[index] ? 'flex-start' : 'center' }">
<view class="package-name" :class="{ 'selected': fanganindex == index }">{{item.name}}</view>
<view v-if="packageNeedScroll[index]" class="package-name package-name-duplicate" style="margin-left: 40rpx;" :class="{ 'selected': fanganindex == index }">{{item.name}}</view>
</view>
</scroll-view>
<view class="package-price">
<text class="price-value">{{item.deadlineRule && item.deadlineRule.basePrice != null ? item.deadlineRule.basePrice : 0}}</text>
<text class="price-unit"></text>
</view>
<view class="package-duration">{{item.deadlineRule && item.deadlineRule.deadlineTime ? item.deadlineRule.deadlineTime : '23:59:59'}}前还车</view>
</template>
</view>
</view>
<view class="wei" v-if="taocanlist.length == 0">
当前车辆车型未配置套餐请管理员前往车型中配置
</view>
<view class="package-info-box" v-if="taocanlist.length != 0">
<view class="package-header">
<view class="package-title">套餐说明</view>
<view class="package-buttons">
<view class="parking-btn" @click="onElectronicFence">
<u-icon name="tags" color="#4297F3" size="22"></u-icon>
<text class="btn-text">电子围栏</text>
</view>
<view class="parking-btn" @click="$emit('check-location')">
<u-icon name="map" color="#4297F3" size="22"></u-icon>
<text class="btn-text">还车点</text>
</view>
<view class="parking-btn" @click="$emit('contact-service')">
<u-icon name="phone-fill" color="#4297F3" size="22"></u-icon>
<text class="btn-text">客服</text>
</view>
</view>
</view>
<view class="package-content">
<view class="info-item" v-if="actiobj.freeRideTime != null && actiobj.freeRideTime != '' && actiobj.freeRideTime != 0">
<view class="info-icon-wrap"><u-icon name="clock" color="#10B981" size="28"></u-icon></view>
<view class="info-text">
免费骑行<text class="highlight">{{actiobj.freeRideTime == null ? '0' : actiobj.freeRideTime}}</text>分钟
</view>
</view>
<view class="info-item" v-if="taocanlist[fanganindex] && taocanlist[fanganindex].ridingRule == 1">
<view class="info-icon-wrap"><u-icon name="red-packet" color="#F17F37" size="28"></u-icon></view>
<view class="info-text">
超出计费<text class="highlight">{{taocanlist[fanganindex].startRule.timeoutPrice}}</text>/<text class="highlight">{{taocanlist[fanganindex].startRule.timeoutTime}}</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'">小时</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'">分钟</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
,不足<text class="highlight">{{taocanlist[fanganindex].startRule.timeoutTime}}</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'">小时</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'">分钟</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
<text class="highlight">{{taocanlist[fanganindex].startRule.timeoutTime}}</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'">小时</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'">分钟</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
</view>
</view>
<view class="info-item" v-if="taocanlist[fanganindex] && taocanlist[fanganindex].ridingRule == 2 && taocanlist[fanganindex].intervalRule && taocanlist[fanganindex].intervalRule.length > 0">
<view class="info-icon-wrap"><u-icon name="red-packet" color="#F17F37" size="28"></u-icon></view>
<view class="info-text">
区间计费
<text v-for="(rule, ruleIndex) in taocanlist[fanganindex].intervalRule" :key="ruleIndex">
<text v-if="rule.end == null || rule.end === undefined">
{{rule.start == null ? '0' : rule.start}}
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
以上每<text class="highlight">{{rule.eachUnit}}</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
<text class="highlight">{{rule.fee}}</text>
</text>
<text v-else>
{{rule.start == null ? '0' : rule.start}}-{{rule.end}}
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
<text class="highlight">{{rule.eachUnit}}</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'"></text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
<text class="highlight">{{rule.fee}}</text>
</text>
<text v-if="ruleIndex < taocanlist[fanganindex].intervalRule.length - 1"></text>
</text>
</view>
</view>
<view class="info-item" v-if="taocanlist[fanganindex] && taocanlist[fanganindex].ridingRule == 3 && taocanlist[fanganindex].deadlineRule">
<view class="info-icon-wrap"><u-icon name="red-packet" color="#F17F37" size="28"></u-icon></view>
<view class="info-text">
支付后第<text class="highlight">{{taocanlist[fanganindex].deadlineRule.dayOffset || 1}}</text>第1天为当天截止<text class="highlight">{{taocanlist[fanganindex].deadlineRule.deadlineTime || '23:59:59'}}</text>前还车收费
<text class="highlight">{{taocanlist[fanganindex].deadlineRule.basePrice || 0}}</text>超时后每<text class="highlight">{{taocanlist[fanganindex].deadlineRule.overtimeUnit || 1}}</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'hours'">小时</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'minutes'">分钟</text>
<text v-if="taocanlist[fanganindex].rentalUnit == 'day'"></text>
收费<text class="highlight">{{taocanlist[fanganindex].deadlineRule.overtimePrice || 0}}</text>
</view>
</view>
<view class="info-item" v-if="bikeobj.areaDispatchFee != null">
<view class="info-icon-wrap"><u-icon name="map" color="#666" size="28"></u-icon></view>
<view class="info-text">
还车点外还车将额外产生调度费<text class="highlight">{{bikeobj.areaDispatchFee == null ? '--' : bikeobj.areaDispatchFee}}</text>
</view>
</view>
<view class="info-item" v-if="bikeobj.areaVehicleManagementFee != null">
<view class="info-icon-wrap"><u-icon name="map" color="#666" size="28"></u-icon></view>
<view class="info-text">
运营区外还车将额外产生调度费<text class="highlight">{{bikeobj.areaVehicleManagementFee == null ? '--' : bikeobj.areaVehicleManagementFee}}</text>
</view>
</view>
<view class="info-item" v-if="instructions != null && instructions != ''">
<view class="info-icon-wrap"><u-icon name="file-text" color="#4297F3" size="28"></u-icon></view>
<view class="info-text">说明{{instructions == null ? '暂无说明' : instructions}}</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'PackageSelector',
props: {
taocanlist: { type: Array, default: () => [] },
fanganindex: { type: Number, default: 0 },
bikeobj: { type: Object, default: () => ({}) },
actiobj: { type: Object, default: () => ({}) },
instructions: { type: String, default: '' }
},
data() {
return {
packageNameScrollLeft: {},
packageNeedScroll: {},
scrollTimers: {},
scrollPauseTimers: {}
}
},
watch: {
// 不在此监听 fanganindex与 onSelect 里 startAutoScroll 重复触发,会叠加多套定时器导致横向滚动失控
taocanlist(newList) {
if (newList.length > 0) {
this.$nextTick(() => {
setTimeout(() => {
this.startAutoScroll(0)
}, 800)
})
}
}
},
beforeDestroy() {
this.clearAllScrollTimers()
},
methods: {
clearAllScrollTimers() {
for (let key in this.scrollTimers) {
if (this.scrollTimers[key]) {
clearInterval(this.scrollTimers[key])
delete this.scrollTimers[key]
}
}
for (let key in this.scrollPauseTimers) {
if (this.scrollPauseTimers[key]) {
clearTimeout(this.scrollPauseTimers[key])
delete this.scrollPauseTimers[key]
}
}
},
onElectronicFence() {
const id = this.bikeobj && this.bikeobj.areaId
if (!id) {
uni.showToast({ title: '未获取到运营区信息', icon: 'none' })
return
}
uni.navigateTo({
url: '/pages/myorder/returned/tingche?areaId=' + id + '&mode=fence'
})
},
onSelect(index, item) {
this.clearAllScrollTimers()
const list = this.taocanlist || []
for (let i = 0; i < list.length; i++) {
this.$set(this.packageNameScrollLeft, i, 0)
this.$set(this.packageNeedScroll, i, false)
}
this.$emit('select', index, item)
this.$nextTick(() => {
this.startAutoScroll(index)
})
},
startAutoScroll(index) {
if (this.scrollTimers[index]) {
clearInterval(this.scrollTimers[index])
delete this.scrollTimers[index]
}
const startKey = 'start_' + index
if (this.scrollPauseTimers[startKey]) {
clearTimeout(this.scrollPauseTimers[startKey])
delete this.scrollPauseTimers[startKey]
}
this.$set(this.packageNeedScroll, index, false)
this.$set(this.packageNameScrollLeft, index, 0)
const packageItem = this.taocanlist[index]
if (!packageItem || !packageItem.name || packageItem.name.length <= 10) return
this.$nextTick(() => {
setTimeout(() => {
const query = uni.createSelectorQuery().in(this)
query.select('#package-name-' + index).boundingClientRect((containerRect) => {
if (containerRect && containerRect.width) {
const query2 = uni.createSelectorQuery().in(this)
query2.select('#package-name-' + index + ' .package-name').boundingClientRect((contentRect) => {
if (contentRect && contentRect.width) {
if (contentRect.width > containerRect.width) {
this.$set(this.packageNeedScroll, index, true)
this.$nextTick(() => {
setTimeout(() => {
const maxScrollLeft = contentRect.width
this.$set(this.packageNameScrollLeft, index, 0)
setTimeout(() => {
this.startLoopScroll(index, maxScrollLeft)
}, 300)
}, 100)
})
} else {
this.$set(this.packageNeedScroll, index, false)
this.$set(this.packageNameScrollLeft, index, 0)
}
}
}).exec()
}
}).exec()
}, 300)
})
},
startLoopScroll(index, maxScrollLeft) {
if (this.scrollTimers[index]) {
clearInterval(this.scrollTimers[index])
delete this.scrollTimers[index]
}
const startKey = 'start_' + index
if (this.scrollPauseTimers[startKey]) {
clearTimeout(this.scrollPauseTimers[startKey])
delete this.scrollPauseTimers[startKey]
}
let currentScroll = 0
const scrollStep = 0.5
const startTimer = setTimeout(() => {
this.scrollTimers[index] = setInterval(() => {
if (!this.scrollTimers[index]) return
currentScroll += scrollStep
if (currentScroll >= maxScrollLeft) currentScroll = 0
this.$set(this.packageNameScrollLeft, index, Math.round(currentScroll))
}, 80)
}, 600)
this.scrollPauseTimers[startKey] = startTimer
},
getPackageDuration(item) {
if (item.ridingRule == 1 && item.startRule) {
let time = item.startRule.startingTime
if (item.rentalUnit == 'hours') return time + '小时'
if (item.rentalUnit == 'minutes') return time + '分钟'
if (item.rentalUnit == 'day') return time + '天'
return time + '分钟'
} else if (item.ridingRule == 2 && item.intervalRule && item.intervalRule.length > 0) {
let lastRule = item.intervalRule[item.intervalRule.length - 1]
let end = lastRule.end
if (end == null || end === undefined) {
let nameMatch = item.name.match(/(\d+)/)
if (nameMatch) end = parseInt(nameMatch[1])
else return '不限'
}
if (item.rentalUnit == 'hours') return end + '小时'
if (item.rentalUnit == 'minutes') return end + '分钟'
if (item.rentalUnit == 'day') return end + '天'
return end + '分钟'
} else if (item.ridingRule == 3 && item.deadlineRule) {
return '截止' + (item.deadlineRule.deadlineTime || '23:59:59')
}
let nameMatch = item.name.match(/(\d+)/)
if (nameMatch) {
let num = parseInt(nameMatch[1])
if (item.name.includes('小时') || item.name.includes('时')) return num + '小时'
if (item.name.includes('天')) return num + '天'
if (item.name.includes('分钟') || item.name.includes('分')) return num + '分钟'
}
return '--'
}
}
}
</script>
<style lang="scss">
.zcfangan {
margin: auto;
padding-right: 0 !important;
box-sizing: border-box;
width: 698rpx;
border-radius: 20rpx;
margin-top: 16rpx;
.name {
font-size: 32rpx;
color: #111827;
font-weight: 700;
letter-spacing: 0.5rpx;
}
.wei {
margin-top: 30rpx;
width: 100%;
text-align: center;
color: #ccc;
}
.fanganlist {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16rpx;
.fanganlist_item {
position: relative;
width: 48%;
padding: 28rpx 20rpx;
background: #fff;
border-radius: 20rpx;
border: 3rpx solid #E5E7EB;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 218rpx;
overflow: hidden;
box-sizing: border-box;
transition: all 0.3s ease;
.check-icon {
position: absolute;
bottom: 0;
right: 0;
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #4297F3 0%, #60A5FA 100%);
clip-path: polygon(100% 0, 100% 100%, 0 100%);
display: flex;
align-items: flex-end;
justify-content: flex-end;
padding: 0 6rpx 6rpx 0;
box-sizing: border-box;
z-index: 10;
.check-mark {
font-size: 28rpx;
font-weight: 700;
color: #FFFFFF;
line-height: 1;
}
}
.package-name-wrapper {
width: 100%;
margin-bottom: 18rpx;
::v-deep .uni-scroll-view { width: 100%; }
::v-deep .uni-scroll-view-content { display: flex; align-items: center; }
}
.package-name-content {
display: flex;
align-items: center;
white-space: nowrap;
}
.package-name {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
text-align: center;
letter-spacing: 0.5rpx;
white-space: nowrap;
display: inline-block;
flex-shrink: 0;
&.selected { color: #4297F3; font-weight: 700; }
}
.package-price {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 10rpx;
.price-value { font-size: 52rpx; font-weight: 700; color: #F17F37; line-height: 1; letter-spacing: -1rpx; }
.price-unit { font-size: 30rpx; font-weight: 600; color: #F17F37; margin-left: 4rpx; }
}
.package-duration { font-size: 26rpx; font-weight: 500; color: #6B7280; text-align: center; }
&.gaoliang {
border-color: #4297F3;
border-width: 3rpx;
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
box-shadow: 0 6rpx 20rpx rgba(66, 151, 243, 0.25);
}
&.interval-billing {
align-items: flex-start;
padding: 24rpx;
min-height: auto;
justify-content: flex-start;
.package-name-wrapper { margin-bottom: 24rpx; width: 100%; }
.interval-list {
width: 100%;
margin-bottom: 16rpx;
background: #F9FAFB;
border-radius: 12rpx;
padding: 12rpx;
.interval-item {
width: 100%;
.interval-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14rpx 0;
.interval-left {
display: flex; align-items: center; flex: 1;
.interval-range { font-size: 28rpx; font-weight: 600; color: #111827; line-height: 1.6; }
}
.interval-right {
display: flex; align-items: baseline; flex-shrink: 0; font-weight: 500;
.interval-price { font-size: 30rpx; font-weight: 700; color: #F17F37; margin-right: 6rpx; }
.interval-unit { font-size: 26rpx; color: #6B7280; }
}
}
.interval-divider {
width: 100%; height: 2rpx;
background: repeating-linear-gradient(to right, #E5E7EB 0, #E5E7EB 8rpx, transparent 8rpx, transparent 16rpx);
margin: 14rpx 0;
}
}
}
}
}
}
}
.package-info-box {
width: 698rpx;
margin: 24rpx auto 0;
background: #fff;
border-radius: 24rpx;
padding: 32rpx;
box-sizing: border-box;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #F3F4F6;
.package-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.package-title { font-size: 32rpx; color: #1F2937; font-weight: 700; letter-spacing: 0.5rpx; }
.package-content {
.info-item {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 8rpx 0;
border-left: 4rpx solid transparent;
&:last-child { margin-bottom: 0; }
.info-icon-wrap {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
}
.info-text {
flex: 1;
min-width: 0;
font-size: 28rpx;
font-weight: 500;
color: #374151;
line-height: 40rpx;
margin-left: 0;
.highlight { color: #F17F37; font-weight: 700; font-size: 30rpx; }
}
}
}
.package-buttons {
display: flex;
align-items: center;
gap: 12rpx;
.parking-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 18rpx;
background: rgba(66, 151, 243, 0.1);
border: 1rpx solid rgba(66, 151, 243, 0.3);
border-radius: 32rpx;
gap: 6rpx;
transition: all 0.2s ease;
&:active { transform: scale(0.96); background: rgba(66, 151, 243, 0.15); }
.btn-text { font-size: 24rpx; color: #4297F3; font-weight: 500; }
}
}
}
</style>