Files
YGChatCS/src/pages-quick/components/Tabs/index.vue
2025-10-21 20:58:32 +08:00

288 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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