chore: initialize qiming workspace repository
This commit is contained in:
322
qiming-mobile/components/pane-tabs/pane-tabs.uvue
Normal file
322
qiming-mobile/components/pane-tabs/pane-tabs.uvue
Normal file
@@ -0,0 +1,322 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user