feat: 快速预定的样式调整
This commit is contained in:
@@ -1,15 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="tab-container relative">
|
<view class="tab-container relative">
|
||||||
<view class="tab-wrapper flex flex-items-center flex-justify-center">
|
<view class="tab-wrapper flex flex-items-center">
|
||||||
<view v-for="(item, index) in tabList" :key="index" :class="[
|
<view v-for="(item, index) in tabList" :key="index" :class="[
|
||||||
'tab-item flex flex-full flex-items-center flex-justify-center relative',
|
'tab-item flex flex-items-center flex-justify-center relative',
|
||||||
activeIndex === index && 'tab-item-active',
|
activeIndex === index && 'tab-item-active',
|
||||||
]" @click="handleTabClick(index)">
|
]" @click="handleTabClick(index)">
|
||||||
<view class="absolute flex flex-items-center">
|
<view class="tab-item-inner flex flex-items-center">
|
||||||
<uni-icons class="icon mr-4" fontFamily="znicons" size="20"
|
<uni-icons class="icon mr-4" fontFamily="znicons" size="20"
|
||||||
:color="activeIndex === index ? '$theme-color-500' : '#525866'">
|
:color="activeIndex === index ? indicatorColor : '#525866'">
|
||||||
{{ zniconsMap[item.icon] }}
|
{{ zniconsMap[item.icon] }}
|
||||||
</uni-icons>
|
</uni-icons>
|
||||||
|
|
||||||
<text :class="[
|
<text :class="[
|
||||||
'font-size-16 font-500 color-525866 ',
|
'font-size-16 font-500 color-525866 ',
|
||||||
activeIndex === index && 'tab-text-active',
|
activeIndex === index && 'tab-text-active',
|
||||||
@@ -17,35 +18,18 @@
|
|||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 每项内的下划线指示器,通过类控制显示/隐藏 -->
|
||||||
|
<view class="tab-item-indicator" :class="{ visible: activeIndex === index }"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 下划线指示器 -->
|
|
||||||
<view :class="[
|
|
||||||
'tab-indicator absolute',
|
|
||||||
indicatorAnimating && 'animating',
|
|
||||||
indicatorInitialized && 'initialized',
|
|
||||||
]" :style="{
|
|
||||||
left: indicatorLeft + 'px',
|
|
||||||
width: indicatorWidth + 'px',
|
|
||||||
}"></view>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { ref, watch } from "vue";
|
||||||
ref,
|
|
||||||
reactive,
|
|
||||||
onMounted,
|
|
||||||
nextTick,
|
|
||||||
watch,
|
|
||||||
getCurrentInstance,
|
|
||||||
} from "vue";
|
|
||||||
import { zniconsMap } from "@/static/fonts/znicons";
|
import { zniconsMap } from "@/static/fonts/znicons";
|
||||||
|
|
||||||
// 获取组件实例
|
|
||||||
const instance = getCurrentInstance();
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -54,7 +38,8 @@ const props = defineProps({
|
|||||||
{ label: "客房", value: "0", icon: "zn-nav-room" },
|
{ label: "客房", value: "0", icon: "zn-nav-room" },
|
||||||
{ label: "门票", value: "1", icon: "zn-nav-ticket" },
|
{ label: "门票", value: "1", icon: "zn-nav-ticket" },
|
||||||
{ label: "餐食", value: "2", icon: "zn-nav-meal" },
|
{ label: "餐食", value: "2", icon: "zn-nav-meal" },
|
||||||
// { label: "套餐", value: "3", icon: "zn-package" },
|
{ label: "套餐", value: "3", icon: "zn-package" },
|
||||||
|
{ label: "文创", value: "4", icon: "zn-package" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
defaultActive: {
|
defaultActive: {
|
||||||
@@ -63,7 +48,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
indicatorColor: {
|
indicatorColor: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "#007AFF",
|
default: '#1890ff',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,25 +58,12 @@ const emit = defineEmits(["change", "update:modelValue"]);
|
|||||||
// 响应式数据
|
// 响应式数据
|
||||||
const activeIndex = ref(props.defaultActive);
|
const activeIndex = ref(props.defaultActive);
|
||||||
const tabList = ref(props.tabs);
|
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点击
|
// 处理Tab点击
|
||||||
const handleTabClick = (index) => {
|
const handleTabClick = (index) => {
|
||||||
if (activeIndex.value === index) return;
|
if (activeIndex.value === index) return;
|
||||||
|
|
||||||
activeIndex.value = index;
|
activeIndex.value = index;
|
||||||
indicatorAnimating.value = true;
|
|
||||||
updateIndicator();
|
|
||||||
|
|
||||||
// 动画结束后移除动画类
|
|
||||||
setTimeout(() => {
|
|
||||||
indicatorAnimating.value = false;
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
emit("change", {
|
emit("change", {
|
||||||
index,
|
index,
|
||||||
@@ -100,87 +72,11 @@ const handleTabClick = (index) => {
|
|||||||
emit("update:modelValue", 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变化
|
// 监听tabs变化
|
||||||
watch(
|
watch(
|
||||||
() => props.tabs,
|
() => props.tabs,
|
||||||
(newTabs) => {
|
(newTabs) => {
|
||||||
tabList.value = newTabs;
|
tabList.value = newTabs;
|
||||||
// 重置初始化状态
|
|
||||||
indicatorInitialized.value = false;
|
|
||||||
// 重新初始化指示器
|
|
||||||
initIndicator();
|
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
@@ -191,71 +87,10 @@ watch(
|
|||||||
(newActive) => {
|
(newActive) => {
|
||||||
if (newActive !== activeIndex.value) {
|
if (newActive !== activeIndex.value) {
|
||||||
activeIndex.value = newActive;
|
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({
|
defineExpose({
|
||||||
setActiveIndex: (index) => {
|
setActiveIndex: (index) => {
|
||||||
@@ -267,7 +102,6 @@ defineExpose({
|
|||||||
getActiveItem: () => tabList.value[activeIndex.value],
|
getActiveItem: () => tabList.value[activeIndex.value],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "./styles/index.scss";
|
@import "./styles/index.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
.tab-wrapper {
|
.tab-wrapper {
|
||||||
background-color: $theme-color-100;
|
background-color: $theme-color-100;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
overflow-x: auto;
|
||||||
|
/* 支持横向滚动 */
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
/* 平滑滚动(移动端) */
|
||||||
|
white-space: nowrap;
|
||||||
|
/* 防止换行 */
|
||||||
|
justify-content: flex-start;
|
||||||
|
/* 覆盖工具类,靠左排列以便滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
/* 不让子项拉伸,按内容宽度排列 */
|
||||||
|
padding: 0 12px;
|
||||||
|
/* 增加横向间距,便于触控 */
|
||||||
|
min-width: 56px;
|
||||||
|
/* 保证可点击区域 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -12,6 +26,13 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 组件模板中使用了绝对定位的内部元素,为保证父元素宽度基于内容,重置该子元素为静态布局 */
|
||||||
|
.tab-item>.absolute {
|
||||||
|
position: static !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-item-active {
|
.tab-item-active {
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -33,40 +54,32 @@
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-indicator {
|
/* 已改为每项内部指示器,移除了全局指示器样式 */
|
||||||
|
|
||||||
|
/* 每项内的指示器(替代全局指示器) */
|
||||||
|
.tab-item-inner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
/* 确保内容(icon/text)位于 .tab-item-active::before 之上 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item-indicator {
|
||||||
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) scaleX(0.9);
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
width: 20px;
|
||||||
background-color: $theme-color-500;
|
background-color: $theme-color-500;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
z-index: 3;
|
|
||||||
transform: translateZ(0); /* 启用硬件加速 */
|
|
||||||
will-change: left, width; /* 优化动画性能 */
|
|
||||||
|
|
||||||
/* 初始状态:未初始化时隐藏 */
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 20px; /* 默认宽度15px */
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
left: 0;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 已初始化状态 */
|
.tab-item-indicator.visible {
|
||||||
.tab-indicator.initialized {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
transform: translateX(-50%) scaleX(1);
|
||||||
|
}
|
||||||
/* 动画增强 */
|
|
||||||
@keyframes tabSwitch {
|
|
||||||
0% {
|
|
||||||
transform: translateZ(0) scaleX(0.8);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateZ(0) scaleX(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-indicator.animating {
|
|
||||||
animation: tabSwitch 0.3s ease-out;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user