chuangte_bike_newxcx/components/order/PackageSelector.vue
2026-06-02 16:35:58 +08:00

604 lines
23 KiB
Vue
Raw Permalink 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="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>