|
|
@@ -13,8 +13,11 @@
|
|
|
<!-- 拖拽容器 -->
|
|
|
<view class="channel-list" id="channelList">
|
|
|
<view v-for="(item, index) in myChannels" :key="item.id" class="channel-item"
|
|
|
- :style="{ width: itemWidth + 'px' }" @longpress="handleLongPress(index)"
|
|
|
- @touchmove="(e) => handleMove(e, index)" @touchend="handleEnd" @touchcancel="handleEnd">
|
|
|
+ :style="{ width: itemWidth + 'px' }"
|
|
|
+ @touchstart="handleTouchStart(index, $event)"
|
|
|
+ @touchmove="(e) => handleMove(e, index)"
|
|
|
+ @touchend="handleEnd"
|
|
|
+ @touchcancel="handleEnd">
|
|
|
<!-- 拖拽中的元素特殊样式 -->
|
|
|
<view class="channel-content" :class="{
|
|
|
'drag-item': isDragging && dragIndex === index
|
|
|
@@ -23,21 +26,22 @@
|
|
|
? `translate(${dragOffsetX}px, ${dragOffsetY}px) scale(1.05)`
|
|
|
: 'none',
|
|
|
zIndex: isDragging && dragIndex === index ? 999 : 1,
|
|
|
- opacity: isDragging && dragIndex === index ? 0.9 : 1, // 提高透明度更自然
|
|
|
- transition: isDragging && dragIndex === index ? 'none' : 'all 0.15s ease-out' // 拖拽中取消过渡,提升跟随速度
|
|
|
+ opacity: isDragging && dragIndex === index ? 0.9 : 1,
|
|
|
+ transition: isDragging && dragIndex === index ? 'none' : 'all 0.15s ease-out'
|
|
|
}" @tap="handleChannelTap(item)">
|
|
|
<text>{{ item.name }}</text>
|
|
|
+ <!-- 保留删除图标 -->
|
|
|
<image v-if="index !== 0" class="delete-icon" src="/static/images/remove_icon.png"
|
|
|
@tap.stop="removeFromMyChannels(index)">
|
|
|
</view>
|
|
|
|
|
|
<!-- 拖拽占位符-->
|
|
|
- <view v-if="isDragging && placeholderIndex === index" class="channel-placeholder"></view>
|
|
|
+ <!-- <view v-if="isDragging && placeholderIndex === index" class="channel-placeholder"></view> -->
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 全部频道区域-->
|
|
|
+ <!-- 全部频道区域 -->
|
|
|
<view class="channel-section">
|
|
|
<view class="section-header">
|
|
|
<text class="section-title">全部频道</text>
|
|
|
@@ -47,7 +51,7 @@
|
|
|
:style="{ width: itemWidth + 'px' }">
|
|
|
<view class="channel-content" :class="isChannelInMyList(item.id)?'active':''">
|
|
|
<text>{{ item.name }}</text>
|
|
|
-
|
|
|
+ <!-- 保留添加图标 -->
|
|
|
<image v-if="!isChannelInMyList(item.id)" class="add-icon" src="/static/images/add_icon.png"
|
|
|
@tap="addToMyChannels(item)">
|
|
|
</image>
|
|
|
@@ -89,15 +93,19 @@
|
|
|
isDragging: false,
|
|
|
dragIndex: -1, // 正在拖拽的索引
|
|
|
placeholderIndex: -1, // 占位符索引
|
|
|
- startX: 0, // 长按初始X
|
|
|
- startY: 0, // 长按初始Y
|
|
|
+ startX: 0, // 触摸初始X
|
|
|
+ startY: 0, // 触摸初始Y
|
|
|
+ elementLeft: 0, // 元素初始左坐标
|
|
|
+ elementTop: 0, // 元素初始上坐标
|
|
|
dragOffsetX: 0, // 拖拽偏移X
|
|
|
dragOffsetY: 0, // 拖拽偏移Y
|
|
|
itemHeight: 60, // 频道项高度
|
|
|
colCount: 0, // 每行显示列数
|
|
|
itemRects: [], // 所有频道项的位置信息
|
|
|
- dragThreshold: 5, // 降低阈值,更灵敏
|
|
|
- lastSwapTime: 0 // 防抖:记录上次交换时间
|
|
|
+ dragThreshold: 3, // 降低触发阈值,更灵敏
|
|
|
+ lastSwapTime: 0, // 防抖:记录上次交换时间
|
|
|
+ touchStartTime: 0, // 触摸开始时间
|
|
|
+ rectTimer: null // 计算位置的定时器
|
|
|
};
|
|
|
},
|
|
|
watch: {
|
|
|
@@ -122,41 +130,40 @@
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
- // 初始化计算布局
|
|
|
this.$nextTick(() => this.calcItemRects());
|
|
|
},
|
|
|
methods: {
|
|
|
- // 计算所有频道项的位置信息(适配uni-app)
|
|
|
+ // 计算所有频道项的位置信息(保留原有逻辑,优化计算精度)
|
|
|
calcItemRects() {
|
|
|
if (!this.myChannels.length) return;
|
|
|
|
|
|
const query = uni.createSelectorQuery().in(this);
|
|
|
query.select('#channelList').boundingClientRect(rect => {
|
|
|
if (!rect) return;
|
|
|
- // 计算每行列数
|
|
|
- this.colCount = Math.floor(rect.width / this.itemWidth);
|
|
|
+ // 计算每行列数(考虑容器内边距)
|
|
|
+ this.colCount = Math.floor((rect.width - 40) / this.itemWidth);
|
|
|
// 预计算每个项的位置
|
|
|
this.itemRects = this.myChannels.map((_, index) => {
|
|
|
const row = Math.floor(index / this.colCount);
|
|
|
const col = index % this.colCount;
|
|
|
return {
|
|
|
- left: rect.left + col * this.itemWidth + 20, // +20是容器padding
|
|
|
- top: rect.top + row * this.itemHeight + 20,
|
|
|
- right: rect.left + (col + 1) * this.itemWidth + 20,
|
|
|
- bottom: rect.top + (row + 1) * this.itemHeight + 20,
|
|
|
- centerX: rect.left + (col + 0.5) * this.itemWidth + 20,
|
|
|
- centerY: rect.top + (row + 0.5) * this.itemHeight + 20
|
|
|
+ left: rect.left + 20 + col * this.itemWidth,
|
|
|
+ top: rect.top + 20 + row * this.itemHeight,
|
|
|
+ right: rect.left + 20 + (col + 1) * this.itemWidth,
|
|
|
+ bottom: rect.top + 20 + (row + 1) * this.itemHeight,
|
|
|
+ centerX: rect.left + 20 + (col + 0.5) * this.itemWidth,
|
|
|
+ centerY: rect.top + 20 + (row + 0.5) * this.itemHeight
|
|
|
};
|
|
|
});
|
|
|
}).exec();
|
|
|
},
|
|
|
|
|
|
- // 判断频道是否已在我的列表中
|
|
|
+ // 判断频道是否已在我的列表中(保留原有功能)
|
|
|
isChannelInMyList(channelId) {
|
|
|
return this.myChannels.some(item => item.id === channelId);
|
|
|
},
|
|
|
|
|
|
- // 添加频道到我的频道
|
|
|
+ // 添加频道到我的频道(保留原有功能)
|
|
|
addToMyChannels(channel) {
|
|
|
if (this.isChannelInMyList(channel.id)) return;
|
|
|
|
|
|
@@ -168,7 +175,7 @@
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- // 从我的频道中移除
|
|
|
+ // 从我的频道中移除(保留原有功能)
|
|
|
removeFromMyChannels(index) {
|
|
|
if (index === 0 || this.isDragging) return;
|
|
|
|
|
|
@@ -180,109 +187,114 @@
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- // 点击频道
|
|
|
+ // 点击频道(保留原有功能)
|
|
|
handleChannelTap(channel) {
|
|
|
if (this.isDragging) return;
|
|
|
this.activeChannel = channel.id;
|
|
|
this.$emit('channel-tap', channel);
|
|
|
},
|
|
|
|
|
|
- // 长按开始拖拽(简化逻辑,提升响应速度)
|
|
|
- handleLongPress(index) {
|
|
|
- if (index === 0) return; // 第一个频道不能拖拽
|
|
|
-
|
|
|
- // 重新计算位置信息
|
|
|
- this.calcItemRects();
|
|
|
+ // ========== 以下是拖拽功能的核心优化 ==========
|
|
|
+ // 触摸开始:记录初始信息,替代原longpress
|
|
|
+ handleTouchStart(index, e) {
|
|
|
+ // 第一个频道不能拖拽
|
|
|
+ if (index === 0) return;
|
|
|
+ this.touchStartTime = Date.now();
|
|
|
+ const touch = e.touches[0];
|
|
|
+ this.startX = touch.clientX;
|
|
|
+ this.startY = touch.clientY;
|
|
|
|
|
|
- this.isDragging = true;
|
|
|
- this.dragIndex = index;
|
|
|
- this.placeholderIndex = index;
|
|
|
- // 直接初始化,无需等待系统信息,提升响应
|
|
|
- this.startX = 0;
|
|
|
- this.startY = 0;
|
|
|
- this.dragOffsetX = 0;
|
|
|
- this.dragOffsetY = 0;
|
|
|
- this.lastSwapTime = Date.now();
|
|
|
+ // 获取当前元素的初始位置
|
|
|
+ const query = uni.createSelectorQuery().in(this);
|
|
|
+ query.selectAll('.channel-item').boundingClientRect(rects => {
|
|
|
+ const currentRect = rects[index];
|
|
|
+ if (currentRect) {
|
|
|
+ this.elementLeft = currentRect.left;
|
|
|
+ this.elementTop = currentRect.top;
|
|
|
+ }
|
|
|
+ }).exec();
|
|
|
},
|
|
|
|
|
|
- // 拖拽移动(优化响应速度)
|
|
|
+ // 拖拽移动:优化跟随流畅度和交换逻辑
|
|
|
handleMove(e, index) {
|
|
|
- if (!this.isDragging || this.dragIndex !== index) return;
|
|
|
+ if (index === 0) return;
|
|
|
|
|
|
- // 阻止默认滚动(兼容多端)
|
|
|
- if (e.preventDefault) {
|
|
|
- e.preventDefault();
|
|
|
- } else {
|
|
|
- e.returnValue = false;
|
|
|
- }
|
|
|
+ // 阻止页面滚动和默认行为
|
|
|
+ e.stopPropagation();
|
|
|
+ if (e.preventDefault) e.preventDefault();
|
|
|
+ else e.returnValue = false;
|
|
|
|
|
|
- // 获取触摸点(uni-app的touches参数)
|
|
|
const touch = e.touches[0];
|
|
|
const currentX = touch.clientX;
|
|
|
const currentY = touch.clientY;
|
|
|
|
|
|
- // 初始化起始位置
|
|
|
- if (this.startX === 0 && this.startY === 0) {
|
|
|
- this.startX = currentX;
|
|
|
- this.startY = currentY;
|
|
|
- return;
|
|
|
+ // 未进入拖拽状态:判断是否触发拖拽(移动距离+触摸时间)
|
|
|
+ if (!this.isDragging) {
|
|
|
+ const moveDistance = Math.hypot(currentX - this.startX, currentY - this.startY);
|
|
|
+ const touchTime = Date.now() - this.touchStartTime;
|
|
|
+ // 移动超过阈值+触摸时间>80ms,触发拖拽(避免误触)
|
|
|
+ if (moveDistance >= this.dragThreshold && touchTime > 80) {
|
|
|
+ this.isDragging = true;
|
|
|
+ this.dragIndex = index;
|
|
|
+ this.placeholderIndex = index;
|
|
|
+ this.lastSwapTime = Date.now();
|
|
|
+ // 实时计算位置
|
|
|
+ this.calcItemRects();
|
|
|
+ } else {
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // 实时计算偏移量(无延迟)
|
|
|
- this.dragOffsetX = currentX - this.startX;
|
|
|
- this.dragOffsetY = currentY - this.startY;
|
|
|
+ // 计算偏移量:基于元素初始位置,跟随更丝滑
|
|
|
+ this.dragOffsetX = currentX - this.elementLeft;
|
|
|
+ this.dragOffsetY = currentY - this.elementTop;
|
|
|
|
|
|
- // 防抖:20ms内只交换一次,避免频繁触发卡顿
|
|
|
+ // 防抖交换:15ms内只交换一次,避免卡顿
|
|
|
const now = Date.now();
|
|
|
- if (now - this.lastSwapTime > 20) {
|
|
|
+ if (now - this.lastSwapTime > 15) {
|
|
|
this.swapChannelItem(currentX, currentY);
|
|
|
this.lastSwapTime = now;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 交换频道项(简化逻辑,减少计算)
|
|
|
+ // 交换频道项:优化碰撞检测,精准交换
|
|
|
swapChannelItem(x, y) {
|
|
|
if (this.itemRects.length === 0) return;
|
|
|
|
|
|
- // 找到当前触摸位置对应的频道索引(简化碰撞检测)
|
|
|
+ // 找到当前触摸点对应的目标项
|
|
|
let targetIndex = -1;
|
|
|
- // 只遍历可视区域附近的项,减少计算
|
|
|
- const minRow = Math.max(0, Math.floor((y - 100) / this.itemHeight));
|
|
|
- const maxRow = Math.min(Math.ceil(this.myChannels.length / this.colCount), Math.floor((y + 100) / this
|
|
|
- .itemHeight));
|
|
|
-
|
|
|
for (let i = 1; i < this.myChannels.length; i++) {
|
|
|
const rect = this.itemRects[i];
|
|
|
- // 扩大检测区域,更容易触发交换
|
|
|
- if (x >= rect.left - 10 && x <= rect.right + 10 && y >= rect.top - 10 && y <= rect.bottom + 10) {
|
|
|
+ // 扩大检测范围,更容易触发交换
|
|
|
+ if (x >= rect.left - 5 && x <= rect.right + 5 && y >= rect.top - 5 && y <= rect.bottom + 5) {
|
|
|
targetIndex = i;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 交换逻辑(减少不必要的判断)
|
|
|
+ // 交换逻辑:只在目标不同时执行
|
|
|
if (targetIndex !== -1 && targetIndex !== this.placeholderIndex) {
|
|
|
- // 交换数组元素(直接操作,无冗余)
|
|
|
- [this.myChannels[this.dragIndex], this.myChannels[targetIndex]] = [this.myChannels[targetIndex], this
|
|
|
- .myChannels[this.dragIndex]
|
|
|
- ];
|
|
|
-
|
|
|
- // 更新索引
|
|
|
+ // 交换数组元素
|
|
|
+ [this.myChannels[this.dragIndex], this.myChannels[targetIndex]] = [this.myChannels[targetIndex], this.myChannels[this.dragIndex]];
|
|
|
+ // 更新占位符和拖拽索引
|
|
|
this.placeholderIndex = targetIndex;
|
|
|
this.dragIndex = targetIndex;
|
|
|
+ // 更新元素初始位置,让拖拽跟随更准确
|
|
|
+ this.elementLeft = this.itemRects[targetIndex].left;
|
|
|
+ this.elementTop = this.itemRects[targetIndex].top;
|
|
|
|
|
|
- // 延迟计算位置,避免频繁触发
|
|
|
+ // 延迟计算位置,避免频繁DOM查询
|
|
|
if (!this.rectTimer) {
|
|
|
this.rectTimer = setTimeout(() => {
|
|
|
this.calcItemRects();
|
|
|
clearTimeout(this.rectTimer);
|
|
|
this.rectTimer = null;
|
|
|
- }, 50);
|
|
|
+ }, 30);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 拖拽结束/取消(清理定时器)
|
|
|
+ // 拖拽结束/取消:重置状态(保留原有逻辑,优化定时器清理)
|
|
|
handleEnd() {
|
|
|
if (!this.isDragging) return;
|
|
|
|
|
|
@@ -292,7 +304,7 @@
|
|
|
this.rectTimer = null;
|
|
|
}
|
|
|
|
|
|
- // 重置状态
|
|
|
+ // 重置拖拽状态
|
|
|
this.isDragging = false;
|
|
|
this.dragIndex = -1;
|
|
|
this.placeholderIndex = -1;
|
|
|
@@ -300,24 +312,33 @@
|
|
|
this.dragOffsetY = 0;
|
|
|
this.startX = 0;
|
|
|
this.startY = 0;
|
|
|
+ this.elementLeft = 0;
|
|
|
+ this.elementTop = 0;
|
|
|
|
|
|
// 保存顺序
|
|
|
this.saveChannelOrder();
|
|
|
},
|
|
|
|
|
|
- // 保存频道顺序
|
|
|
+ // 保存频道顺序(保留原有功能)
|
|
|
saveChannelOrder() {
|
|
|
this.$emit('order-change', JSON.parse(JSON.stringify(this.myChannels)));
|
|
|
this.$emit('channels-change', {
|
|
|
myChannels: this.myChannels,
|
|
|
allChannels: this.allChannels
|
|
|
});
|
|
|
+ },
|
|
|
+
|
|
|
+ // 关闭弹窗(保留原有功能)
|
|
|
+ handleClose() {
|
|
|
+ this.show = false;
|
|
|
+ this.$emit('close');
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
+ // 保留你所有的样式,仅优化拖拽相关的样式
|
|
|
.channel-container {
|
|
|
border-radius: 32rpx 32rpx 0rpx 0rpx;
|
|
|
padding: 24rpx;
|
|
|
@@ -363,7 +384,6 @@
|
|
|
|
|
|
.channel-item {
|
|
|
position: relative;
|
|
|
- // padding: 10rpx;
|
|
|
box-sizing: border-box;
|
|
|
margin-bottom: 10rpx;
|
|
|
margin-right: 18rpx;
|
|
|
@@ -388,14 +408,17 @@
|
|
|
color: #999999;
|
|
|
}
|
|
|
|
|
|
- // 拖拽中的元素
|
|
|
+ // 拖拽中的元素 - 优化阴影和透明度
|
|
|
&.drag-item {
|
|
|
position: relative;
|
|
|
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
|
|
- background-color: inherit;
|
|
|
+ // 增强阴影效果:增大阴影范围、提高不透明度,让阴影更明显
|
|
|
+ box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.15);
|
|
|
+ // 降低透明度,让拖拽元素有半透明效果(0.8比0.9更明显)
|
|
|
+ opacity: 0.8;
|
|
|
+ background-color: #fff; // 增加白色背景,让半透明效果更自然
|
|
|
}
|
|
|
|
|
|
- // 图标样式
|
|
|
+ // 图标样式 - 完全保留
|
|
|
.add-icon,
|
|
|
.delete-icon,
|
|
|
.added-mark {
|
|
|
@@ -412,8 +435,6 @@
|
|
|
}
|
|
|
|
|
|
.add-icon {
|
|
|
- // background-color: #52c41a;
|
|
|
-
|
|
|
.icon-plus {
|
|
|
font-size: 30rpx;
|
|
|
color: #fff;
|
|
|
@@ -422,8 +443,6 @@
|
|
|
}
|
|
|
|
|
|
.delete-icon {
|
|
|
- // background-color: #ff4d4f;
|
|
|
-
|
|
|
.icon-minus {
|
|
|
font-size: 36rpx;
|
|
|
color: #fff;
|
|
|
@@ -444,13 +463,14 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 拖拽占位符
|
|
|
- .channel-placeholder {
|
|
|
- width: 100%;
|
|
|
- height: 60rpx;
|
|
|
- border-radius: 30rpx;
|
|
|
- box-sizing: border-box;
|
|
|
- }
|
|
|
+ // 拖拽占位符 - 优化背景色,更贴近UI
|
|
|
+ // .channel-placeholder {
|
|
|
+ // width: 100%;
|
|
|
+ // height: 60rpx;
|
|
|
+ // border-radius: 30rpx;
|
|
|
+ // box-sizing: border-box;
|
|
|
+ // background-color: #E8EBF0;
|
|
|
+ // }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -467,4 +487,12 @@
|
|
|
text-align: center;
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
+
|
|
|
+ .w40 {
|
|
|
+ width: 40rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ .h40 {
|
|
|
+ height: 40rpx;
|
|
|
+ }
|
|
|
+</style>
|