chore: initialize qiming workspace repository
This commit is contained in:
178
qiming-mobile/components/pane-tabs/README.md
Normal file
178
qiming-mobile/components/pane-tabs/README.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Custom Tabs 组件使用文档
|
||||
|
||||
## 组件特性
|
||||
|
||||
✅ **高度自适应** - 自动继承父容器高度,内容区域自动撑满
|
||||
✅ **懒加载** - 支持 tab 内容懒加载,提升性能
|
||||
✅ **平滑动画** - 切换动画流畅自然
|
||||
✅ **灵活配置** - 支持自定义头部高度、颜色等
|
||||
✅ **响应式指示器** - 自动计算指示器位置和宽度
|
||||
|
||||
## 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="page">
|
||||
<custom-tabs v-model="activeTab" :tabs="tabs">
|
||||
<template #recent>
|
||||
<view class="tab-content"> 最近使用内容 </view>
|
||||
</template>
|
||||
|
||||
<template #history>
|
||||
<view class="tab-content"> 会话记录内容 </view>
|
||||
</template>
|
||||
</custom-tabs>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import CustomTabs from "@/components/custom-tabs/custom-tabs.uvue";
|
||||
|
||||
const activeTab = ref(0);
|
||||
|
||||
const tabs = [
|
||||
{ label: "最近使用", name: "recent" },
|
||||
{ label: "会话记录", name: "history" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
height: 100vh;
|
||||
|
||||
.tab-content {
|
||||
padding: 32rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------------------- | ------------------- | -------------------------------------- | --------- |
|
||||
| tabs | Tab 数据列表 | `Array<{label: string, name: string}>` | `[]` |
|
||||
| modelValue / v-model | 当前激活的 tab 索引 | `number` | `0` |
|
||||
| headerHeight | Tab 头部高度 | `string` | `'88rpx'` |
|
||||
| animated | 是否开启切换动画 | `boolean` | `true` |
|
||||
| lazyLoad | 是否懒加载 tab 内容 | `boolean` | `true` |
|
||||
|
||||
## Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ----------------- | ----------------------- | --------------------------- |
|
||||
| update:modelValue | 当前激活 tab 改变时触发 | `(index: number)` |
|
||||
| change | 切换 tab 时触发 | `(index: number, tab: Tab)` |
|
||||
|
||||
## Slots
|
||||
|
||||
每个 tab 对应一个具名插槽,插槽名称为 `tabs` 数组中的 `name` 字段。
|
||||
|
||||
## 完整示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<custom-tabs
|
||||
v-model="currentTab"
|
||||
:tabs="tabList"
|
||||
:header-height="'100rpx'"
|
||||
:animated="true"
|
||||
:lazy-load="true"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<!-- 最近使用 -->
|
||||
<template #recent>
|
||||
<scroll-view class="scroll-content" scroll-y>
|
||||
<view class="content-wrapper">
|
||||
<text>最近使用的内容</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<!-- 会话记录 -->
|
||||
<template #history>
|
||||
<scroll-view class="scroll-content" scroll-y>
|
||||
<view class="content-wrapper">
|
||||
<text>会话记录内容</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<!-- 收藏 -->
|
||||
<template #favorites>
|
||||
<scroll-view class="scroll-content" scroll-y>
|
||||
<view class="content-wrapper">
|
||||
<text>收藏内容</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
</custom-tabs>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import CustomTabs from "@/components/custom-tabs/custom-tabs.uvue";
|
||||
|
||||
const currentTab = ref(0);
|
||||
|
||||
const tabList = [
|
||||
{ label: "最近使用", name: "recent" },
|
||||
{ label: "会话记录", name: "history" },
|
||||
{ label: "收藏", name: "favorites" },
|
||||
];
|
||||
|
||||
const handleTabChange = (index: number, tab: any) => {
|
||||
console.log("切换到", tab.label, "索引:", index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.scroll-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 32rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 高度自适应说明
|
||||
|
||||
组件使用了 **Flexbox** 布局来实现高度自适应:
|
||||
|
||||
1. **父容器约束**:组件根元素使用 `h-full flex flex-col`,确保继承父级高度
|
||||
2. **内容区域自动撑满**:内容区域使用 `flex-1`,自动填充剩余空间
|
||||
3. **使用建议**:确保父容器有明确的高度(如 `height: 100vh` 或固定高度)
|
||||
|
||||
## 自定义样式
|
||||
|
||||
如需自定义样式,可以通过覆盖 CSS 变量或者深度选择器:
|
||||
|
||||
```scss
|
||||
// 自定义激活颜色
|
||||
::v-deep .custom-tabs {
|
||||
.tab-item.active .tab-label {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
background: linear-gradient(90deg, #ff6b6b 0%, #ff8787 100%);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保传入的 `tabs` 数组中的 `name` 字段与插槽名称一致
|
||||
2. 父容器需要有明确的高度,否则组件无法正确撑满
|
||||
3. 懒加载模式下,首次进入的 tab 会立即加载,其他 tab 在首次切换时加载
|
||||
4. 如果内容需要滚动,请在插槽内使用 `scroll-view` 组件
|
||||
114
qiming-mobile/components/pane-tabs/example.uvue
Normal file
114
qiming-mobile/components/pane-tabs/example.uvue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<view class="page-example h-full flex flex-col">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="title">{{ t("Mobile.Common.appName") }} Tab</text>
|
||||
</view>
|
||||
|
||||
<!-- Tab 组件 - 新的子组件方式 -->
|
||||
<view class="flex-1">
|
||||
<pane-tabs v-model="activeTab" @change="handleTabChange">
|
||||
<pane-tab key-value="recent" :tab="t('Mobile.Common.recentUsed')">
|
||||
<scroll-view class="scroll-content" scroll-y>
|
||||
<view class="content-wrapper">
|
||||
<view class="demo-card" v-for="item in 10" :key="item">
|
||||
<text class="card-title">{{ t("Mobile.Common.recentUsed") }} {{ item }}</text>
|
||||
<text class="card-desc">{{ t("Mobile.Common.noData") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</pane-tab>
|
||||
|
||||
<pane-tab key-value="history" :tab="t('Mobile.Common.conversationHistory')">
|
||||
<scroll-view class="h-full" scroll-y>
|
||||
<view class="content-wrapper">
|
||||
<view class="demo-card" v-for="item in 15" :key="item">
|
||||
<text class="card-title">{{ t("Mobile.Common.conversationHistory") }} {{ item }}</text>
|
||||
<text class="card-desc">{{ t("Mobile.Common.noData") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</pane-tab>
|
||||
|
||||
<pane-tab key-value="favorites" :tab="t('Mobile.Conversation.favorite')" :force-render="true">
|
||||
<scroll-view class="h-full" scroll-y>
|
||||
<view class="content-wrapper">
|
||||
<view class="demo-card" v-for="item in 8" :key="item">
|
||||
<text class="card-title">{{ t("Mobile.Conversation.favorite") }} {{ item }}</text>
|
||||
<text class="card-desc">{{ t("Mobile.Common.noData") }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</pane-tab>
|
||||
</pane-tabs>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import PaneTabs from "@/components/pane-tabs/pane-tabs.uvue";
|
||||
import PaneTab from "@/components/pane-tabs/pane-tab.uvue";
|
||||
import { useI18n } from "@/utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const activeTab = ref("recent");
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
console.log("tab changed:", key);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-example {
|
||||
background: #f5f5f5;
|
||||
|
||||
.page-header {
|
||||
height: 88rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 24rpx;
|
||||
|
||||
.demo-card {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
63
qiming-mobile/components/pane-tabs/pane-tab.uvue
Normal file
63
qiming-mobile/components/pane-tabs/pane-tab.uvue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="pane-tab">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// Props 定义
|
||||
interface Props {
|
||||
tab: string; // 标签文字
|
||||
keyValue: string; // 唯一标识
|
||||
forceRender?: boolean; // 是否强制渲染
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
forceRender: false,
|
||||
});
|
||||
|
||||
// 从父组件注入 paneTabs
|
||||
const paneTabs = inject<any>("paneTabs");
|
||||
|
||||
// 组件挂载时注册到父组件
|
||||
onMounted(() => {
|
||||
if (paneTabs != null && (paneTabs as any).registerTab != null) {
|
||||
const registerFn = (paneTabs as any).registerTab as (item : any) => void;
|
||||
registerFn({
|
||||
key: props.keyValue,
|
||||
label: props.tab,
|
||||
forceRender: props.forceRender,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 监听标题变化,实时更新父组件中的标签信息
|
||||
watch(() => props.tab, (newTab : string) => {
|
||||
if (paneTabs != null && (paneTabs as any).tabs != null) {
|
||||
const tabsRef = (paneTabs as any).tabs as Ref<any[]>;
|
||||
tabsRef.value.forEach((t : any) => {
|
||||
if (t.key == props.keyValue) {
|
||||
t.label = newTab;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时从父组件注销
|
||||
onUnmounted(() => {
|
||||
if (paneTabs && paneTabs.unregisterTab) {
|
||||
paneTabs.unregisterTab(props.keyValue);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pane-tab {
|
||||
flex: 0 0 auto;
|
||||
width: 100vw; // 占满视口宽度
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
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