318 lines
6.4 KiB
Plaintext
318 lines
6.4 KiB
Plaintext
<template>
|
||
<view class="cl-index-bar" :class="[pt.className]">
|
||
<view
|
||
class="cl-index-bar__list"
|
||
@touchstart="onTouchStart"
|
||
@touchmove.stop.prevent="onTouchMove"
|
||
@touchend="onTouchEnd"
|
||
>
|
||
<view class="cl-index-bar__item" v-for="(item, index) in list" :key="index">
|
||
<view
|
||
class="cl-index-bar__item-inner"
|
||
:class="{
|
||
'is-active': activeIndex == index
|
||
}"
|
||
>
|
||
<text
|
||
class="cl-index-bar__item-text"
|
||
:class="{
|
||
'is-active': activeIndex == index || isDark
|
||
}"
|
||
>{{ item }}</text
|
||
>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="cl-index-bar__alert" v-show="showAlert">
|
||
<view class="cl-index-bar__alert-icon dark:!bg-surface-800">
|
||
<view class="cl-index-bar__alert-arrow dark:!bg-surface-800"></view>
|
||
<text class="cl-index-bar__alert-text dark:!text-white">{{ alertText }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, getCurrentInstance, nextTick, onMounted, ref, watch, type PropType } from "vue";
|
||
import { isDark, isEmpty, parsePt } from "@/cool";
|
||
|
||
defineOptions({
|
||
name: "cl-index-bar"
|
||
});
|
||
|
||
const props = defineProps({
|
||
pt: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
modelValue: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
list: {
|
||
type: Array as PropType<string[]>,
|
||
default: () => []
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(["update:modelValue", "change"]);
|
||
|
||
const { proxy } = getCurrentInstance()!;
|
||
|
||
type PassThrough = {
|
||
className?: string;
|
||
};
|
||
|
||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||
|
||
// 存储索引条整体的位置信息
|
||
const barRect = ref({
|
||
height: 0,
|
||
width: 0,
|
||
left: 0,
|
||
top: 0
|
||
} as NodeInfo);
|
||
|
||
// 存储所有索引项的位置信息数组
|
||
const itemsRect = ref<NodeInfo[]>([]);
|
||
|
||
// 是否正在触摸
|
||
const isTouching = ref(false);
|
||
|
||
// 是否显示提示弹窗
|
||
const showAlert = ref(false);
|
||
|
||
// 当前提示弹窗显示的文本
|
||
const alertText = ref("");
|
||
|
||
// 当前触摸过程中的临时索引
|
||
const activeIndex = ref(-1);
|
||
|
||
/**
|
||
* 获取索引条及其所有子项的位置信息
|
||
* 用于后续触摸时判断手指所在的索引项
|
||
*/
|
||
function getRect() {
|
||
nextTick(() => {
|
||
setTimeout(() => {
|
||
uni.createSelectorQuery()
|
||
.in(proxy)
|
||
.select(".cl-index-bar")
|
||
.boundingClientRect()
|
||
.exec((bar) => {
|
||
if (isEmpty(bar)) {
|
||
return;
|
||
}
|
||
|
||
// 获取索引条整体的位置信息
|
||
barRect.value = bar[0] as NodeInfo;
|
||
|
||
// 获取所有索引项的位置信息
|
||
uni.createSelectorQuery()
|
||
.in(proxy)
|
||
.selectAll(".cl-index-bar__item")
|
||
.boundingClientRect()
|
||
.exec((items) => {
|
||
if (isEmpty(items)) {
|
||
getRect();
|
||
return;
|
||
}
|
||
|
||
itemsRect.value = items[0] as NodeInfo[];
|
||
});
|
||
});
|
||
}, 350);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 根据触摸点的Y坐标,计算出最接近的索引项下标
|
||
* @param clientY 触摸点的Y坐标(相对于屏幕)
|
||
* @returns 最接近的索引项下标
|
||
*/
|
||
function getIndex(clientY: number): number {
|
||
if (itemsRect.value.length == 0) {
|
||
// 没有索引项时,默认返回0
|
||
return 0;
|
||
}
|
||
|
||
// 初始化最接近的索引和最小距离
|
||
let closestIndex = 0;
|
||
let minDistance = Number.MAX_VALUE;
|
||
|
||
// 遍历所有索引项,找到距离触摸点最近的项
|
||
for (let i = 0; i < itemsRect.value.length; i++) {
|
||
const item = itemsRect.value[i];
|
||
// 计算每个item的中心点Y坐标
|
||
const itemCenterY = (item.top ?? 0) + (item.height ?? 0) / 2;
|
||
// 计算触摸点到中心点的距离
|
||
const distance = Math.abs(clientY - itemCenterY);
|
||
|
||
// 更新最小距离和索引
|
||
if (distance < minDistance) {
|
||
minDistance = distance;
|
||
closestIndex = i;
|
||
}
|
||
}
|
||
|
||
// 边界处理,防止越界
|
||
if (closestIndex < 0) {
|
||
closestIndex = 0;
|
||
} else if (closestIndex >= props.list.length) {
|
||
closestIndex = props.list.length - 1;
|
||
}
|
||
|
||
return closestIndex;
|
||
}
|
||
|
||
/**
|
||
* 更新触摸过程中的显示状态
|
||
* @param index 新的索引
|
||
*/
|
||
function updateActive(index: number) {
|
||
// 更新当前触摸索引
|
||
activeIndex.value = index;
|
||
// 更新弹窗提示文本
|
||
alertText.value = props.list[index];
|
||
}
|
||
|
||
/**
|
||
* 触摸开始事件处理
|
||
* @param e 触摸事件对象
|
||
*/
|
||
function onTouchStart(e: TouchEvent) {
|
||
// 标记为正在触摸
|
||
isTouching.value = true;
|
||
|
||
// 显示提示弹窗
|
||
showAlert.value = true;
|
||
|
||
// 获取第一个触摸点
|
||
const touch = e.touches[0];
|
||
|
||
// 计算对应的索引
|
||
const index = getIndex(touch.clientY);
|
||
|
||
// 更新显示状态
|
||
updateActive(index);
|
||
}
|
||
|
||
/**
|
||
* 触摸移动事件处理
|
||
* @param e 触摸事件对象
|
||
*/
|
||
function onTouchMove(e: TouchEvent) {
|
||
// 未处于触摸状态时不处理
|
||
if (!isTouching.value) return;
|
||
|
||
// 获取第一个触摸点
|
||
const touch = e.touches[0];
|
||
|
||
// 计算对应的索引
|
||
const index = getIndex(touch.clientY);
|
||
|
||
// 更新显示状态
|
||
updateActive(index);
|
||
}
|
||
|
||
/**
|
||
* 触摸结束事件处理
|
||
* 结束后延迟隐藏提示弹窗,并确认最终选中的索引
|
||
*/
|
||
function onTouchEnd() {
|
||
isTouching.value = false; // 标记为未触摸
|
||
|
||
// 更新值
|
||
if (props.modelValue != activeIndex.value) {
|
||
emit("update:modelValue", activeIndex.value);
|
||
emit("change", activeIndex.value);
|
||
}
|
||
|
||
// 延迟500ms后隐藏提示弹窗,提升用户体验
|
||
setTimeout(() => {
|
||
showAlert.value = false;
|
||
}, 500);
|
||
}
|
||
|
||
watch(
|
||
computed(() => props.modelValue),
|
||
(val: number) => {
|
||
activeIndex.value = val;
|
||
},
|
||
{
|
||
immediate: true
|
||
}
|
||
);
|
||
|
||
onMounted(() => {
|
||
watch(
|
||
computed(() => props.list),
|
||
() => {
|
||
getRect();
|
||
},
|
||
{
|
||
immediate: true
|
||
}
|
||
);
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.cl-index-bar {
|
||
@apply flex flex-col items-center justify-center;
|
||
@apply absolute bottom-0 right-0 h-full;
|
||
z-index: 110;
|
||
|
||
&__item {
|
||
@apply flex flex-col items-center justify-center;
|
||
width: 50rpx;
|
||
height: 34rpx;
|
||
|
||
&-inner {
|
||
@apply rounded-full flex flex-row items-center justify-center;
|
||
width: 30rpx;
|
||
height: 30rpx;
|
||
|
||
&.is-active {
|
||
@apply bg-primary-500;
|
||
}
|
||
}
|
||
|
||
&-text {
|
||
@apply text-xs text-surface-500;
|
||
|
||
&.is-active {
|
||
@apply text-white;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.cl-index-bar__alert {
|
||
@apply absolute bottom-0 right-8 h-full flex flex-col items-center justify-center;
|
||
width: 120rpx;
|
||
z-index: 110;
|
||
|
||
&-icon {
|
||
@apply rounded-full flex flex-row items-center justify-center;
|
||
@apply bg-surface-300;
|
||
height: 80rpx;
|
||
width: 80rpx;
|
||
overflow: visible;
|
||
}
|
||
|
||
&-arrow {
|
||
@apply bg-surface-300 absolute;
|
||
right: -8rpx;
|
||
height: 40rpx;
|
||
width: 40rpx;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
&-text {
|
||
@apply text-white text-2xl;
|
||
}
|
||
}
|
||
</style>
|