Files
qiming/qiming-mobile/components/pane-tabs/pane-tabs.uvue

323 lines
7.8 KiB
Plaintext
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 id="pane-tabs-component" class="pane-tabs h-full flex flex-col">
<!-- Tab 导航栏 -->
<view
id="tabs-header"
class="tabs-header"
:style="{ height: headerHeight }"
>
<view class="tabs-nav">
<view
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: modelValue === tab.key }]"
@click="handleTabClick(tab.key)"
>
<text class="tab-label">{{ translateText(tab.label) }}</text>
</view>
</view>
<!-- 激活指示器 -->
<view
id="tab-indicator"
class="tab-indicator"
:style="{
width: indicatorWidth + 'px',
left: indicatorLeft + 'px',
}"
/>
</view>
<!-- Tab 内容区域 -->
<view
class="tabs-content flex-1"
:style="{ paddingBottom: withBottomPadding ? '90rpx' : '0' }"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<view
class="tabs-content-wrapper"
:style="{
width: `${tabs.length * 100}%`,
transform: `translateX(-${(getCurrentIndex() * 100) / tabs.length}%)`,
transition: isSwiping
? 'none'
: animated
? 'transform 0.3s ease'
: 'none',
}"
>
<!-- 渲染子组件 -->
<slot />
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { translateText } from "@/utils/i18n";
// Props定义
interface TabItem {
key: string;
label: string;
forceRender?: boolean;
}
interface Props {
modelValue?: string;
headerHeight?: string;
animated?: boolean;
lazyLoad?: boolean;
withBottomPadding?: boolean; // 是否添加底部内边距默认90rpx
}
const props = withDefaults(defineProps<Props>(), {
modelValue: "",
headerHeight: "88rpx",
animated: true,
lazyLoad: true,
withBottomPadding: false,
});
// Emits定义
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
(e: "change", key: string): void;
}>();
// 存储所有 tab 项
const tabs = ref<TabItem[]>([]);
// 注册 tab 的函数
const registerTab = (tab: TabItem) => {
tabs.value.push(tab);
};
// 注销 tab 的函数
const unregisterTab = (key: string) => {
const index = tabs.value.findIndex((t) => t.key === key);
if (index > -1) {
tabs.value.splice(index, 1);
}
};
// 通过 provide 提供注册函数给子组件
provide("paneTabs", {
registerTab,
unregisterTab,
activeKey: computed(() => props.modelValue),
lazyLoad: computed(() => props.lazyLoad),
});
// 当前激活的 tab 索引
const currentIndex = ref<number>(0);
// 指示器宽度
const indicatorWidth = ref<number>(0);
// 指示器位置
const indicatorLeft = ref<number>(0);
// 根据 key 获取索引
const getIndexByKey = (key: string): number => {
return tabs.value.findIndex((tab) => tab.key === key);
};
// 获取当前索引
const getCurrentIndex = (): number => {
return getIndexByKey(props.modelValue);
};
// 监听 modelValue 变化
watch(
() => props.modelValue,
(newVal) => {
const newIndex = getIndexByKey(newVal);
if (newIndex !== -1 && newIndex !== currentIndex.value) {
currentIndex.value = newIndex;
updateIndicator(newIndex);
}
},
);
// 监听 tabs 变化,初始化第一个 tab
watch(
tabs,
(newTabs) => {
if (newTabs.length > 0 && !props.modelValue) {
emit("update:modelValue", newTabs[0].key);
}
},
{ immediate: true },
);
// 点击 tab
const handleTabClick = (key: string) => {
if (props.modelValue === key) return;
const index = getIndexByKey(key);
if (index === -1) return;
currentIndex.value = index;
emit("update:modelValue", key);
emit("change", key);
updateIndicator(index);
};
// 更新指示器位置
const updateIndicator = async (index: number) => {
await nextTick();
// 使用计算方式而非 DOM 查询(解决微信小程序组件内无法查询元素的问题)
const systemInfo = uni.getSystemInfoSync();
const screenWidth = systemInfo.windowWidth || 375;
// 每个 tab 的宽度 = 屏幕宽度 / tab 数量
const tabWidth = screenWidth / tabs.value.length;
// 指示器宽度设为 tab 宽度
indicatorWidth.value = tabWidth;
// 指示器位置 = index * tab 宽度
indicatorLeft.value = index * tabWidth;
};
// 滑动相关状态
const touchStartX = ref<number>(0);
const touchStartY = ref<number>(0);
const isSwiping = ref<boolean>(false);
const swipeThreshold = 50; // 最小滑动距离阈值单位px
// 触摸开始
const handleTouchStart = (e: any) => {
if (e.touches && e.touches.length > 0) {
touchStartX.value = e.touches[0].pageX;
touchStartY.value = e.touches[0].pageY;
isSwiping.value = false;
}
};
// 触摸移动
const handleTouchMove = (e: any) => {
if (!e.touches || e.touches.length === 0) return;
const touchCurrentX = e.touches[0].pageX;
const touchCurrentY = e.touches[0].pageY;
const diffX = Math.abs(touchCurrentX - touchStartX.value);
const diffY = Math.abs(touchCurrentY - touchStartY.value);
// 只有横向滑动距离大于纵向滑动距离时,才认为是在切换标签
if (diffX > diffY && diffX > 10) {
isSwiping.value = true;
}
};
// 触摸结束
const handleTouchEnd = (e: any) => {
if (!isSwiping.value) return;
const touchEndX = e.changedTouches[0].pageX;
const diffX = touchEndX - touchStartX.value;
// 判断滑动方向和距离
if (Math.abs(diffX) > swipeThreshold) {
const currentIdx = getCurrentIndex();
if (diffX > 0) {
// 向右滑动,切换到前一个标签
if (currentIdx > 0) {
const prevTab = tabs.value[currentIdx - 1];
handleTabClick(prevTab.key);
}
} else {
// 向左滑动,切换到下一个标签
if (currentIdx < tabs.value.length - 1) {
const nextTab = tabs.value[currentIdx + 1];
handleTabClick(nextTab.key);
}
}
}
isSwiping.value = false;
};
// 组件挂载后初始化指示器
onMounted(() => {
setTimeout(() => {
const index = getIndexByKey(props.modelValue);
if (index !== -1) {
currentIndex.value = index;
updateIndicator(index);
}
}, 100);
});
// 暴露方法
defineExpose({
updateIndicator,
});
</script>
<style lang="scss" scoped>
.pane-tabs {
width: 100%;
background: #fff;
.tabs-header {
position: relative;
background: #fff;
border-bottom: 2rpx solid #f0f0f0;
.tabs-nav {
display: flex;
flex-direction: row;
height: 100%;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
.tab-label {
font-size: 28rpx;
color: #666;
font-weight: 600;
transition: all 0.3s ease;
}
&.active {
.tab-label {
color: #5147ff;
}
}
}
}
.tab-indicator {
position: absolute;
bottom: 0;
height: 3rpx;
background: linear-gradient(90deg, #5147ff 0%, #5147ff 100%);
border-radius: 3rpx;
transition: all 0.3s ease;
}
}
.tabs-content {
position: relative;
overflow: hidden;
width: 100%;
.tabs-content-wrapper {
display: flex;
flex-direction: row;
height: 100%;
}
}
}
</style>