Files
lucky_shop/components-diy/diy-tab.vue

1284 lines
46 KiB
Vue
Raw Permalink 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="tab-diy-component-preview-container"
:class="[containerClass, styleClass]" :style="[containerStyle]">
<!-- 顶部标签位置:标签导航 + 指示器 + 内容区域 -->
<template v-if="mergedValue.tabPosition === 'top'">
<!-- 标签导航 -->
<view class="tab-nav" :style="[tabNavStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getTabKey(tab, idx)" class="tab-item"
:class="{ active: activeTab === idx }"
:style="[tabItemStyle, activeTab === idx ? tabItemActiveStyle : {}]" @tap="switchTab(idx)">
<text class="tab-text" :style="[tabTextStyle, activeTab === idx ? tabTextActiveStyle : {}]">
{{ getTabTitle(tab.title) }}
</text>
</view>
</view>
<!-- 指示器 -->
<view v-if="mergedValue.showIndicator" class="tab-indicator" :style="[
indicatorStyle,
{
top: indicatorVars.top,
left: indicatorVars.left,
right: indicatorVars.right,
bottom: indicatorVars.bottom,
width: indicatorVars.width,
height: indicatorVars.height
}
]"></view>
<!-- 内容区域 -->
<view class="tab-content" :style="[contentStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getPanelKey(tab, idx)" class="tab-panel"
:class="{ active: activeTab === idx }"
:style="[panelStyle, activeTab === idx ? panelActiveStyle : {}]">
<view v-if="tab.components && tab.components.length > 0" class="tab-components">
<!-- 渲染每个标签下的组件 -->
<diy-group v-if="tab.components" :diyData="{ value: tab.components, global: diyGlobal, tabIndex: idx }"
:scrollTop="tab.scrollTop || 0" />
</view>
</view>
</view>
</template>
<!-- 底部标签位置:内容区域 + 指示器 + 标签导航 -->
<template v-else-if="mergedValue.tabPosition === 'bottom'">
<!-- 内容区域 -->
<view class="tab-content" :style="[contentStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getPanelKey(tab, idx)" class="tab-panel"
:class="{ active: activeTab === idx }"
:style="[panelStyle, activeTab === idx ? panelActiveStyle : {}]">
<view v-if="tab.components && tab.components.length > 0" class="tab-components">
<!-- 渲染每个标签下的组件 -->
<diy-group v-if="tab.components" :diyData="{ value: tab.components, global: diyGlobal, tabIndex: idx }"
:scrollTop="tab.scrollTop || 0" />
</view>
</view>
</view>
<!-- 指示器 -->
<view v-if="mergedValue.showIndicator" class="tab-indicator" :style="[
indicatorStyle,
{
top: indicatorVars.top,
left: indicatorVars.left,
right: indicatorVars.right,
bottom: indicatorVars.bottom,
width: indicatorVars.width,
height: indicatorVars.height
}
]"></view>
<!-- 标签导航 -->
<view class="tab-nav" :style="[tabNavStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getTabKey(tab, idx)" class="tab-item"
:class="[{ active: activeTab === idx }, { 'hover-animation': mergedValue.animation }]"
:style="[tabItemStyle, activeTab === idx ? tabItemActiveStyle : {}, tabItemHoverStyle]" @tap="switchTab(idx)">
<text class="tab-text" :style="[tabTextStyle, activeTab === idx ? tabTextActiveStyle : {}, mergedValue.animation ? tabTextHoverStyle : {}]">
{{ getTabTitle(tab.title) }}
</text>
</view>
</view>
</template>
<!-- 左侧标签位置:标签导航 + 指示器 + 内容区域 (水平布局) -->
<template v-else-if="mergedValue.tabPosition === 'left'">
<view class="tab-layout-horizontal">
<!-- 左侧标签导航 -->
<view class="tab-nav tab-nav-vertical" :style="[tabNavStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getTabKey(tab, idx)" class="tab-item tab-item-vertical"
:class="[{ active: activeTab === idx }, { 'hover-animation': mergedValue.animation }]"
:style="[tabItemStyle, activeTab === idx ? tabItemActiveStyle : {}, tabItemHoverStyle]" @tap="switchTab(idx)">
<text class="tab-text" :style="[tabTextStyle, activeTab === idx ? tabTextActiveStyle : {}, mergedValue.animation ? tabTextHoverStyle : {}]">
{{ getTabTitle(tab.title) }}
</text>
</view>
</view>
<!-- 左侧指示器 -->
<view v-if="mergedValue.showIndicator" class="tab-indicator tab-indicator-vertical" :style="[
indicatorStyle,
{
top: indicatorVars.top,
left: indicatorVars.left,
right: indicatorVars.right,
bottom: indicatorVars.bottom,
width: indicatorVars.width,
height: indicatorVars.height
}
]"></view>
<!-- 右侧内容区域 -->
<view class="tab-content" :style="[contentStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getPanelKey(tab, idx)" class="tab-panel"
:class="{ active: activeTab === idx }"
:style="[panelStyle, activeTab === idx ? panelActiveStyle : {}]">
<view v-if="tab.components && tab.components.length > 0" class="tab-components">
<!-- 渲染每个标签下的组件 -->
<diy-group v-if="tab.components" :diyData="{ value: tab.components, global: diyGlobal }"
:scrollTop="tab.scrollTop || 0" />
</view>
</view>
</view>
</view>
</template>
<!-- 右侧标签位置:内容区域 + 指示器 + 标签导航 (水平布局) -->
<template v-else-if="mergedValue.tabPosition === 'right'">
<view class="tab-layout-horizontal">
<!-- 左侧内容区域 -->
<view class="tab-content" :style="[contentStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getPanelKey(tab, idx)" class="tab-panel"
:class="{ active: activeTab === idx }"
:style="[panelStyle, activeTab === idx ? panelActiveStyle : {}]">
<view v-if="tab.components && tab.components.length > 0" class="tab-components">
<!-- 渲染每个标签下的组件 -->
<diy-group v-if="tab.components" :diyData="{ value: tab.components, global: diyGlobal }"
:scrollTop="tab.scrollTop || 0" />
</view>
</view>
</view>
<!-- 右侧指示器 -->
<view v-if="mergedValue.showIndicator" class="tab-indicator tab-indicator-vertical" :style="[
indicatorStyle,
{
top: indicatorVars.top,
left: indicatorVars.left,
right: indicatorVars.right,
bottom: indicatorVars.bottom,
width: indicatorVars.width,
height: indicatorVars.height
}
]"></view>
<!-- 右侧标签导航 -->
<view class="tab-nav tab-nav-vertical" :style="[tabNavStyle]">
<view v-for="(tab, idx) in mergedValue.tabs" :key="getTabKey(tab, idx)" class="tab-item tab-item-vertical"
:class="[{ active: activeTab === idx }, { 'hover-animation': mergedValue.animation }]"
:style="[tabItemStyle, activeTab === idx ? tabItemActiveStyle : {}, tabItemHoverStyle]" @tap="switchTab(idx)">
<text class="tab-text" :style="[tabTextStyle, activeTab === idx ? tabTextActiveStyle : {}, mergedValue.animation ? tabTextHoverStyle : {}]">
{{ getTabTitle(tab.title) }}
</text>
</view>
</view>
</view>
</template>
</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, // 设置当前激活的标签索引
indicatorVars: { // 指示器CSS变量
top: '',
left: '',
right: '',
bottom: '',
width: '',
height: ''
}
};
},
// 组件创建钩子
created() {
console.log(`diy-tab-create`, {
value: this.mergedValue,
tabs: this.mergedValue.tabs
});
},
// 组件挂载钩子
mounted() {
// 初始化指示器位置
this.$nextTick(() => {
this.updateIndicatorPosition();
});
},
// 计算属性
computed: {
// 合并默认值和传入值
mergedValue() {
// 标签页数据配置
const tabsConfig = {
tabs: []
};
// 基础配置
const baseConfig = {
showIndicator: true,
activeTabIndex: 0,
tabStyle: 'default',
tabPosition: 'top',
animation: true
};
// 导航栏样式
const navConfig = {
tabHeight: 24,
tabBgColor: '#ffffff',
tabPadding: '0',
tabGap: 10,
navAlign: 'start'
};
// 标签项样式
const tabItemConfig = {
tabGap: 10,
fontSize: 14,
fontWeight: 400,
activeColor: '#339af0',
inactiveColor: '#6c757d',
hoverColor: '#3b82f6',
activeFontWeight: 500
};
// 卡片样式
const cardConfig = {
cardBgColor: '#f8f9fa',
cardActiveBgColor: '#339af0',
cardTextColor: '#6c757d',
cardActiveTextColor: '#ffffff',
cardBorderRadius: '16px',
cardMargin: '0 5px',
cardPadding: '0 10px',
cardActiveHoverBgColor: '#228be6',
cardActiveHoverTextColor: '#ffffff'
};
// 下划线样式
const underlineConfig = {
underlineActiveColor: '#339af0',
underlineInactiveColor: '#e0e0e0',
underlineHeight: 2,
underlineBorderRadius: '1px',
underlineMargin: 10
};
// 指示器样式
const indicatorConfig = {
indicatorColor: '#339af0',
indicatorHeight: 2,
indicatorWidth: '100%',
indicatorBorderRadius: '1px'
};
// 内容区域样式
const contentConfig = {
contentPadding: 10,
contentBgColor: '#f5f5f5',
contentMinHeight: 200
};
// 边框样式
const borderConfig = {
showBorder: true,
borderColor: '#e0e0e0',
borderWidth: 1,
borderRadius: 4
};
// 动画配置
const animationConfig = {
animationDuration: 0.3,
animationTimingFunction: 'ease-in-out'
};
// 导航栏边框样式
const navBorderConfig = {
showNavBorder: true,
navBorderColor: '#e0e0e0',
navBorderWidth: 1
};
// 自定义样式配置
const customStylesConfig = {
customStyles: {
container: {},
nav: {},
tabItem: {},
tabItemActive: {},
tabItemHover: {},
tabText: {},
tabTextActive: {},
tabTextHover: {},
indicator: {},
content: {},
panel: {},
panelActive: {}
}
};
// 合并所有配置
const defaults = {
...tabsConfig,
...baseConfig,
...navConfig,
...tabItemConfig,
...cardConfig,
...underlineConfig,
...indicatorConfig,
...contentConfig,
...borderConfig,
...navBorderConfig,
...animationConfig,
...customStylesConfig
};
// 使用展开运算符合并默认值和传入值
return { ...defaults, ...this.value };
},
// 容器样式(与后端版本一致)
containerStyle() {
let style = {
...this.mergedValue.customStyles.container
};
// 应用边框样式
if (this.mergedValue.showBorder) {
style.border = this.formatStyleValue(this.mergedValue.borderWidth, 1) + ' solid ' + (this.mergedValue.borderColor || '#e0e0e0');
style.borderRadius = this.formatStyleValue(this.mergedValue.borderRadius, 4);
} else {
style.border = 'none';
style.borderRadius = '0';
}
// 应用下划线样式 - 不依赖指示器组件
style['--underline-active-color'] = this.mergedValue.underlineActiveColor || '#339af0';
style['--underline-inactive-color'] = this.mergedValue.underlineInactiveColor || '#e0e0e0';
style['--underline-height'] = this.formatStyleValue(this.mergedValue.underlineHeight, 2);
// 设置通用CSS变量
style['--active-color'] = this.mergedValue.activeColor || '#339af0';
style['--inactive-color'] = this.mergedValue.inactiveColor || '#6c757d';
style['--indicator-color'] = this.mergedValue.indicatorColor || '#339af0';
style['--card-bg-color'] = this.mergedValue.cardBgColor || '#f8f9fa';
style['--card-text-color'] = this.mergedValue.cardTextColor || '#6c757d';
style['--card-active-bg-color'] = this.mergedValue.cardActiveBgColor || '#339af0';
style['--card-active-text-color'] = this.mergedValue.cardActiveTextColor || '#ffffff';
style['--card-active-hover-bg-color'] = this.mergedValue.cardActiveHoverBgColor || '#228be6';
style['--card-active-hover-text-color'] = this.mergedValue.cardActiveHoverTextColor || '#ffffff';
// 应用动画配置
if (this.mergedValue.animation) {
style['--animation-duration'] = (this.mergedValue.animationDuration || 0.3) + 's';
style['--animation-timing-function'] = this.mergedValue.animationTimingFunction || 'ease-in-out';
}
return style;
},
// 导航栏样式(与后端版本一致)
tabNavStyle() {
let style = {
backgroundColor: this.mergedValue.tabBgColor,
padding: this.mergedValue.tabPadding,
...this.mergedValue.customStyles.nav
};
// 对于水平布局(顶部和底部),设置固定高度
if (this.mergedValue.tabPosition === 'top' || this.mergedValue.tabPosition === 'bottom') {
style.height = this.formatStyleValue(this.mergedValue.tabHeight, 24);
// 应用对齐方式
if (this.mergedValue.navAlign === 'start') {
style.justifyContent = 'flex-start';
} else if (this.mergedValue.navAlign === 'center') {
style.justifyContent = 'center';
} else if (this.mergedValue.navAlign === 'end') {
style.justifyContent = 'flex-end';
}
}
// 对于垂直布局(左侧和右侧),不设置固定高度,让它自适应内容
else {
// 垂直布局应用对齐方式
if (this.mergedValue.navAlign === 'start') {
style.alignItems = 'flex-start';
} else if (this.mergedValue.navAlign === 'center') {
style.alignItems = 'center';
} else if (this.mergedValue.navAlign === 'end') {
style.alignItems = 'flex-end';
}
}
// 应用导航栏边框样式
if (this.mergedValue.showNavBorder) {
const reversePosition = this.getReversePosition(this.mergedValue.tabPosition);
if (reversePosition) {
style[`border-${reversePosition}`] = this.formatStyleValue(this.mergedValue.navBorderWidth, 1) + ' solid ' + (this.mergedValue.navBorderColor || '#e0e0e0');
}
} else {
style.border = 'none';
}
return style;
},
// 标签项样式(与后端版本一致)
tabItemStyle() {
let style = {
...this.mergedValue.customStyles.tabItem
};
// 根据tabStyle调整样式
if (this.mergedValue.tabStyle === 'card') {
// 卡片样式:使用card相关属性
style.borderRadius = this.formatStyleValue(this.mergedValue.cardBorderRadius, 16);
style.margin = this.mergedValue.cardMargin || '0 5px';
style.padding = this.mergedValue.cardPadding || '8px 16px';
} else {
// 对于非卡片样式,使用原有的间距逻辑
if (this.mergedValue.tabPosition === 'top' || this.mergedValue.tabPosition === 'bottom') {
style.marginRight = this.formatStyleValue(this.mergedValue.tabGap);
} else {
style.marginBottom = this.formatStyleValue(this.mergedValue.tabGap);
}
}
return style;
},
// 标签项激活样式(与后端版本一致)
tabItemActiveStyle() {
return {
...this.mergedValue.customStyles.tabItemActive
};
},
// 标签文本样式(与后端版本一致)
tabTextStyle() {
return {
fontSize: this.formatStyleValue(this.mergedValue.fontSize, 14),
fontWeight: this.mergedValue.fontWeight || 400,
color: this.mergedValue.inactiveColor,
...this.mergedValue.customStyles.tabText
};
},
// 标签文本激活样式(与后端版本一致)
tabTextActiveStyle() {
return {
color: this.mergedValue.activeColor,
fontWeight: this.mergedValue.activeFontWeight || 500,
...this.mergedValue.customStyles.tabTextActive
};
},
// 标签项悬停样式
tabItemHoverStyle() {
return {
...this.mergedValue.customStyles.tabItemHover
};
},
// 标签文本悬停样式
tabTextHoverStyle() {
return {
color: this.mergedValue.hoverColor || '#3b82f6',
...this.mergedValue.customStyles.tabTextHover
};
},
// 指示器样式(与后端版本一致)
indicatorStyle() {
// 计算高度,考虑不同样式
let indicatorHeight = this.mergedValue.indicatorHeight;
if (this.mergedValue.tabStyle === 'underline') {
indicatorHeight = this.mergedValue.underlineHeight || indicatorHeight;
}
let style = {
backgroundColor: this.mergedValue.indicatorColor,
height: this.formatStyleValue(indicatorHeight, 2),
...this.mergedValue.customStyles.indicator
};
// 根据tabStyle调整样式
if (this.mergedValue.tabStyle === 'underline') {
style.borderRadius = this.formatStyleValue(this.mergedValue.underlineBorderRadius, 0);
style.marginLeft = this.formatStyleValue(this.mergedValue.underlineMargin, 0);
style.marginRight = this.formatStyleValue(this.mergedValue.underlineMargin, 0);
} else if (this.mergedValue.tabStyle === 'card') {
// 卡片样式:指示器应该与卡片样式协调
style.borderRadius = this.formatStyleValue(this.mergedValue.cardBorderRadius, 16);
style.boxShadow = '0 2px 10px rgba(51, 154, 240, 0.4)';
}
return style;
},
// 内容区域样式(与后端版本一致)
contentStyle() {
var marginVar = {};
// 对于卡片样式,不需要为内容区域添加边距
if (this.mergedValue.tabStyle !== 'card') {
marginVar = {
[`margin-${this.mergedValue.tabPosition}`]: this.formatStyleValue(this.mergedValue.indicatorHeight, 2)
};
}
return {
padding: this.formatStyleValue(this.mergedValue.contentPadding, 10),
backgroundColor: this.mergedValue.contentBgColor,
minHeight: this.formatStyleValue(this.mergedValue.contentMinHeight, 200),
...this.mergedValue.customStyles.content,
...marginVar
};
},
// 面板样式(与后端版本一致)
panelStyle() {
return {
...this.mergedValue.customStyles.panel
};
},
// 面板激活样式(与后端版本一致)
panelActiveStyle() {
return {
...this.mergedValue.customStyles.panelActive
};
},
// 容器类名(与后端版本一致)
containerClass() {
const positionMap = {
'top': 'tab-position-top',
'bottom': 'tab-position-bottom',
'left': 'tab-position-left',
'right': 'tab-position-right'
};
return positionMap[this.mergedValue.tabPosition || 'top'] || 'tab-position-top';
},
// 样式类名(与后端版本一致)
styleClass() {
const styleMap = {
'default': 'tab-style-default',
'underline': 'tab-style-underline',
'card': 'tab-style-card'
};
return styleMap[this.mergedValue.tabStyle || 'default'] || 'tab-style-default';
}
},
// 监听器
watch: {
// 监听激活标签变化,更新指示器
activeTab: {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.showIndicator': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.tabPosition': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.tabStyle': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.indicatorColor': {
handler() {
this.updateIndicatorPosition();
}
},
'mergedValue.indicatorHeight': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.indicatorWidth': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.tabHeight': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.tabPadding': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.tabGap': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.fontSize': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.underlineActiveColor': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.underlineInactiveColor': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.underlineHeight': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.underlineBorderRadius': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
},
'mergedValue.underlineMargin': {
handler() {
this.$nextTick(() => {
this.updateIndicatorPosition();
});
}
}
},
// 组件方法
methods: {
// 获取面板key
getPanelKey(tab, idx) {
return `panel-${tab?.id ?? idx}`;
},
// 获取标签项key
getTabKey(tab, idx) {
return `tab-${tab?.id ?? idx}`;
},
// 更新指示器位置(与后端版本一致)
updateIndicatorPosition() {
this.$nextTick(() => {
// 使用 uni.createSelectorQuery 替代 DOM API
const query = uni.createSelectorQuery().in(this);
// 获取容器、指示器、激活标签、导航栏的尺寸
query.select('.tab-diy-component-preview-container').boundingClientRect();
query.select('.tab-indicator').boundingClientRect();
query.select(`.tab-item.active`).boundingClientRect();
query.select('.tab-nav').boundingClientRect();
query.exec(res => {
if (!res || res.length < 4) return;
const [container, indicator, activeTab, nav] = res;
if (!container || !indicator || !activeTab || !nav) return;
// 检查是否应该显示指示器
if (!this.mergedValue.showIndicator) return;
// 计算基础宽度和高度,考虑不同样式
let indicatorWidth = this.mergedValue.indicatorWidth || activeTab.width;
let indicatorHeight = this.mergedValue.indicatorHeight;
if (this.mergedValue.tabStyle === 'underline') {
indicatorHeight = this.mergedValue.underlineHeight || indicatorHeight;
} else if (this.mergedValue.tabStyle === 'card') {
// 卡片样式:指示器高度应该与卡片标签项的高度协调
indicatorHeight = this.mergedValue.tabHeight || indicatorHeight;
}
// 根据标签位置计算指示器位置,更新CSS变量
if (this.mergedValue.tabPosition === 'left') {
// 左侧布局:指示器在tab-content的左侧垂直显示
let indicatorHeightVertical = indicatorHeight;
let indicatorTop = this.formatStyleValue(activeTab.top - container.top);
let indicatorHeightVerticalValue = this.formatStyleValue(activeTab.height);
// 如果是下划线样式,调整垂直布局的样式
if (this.mergedValue.tabStyle === 'underline') {
indicatorHeightVerticalValue = this.formatStyleValue(activeTab.height - (parseInt(this.mergedValue.underlineMargin || 0) * 2));
indicatorTop = this.formatStyleValue(activeTab.top - container.top + parseInt(this.mergedValue.underlineMargin || 0));
}
this.indicatorVars = {
top: indicatorTop,
left: this.formatStyleValue(nav.width),
right: 'auto',
bottom: 'auto',
width: typeof indicatorHeightVertical === 'string' ? indicatorHeightVertical : this.formatStyleValue(indicatorHeightVertical),
height: indicatorHeightVerticalValue
};
} else if (this.mergedValue.tabPosition === 'right') {
// 右侧布局:指示器在tab-content的右侧垂直显示
let indicatorHeightVertical = indicatorHeight;
let indicatorTop = this.formatStyleValue(activeTab.top - container.top);
let indicatorHeightVerticalValue = this.formatStyleValue(activeTab.height);
// 如果是下划线样式,调整垂直布局的样式
if (this.mergedValue.tabStyle === 'underline') {
indicatorHeightVerticalValue = this.formatStyleValue(activeTab.height - (parseInt(this.mergedValue.underlineMargin || 0) * 2));
indicatorTop = this.formatStyleValue(activeTab.top - container.top + parseInt(this.mergedValue.underlineMargin || 0));
}
this.indicatorVars = {
top: indicatorTop,
left: 'auto',
right: this.formatStyleValue(nav.width),
bottom: 'auto',
width: typeof indicatorHeightVertical === 'string' ? indicatorHeightVertical : this.formatStyleValue(indicatorHeightVertical),
height: indicatorHeightVerticalValue
};
} else if (this.mergedValue.tabPosition === 'bottom') {
// 底部布局:指示器在标签导航上方水平显示
let indicatorHeightValue = indicatorHeight;
let indicatorTopValue = this.formatStyleValue(container.bottom - nav.top);
// 如果是下划线样式,调整底部布局的样式
if (this.mergedValue.tabStyle === 'underline') {
indicatorHeightValue = this.formatStyleValue(this.mergedValue.underlineHeight, indicatorHeight);
}
this.indicatorVars = {
top: 'auto',
left: this.formatStyleValue(activeTab.left - container.left + parseInt(this.mergedValue.underlineMargin || 0)),
right: 'auto',
bottom: indicatorTopValue,
width: typeof indicatorWidth === 'string' ? indicatorWidth : this.formatStyleValue(indicatorWidth),
height: indicatorHeightValue
};
} else {
// 顶部布局(默认):指示器在标签导航下方水平显示
let indicatorHeightValue = indicatorHeight;
let indicatorTopValue = this.formatStyleValue(nav.height);
// 如果是下划线样式,调整顶部布局的样式
if (this.mergedValue.tabStyle === 'underline') {
indicatorHeightValue = this.formatStyleValue(this.mergedValue.underlineHeight, indicatorHeight);
}
this.indicatorVars = {
top: indicatorTopValue,
left: this.formatStyleValue(activeTab.left - container.left + parseInt(this.mergedValue.underlineMargin || 0)),
right: 'auto',
bottom: 'auto',
width: typeof indicatorWidth === 'string' ? indicatorWidth : this.formatStyleValue(indicatorWidth),
height: indicatorHeightValue
};
}
});
});
},
// 格式化样式值(与后端版本一致)
formatStyleValue(value, defaultValue) {
if (value === undefined || value === null || value === '') {
if (defaultValue !== undefined) {
return typeof defaultValue === 'number' ? defaultValue + 'px' : defaultValue;
}
return '0';
}
// 如果是数字,添加 px 单位
if (typeof value === 'number') {
return value + 'px';
}
// 如果是字符串且不包含单位,添加 px
if (typeof value === 'string' && !isNaN(value) && !value.includes('px') && !value.includes('%') && !value.includes('rem') && !value.includes('em') && !value.includes('vh') && !value.includes('vw')) {
return value + 'px';
}
return value;
},
// 获取反向位置(与后端版本一致)
getReversePosition(position) {
const reverseMap = {
'top': 'bottom',
'bottom': 'top',
'left': 'right',
'right': 'left'
};
return reverseMap[position] || '';
},
// 获取自定义样式
getCustomStyle(type) {
const customStyles = this.mergedValue.customStyles || {};
return customStyles[type] || {};
},
// 获取标签文字颜色
activeColor(index) {
if (this.mergedValue.tabStyle === 'card') {
// 卡片样式:使用卡片文字颜色
return index === this.activeTab
? (this.mergedValue.cardActiveTextColor || '#ffffff')
: (this.mergedValue.cardTextColor || this.mergedValue.inactiveColor);
}
// 其他样式:使用通用文字颜色
return index === this.activeTab ? this.mergedValue.activeColor : this.mergedValue.inactiveColor;
},
// 切换标签
switchTab(index) {
this.activeTab = index;
},
// 国际化方法:获取标签标题
getTabTitle(title) {
// 如果 title 是对象,根据当前语言返回对应值
if (typeof title === 'object' && title !== null) {
const locale = this.$langConfig?.getCurrentLocale?.() || 'zh-cn';
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>
// ===== 主容器样式 =====
.tab-diy-component-preview-container {
width: 100%;
min-height: 0px;
display: flex;
border: 1px solid #e0e0e0;
border-radius: 4px;
overflow: hidden;
position: relative;
flex-direction: column;
// 顶部标签位置 (默认)
&.tab-position-top {
display: flex;
flex-direction: column;
}
// 底部标签位置
&.tab-position-bottom {
display: flex;
flex-direction: column;
.tab-content {
order: 1;
}
.tab-indicator {
order: 2;
bottom: auto;
top: 0;
}
.tab-nav {
order: 3;
border-bottom: none;
border-top: 1px solid #e0e0e0;
}
}
// 左侧标签位置
&.tab-position-left {
display: flex;
.tab-layout-horizontal {
display: flex;
flex-direction: row;
align-items: stretch;
}
.tab-nav-vertical {
display: flex;
flex-direction: column;
min-width: 120px;
border-bottom: none;
border-right: 1px solid #e0e0e0;
}
.tab-item-vertical {
padding: 15px 20px;
}
.tab-indicator-vertical {
position: absolute;
right: 0;
top: 0;
background-color: #339af0;
transition: all 0.3s ease;
}
.tab-content {
flex: 1;
}
}
// 右侧标签位置
&.tab-position-right {
display: flex;
.tab-layout-horizontal {
display: flex;
flex-direction: row;
align-items: stretch;
}
.tab-nav-vertical {
display: flex;
flex-direction: column;
min-width: 120px;
border-bottom: none;
border-left: 1px solid #e0e0e0;
}
.tab-item-vertical {
padding: 15px 20px;
}
.tab-indicator-vertical {
position: absolute;
left: 0;
top: 0;
background-color: #339af0;
transition: all 0.3s ease;
}
.tab-content {
flex: 1;
}
}
}
// ===== 水平布局包装器(用于左右布局)=====
.tab-layout-horizontal {
display: flex;
flex: 1;
width: 100%;
}
// ===== 标签导航栏样式 =====
.tab-nav {
display: flex;
align-items: center;
padding: 5px 0;
border-bottom: 1px solid #e0e0e0;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
position: relative;
// 隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
}
// 垂直导航栏
.tab-nav-vertical {
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
white-space: normal;
padding: 0;
}
// ===== 标签项样式 =====
.tab-item {
padding: 0px 20px 0px;
cursor: pointer;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
user-select: none;
&.active {
color: var(--active-color, #339af0);
}
// 悬停动画效果
&.hover-animation {
transition: all var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out);
&:hover {
color: var(--hover-color, #3b82f6);
transform: translateY(-1px);
}
}
// 减少动画效果(针对偏好减少动画的用户)
@media (prefers-reduced-motion: reduce) {
transition: none;
}
}
// 垂直标签项
.tab-item-vertical {
width: 100%;
height: auto;
white-space: normal;
}
// ===== 标签文本样式 =====
.tab-text {
font-size: 14px;
font-weight: 400;
color: inherit;
}
// ===== 标签指示器样式 =====
.tab-indicator {
position: absolute;
background-color: var(--indicator-color, #339af0);
transition: all var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out);
will-change: left, right, top, bottom, width, height;
bottom: 0;
left: 16px;
right: 16px;
// 支持圆角
border-radius: var(--indicator-border-radius, 1px);
}
// 垂直指示器(用于左右布局)
.tab-indicator-vertical {
position: relative;
width: 2px;
height: auto;
bottom: auto;
left: auto;
right: auto;
}
// ===== 标签内容区域样式 =====
.tab-content {
padding: 20px;
min-height: 200px;
box-sizing: border-box;
width: 100%;
flex: 1;
overflow: hidden;
position: relative;
}
// ===== 标签面板样式 =====
.tab-panel {
display: none;
opacity: 0;
transform: translateY(8px);
transition: opacity var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out), transform var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out);
height: 100%;
box-sizing: border-box;
width: 100%;
min-height: 200px;
// 激活状态
&.active {
display: block;
opacity: 1;
transform: translateY(0);
transition: opacity var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out), transform var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out);
}
// 减少动画效果
@media (prefers-reduced-motion: reduce) {
transition: none;
}
.tab-components {
width: 100%;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
padding-right: 8px;
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
}
}
// ===== 默认样式 =====
.tab-style-default .tab-item {
position: relative;
}
// ===== 下划线样式 - 完全由边框样式控制,不依赖指示器组件 =====
.tab-style-underline .tab-item {
border-bottom: var(--underline-height, 2px) solid var(--underline-inactive-color, #e0e0e0);
&.active {
border-bottom: var(--underline-height, 2px) solid var(--underline-active-color, #339af0);
}
}
// ===== 卡片样式 =====
.tab-style-card .tab-nav {
background-color: var(--card-bg-color, #f5f5f5);
padding: 10px;
}
.tab-style-card .tab-item {
background-color: var(--card-item-bg-color, #ffffff);
border-radius: 16px;
margin: 0 5px;
padding: 0 10px;
position: relative;
overflow: hidden;
color: var(--card-text-color, #6c757d);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
height: auto;
padding: 8px 16px;
&.hover-animation {
transition: all var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out);
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
transform: translateY(-1px);
background-color: #f8f9fa;
}
}
&.active {
background-color: var(--card-active-bg-color, #339af0);
color: var(--card-active-text-color, #ffffff);
box-shadow: 0 2px 10px rgba(51, 154, 240, 0.4);
transform: translateY(-1px);
&.hover-animation:hover {
background-color: var(--card-active-hover-bg-color, #228be6);
color: var(--card-active-hover-text-color, #ffffff);
box-shadow: 0 3px 12px rgba(51, 154, 240, 0.45);
}
}
}
// 卡片样式 - 垂直方向
.tab-style-card .tab-nav-vertical {
background-color: #f5f5f5;
padding: 10px 0;
}
.tab-style-card .tab-item-vertical {
background-color: var(--card-item-bg-color, #ffffff);
border-radius: 16px;
margin: 5px 10px;
padding: 12px 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-bottom: none;
position: relative;
overflow: hidden;
color: var(--card-text-color, #6c757d);
&.hover-animation {
transition: all var(--animation-duration, 0.3s) var(--animation-timing-function, ease-in-out);
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
transform: translateX(-2px);
background-color: #f8f9fa;
}
}
&.active {
background-color: var(--card-active-bg-color, #339af0);
color: var(--card-active-text-color, #ffffff);
box-shadow: 0 2px 10px rgba(51, 154, 240, 0.4);
transform: translateX(-2px);
&.hover-animation:hover {
background-color: #228be6;
box-shadow: 0 3px 12px rgba(51, 154, 240, 0.45);
}
}
}
// 隐藏卡片样式下的指示器
.tab-style-card .tab-indicator {
display: none;
}
</style>