486 lines
13 KiB
Vue
486 lines
13 KiB
Vue
<template>
|
||
<view class="location-display" :class="{ 'scroll ': isScrolled }" @click="handleLocationClick">
|
||
<text v-show="!positioningType" class="iconfont icon-ic_location1"></text>
|
||
<view class="location-text">
|
||
<!-- 小程序使用CSS动画类 -->
|
||
<!-- #ifdef MP -->
|
||
<text class="text-content mp-text" :class="animationClass" :style="{transform: `translateX(${textOffset}px)`}">{{ displayText }}</text>
|
||
<!-- #endif -->
|
||
<!-- 其他平台使用JS动画 -->
|
||
<!-- #ifndef MP -->
|
||
<text class="text-content" :style="{transform: `translateX(${textOffset}px)`}">{{ displayText }}</text>
|
||
<!-- #endif -->
|
||
</view>
|
||
<text v-show="positioningType" class="iconfont icon-ic_downarrow ml-4"></text>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'LocationDisplay',
|
||
props: {
|
||
// 显示的位置文字
|
||
text: {
|
||
type: String,
|
||
default: '定位中...'
|
||
},
|
||
// 最大显示宽度
|
||
maxWidth: {
|
||
type: String,
|
||
default: '118rpx'
|
||
},
|
||
// 最大高度
|
||
maxHeight: {
|
||
type: String,
|
||
default: '42rpx'
|
||
},
|
||
// 是否处于滚动状态 外界用于时候更换颜色
|
||
isScrolled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 是否启用文字滚动功能
|
||
enableScroll: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
positioningType:{
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
|
||
},
|
||
data() {
|
||
return {
|
||
textOffset: 0, // 文字偏移量
|
||
scrollTimer: null, // 滚动定时器
|
||
isScrolling: false, // 是否正在滚动
|
||
// 小程序CSS动画状态
|
||
animationClass: '', // 动画类名
|
||
maxDistance: 0, // 最大滚动距离
|
||
currentLocationName: '', // 当前位置名称
|
||
storageTimer: null, // 位置监听定时器
|
||
isDestroyed: false // 组件销毁标记
|
||
}
|
||
},
|
||
computed: {
|
||
displayText() {
|
||
// 如果外部传入了text,优先使用外部text
|
||
if (this.text && this.text !== '定位中...') {
|
||
return this.text;
|
||
}
|
||
if(this.currentLocationName){
|
||
return this.currentLocationName
|
||
} else {
|
||
// 否则使用内部管理的位置数据
|
||
return this.positioningType ? '选择圈层' : '选择位置';
|
||
}
|
||
|
||
}
|
||
},
|
||
mounted() {
|
||
// 只有在没有外部传入text时,才启用内部位置管理
|
||
if (!this.text || this.text === '定位中...') {
|
||
this.initLocationData();
|
||
this.setupStorageListener();
|
||
}
|
||
|
||
// 启动滚动
|
||
this.resetAndStartScroll();
|
||
},
|
||
created() {
|
||
// LocationDisplay 组件初始化
|
||
},
|
||
watch: {
|
||
text() {
|
||
this.resetAndStartScroll();
|
||
},
|
||
displayText() {
|
||
this.resetAndStartScroll();
|
||
},
|
||
enableScroll() {
|
||
if (!this.enableScroll) {
|
||
this.stopScroll();
|
||
this.textOffset = 0;
|
||
} else {
|
||
this.resetAndStartScroll();
|
||
}
|
||
}
|
||
},
|
||
beforeDestroy() {
|
||
this.isDestroyed = true;
|
||
this.stopScroll();
|
||
this.stopLocationListener();
|
||
},
|
||
methods: {
|
||
// 重置并开始滚动 - 临时停用
|
||
resetAndStartScroll() {
|
||
this.stopScroll();
|
||
this.textOffset = 0;
|
||
this.isScrolling = false;
|
||
|
||
// 临时停用滚动功能
|
||
return;
|
||
|
||
if (!this.enableScroll) return;
|
||
|
||
this.$nextTick(() => {
|
||
this.startSimpleScroll();
|
||
});
|
||
},
|
||
|
||
// 完整的双向滚动逻辑
|
||
startSimpleScroll() {
|
||
if (this.isScrolling) return;
|
||
|
||
// 先计算需要滚动的距离
|
||
this.$nextTick(() => {
|
||
this.calculateScrollDistance();
|
||
});
|
||
},
|
||
|
||
// 计算滚动距离
|
||
calculateScrollDistance() {
|
||
const query = uni.createSelectorQuery().in(this);
|
||
query.select('.text-content').boundingClientRect();
|
||
query.select('.location-text').boundingClientRect();
|
||
query.exec((res) => {
|
||
if (res[0] && res[1]) {
|
||
const textWidth = res[0].width; // 文字总宽度
|
||
const containerWidth = res[1].width; // 容器宽度
|
||
let overflowWidth = textWidth - containerWidth; // 溢出宽度
|
||
|
||
// 重新计算正确的滚动距离
|
||
// 目标:让文字末尾刚好显示在容器右边缘,而不是让文字滚动消失
|
||
// 正确公式:滚动距离 = 文字宽度 - 容器宽度 (这样文字末尾刚好到达容器右边)
|
||
// 但实际上我们不需要滚动这么远,只需要让末尾文字可见即可
|
||
|
||
// 小程序保守策略:确保文字末尾能显示,但保留开头部分
|
||
// #ifdef MP
|
||
// 策略:滚动距离 = 溢出宽度 - 10px,这样文字末尾显示,开头也保留10px可见
|
||
overflowWidth = Math.max(0, overflowWidth - 10);
|
||
// #endif
|
||
|
||
if (overflowWidth > 0) {
|
||
// 需要滚动,开始双向滚动
|
||
this.startTwoWayScroll(overflowWidth);
|
||
} else {
|
||
// console.log('LocationDisplay无需滚动');
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 双向滚动:先向左显示末尾,再向右回到开头
|
||
startTwoWayScroll(maxDistance) {
|
||
// 延迟1秒开始第一阶段滚动
|
||
setTimeout(() => {
|
||
if (this.isDestroyed || !this.enableScroll) return;
|
||
|
||
this.isScrolling = true;
|
||
this.textOffset = 0;
|
||
|
||
// 第一阶段:向左滚动显示文字末尾
|
||
this.scrollToEnd(maxDistance);
|
||
}, 1000);
|
||
},
|
||
|
||
// 第一阶段:向左滚动到末尾
|
||
scrollToEnd(maxDistance) {
|
||
// 小程序优化:降低频率,增加步进
|
||
// #ifdef MP
|
||
const scrollStep = 1; // 小程序用更大步进
|
||
const scrollInterval = 30; // 小程序用更长间隔
|
||
// #endif
|
||
|
||
// H5保持丝滑
|
||
// #ifdef H5
|
||
const scrollStep = 0.5;
|
||
const scrollInterval = 20;
|
||
// #endif
|
||
|
||
// APP采用中等参数
|
||
// #ifdef APP-PLUS
|
||
const scrollStep = 0.8;
|
||
const scrollInterval = 25;
|
||
// #endif
|
||
this.scrollTimer = setInterval(() => {
|
||
// 小程序模式:每次更新前检查是否会超出边界
|
||
// #ifdef MP
|
||
const nextOffset = this.textOffset - scrollStep;
|
||
if (nextOffset < -maxDistance) {
|
||
// 直接定位到目标位置,不再继续滚动
|
||
this.textOffset = -maxDistance;
|
||
clearInterval(this.scrollTimer);
|
||
|
||
setTimeout(() => {
|
||
this.scrollToStart();
|
||
}, 2000);
|
||
return;
|
||
}
|
||
this.textOffset = nextOffset;
|
||
// #endif
|
||
|
||
// H5和APP正常滚动
|
||
// #ifndef MP
|
||
this.textOffset -= scrollStep;
|
||
|
||
// 滚动到末尾位置(文字末尾刚好显示完整)
|
||
if (this.textOffset <= -maxDistance) {
|
||
this.textOffset = -maxDistance; // 精确定位
|
||
clearInterval(this.scrollTimer);
|
||
|
||
// 停留2秒后开始第二阶段
|
||
setTimeout(() => {
|
||
this.scrollToStart();
|
||
}, 2000);
|
||
}
|
||
// #endif
|
||
}, scrollInterval);
|
||
},
|
||
|
||
// 第二阶段:向右滚动回到开头
|
||
scrollToStart() {
|
||
// 小程序优化:使用相同的平台参数
|
||
// #ifdef MP
|
||
const scrollStep = 1;
|
||
const scrollInterval = 30;
|
||
// #endif
|
||
|
||
// H5保持丝滑
|
||
// #ifdef H5
|
||
const scrollStep = 0.5;
|
||
const scrollInterval = 20;
|
||
// #endif
|
||
|
||
// APP采用中等参数
|
||
// #ifdef APP-PLUS
|
||
const scrollStep = 0.8;
|
||
const scrollInterval = 25;
|
||
// #endif
|
||
this.scrollTimer = setInterval(() => {
|
||
// 小程序模式:每次更新前检查是否会超出边界
|
||
// #ifdef MP
|
||
const nextOffset = this.textOffset + scrollStep;
|
||
if (nextOffset > 0) {
|
||
// 直接定位到起始位置,不再继续滚动
|
||
this.textOffset = 0;
|
||
this.stopScroll();
|
||
return;
|
||
}
|
||
this.textOffset = nextOffset;
|
||
// #endif
|
||
|
||
// H5和APP正常滚动
|
||
// #ifndef MP
|
||
this.textOffset += scrollStep;
|
||
|
||
// 回到初始位置
|
||
if (this.textOffset >= 0) {
|
||
this.textOffset = 0; // 精确定位到起始位置
|
||
this.stopScroll();
|
||
}
|
||
// #endif
|
||
}, scrollInterval);
|
||
},
|
||
|
||
|
||
// 停止滚动
|
||
stopScroll() {
|
||
if (this.scrollTimer) {
|
||
clearInterval(this.scrollTimer);
|
||
this.scrollTimer = null;
|
||
}
|
||
this.isScrolling = false;
|
||
},
|
||
|
||
// 点击组件圈层
|
||
handleLocationClick() {
|
||
this.$emit('click');
|
||
},
|
||
|
||
// ============ 位置管理相关方法 ============
|
||
|
||
// 从缓存获取位置数据 - 以location_info为准
|
||
initLocationData() {
|
||
if (this.isDestroyed) return;
|
||
|
||
try {
|
||
// 优先使用location_info(用户最新的主动定位选择)
|
||
const locationInfo = uni.getStorageSync('location_info');
|
||
if (locationInfo && (locationInfo.name || locationInfo.address) && !this.positioningType) {
|
||
this.currentLocationName = locationInfo.name || locationInfo.address;
|
||
return;
|
||
}
|
||
|
||
// 备选:使用areas_info(圈层选择的位置)
|
||
const areasInfo = uni.getStorageSync('areas_info');
|
||
if (areasInfo && Array.isArray(areasInfo) && areasInfo.length > 0 && this.positioningType) {
|
||
// 显示最新选择的圈层
|
||
const latestArea = areasInfo[0];
|
||
this.currentLocationName = latestArea.name || latestArea.address || '选择圈层';
|
||
return;
|
||
}
|
||
|
||
// 默认显示
|
||
this.currentLocationName = this.positioningType ? '选择圈层' : '选择位置';
|
||
} catch (error) {
|
||
this.currentLocationName = this.positioningType ? '选择圈层' : '选择位置';
|
||
}
|
||
},
|
||
|
||
// 设置存储监听
|
||
setupStorageListener() {
|
||
if (this.isDestroyed) return;
|
||
|
||
// 使用定时器定期检查缓存变化
|
||
this.storageTimer = setInterval(() => {
|
||
if (!this.isDestroyed) {
|
||
this.checkLocationUpdate();
|
||
}
|
||
}, 1000); // 每秒检查一次
|
||
},
|
||
|
||
// 检查位置更新 - 以location_info为准
|
||
checkLocationUpdate() {
|
||
if (this.isDestroyed) return;
|
||
|
||
try {
|
||
let newLocationName = this.positioningType ? '选择圈层' : '选择位置';
|
||
|
||
// 优先使用location_info(用户最新的主动定位选择)
|
||
const locationInfo = uni.getStorageSync('location_info');
|
||
if (locationInfo && (locationInfo.name || locationInfo.address)&& !this.positioningType) {
|
||
newLocationName = locationInfo.name || locationInfo.address;
|
||
} else {
|
||
// 备选:使用areas_info(圈层选择的位置)
|
||
const areasInfo = uni.getStorageSync('areas_info');
|
||
if (areasInfo && Array.isArray(areasInfo) && areasInfo.length > 0 && this.positioningType) {
|
||
const latestArea = areasInfo[0];
|
||
newLocationName = latestArea.name || latestArea.address || '选择圈层';
|
||
}
|
||
}
|
||
|
||
// 如果地址发生变化,更新显示
|
||
if (newLocationName !== this.currentLocationName) {
|
||
this.currentLocationName = newLocationName;
|
||
// displayText计算属性会自动更新,触发watch重新计算滚动
|
||
}
|
||
} catch (error) {
|
||
// 位置更新失败,忽略
|
||
}
|
||
},
|
||
|
||
// 停止位置监听
|
||
stopLocationListener() {
|
||
if (this.storageTimer) {
|
||
clearInterval(this.storageTimer);
|
||
this.storageTimer = null;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.scroll{
|
||
.iconfont {
|
||
color: #000 !important;
|
||
}
|
||
|
||
.text-content {
|
||
color: #000 !important;
|
||
}
|
||
}
|
||
.location-display {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
height: 42rpx;
|
||
max-height: 42rpx;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
margin-right: 8rpx;
|
||
width: 154rpx;
|
||
|
||
.iconfont {
|
||
font-size: 26rpx;
|
||
color: #fff;
|
||
margin-right: 2rpx;
|
||
flex-shrink: 0;
|
||
height: 24rpx;
|
||
line-height: 24rpx;
|
||
margin-right: 4rpx;
|
||
/* #ifdef H5 */
|
||
font-size: 24rpx;
|
||
/* #endif */
|
||
}
|
||
|
||
.location-text {
|
||
line-height: 32px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.text-content {
|
||
/* 基础样式 */
|
||
height: 18px;
|
||
font-family: PingFang SC, PingFang SC;
|
||
font-weight: 400;
|
||
font-size: 13px;
|
||
color: #FFFFFF;
|
||
line-height: 18px;
|
||
text-align: left;
|
||
font-style: normal;
|
||
text-transform: none;
|
||
display: inline-block;
|
||
|
||
/* H5平台特殊处理 */
|
||
/* #ifdef H5 */
|
||
min-width: 100%;
|
||
font-size: 12px;
|
||
line-height: 42rpx;
|
||
height: 42rpx;
|
||
display: flex;
|
||
/* 禁用CSS transition,让JS动画完全控制 */
|
||
transition: none !important;
|
||
transform: translateZ(0); /* 启用硬件加速 */
|
||
will-change: transform; /* 优化渲染性能 */
|
||
/* #endif */
|
||
|
||
/* 小程序/APP平台 */
|
||
/* #ifdef MP || APP-PLUS */
|
||
width: auto;
|
||
min-width: 500rpx;
|
||
/* 禁用transition,避免小程序抖动 */
|
||
transition: none !important;
|
||
/* 小程序强制GPU渲染优化 */
|
||
transform: translateZ(0) translate3d(0,0,0);
|
||
will-change: transform;
|
||
backface-visibility: hidden;
|
||
/* #endif */
|
||
|
||
/* 小程序专用CSS动画 */
|
||
/* #ifdef MP */
|
||
&.mp-text {
|
||
/* 基础优化 */
|
||
transform: translateZ(0);
|
||
will-change: transform;
|
||
|
||
/* 动画效果暂时禁用,继续使用JS控制 */
|
||
/* transition: transform 3s ease-in-out; */
|
||
}
|
||
|
||
&.mp-scroll-to-end {
|
||
/* 滚动到末尾的动画 - 暂时不用 */
|
||
}
|
||
|
||
&.mp-scroll-to-start {
|
||
/* 滚动回起始位置的动画 - 暂时不用 */
|
||
}
|
||
/* #endif */
|
||
}
|
||
}
|
||
</style> |