chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

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