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>
|