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