288 lines
7.0 KiB
Vue
288 lines
7.0 KiB
Vue
<template>
|
||
<view class="tab-container relative">
|
||
<view class="tab-wrapper flex flex-items-center flex-justify-center">
|
||
<view
|
||
v-for="(item, index) in tabList"
|
||
:key="index"
|
||
:class="[
|
||
'tab-item flex flex-full flex-items-center flex-justify-center relative',
|
||
activeIndex === index && 'tab-item-active',
|
||
]"
|
||
@click="handleTabClick(index)"
|
||
>
|
||
<view class="absolute flex flex-items-center">
|
||
<uni-icons
|
||
class="icon mr-4"
|
||
fontFamily="znicons"
|
||
size="20"
|
||
:color="activeIndex === index ? '#2D91FF' : '#525866'"
|
||
>
|
||
{{ zniconsMap[item.icon] }}
|
||
</uni-icons>
|
||
<text
|
||
:class="[
|
||
'font-size-16 font-500 color-525866 ',
|
||
activeIndex === index && 'tab-text-active',
|
||
]"
|
||
>
|
||
{{ item.label }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下划线指示器 -->
|
||
<view
|
||
:class="[
|
||
'tab-indicator absolute',
|
||
indicatorAnimating && 'animating',
|
||
indicatorInitialized && 'initialized',
|
||
]"
|
||
:style="{
|
||
left: indicatorLeft + 'px',
|
||
width: indicatorWidth + 'px',
|
||
}"
|
||
></view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
reactive,
|
||
onMounted,
|
||
nextTick,
|
||
watch,
|
||
getCurrentInstance,
|
||
} from "vue";
|
||
import { zniconsMap } from "@/static/fonts/znicons";
|
||
|
||
// 获取组件实例
|
||
const instance = getCurrentInstance();
|
||
|
||
// Props
|
||
const props = defineProps({
|
||
tabs: {
|
||
type: Array,
|
||
default: () => [
|
||
{ label: "客房", value: "0", icon: "zn-nav-room" },
|
||
{ label: "门票", value: "1", icon: "zn-nav-ticket" },
|
||
{ label: "餐食", value: "2", icon: "zn-nav-meal" },
|
||
// { label: "套餐", value: "3", icon: "zn-package" },
|
||
],
|
||
},
|
||
defaultActive: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
indicatorColor: {
|
||
type: String,
|
||
default: "#007AFF",
|
||
},
|
||
});
|
||
|
||
// Emits
|
||
const emit = defineEmits(["change", "update:modelValue"]);
|
||
|
||
// 响应式数据
|
||
const activeIndex = ref(props.defaultActive);
|
||
const tabList = ref(props.tabs);
|
||
const indicatorLeft = ref(0);
|
||
const indicatorWidth = ref(0);
|
||
const tabItemRects = reactive([]);
|
||
const isUpdating = ref(false);
|
||
const indicatorAnimating = ref(false);
|
||
const indicatorInitialized = ref(false);
|
||
|
||
// 处理Tab点击
|
||
const handleTabClick = (index) => {
|
||
if (activeIndex.value === index) return;
|
||
|
||
activeIndex.value = index;
|
||
indicatorAnimating.value = true;
|
||
updateIndicator();
|
||
|
||
// 动画结束后移除动画类
|
||
setTimeout(() => {
|
||
indicatorAnimating.value = false;
|
||
}, 300);
|
||
|
||
emit("change", {
|
||
index,
|
||
item: tabList.value[index],
|
||
});
|
||
emit("update:modelValue", index);
|
||
};
|
||
|
||
// 更新指示器位置
|
||
const updateIndicator = async () => {
|
||
if (isUpdating.value) return;
|
||
isUpdating.value = true;
|
||
|
||
await nextTick();
|
||
|
||
// 检查实例是否存在
|
||
if (!instance) {
|
||
console.warn("Component instance not available");
|
||
isUpdating.value = false;
|
||
return;
|
||
}
|
||
|
||
// 检查 uni.createSelectorQuery 是否可用
|
||
if (!uni || !uni.createSelectorQuery) {
|
||
console.warn("uni.createSelectorQuery not available");
|
||
isUpdating.value = false;
|
||
return;
|
||
}
|
||
|
||
const query = uni.createSelectorQuery().in(instance);
|
||
|
||
// 同时获取tab项和容器的位置信息
|
||
query.selectAll(".tab-item").boundingClientRect();
|
||
query.select(".tab-wrapper").boundingClientRect();
|
||
|
||
query.exec((res) => {
|
||
try {
|
||
const [tabRects, wrapperRect] = res || [];
|
||
|
||
if (tabRects && tabRects.length > 0 && wrapperRect) {
|
||
tabItemRects.splice(0, tabItemRects.length, ...tabRects);
|
||
|
||
const activeRect = tabRects[activeIndex.value];
|
||
if (activeRect) {
|
||
// 计算相对于容器的位置,居中显示
|
||
const tabCenter =
|
||
activeRect.left - wrapperRect.left + activeRect.width / 2;
|
||
indicatorLeft.value = tabCenter - 10; // 15px宽度的一半
|
||
// 固定宽度15px,不动态计算宽度
|
||
indicatorWidth.value = 20;
|
||
|
||
// 标记为已初始化
|
||
if (!indicatorInitialized.value) {
|
||
indicatorInitialized.value = true;
|
||
}
|
||
}
|
||
} else {
|
||
console.warn("Failed to get tab rects or wrapper rect");
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in updateIndicator exec:", error);
|
||
} finally {
|
||
isUpdating.value = false;
|
||
}
|
||
});
|
||
};
|
||
|
||
// 监听activeIndex变化
|
||
watch(
|
||
() => activeIndex.value,
|
||
() => {
|
||
// 如果是初始化阶段,使用initIndicator
|
||
if (indicatorLeft.value === 0 && indicatorWidth.value === 0) {
|
||
initIndicator();
|
||
} else {
|
||
updateIndicator();
|
||
}
|
||
}
|
||
);
|
||
|
||
// 监听tabs变化
|
||
watch(
|
||
() => props.tabs,
|
||
(newTabs) => {
|
||
tabList.value = newTabs;
|
||
// 重置初始化状态
|
||
indicatorInitialized.value = false;
|
||
// 重新初始化指示器
|
||
initIndicator();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
// 监听defaultActive变化
|
||
watch(
|
||
() => props.defaultActive,
|
||
(newActive) => {
|
||
if (newActive !== activeIndex.value) {
|
||
activeIndex.value = newActive;
|
||
// 重置初始化状态
|
||
indicatorInitialized.value = false;
|
||
initIndicator();
|
||
}
|
||
}
|
||
);
|
||
|
||
// 初始化指示器
|
||
const initIndicator = async (retryCount = 0) => {
|
||
// 等待DOM完全渲染
|
||
await nextTick();
|
||
|
||
// 延迟一帧确保布局完成
|
||
setTimeout(() => {
|
||
// 检查实例是否存在
|
||
if (!instance) {
|
||
console.warn("Component instance not available in initIndicator");
|
||
return;
|
||
}
|
||
|
||
// 检查 uni.createSelectorQuery 是否可用
|
||
if (!uni || !uni.createSelectorQuery) {
|
||
console.warn("uni.createSelectorQuery not available in initIndicator");
|
||
return;
|
||
}
|
||
|
||
const query = uni.createSelectorQuery().in(instance);
|
||
query.selectAll(".tab-item").boundingClientRect();
|
||
query.select(".tab-wrapper").boundingClientRect();
|
||
|
||
query.exec((res) => {
|
||
try {
|
||
const [tabRects, wrapperRect] = res || [];
|
||
|
||
// 如果DOM元素还未准备好,重试
|
||
if (
|
||
(!tabRects || tabRects.length === 0 || !wrapperRect) &&
|
||
retryCount < 3
|
||
) {
|
||
setTimeout(() => {
|
||
initIndicator(retryCount + 1);
|
||
}, 100);
|
||
return;
|
||
}
|
||
|
||
// 执行正常的更新逻辑
|
||
updateIndicator();
|
||
} catch (error) {
|
||
console.error("Error in initIndicator exec:", error);
|
||
// 如果出错且还有重试次数,尝试重试
|
||
if (retryCount < 3) {
|
||
setTimeout(() => {
|
||
initIndicator(retryCount + 1);
|
||
}, 100);
|
||
}
|
||
}
|
||
});
|
||
}, 50);
|
||
};
|
||
|
||
// 组件挂载后初始化指示器
|
||
onMounted(() => {
|
||
initIndicator();
|
||
});
|
||
|
||
// 暴露方法
|
||
defineExpose({
|
||
setActiveIndex: (index) => {
|
||
if (index >= 0 && index < tabList.value.length) {
|
||
handleTabClick(index);
|
||
}
|
||
},
|
||
getActiveIndex: () => activeIndex.value,
|
||
getActiveItem: () => tabList.value[activeIndex.value],
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
@import "./styles/index.scss";
|
||
</style>
|