391 lines
15 KiB
Vue
391 lines
15 KiB
Vue
<template>
|
||
<view class="expected-station">
|
||
<view class="sex-search" v-if="search">
|
||
<uni-icons class="iconsearch" type="search" size="20"></uni-icons>
|
||
<input class="uni-input searchinput" confirm-type="search" />
|
||
</view>
|
||
<view class="sex-content">
|
||
<scroll-view ref="leftScroll" :show-scrollbar="false" :scroll-y="true" class="sex-content-left">
|
||
<view
|
||
v-for="item in copyTree"
|
||
:key="item.id"
|
||
class="left-list-btn"
|
||
:class="{ 'left-list-btned': item.id === leftValue.id }"
|
||
@click="changeStationLog(item)"
|
||
>
|
||
{{ item.label }}
|
||
<!-- <view class="positionNum" v-show="item.checkednumber">
|
||
{{ item.checkednumber }}
|
||
</view> -->
|
||
</view>
|
||
</scroll-view>
|
||
<scroll-view
|
||
:show-scrollbar="false"
|
||
:scroll-top="scrollTop"
|
||
@scroll="scrollTopBack"
|
||
:scroll-y="true"
|
||
class="sex-content-right"
|
||
>
|
||
<view v-for="item in rightValue" :key="item.id">
|
||
<view class="secondary-title">{{ item.label }}</view>
|
||
<view class="grid-sex">
|
||
<view
|
||
v-for="item in item.children"
|
||
:key="item.id"
|
||
:class="{ 'sex-right-btned': item.checked }"
|
||
class="sex-right-btn"
|
||
@click="addItem(item)"
|
||
>
|
||
{{ item.label }}
|
||
</view>
|
||
<!-- <view class="sex-right-btn sex-right-btned" @click="addItem()">客户经理</view>
|
||
<view class="sex-right-btn" @click="addItem()">客户经理</view> -->
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'expected-station',
|
||
data() {
|
||
return {
|
||
leftValue: {},
|
||
rightValue: [],
|
||
stationCateLog: 0,
|
||
copyTree: [],
|
||
scrollTop: 0,
|
||
scrollTimer: null,
|
||
lastScrollTime: 0,
|
||
};
|
||
},
|
||
props: {
|
||
station: {
|
||
type: Array,
|
||
default: [],
|
||
},
|
||
search: {
|
||
type: Boolean,
|
||
default: true,
|
||
},
|
||
max: {
|
||
type: Number,
|
||
default: 5,
|
||
},
|
||
},
|
||
created() {
|
||
this.copyTree = this.station;
|
||
if (this.copyTree.length) {
|
||
this.leftValue = this.copyTree[0];
|
||
this.rightValue = this.copyTree[0].children;
|
||
}
|
||
},
|
||
watch: {
|
||
station(newVal) {
|
||
this.copyTree = this.station;
|
||
if (this.copyTree.length) {
|
||
this.leftValue = this.copyTree[0];
|
||
this.rightValue = this.copyTree[0].children;
|
||
}
|
||
},
|
||
},
|
||
beforeDestroy() {
|
||
// 组件销毁前清除定时器
|
||
if (this.scrollTimer) {
|
||
clearTimeout(this.scrollTimer);
|
||
this.scrollTimer = null;
|
||
}
|
||
},
|
||
methods: {
|
||
changeStationLog(item) {
|
||
this.leftValue = item;
|
||
this.rightValue = item.children;
|
||
this.scrollTop = 0;
|
||
|
||
// 使用更激进的防抖策略,避免频繁的左侧滚动
|
||
if (this.scrollTimer) {
|
||
clearTimeout(this.scrollTimer);
|
||
}
|
||
|
||
this.scrollTimer = setTimeout(() => {
|
||
// 确保左侧列表滚动到正确位置,让当前选中的标签可见
|
||
const index = this.copyTree.findIndex(i => i.id === item.id);
|
||
if (index !== -1 && this.$refs.leftScroll) {
|
||
const itemHeight = 160; // 每个选项高度
|
||
const visibleHeight = 4 * itemHeight; // 可见区域高度
|
||
|
||
// 计算目标位置
|
||
const targetTop = index * itemHeight;
|
||
|
||
// 获取当前左侧列表的滚动位置
|
||
const currentLeftScrollTop = this.$refs.leftScroll.scrollTop || 0;
|
||
|
||
// 只有当目标位置不在当前可见区域内时才滚动
|
||
if (targetTop < currentLeftScrollTop || targetTop > currentLeftScrollTop + visibleHeight - itemHeight) {
|
||
let targetScrollTop = targetTop;
|
||
|
||
// 如果选中的标签在底部,确保它不会滚动出可见区域
|
||
if (targetTop > this.$refs.leftScroll.scrollHeight - visibleHeight) {
|
||
targetScrollTop = Math.max(0, this.$refs.leftScroll.scrollHeight - visibleHeight);
|
||
}
|
||
|
||
this.$refs.leftScroll.scrollTo({
|
||
top: targetScrollTop,
|
||
duration: 100 // 进一步减少滚动动画时间
|
||
});
|
||
}
|
||
}
|
||
}, 100);
|
||
},
|
||
scrollTopBack(e) {
|
||
const currentTime = Date.now();
|
||
|
||
// 更严格的防抖处理:如果距离上次滚动时间太短,则跳过处理
|
||
if (currentTime - this.lastScrollTime < 80) {
|
||
return;
|
||
}
|
||
this.lastScrollTime = currentTime;
|
||
|
||
// 清除之前的定时器
|
||
if (this.scrollTimer) {
|
||
clearTimeout(this.scrollTimer);
|
||
}
|
||
|
||
// 设置新的定时器,延迟处理滚动事件
|
||
this.scrollTimer = setTimeout(() => {
|
||
this.scrollTop = e.detail.scrollTop;
|
||
|
||
// 滚动时自动选中对应的左侧标签
|
||
const scrollTop = e.detail.scrollTop;
|
||
let currentSection = null;
|
||
let minDistance = Infinity;
|
||
|
||
// 遍历所有右侧的二级标题,找到当前最接近顶部的那个
|
||
this.rightValue.forEach((section, index) => {
|
||
// 更精确地估算每个section的位置(考虑标题高度和内容高度)
|
||
const sectionTop = index * 280; // 增加估算高度,避免过于敏感
|
||
const distance = Math.abs(sectionTop - scrollTop);
|
||
|
||
if (distance < minDistance) {
|
||
minDistance = distance;
|
||
currentSection = section;
|
||
}
|
||
});
|
||
|
||
// 只有当距离足够近时才切换左侧标签,避免过于敏感
|
||
if (currentSection && minDistance < 100 && this.leftValue.id !== currentSection.parentId) {
|
||
const parentItem = this.copyTree.find(item => item.id === currentSection.parentId);
|
||
if (parentItem) {
|
||
this.leftValue = parentItem;
|
||
|
||
// 使用更激进的防抖策略,避免频繁的左侧滚动
|
||
if (this.scrollTimer) {
|
||
clearTimeout(this.scrollTimer);
|
||
}
|
||
|
||
this.scrollTimer = setTimeout(() => {
|
||
// 只在必要时滚动左侧列表,避免频繁滚动导致的抖动
|
||
const index = this.copyTree.findIndex(i => i.id === parentItem.id);
|
||
if (index !== -1 && this.$refs.leftScroll) {
|
||
// 获取当前左侧列表的滚动位置
|
||
const currentLeftScrollTop = this.$refs.leftScroll.scrollTop || 0;
|
||
const itemHeight = 160; // 每个选项高度
|
||
const visibleHeight = 4 * itemHeight; // 可见区域高度
|
||
|
||
// 计算目标位置
|
||
const targetTop = index * itemHeight;
|
||
|
||
// 只有当目标位置不在当前可见区域内时才滚动
|
||
if (targetTop < currentLeftScrollTop || targetTop > currentLeftScrollTop + visibleHeight - itemHeight) {
|
||
let targetScrollTop = targetTop;
|
||
|
||
// 如果选中的标签在底部,确保它不会滚动出可见区域
|
||
if (targetTop > this.$refs.leftScroll.scrollHeight - visibleHeight) {
|
||
targetScrollTop = Math.max(0, this.$refs.leftScroll.scrollHeight - visibleHeight);
|
||
}
|
||
|
||
this.$refs.leftScroll.scrollTo({
|
||
top: targetScrollTop,
|
||
duration: 100 // 进一步减少滚动动画时间
|
||
});
|
||
}
|
||
}
|
||
}, 150); // 增加延迟,避免滚动冲突
|
||
}
|
||
}
|
||
}, 80); // 增加防抖延迟到80ms
|
||
},
|
||
addItem(item) {
|
||
let titiles = [];
|
||
let labels = [];
|
||
let count = 0;
|
||
|
||
// 先统计已选中的职位数量
|
||
for (const firstLayer of this.copyTree) {
|
||
for (const secondLayer of firstLayer.children) {
|
||
for (const thirdLayer of secondLayer.children) {
|
||
if (thirdLayer.checked) {
|
||
count++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (const firstLayer of this.copyTree) {
|
||
firstLayer.checkednumber = 0; // 初始化当前层级的 checked 计数
|
||
for (const secondLayer of firstLayer.children) {
|
||
for (const thirdLayer of secondLayer.children) {
|
||
// **如果是当前点击的职位**
|
||
if (thirdLayer.id === item.id) {
|
||
if (!thirdLayer.checked && count >= 5) {
|
||
// 如果已经选了 5 个,并且点击的是未选中的职位,则禁止选择
|
||
uni.showToast({
|
||
title: `最多选择5个职位`,
|
||
icon: 'none',
|
||
});
|
||
continue; // 跳过后续逻辑,继续循环
|
||
}
|
||
// 切换选中状态
|
||
thirdLayer.checked = !thirdLayer.checked;
|
||
}
|
||
// 统计被选中的第三层节点
|
||
if (thirdLayer.checked) {
|
||
titiles.push(`${thirdLayer.id}`);
|
||
labels.push(`${thirdLayer.label}`);
|
||
firstLayer.checkednumber++; // 累加计数器
|
||
}
|
||
}
|
||
}
|
||
}
|
||
titiles = titiles.join(',');
|
||
labels = labels.join(',');
|
||
this.$emit('onChange', {
|
||
ids: titiles,
|
||
labels,
|
||
});
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="stylus" scoped>
|
||
.secondary-title{
|
||
color: #333333
|
||
font-size: 28rpx
|
||
padding: 40rpx 0 10rpx 30rpx;
|
||
}
|
||
.expected-station{
|
||
width: 100%;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%
|
||
}
|
||
.sex-search
|
||
width: calc(100% - 28rpx - 28rpx);
|
||
padding: 10rpx 28rpx;
|
||
display: grid;
|
||
// grid-template-columns: 50rpx auto;
|
||
position: relative;
|
||
.iconsearch
|
||
position: absolute;
|
||
left: 40rpx;
|
||
top: 20rpx;
|
||
.searchinput
|
||
border-radius: 10rpx;
|
||
background: #FFFFFF;
|
||
padding: 10rpx 0 10rpx 58rpx;
|
||
.sex-content
|
||
background: #FFFFFF;
|
||
// border-radius: 20rpx;
|
||
width: 100%;
|
||
margin-top: 20rpx;
|
||
display: flex;
|
||
border-bottom: 2px solid #D9D9D9;
|
||
overflow: hidden;
|
||
height: 100%;
|
||
.sex-content-left
|
||
width: 198rpx;
|
||
padding: 20rpx 0 0 0;
|
||
/* 添加硬件加速优化 */
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
will-change: transform;
|
||
.left-list-btn
|
||
padding: 0 40rpx 0 24rpx;
|
||
display: grid;
|
||
place-items: center;
|
||
height: 100rpx;
|
||
text-align: center;
|
||
color: #606060;
|
||
font-size: 28rpx;
|
||
position: relative
|
||
margin-top: 60rpx
|
||
/* 优化渲染性能 */
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
.left-list-btn:first-child
|
||
margin-top: 0
|
||
// .positionNum
|
||
// position: absolute
|
||
// right: 0
|
||
// top: 50%;
|
||
// transform: translate(0, -50%)
|
||
// color: #FFFFFF
|
||
// background: #4778EC
|
||
// border-radius: 50%
|
||
// width: 36rpx;
|
||
// height: 36rpx;
|
||
.left-list-btned
|
||
color: #4778EC;
|
||
position: relative;
|
||
// .left-list-btned::after
|
||
// position: absolute;
|
||
// left: 20rpx;
|
||
// content: '';
|
||
// width: 7rpx;
|
||
// height: 38rpx;
|
||
// background: #4778EC;
|
||
// border-radius: 0rpx 0rpx 0rpx 0rpx;
|
||
|
||
.sex-content-right
|
||
// border-left: 2px solid #D9D9D9;
|
||
background: #F6F6F6;
|
||
flex: 1;
|
||
/* 添加硬件加速优化 */
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
will-change: transform;
|
||
.grid-sex
|
||
display: grid;
|
||
grid-template-columns: 50% 50%;
|
||
place-items: center;
|
||
padding: 0 20rpx 40rpx 20rpx;
|
||
.sex-right-btn
|
||
width: 228rpx;
|
||
height: 80rpx;
|
||
font-size: 28rpx;
|
||
line-height: 41rpx;
|
||
text-align: center;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 12rpx;
|
||
margin-top: 30rpx;
|
||
background: #E8EAEE;
|
||
color: #606060;
|
||
/* 优化渲染性能 */
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
.sex-right-btned
|
||
font-weight: 500
|
||
width: 224rpx;
|
||
height: 76rpx;
|
||
background: rgba(37,107,250,0.06);
|
||
border: 2rpx solid #256BFA;
|
||
color: #256BFA
|
||
/* 优化渲染性能 */
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
</style>
|