1276 lines
46 KiB
Vue
1276 lines
46 KiB
Vue
<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 }"
|
||
: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 }"
|
||
: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: 300px;
|
||
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>
|