185 lines
3.9 KiB
Vue
185 lines
3.9 KiB
Vue
<template>
|
||
<div class="my-tabs">
|
||
<!-- 导航栏 -->
|
||
<div class="my-tabs__nav">
|
||
<div
|
||
v-for="pane in panes"
|
||
:key="pane.name"
|
||
:class="{
|
||
'my-tabs__item': true,
|
||
'is-active': activeName === pane.name,
|
||
'is-disabled': pane.disabled
|
||
}"
|
||
@click="handleClick(pane)"
|
||
>
|
||
<!-- 标签文本 -->
|
||
<span>{{ pane.label }}</span>
|
||
|
||
<!-- 关闭按钮 -->
|
||
<i
|
||
v-if="pane.closable"
|
||
class="my-tabs__close"
|
||
@click.stop="handleClose(pane)"
|
||
>×</i>
|
||
</div>
|
||
|
||
<!-- 底部横线和滑块 -->
|
||
<div class="my-tabs__slider-wrapper">
|
||
<div class="my-tabs__slider-track"></div>
|
||
<div
|
||
class="my-tabs__slider"
|
||
ref="slider"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区 -->
|
||
<div class="my-tabs__content">
|
||
<slot></slot>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
|
||
<script>
|
||
export default {
|
||
name: 'MyTabs',
|
||
model: {
|
||
prop: 'value',
|
||
event: 'input'
|
||
},
|
||
props: {
|
||
value: [String, Number], // 当前激活
|
||
type: { type: String, default: '' }, // 后期可扩展 card/border-card
|
||
closable: Boolean // 全局是否允许关闭
|
||
},
|
||
data() {
|
||
return {
|
||
panes: [] // 缓存所有 MyTabPane 实例
|
||
}
|
||
},
|
||
computed: {
|
||
activeName: {
|
||
get() { return this.value },
|
||
set(val) { this.$emit('input', val) }
|
||
}
|
||
},
|
||
watch: {
|
||
activeName() {
|
||
this.$nextTick(() => {
|
||
this.updateSliderPosition();
|
||
});
|
||
},
|
||
panes() {
|
||
this.$nextTick(() => {
|
||
this.updateSliderPosition();
|
||
});
|
||
}
|
||
},
|
||
mounted() {
|
||
this.$nextTick(() => {
|
||
this.updateSliderPosition();
|
||
});
|
||
},
|
||
methods: {
|
||
/* 收集子组件 */
|
||
registerPane(pane) {
|
||
this.panes.push(pane)
|
||
// 默认选中第一个
|
||
if (!this.activeName && this.panes.length === 1) {
|
||
this.activeName = pane.name
|
||
}
|
||
},
|
||
unregisterPane(pane) {
|
||
const idx = this.panes.indexOf(pane)
|
||
if (idx > -1) this.panes.splice(idx, 1)
|
||
},
|
||
/* 点击导航 */
|
||
handleClick(pane) {
|
||
if (pane.disabled) return
|
||
this.activeName = pane.name
|
||
this.$emit('tab-click', pane)
|
||
},
|
||
/* 关闭 */
|
||
handleClose(pane) {
|
||
this.$emit('edit', pane.name, 'remove')
|
||
// 业务层把 v-model 绑定的数组删掉即可
|
||
},
|
||
/* 更新滑块位置和宽度 */
|
||
updateSliderPosition() {
|
||
if (!this.activeName || !this.$refs.slider) return;
|
||
|
||
const activeTab = this.$el.querySelector(`.my-tabs__item.is-active`);
|
||
if (!activeTab) return;
|
||
|
||
const textElement = activeTab.querySelector('span');
|
||
if (!textElement) return;
|
||
|
||
const { width } = textElement.getBoundingClientRect();
|
||
const { left } = activeTab.getBoundingClientRect();
|
||
const navLeft = this.$el.querySelector('.my-tabs__nav').getBoundingClientRect().left;
|
||
|
||
this.$refs.slider.style.width = `${width}px`;
|
||
this.$refs.slider.style.left = `${left - navLeft + (activeTab.offsetWidth - width) / 2}px`;
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.my-tabs__nav {
|
||
display: flex;
|
||
position: relative;
|
||
padding-bottom: 14px;
|
||
}
|
||
.my-tabs__item {
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
font-size: 20px;
|
||
&:hover {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
.my-tabs__item.is-active {
|
||
color: #409eff;
|
||
background: linear-gradient(90deg, $linear-gradient-start 22%, $linear-gradient-end 73%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
font-weight: bold;
|
||
}
|
||
.my-tabs__item.is-disabled {
|
||
color: #1e293b;
|
||
cursor: not-allowed;
|
||
font-family: 'Poppins-Regular';
|
||
}
|
||
.my-tabs__close { margin-left: 6px; color: #999; }
|
||
.my-tabs__close:hover { color: #f56c6c; }
|
||
.my-tabs__content { padding: 15px 0; }
|
||
|
||
/* 底部横线和滑块样式 */
|
||
.my-tabs__slider-wrapper {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.my-tabs__slider-track {
|
||
width: 100%;
|
||
height: 4px;
|
||
background-color: #e2e8f0;
|
||
}
|
||
.my-tabs__slider {
|
||
height: 4px;
|
||
background: $header-backgroungd;
|
||
border-radius: 4px;
|
||
position: absolute;
|
||
bottom: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
</style>
|
||
|