Files
lucky_shop/components-diy/diy-tab.vue
ZF sun e2b89c348f refactor(diy-tab): 优化标签组件结构和默认配置
- 使用 tab.id 作为 v-for 的 key 提高渲染性能
- 移除默认标签数据,改为空数组
- 添加 activeTabIndex 配置项
- 调整代码格式和注释
2026-01-30 17:41:23 +08:00

834 lines
28 KiB
Vue
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>
<!-- DIY 标签页组件 - 支持多种样式和位置的标签页切换 -->
<view data-component-name="diy-tab" class="diy-tab" :class="'tab-position-' + mergedValue.tabPosition"
:style="[getCustomStyle('container')]">
<!-- 标签导航栏 -->
<view class="tab-nav" :style="[tabNavStyle, getCustomStyle('nav')]">
<!-- 标签项循环渲染 -->
<view v-for="(tab, index) in mergedValue.tabs" :key="tab.id || index"
:class="['tab-item', mergedValue.tabStyle, { active: activeTab === index }]" @tap="switchTab(index)"
:style="[tabItemStyle(index), getCustomStyle('tabItem'), activeTab === index ? getCustomStyle('tabItemActive') : {}]">
<!-- 标签文本 -->
<text
:style="[tabTextStyle(index), getCustomStyle('tabText'), activeTab === index ? getCustomStyle('tabTextActive') : {}]">{{
getTabTitle(tab.title) }}</text>
<!-- 标签指示器底部线条 -->
<view v-if="mergedValue.showIndicator" class="tab-indicator"
:style="[tabIndicatorStyle(index), getCustomStyle('indicator')]"></view>
</view>
</view>
<!-- 标签内容区域 -->
<view class="tab-content" :style="[tabContentStyle, getCustomStyle('content')]">
<!-- 标签面板循环渲染 -->
<view v-for="(tab, index) in mergedValue.tabs" :key="index"
:class="['tab-panel', { active: activeTab === index }]"
:style="[tabPanelStyle(index), getCustomStyle('panel'), activeTab === index ? getCustomStyle('panelActive') : {}]">
<!-- 渲染每个标签下的组件 -->
<diy-group v-if="tab.components" :diyData="{ value: tab.components, global: diyGlobal }"
:scrollTop="tab.scrollTop || 0" />
</view>
</view>
</view>
</template>
<script>
// 导入 DIY 混入
import DiyMinx from './minx.js'
export default {
name: 'diy-tab',
// 组件注册 - 使用懒加载解决循环依赖
components: {
diyGroup: () => import('./diy-group.vue')
},
// 组件属性定义
props: {
// 标签页配置对象
value: {
type: Object,
default: () => ({})
},
// 全局配置对象
diyGlobal: {
type: Object,
default: () => ({})
}
},
// 混入
mixins: [DiyMinx],
// 组件数据
data() {
return {
activeTab: this.value?.activeTabIndex ?? 0, // 设置当前激活的标签索引
};
},
// 组件创建钩子
created() {
console.log(`diy-tab-create`, {
value: this.mergedValue,
tabs: this.mergedValue.tabs
});
},
// 计算属性
computed: {
// 合并默认值和传入值
mergedValue() {
// 标签页数据配置
const tabsConfig = {
/**
* 标签页数据配置
* @type {Array<{title: string|Object, scrollTop: number, components: Array}>}
* @property {string} id 标签唯一标识
* @property {string|Object} title - 标签标题
* • 字符串: 普通文本或国际化键(如 'tab.home'
* • 对象: 多语言映射(如 { 'zh-cn': '首页', 'en-us': 'Home' }
* @property {number} scrollTop - 标签滚动位置
* @property {Array} components - 标签下的组件列表
*/
tabs: []
};
// 基础配置
const baseConfig = {
/**
* 是否显示指示器
* @type {boolean}
* @default true
*/
showIndicator: true,
/**
* 激活的标签索引
* @type {number}
* @default 0
*/
activeTabIndex: 0,
/**
* 标签样式
* @type {string}
* @default 'default'
* @values 'default', 'underline', 'card'
*/
tabStyle: 'default',
/**
* 标签位置
* @type {string}
* @default 'top'
* @values 'top', 'bottom', 'left', 'right'
*/
tabPosition: 'top'
};
// 导航栏样式
const navConfig = {
/**
* 标签栏高度
* @type {number|string}
* @default 24
* @unit 像素(当为数字时)
* @range 建议值20-80
* @format
* • 数字: 像素值(如 24
* • 字符串: CSS长度值如 '24px', '3rem', '4em'
* • CSS变量: 'var(--tab-height)'
* • 百分比: '10%' (相对父元素高度)
*/
tabHeight: 24,
/**
* 标签栏背景色
* @type {string}
* @default '#ffffff'
* @format CSS颜色值
*/
tabBgColor: '#ffffff',
/**
* 标签栏内边距
* @type {string}
* @default '0'
* @format CSS padding值
* @examples
* • 单个值: '0', '10px', '1rem' (四向相同)
* • 两个值: '10px 20px' (上下 左右)
* • 三个值: '10px 20px 30px' (上 左右 下)
* • 四个值: '10px 20px 30px 40px' (上 右 下 左)
* • CSS变量: 'var(--tab-padding)'
* • 百分比: '5% 10%' (相对父元素宽度)
* @note 卡片样式下会忽略此配置,自动使用基于 tabGap 的内边距
*/
tabPadding: '0'
};
// 标签项样式
const tabItemConfig = {
/**
* 标签间距
* @type {number|string}
* @default 10
* @unit 像素(当为数字时)
* @range 建议值0-30
* @format
* • 数字: 像素值(如 10
* • 字符串: CSS长度值如 '10px', '0.5rem', '1em'
* • CSS变量: 'var(--tab-gap)'
* • 百分比: '5%' (相对父元素宽度)
*/
tabGap: 10,
/**
* 字体大小
* @type {number|string}
* @default 14
* @unit 像素(当为数字时)
* @range 建议值10-20
* @format
* • 数字: 像素值(如 14
* • 字符串: CSS长度值如 '14px', '0.875rem', '1.4em'
* • CSS变量: 'var(--font-size)'
*/
fontSize: 14,
/**
* 激活状态颜色
* @type {string}
* @default '#ff4444'
* @format CSS颜色值
*/
activeColor: '#ff4444',
/**
* 非激活状态颜色
* @type {string}
* @default '#666666'
* @format CSS颜色值
*/
inactiveColor: '#666666'
};
// 卡片样式
const cardConfig = {
/**
* 卡片默认背景色
* @type {string}
* @default '#f5f5f5'
* @format CSS颜色值
*/
cardBgColor: '#f5f5f5',
/**
* 卡片激活背景色
* @type {string}
* @default '#ff4444'
* @format CSS颜色值
*/
cardActiveBgColor: '#ff4444',
/**
* 卡片默认文字颜色
* @type {string}
* @default '#666666'
* @format CSS颜色值
*/
cardTextColor: '#666666',
/**
* 卡片激活文字颜色
* @type {string}
* @default '#ffffff'
* @format CSS颜色值
*/
cardActiveTextColor: '#ffffff',
/**
* 卡片圆角大小
* @type {string}
* @default '16px'
* @format CSS长度值
*/
cardBorderRadius: '16px',
/**
* 卡片外边距
* @type {string}
* @default '0 5px'
* @format CSS margin值
*/
cardMargin: '0 5px',
/**
* 卡片内边距
* @type {string}
* @default '0 10px'
* @format CSS padding值
*/
cardPadding: '0 10px'
};
// 下划线样式
const underlineConfig = {
/**
* 下划线颜色
* @type {string}
* @default '#ff4444'
* @format CSS颜色值
*/
underlineColor: '#ff4444',
/**
* 下划线高度
* @type {number}
* @default 2
* @unit 像素
*/
underlineHeight: 2,
/**
* 下划线圆角大小
* @type {string}
* @default '1px'
* @format CSS长度值
*/
underlineBorderRadius: '1px',
/**
* 下划线左右边距
* @type {number}
* @default 10
* @unit 像素
*/
underlineMargin: 10
};
// 指示器样式
const indicatorConfig = {
/**
* 指示器颜色
* @type {string}
* @default '#ff4444'
* @format CSS颜色值
*/
indicatorColor: '#ff4444',
/**
* 指示器高度
* @type {number}
* @default 2
* @unit 像素
*/
indicatorHeight: 2
};
// 内容区域样式
const contentConfig = {
/**
* 内容区内边距
* @type {number|string}
* @default 10
* @unit 像素(当为数字时)
* @range 建议值0-50
* @format
* • 数字: 像素值(如 10
* • 字符串: CSS长度值如 '10px', '1rem', '2em'
* • CSS变量: 'var(--content-padding)'
* • 百分比: '5%' (相对父元素宽度)
*/
contentPadding: 10,
/**
* 内容区背景色
* @type {string}
* @default '#f5f5f5'
* @format CSS颜色值
* @examples
* • 十六进制: '#ffffff', '#f5f5f5'
* • RGB: 'rgb(255, 255, 255)'
* • RGBA: 'rgba(255, 255, 255, 0.5)'
* • HSL: 'hsl(0, 0%, 100%)'
* • CSS变量: 'var(--content-bg-color)'
* • 预定义颜色: 'white', 'black', 'gray'
*/
contentBgColor: '#f5f5f5',
/**
* 内容区最小高度
* @type {number|string}
* @default 200
* @unit 像素(当为数字时)
* @range 建议值50-1000
* @format
* • 数字: 像素值(如 200
* • 字符串: CSS长度值如 '200px', '20vh', '5rem'
* • CSS变量: 'var(--content-min-height)'
* • 百分比: '50%' (相对父元素高度)
*/
contentMinHeight: 200
};
// 自定义样式配置
const customStylesConfig = {
/**
* 自定义样式配置
* @type {Object}
* @description 允许外部通过完整的 CSS 样式对象完全覆盖各个部分的样式
* @property {Object} container - 容器样式
* @example { width: '100%', height: '500px', backgroundColor: '#f0f0f0' }
* @property {Object} nav - 导航栏样式
* @example { backgroundColor: '#ffffff', borderBottom: '1px solid #e0e0e0' }
* @property {Object} tabItem - 标签项样式(非激活状态)
* @example { padding: '10px 20px', borderRadius: '4px' }
* @property {Object} tabItemActive - 标签项激活样式
* @example { backgroundColor: '#ff4444', color: '#ffffff' }
* @property {Object} tabText - 标签文本样式(非激活状态)
* @example { fontSize: '14px', color: '#666666' }
* @property {Object} tabTextActive - 标签文本激活样式
* @example { fontSize: '16px', color: '#ffffff', fontWeight: 'bold' }
* @property {Object} indicator - 指示器样式
* @example { backgroundColor: '#ff4444', height: '3px' }
* @property {Object} content - 内容区域样式
* @example { padding: '20px', backgroundColor: '#f9f9f9' }
* @property {Object} panel - 标签面板样式(非激活状态)
* @example { opacity: 0.5, transform: 'translateY(10px)' }
* @property {Object} panelActive - 标签面板激活样式
* @example { opacity: 1, transform: 'translateY(0)' }
*/
customStyles: {
container: {},
nav: {},
tabItem: {},
tabItemActive: {},
tabText: {},
tabTextActive: {},
indicator: {},
content: {},
panel: {},
panelActive: {}
}
};
// 合并所有配置
const defaults = {
...tabsConfig,
...baseConfig,
...navConfig,
...tabItemConfig,
...cardConfig,
...underlineConfig,
...indicatorConfig,
...contentConfig,
...customStylesConfig
};
// 使用展开运算符合并默认值和传入值
return { ...defaults, ...this.value };
},
// 判断是否为水平布局(顶部或底部)
isHorizontal() {
return ['top', 'bottom'].includes(this.mergedValue.tabPosition);
},
// 判断是否为垂直布局(左侧或右侧)
isVertical() {
return ['left', 'right'].includes(this.mergedValue.tabPosition);
},
// 判断是否为卡片样式
isCardStyle() {
return this.mergedValue.tabStyle === 'card';
},
// 判断是否为下划线样式
isUnderlineStyle() {
return this.mergedValue.tabStyle === 'underline';
},
// 计算标签导航栏样式
tabNavStyle() {
const style = {
backgroundColor: this.mergedValue.tabBgColor || '#ffffff'
};
// 根据布局方向设置尺寸
if (this.isHorizontal) {
// 水平布局:设置高度
style.height = this.mergedValue.tabHeight + 'px';
} else {
// 垂直布局:设置宽度和高度
style.width = this.mergedValue.tabHeight + 'px';
style.height = '100%';
style.flexDirection = 'column';
}
// 设置导航栏内边距
if (this.mergedValue.tabPadding) {
style.padding = this.mergedValue.tabPadding;
}
// 卡片样式下使用标签间距作为内边距
if (this.isCardStyle) {
style.padding = this.getPadding(this.mergedValue.tabGap);
}
return style;
},
// 计算标签项样式(返回函数)
tabItemStyle() {
return (index) => {
const style = {};
if (!this.isCardStyle) {
// 非卡片样式:设置内边距
style.padding = this.getPadding(this.mergedValue.tabGap);
} else {
// 卡片样式:设置外边距、内边距、圆角和背景色
style.margin = this.getCardMargin();
style.padding = this.getCardPadding();
style.borderRadius = this.mergedValue.cardBorderRadius || '16px';
// 根据激活状态设置不同的背景色
style.backgroundColor = index === this.activeTab
? (this.mergedValue.cardActiveBgColor || this.mergedValue.activeColor)
: (this.mergedValue.cardBgColor || '#f5f5f5');
}
return style;
};
},
// 计算标签内容区域样式
tabContentStyle() {
return {
padding: this.mergedValue.contentPadding + 'px',
backgroundColor: this.mergedValue.contentBgColor || '#f5f5f5',
minHeight: (this.mergedValue.contentMinHeight || 200) + 'px'
};
},
// 计算标签文本样式(返回函数)
tabTextStyle() {
return (index) => ({
color: this.activeColor(index),
fontSize: (this.mergedValue.fontSize || 14) + 'px'
});
},
// 计算标签指示器样式(返回函数)
tabIndicatorStyle() {
return (index) => {
const style = {
backgroundColor: this.mergedValue.indicatorColor || this.mergedValue.activeColor,
transform: index === this.activeTab ? 'scaleX(1)' : 'scaleX(0)',
opacity: index === this.activeTab ? 1 : 0
};
// 卡片样式下隐藏指示器
if (this.isCardStyle) {
style.display = 'none';
} else if (this.isUnderlineStyle) {
// 下划线样式使用下划线颜色
style.backgroundColor = this.mergedValue.underlineColor || this.mergedValue.activeColor;
}
// 根据样式类型选择指示器尺寸
const indicatorSize = (this.isUnderlineStyle ? this.mergedValue.underlineHeight : this.mergedValue.indicatorHeight) || 2;
// 根据布局方向设置指示器样式
if (this.isHorizontal) {
// 水平布局:设置高度,使用 scaleX 动画
style.height = indicatorSize + 'px';
style.transform = index === this.activeTab ? 'scaleX(1)' : 'scaleX(0)';
} else {
// 垂直布局:设置宽度,使用 scaleY 动画
style.width = indicatorSize + 'px';
style.height = 'auto';
style.transform = index === this.activeTab ? 'scaleY(1)' : 'scaleY(0)';
}
return style;
};
},
// 计算标签面板样式(返回函数)
tabPanelStyle() {
return (index) => {
const isActive = index === this.activeTab;
const style = {
display: isActive ? 'block' : 'none',
opacity: isActive ? 1 : 0
};
// 根据标签位置定义不同的动画效果
const transformMap = {
top: ['translateY(0)', 'translateY(10px)'], // 顶部:从下往上滑入
bottom: ['translateY(0)', 'translateY(-10px)'], // 底部:从上往下滑入
left: ['translateX(0)', 'translateX(10px)'], // 左侧:从右往左滑入
right: ['translateX(0)', 'translateX(-10px)'] // 右侧:从左往右滑入
};
// 获取对应的变换值,默认使用无变换
const transforms = transformMap[this.mergedValue.tabPosition] || ['translate(0)', 'translate(0)'];
// 根据激活状态应用不同的变换
style.transform = isActive ? transforms[0] : transforms[1];
return style;
};
}
},
// 组件方法
methods: {
// 获取自定义样式
getCustomStyle(type) {
const customStyles = this.mergedValue.customStyles || {};
return customStyles[type] || {};
},
// 获取标签文字颜色
activeColor(index) {
if (this.isCardStyle) {
// 卡片样式:使用卡片文字颜色
return index === this.activeTab
? (this.mergedValue.cardActiveTextColor || '#ffffff')
: (this.mergedValue.cardTextColor || this.mergedValue.inactiveColor);
}
// 其他样式:使用通用文字颜色
return index === this.activeTab ? this.mergedValue.activeColor : this.mergedValue.inactiveColor;
},
// 根据布局方向获取内边距
getPadding(gap) {
return this.isHorizontal ? `0 ${gap}px` : `${gap}px 0`;
},
// 根据布局方向获取外边距
getMargin(gap) {
return this.isHorizontal ? `0 ${gap}px` : `${gap}px 0`;
},
// 获取卡片外边距
getCardMargin() {
return this.mergedValue.cardMargin || this.getMargin(this.mergedValue.tabGap / 2);
},
// 获取卡片内边距
getCardPadding() {
return this.mergedValue.cardPadding || (this.isHorizontal ? '0 10px' : '10px 0');
},
// 切换标签
switchTab(index) {
this.activeTab = index;
},
// 国际化方法:获取标签标题
getTabTitle(title) {
const locale = this.$langConfig.getCurrentLocale();
// 如果 title 是对象,根据当前语言返回对应值
if (typeof title === 'object' && title !== null) {
return title[locale] || title['zh-cn'] || Object.values(title)[0] || '';
}
// 如果 title 是字符串,保持原有逻辑
if (typeof title === 'string' && title.includes('.')) {
// 包含点号的标题视为国际化键,使用全局挂载的 $lang 方法翻译
return this.$lang ? this.$lang(title) : title;
}
// 不包含点号的标题直接返回
return title;
}
}
}
</script>
<style lang="scss" scoped>
// ===== 标签布局 Mixin =====
// 用于定义不同位置标签的布局方式
@mixin tab-layout($direction, $nav-order, $content-order) {
flex-direction: $direction;
.tab-nav {
order: $nav-order;
}
.tab-content {
order: $content-order;
flex: 1;
}
}
// ===== 指示器位置 Mixin =====
// 用于定义不同位置指示器的定位
@mixin indicator-position($position, $start, $end) {
#{$position}: 0;
#{$start}: 0;
#{$end}: 0;
width: 2px;
height: auto;
}
// ===== 主容器样式 =====
.diy-tab {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
// 默认顶部布局
@include tab-layout(column, 1, 2);
// 底部布局
&.tab-position-bottom {
@include tab-layout(column, 2, 1);
}
// 左侧布局
&.tab-position-left {
@include tab-layout(row, 1, 2);
}
// 右侧布局
&.tab-position-right {
@include tab-layout(row, 2, 1);
}
// ===== 标签导航栏样式 =====
.tab-nav {
display: flex;
align-items: center;
justify-content: flex-start;
overflow-x: auto; // 水平滚动
overflow-y: hidden; // 禁止垂直滚动
white-space: nowrap; // 不换行
position: relative;
// 隐藏滚动条Webkit 浏览器)
&::-webkit-scrollbar {
display: none;
}
// 隐藏滚动条IE/Edge
-ms-overflow-style: none;
// 隐藏滚动条Firefox
scrollbar-width: none;
}
// ===== 标签项样式 =====
.tab-item {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
padding: 0 16px;
height: 100%;
transition: all 0.3s ease;
// 激活状态
&.active {
font-weight: 500;
}
// 减少动画效果(针对偏好减少动画的用户)
@media (prefers-reduced-motion: reduce) {
transition: none;
}
}
// 左右布局下的标签项样式
&.tab-position-left .tab-item,
&.tab-position-right .tab-item {
width: 100%;
height: auto;
padding: 10px 0;
white-space: normal; // 允许换行
}
// ===== 标签文本样式 =====
.tab-text {
font-size: 14px;
transition: color 0.3s ease;
}
// ===== 标签指示器样式 =====
.tab-indicator {
position: absolute;
bottom: 0;
left: 16px;
right: 16px;
transition: all 0.3s ease;
transform-origin: center;
}
// 左侧布局的指示器
&.tab-position-left .tab-indicator {
@include indicator-position(left, top, bottom);
}
// 右侧布局的指示器
&.tab-position-right .tab-indicator {
@include indicator-position(right, top, bottom);
}
// ===== 标签内容区域样式 =====
.tab-content {
width: 100%;
overflow: hidden;
position: relative;
}
// ===== 标签面板样式 =====
.tab-panel {
width: 100%;
min-height: 200px;
transition: all 0.3s ease;
// 减少动画效果
@media (prefers-reduced-motion: reduce) {
transition: none;
}
}
// ===== 默认和下划线样式 =====
.tab-item.default,
.tab-item.underline {
padding: 0 10px;
transition: all 0.3s ease;
&.active {
color: #ff4444;
font-weight: 500;
}
}
// 下划线样式的伪元素
.tab-item.underline.active::after {
content: '';
position: absolute;
bottom: 0;
left: 10px;
right: 10px;
transition: all 0.3s ease;
}
// ===== 卡片样式 =====
.tab-item.card {
border-radius: 16px;
transition: all 0.3s ease;
&.active {
font-weight: 500;
}
}
}
</style>