feat: 调整项目结构
This commit is contained in:
259
src/pages-order/order/components/Tabs/README.md
Normal file
259
src/pages-order/order/components/Tabs/README.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Tab 切换组件
|
||||
|
||||
一个功能完整的 Tab 切换组件,支持动画过渡、自定义内容和响应式设计。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ **多标签切换**:支持任意数量的标签页切换
|
||||
- ✅ **动画指示器**:选中状态下划线,支持平滑滑动动画
|
||||
- ✅ **自定义内容**:支持插槽自定义标签内容
|
||||
- ✅ **响应式设计**:适配不同屏幕尺寸
|
||||
- ✅ **动态宽度**:下划线宽度根据文字宽度动态调整
|
||||
- ✅ **事件支持**:完整的切换事件和双向绑定
|
||||
- ✅ **主题定制**:支持自定义指示器颜色
|
||||
- ✅ **uniapp 兼容**:使用 uniapp 内置组件开发
|
||||
|
||||
## 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab
|
||||
:tabs="tabList"
|
||||
:defaultActive="0"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Tab from './components/Tab/index.vue'
|
||||
|
||||
const tabList = ref([
|
||||
{ label: '全部订单', value: 'all' },
|
||||
{ label: '服务工单', value: 'service' }
|
||||
])
|
||||
|
||||
const handleTabChange = ({ index, item }) => {
|
||||
console.log('切换到:', item.label)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 自定义标签内容
|
||||
|
||||
使用 `tab-item` 插槽可以完全自定义标签的显示内容:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab :tabs="customTabs" @change="handleChange">
|
||||
<template #tab-item="{ item, index, isActive }">
|
||||
<view class="custom-tab">
|
||||
<image v-if="item.icon" :src="item.icon" class="tab-icon" />
|
||||
<text :class="{ 'active-text': isActive }">{{ item.label }}</text>
|
||||
<view v-if="item.badge" class="badge">{{ item.badge }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</Tab>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const customTabs = ref([
|
||||
{
|
||||
label: '待处理',
|
||||
value: 'pending',
|
||||
icon: '/static/pending.png',
|
||||
badge: '3'
|
||||
},
|
||||
{
|
||||
label: '已完成',
|
||||
value: 'completed',
|
||||
icon: '/static/completed.png'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
## 双向绑定
|
||||
|
||||
支持 `v-model` 双向绑定当前选中的索引:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab v-model="activeIndex" :tabs="tabList" />
|
||||
<text>当前选中索引:{{ activeIndex }}</text>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const activeIndex = ref(0)
|
||||
const tabList = ref([
|
||||
{ label: '标签1', value: 'tab1' },
|
||||
{ label: '标签2', value: 'tab2' }
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
## 动态标签
|
||||
|
||||
支持动态添加和删除标签:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab :tabs="dynamicTabs" @change="handleChange" />
|
||||
<button @click="addTab">添加标签</button>
|
||||
<button @click="removeTab">删除标签</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const dynamicTabs = ref([
|
||||
{ label: '标签1', value: 'tab1' }
|
||||
])
|
||||
|
||||
const addTab = () => {
|
||||
const newIndex = dynamicTabs.value.length + 1
|
||||
dynamicTabs.value.push({
|
||||
label: `标签${newIndex}`,
|
||||
value: `tab${newIndex}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeTab = () => {
|
||||
if (dynamicTabs.value.length > 1) {
|
||||
dynamicTabs.value.pop()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| tabs | Array | `[{label:'全部订单',value:'all'},{label:'服务工单',value:'service'}]` | 标签数据数组 |
|
||||
| defaultActive | Number | `0` | 默认选中的标签索引 |
|
||||
| indicatorColor | String | `#007AFF` | 指示器颜色 |
|
||||
| modelValue | Number | - | 当前选中索引(用于 v-model) |
|
||||
|
||||
### tabs 数组项结构
|
||||
|
||||
```typescript
|
||||
interface TabItem {
|
||||
label: string // 标签显示文本
|
||||
value: string // 标签值
|
||||
[key: string]: any // 其他自定义属性
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 参数 |
|
||||
|--------|------|------|
|
||||
| change | 标签切换时触发 | `{ index: number, item: TabItem }` |
|
||||
| update:modelValue | 用于 v-model 双向绑定 | `index: number` |
|
||||
|
||||
## Slots
|
||||
|
||||
| 插槽名 | 说明 | 作用域参数 |
|
||||
|--------|------|------------|
|
||||
| tab-item | 自定义标签内容 | `{ item: TabItem, index: number, isActive: boolean }` |
|
||||
|
||||
## 方法
|
||||
|
||||
通过 `ref` 可以调用组件的方法:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab ref="tabRef" :tabs="tabList" />
|
||||
<button @click="switchToTab(1)">切换到第二个标签</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const tabRef = ref()
|
||||
|
||||
const switchToTab = (index) => {
|
||||
tabRef.value.setActiveIndex(index)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
| 方法名 | 说明 | 参数 | 返回值 |
|
||||
|--------|------|------|--------|
|
||||
| setActiveIndex | 设置当前选中的标签 | `index: number` | - |
|
||||
| getActiveIndex | 获取当前选中的标签索引 | - | `number` |
|
||||
| getActiveItem | 获取当前选中的标签项 | - | `TabItem` |
|
||||
|
||||
## 样式定制
|
||||
|
||||
### CSS 变量
|
||||
|
||||
组件支持通过 CSS 变量进行样式定制:
|
||||
|
||||
```scss
|
||||
.tab-container {
|
||||
--tab-bg-color: #fff; // 背景色
|
||||
--tab-text-color: #666; // 文字颜色
|
||||
--tab-active-color: #333; // 选中文字颜色
|
||||
--tab-indicator-color: #007AFF; // 指示器颜色
|
||||
--tab-border-color: #f0f0f0; // 边框颜色
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义主题
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Tab
|
||||
:tabs="tabList"
|
||||
indicatorColor="#ff4d4f"
|
||||
class="custom-tab"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.custom-tab {
|
||||
--tab-indicator-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-tab .tab-text-active {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
- **框架**:Vue 3 组合式 API
|
||||
- **平台**:uniapp 跨平台开发
|
||||
- **动画**:CSS3 transition + transform
|
||||
- **响应式**:CSS media queries
|
||||
- **兼容性**:微信小程序、H5、App
|
||||
|
||||
## 设计规范
|
||||
|
||||
- 遵循微信小程序设计规范
|
||||
- 支持无障碍访问
|
||||
- 响应式设计,适配不同设备
|
||||
- 流畅的动画过渡效果
|
||||
- 一致的视觉风格
|
||||
|
||||
## 兼容性
|
||||
|
||||
| 平台 | 支持情况 |
|
||||
|------|----------|
|
||||
| 微信小程序 | ✅ 完全支持 |
|
||||
| H5 | ✅ 完全支持 |
|
||||
| App | ✅ 完全支持 |
|
||||
| 支付宝小程序 | ✅ 完全支持 |
|
||||
| 百度小程序 | ✅ 完全支持 |
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0
|
||||
- ✨ 初始版本发布
|
||||
- ✨ 支持基础标签切换功能
|
||||
- ✨ 支持动画指示器
|
||||
- ✨ 支持自定义标签内容
|
||||
- ✨ 支持响应式设计
|
||||
- ✨ 支持事件和双向绑定
|
||||
|
||||
## 备注
|
||||
|
||||
仅供学习、交流使用,请勿用于商业用途。
|
||||
275
src/pages-order/order/components/Tabs/index.vue
Normal file
275
src/pages-order/order/components/Tabs/index.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<view class="tab-container">
|
||||
<view class="tab-wrapper">
|
||||
<view
|
||||
v-for="(item, index) in tabList"
|
||||
:key="index"
|
||||
:class="['tab-item', activeIndex === index && 'tab-item-active']"
|
||||
@click="handleTabClick(index)"
|
||||
>
|
||||
<slot
|
||||
name="tab-item"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:isActive="activeIndex === index"
|
||||
>
|
||||
<text
|
||||
:class="['tab-text', activeIndex === index && 'tab-text-active']"
|
||||
>
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线指示器 -->
|
||||
<view
|
||||
:class="[
|
||||
'tab-indicator',
|
||||
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";
|
||||
|
||||
// 获取组件实例
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ label: "全部订单", value: "all" },
|
||||
{ label: "服务工单", value: "service" },
|
||||
],
|
||||
},
|
||||
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 - 7.5; // 15px宽度的一半
|
||||
// 固定宽度15px,不动态计算宽度
|
||||
indicatorWidth.value = 15;
|
||||
|
||||
// 标记为已初始化
|
||||
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">
|
||||
@use "./styles/index.scss";
|
||||
</style>
|
||||
0
src/pages-order/order/components/Tabs/propmt.md
Normal file
0
src/pages-order/order/components/Tabs/propmt.md
Normal file
100
src/pages-order/order/components/Tabs/styles/index.scss
Normal file
100
src/pages-order/order/components/Tabs/styles/index.scss
Normal file
@@ -0,0 +1,100 @@
|
||||
.tab-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-text-active {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-item-active {
|
||||
.tab-text {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
min-height: 3px; /* 确保最小高度 */
|
||||
background-color: #007AFF;
|
||||
border-radius: 10px;
|
||||
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: 1;
|
||||
transform: translateZ(0); /* 启用硬件加速 */
|
||||
will-change: left, width; /* 优化动画性能 */
|
||||
|
||||
/* 初始状态:未初始化时隐藏 */
|
||||
opacity: 0;
|
||||
width: 15px; /* 默认宽度15px */
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 已初始化状态 */
|
||||
.tab-indicator.initialized {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* 点击效果 */
|
||||
.tab-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 自定义主题色支持 */
|
||||
.tab-container[data-indicator-color="red"] .tab-indicator {
|
||||
background-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.tab-container[data-indicator-color="green"] .tab-indicator {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.tab-container[data-indicator-color="orange"] .tab-indicator {
|
||||
background-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 动画增强 */
|
||||
@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;
|
||||
}
|
||||
309
src/pages-order/order/components/Tabs/test.vue
Normal file
309
src/pages-order/order/components/Tabs/test.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<view class="test-page">
|
||||
<view class="test-section">
|
||||
<text class="test-title">Tab组件测试</text>
|
||||
|
||||
<!-- 基础测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">基础用法</text>
|
||||
<Tab
|
||||
:tabs="basicTabs"
|
||||
:defaultActive="0"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentTab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 多标签测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">多标签测试</text>
|
||||
<Tab
|
||||
:tabs="multiTabs"
|
||||
:defaultActive="1"
|
||||
@change="handleMultiTabChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentMultiTab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速切换测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">快速切换测试</text>
|
||||
<Tab
|
||||
ref="fastTabRef"
|
||||
:tabs="fastTabs"
|
||||
:defaultActive="0"
|
||||
@change="handleFastTabChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentFastTab.label }}</text>
|
||||
</view>
|
||||
<view class="test-buttons">
|
||||
<button
|
||||
v-for="(tab, index) in fastTabs"
|
||||
:key="index"
|
||||
class="test-btn"
|
||||
@click="switchToTab(index)"
|
||||
>
|
||||
切换到{{ tab.label }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 初始化测试 -->
|
||||
<view class="test-item">
|
||||
<text class="item-title">初始化测试</text>
|
||||
<text class="test-desc">测试指示器的动态高度和宽度初始化及错误处理</text>
|
||||
<Tab
|
||||
v-if="showInitTest"
|
||||
:tabs="initTestTabs"
|
||||
:defaultActive="initActiveIndex"
|
||||
@change="handleInitTestChange"
|
||||
/>
|
||||
<view class="result">
|
||||
<text>当前选中: {{ currentInitTab.label }}</text>
|
||||
</view>
|
||||
<view class="test-buttons">
|
||||
<button class="test-btn" @click="toggleInitTest">
|
||||
{{ showInitTest ? '隐藏' : '显示' }}组件
|
||||
</button>
|
||||
<button class="test-btn" @click="changeInitActive">
|
||||
切换默认激活项 (当前: {{ initActiveIndex }})
|
||||
</button>
|
||||
<button class="test-btn" @click="addInitTab">
|
||||
添加Tab
|
||||
</button>
|
||||
<button class="test-btn" @click="removeInitTab">
|
||||
移除Tab
|
||||
</button>
|
||||
<button class="test-btn" @click="rapidToggle">
|
||||
快速切换测试
|
||||
</button>
|
||||
</view>
|
||||
<view class="test-info">
|
||||
<text>错误处理测试:组件现在能够安全处理实例为null的情况</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Tab from './index.vue'
|
||||
|
||||
// 基础标签
|
||||
const basicTabs = ref([
|
||||
{ label: '全部订单', value: 'all' },
|
||||
{ label: '服务工单', value: 'service' }
|
||||
])
|
||||
|
||||
// 多标签
|
||||
const multiTabs = ref([
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '待支付', value: 'unpaid' },
|
||||
{ label: '待确认', value: 'unconfirmed' },
|
||||
{ label: '进行中', value: 'processing' },
|
||||
{ label: '已完成', value: 'completed' }
|
||||
])
|
||||
|
||||
// 快速切换测试
|
||||
const fastTabs = ref([
|
||||
{ label: '标签A', value: 'a' },
|
||||
{ label: '标签B', value: 'b' },
|
||||
{ label: '标签C', value: 'c' },
|
||||
{ label: '标签D', value: 'd' }
|
||||
])
|
||||
|
||||
// 初始化测试
|
||||
const initTestTabs = ref([
|
||||
{ label: '初始1', value: 'init1' },
|
||||
{ label: '初始2', value: 'init2' },
|
||||
{ label: '初始3', value: 'init3' }
|
||||
])
|
||||
|
||||
// 当前选中索引
|
||||
const currentTabIndex = ref(0)
|
||||
const currentMultiTabIndex = ref(1)
|
||||
const currentFastTabIndex = ref(0)
|
||||
const currentInitTabIndex = ref(1)
|
||||
|
||||
// 初始化测试状态
|
||||
const showInitTest = ref(true)
|
||||
const initActiveIndex = ref(1)
|
||||
|
||||
// 计算当前选中项
|
||||
const currentTab = computed(() => basicTabs.value[currentTabIndex.value] || {})
|
||||
const currentMultiTab = computed(() => multiTabs.value[currentMultiTabIndex.value] || {})
|
||||
const currentFastTab = computed(() => fastTabs.value[currentFastTabIndex.value] || {})
|
||||
const currentInitTab = computed(() => initTestTabs.value[currentInitTabIndex.value] || {})
|
||||
|
||||
// Tab引用
|
||||
const fastTabRef = ref(null)
|
||||
|
||||
// 事件处理
|
||||
const handleTabChange = ({ index, item }) => {
|
||||
currentTabIndex.value = index
|
||||
console.log('基础Tab切换:', item)
|
||||
}
|
||||
|
||||
const handleMultiTabChange = ({ index, item }) => {
|
||||
currentMultiTabIndex.value = index
|
||||
console.log('多标签Tab切换:', item)
|
||||
}
|
||||
|
||||
const handleFastTabChange = ({ index, item }) => {
|
||||
currentFastTabIndex.value = index
|
||||
console.log('快速Tab切换:', item)
|
||||
}
|
||||
|
||||
const handleInitTestChange = ({ index, item }) => {
|
||||
currentInitTabIndex.value = index
|
||||
console.log('初始化Tab切换:', item)
|
||||
}
|
||||
|
||||
// 程序化切换
|
||||
const switchToTab = (index) => {
|
||||
currentFastTabIndex.value = index
|
||||
if (fastTabRef.value) {
|
||||
fastTabRef.value.setActiveIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化测试方法
|
||||
const toggleInitTest = () => {
|
||||
showInitTest.value = !showInitTest.value
|
||||
}
|
||||
|
||||
const changeInitActive = () => {
|
||||
initActiveIndex.value = (initActiveIndex.value + 1) % initTestTabs.value.length
|
||||
}
|
||||
|
||||
const addInitTab = () => {
|
||||
const newIndex = initTestTabs.value.length + 1
|
||||
initTestTabs.value.push({
|
||||
label: `初始${newIndex}`,
|
||||
value: `init${newIndex}`
|
||||
})
|
||||
}
|
||||
|
||||
const removeInitTab = () => {
|
||||
if (initTestTabs.value.length > 1) {
|
||||
initTestTabs.value.pop()
|
||||
if (initActiveIndex.value >= initTestTabs.value.length) {
|
||||
initActiveIndex.value = initTestTabs.value.length - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rapidToggle = () => {
|
||||
// 快速切换测试,模拟可能导致实例为null的场景
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
showInitTest.value = !showInitTest.value
|
||||
count++
|
||||
if (count >= 6) {
|
||||
clearInterval(interval)
|
||||
showInitTest.value = true
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.test-item {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.test-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.test-desc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.test-info {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007AFF;
|
||||
}
|
||||
|
||||
.test-info text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
background-color: #007AFF;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user