整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何使用JavaScript实现父级tabs悬停显示子级导航?问题示例分析

何使用JavaScript代码解决父级选项卡鼠标悬停时子级菜单无法操作的问题。解决方案包括在子级导航上添加一个类并使用JavaScript代码监听鼠标事件来显示或隐藏子级菜单。如果无法解决问题,建议检查CSS样式是否正确应用、JavaScript代码是否正确工作、HTML结构是否正确、浏览器兼容性是否良好等问题。

这个问题可以通过添加一些JavaScript代码来解决。以下是一种可能的解决方案:

你需要为你的子级导航添加一个类,比如 "subnav"。

你可以使用JavaScript代码来监听鼠标事件,当鼠标移入父级tabs时,添加一个类来显示子级导航。当鼠标移出tabs元素时,移除这个类来隐藏子级导航。如下所示:

HTML如下:

<div class="tabs">

<a href="#">Tab 1</a>

<a href="#">Tab 2</a>

<a href="#">Tab 3</a>

<div class="subnav">

<a href="#">Subnav 1</a>

<a href="#">Subnav 2</a>

<a href="#">Subnav 3</a>

</div>

</div>

CSS如下:

.tabs {

position: relative;

}

.subnav {

position: absolute;

top: 100%;

left: 0;

display: none;

}

.tabs:hover .subnav {

display: block;

}

JavaScript如下:

var tabs = document.querySelector('.tabs');

var subnav = document.querySelector('.subnav');

tabs.addEventListener('mouseenter', function() {

subnav.classList.add('show');

});

tabs.addEventListener('mouseleave', function() {

subnav.classList.remove('show');

});

在这个代码中,当鼠标移入父级tabs时,使用 "mouseenter" 事件来添加一个 "show" 类来显示子级导航。当鼠标移出tabs元素时,使用 "mouseleave" 事件来移除这个类来隐藏子级导航。

如果代码已经和上述的示例代码类似,并且仍然无法操作子级菜单,那么你可以尝试以下几个步骤来诊断问题:

1、确认CSS样式是否正确应用

确保CSS样式被正确地应用于您的HTML代码中。检查CSS选择器是否正确,检查样式表中是否存在任何语法错误。

2、确认JavaScript代码是否正确工作

检查JavaScript代码是否正确工作。可以在控制台中打印一些调试信息,例如在 mouseenter 和 mouseleave 事件处理程序中添加 console.log 语句来查看它们是否被正确调用。

3、检查HTML结构

确保您的HTML结构正确,所有必需的元素都存在。检查类名和ID是否正确命名,并且没有拼写错误。

4、检查浏览器兼容性

检查浏览器兼容性。有些浏览器可能不支持某些CSS或JavaScript功能。您可以使用浏览器的开发者工具来检查这些问题,并尝试在其他浏览器中测试您的代码。


加图片注释,不超过 140 字(可选)

【成品镇楼图】 基础概念 Tabs组件是一种常见的用户界面组件,用于在一个界面中展示多个内容区域,并允许用户通过点击不同的标签来切换可见的内容。它的实现原理主要涉及HTML、CSS和JavaScript,以下是一个基本的实现步骤: 1. HTML结构 首先,需要定义一个基本的HTML结构,包括标签(tabs)和内容(tab content)部分。每个标签对应一个内容区域。

<div class="tabs">
  <div class="tab" data-tab="1">Tab 1</div>
  <div class="tab" data-tab="2">Tab 2</div>
  <div class="tab" data-tab="3">Tab 3</div>
</div>
<div class="tab-content" data-tab="1">Content 1</div>
<div class="tab-content" data-tab="2">Content 2</div>
<div class="tab-content" data-tab="3">Content 3</div>

2. CSS样式

接下来,使用CSS来定义标签和内容区域的样式,尤其是如何在不同的标签被选中时显示或隐藏内容。

.tabs {
  display: flex;
}

.tab {
  padding: 10px;
  cursor: pointer;
  background-color: #f1f1f1;
  border: 1px solid #ccc;
  margin-right: 2px;
}

.tab.active {
  background-color: #ddd;
}

.tab-content {
  display: none;
  padding: 10px;
  border: 1px solid #ccc;
  margin-top: 10px;
}

.tab-content.active {
  display: block;
}

3. JavaScript交互

最后,使用JavaScript来处理标签的点击事件,并根据点击的标签来显示相应的内容区域。

document.addEventListener('DOMContentLoaded', function() {
  const tabs = document.querySelectorAll('.tab');
  const contents = document.querySelectorAll('.tab-content');

  tabs.forEach(tab => {
    tab.addEventListener('click', function() {
      const tabId = this.getAttribute('data-tab');

      // 移除所有标签和内容的active类
      tabs.forEach(t => t.classList.remove('active'));
      contents.forEach(c => c.classList.remove('active'));

      // 为当前选中的标签和相应的内容添加active类
      this.classList.add('active');
      document.querySelector(`.tab-content[data-tab="${tabId}"]`).classList.add('active');
    });
  });
});

工作原理

  1. HTML结构:定义标签和内容区域,使用data-tab属性来关联标签和内容。
  2. CSS样式:初始状态下所有内容区域都是隐藏的(display: none),只有被选中的内容区域才会显示(display: block)。
  3. JavaScript交互
  • 监听每个标签的点击事件。
  • 当一个标签被点击时,移除所有标签和内容区域的active类。
  • 为被点击的标签和相应的内容区域添加active类,从而显示对应的内容。

通过这种方式,Tabs组件能够在用户点击不同的标签时,动态地显示和隐藏相应的内容区域,从而实现标签切换的功能。 Vue3 在Vue 3中,可以使用组件化的方式来实现Tabs组件。以下是一个基本的实现步骤: 1. 创建Tabs组件 首先,创建一个Tabs组件,用于容纳所有的标签和内容。

<!-- Tabs.vue -->
<template>
  <div>
    <div class="tabs">
      <div
        v-for="(tab, index) in tabs"
        :key="index"
        :class="['tab', { active: activeTab === index }]"
        @click="selectTab(index)"
      >
        {{ tab.label }}
      </div>
    </div>
    <div class="tab-content">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      activeTab: 0,
      tabs: []
    };
  },
  methods: {
    selectTab(index) {
      this.activeTab = index;
    },
    addTab(tab) {
      this.tabs.push(tab);
    }
  },
  provide() {
    return {
      registerTab: this.addTab,
      activeTab: () => this.activeTab
    };
  }
};
</script>

<style>
.tabs {
  display: flex;
}

.tab {
  padding: 10px;
  cursor: pointer;
  background-color: #f1f1f1;
  border: 1px solid #ccc;
  margin-right: 2px;
}

.tab.active {
  background-color: #ddd;
}

.tab-content {
  padding: 10px;
  border: 1px solid #ccc;
  margin-top: 10px;
}
</style>

2. 创建Tab组件

接下来,创建一个Tab组件,用于定义每个标签和其对应的内容。

<!-- Tab.vue -->
<template>
  <div v-show="isActive">
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    label: {
      type: String,
      required: true
    }
  },
  inject: ['registerTab', 'activeTab'],
  computed: {
    isActive() {
      return this.activeTab() === this.index;
    }
  },
  data() {
    return {
      index: null
    };
  },
  mounted() {
    this.index = this.$parent.tabs.length;
    this.registerTab(this);
  }
};
</script>

3. 使用Tabs和Tab组件

最后,在你的主组件中使用TabsTab组件。

<!-- App.vue -->
<template>
  <Tabs>
    <Tab label="Tab 1">Content 1</Tab>
    <Tab label="Tab 2">Content 2</Tab>
    <Tab label="Tab 3">Content 3</Tab>
  </Tabs>
</template>

<script>
import Tabs from './Tabs.vue';
import Tab from './Tab.vue';

export default {
  components: {
    Tabs,
    Tab
  }
};
</script>

工作原理

  1. Tabs组件:管理标签和内容的显示。它包含一个tabs数组,用于存储所有的标签信息,以及一个activeTab变量,用于记录当前选中的标签索引。selectTab方法用于切换标签,addTab方法用于添加标签。
  2. Tab组件:定义每个标签和其对应的内容。它通过props接收标签的名称,并在mounted生命周期钩子中将自己添加到父组件的tabs数组中。isActive变量用于控制内容的显示与隐藏。
  3. 主组件:使用TabsTab组件,通过slot机制将内容传递给Tabs组件,并根据activeTab动态显示对应的内容。

通过这种方式,可以在Vue 3中实现一个功能完整的Tabs组件。 ColorUI Nav组件源码

<template>
	<view>
		<cu-custom bgColor="bg-gradual-pink" :isBack="true"><block slot="backText">返回</block><block slot="content">导航栏</block></cu-custom>
		<view v-for="(item,index) in 10" :key="index" v-if="index==TabCur" class="bg-grey padding margin text-center">
			Tab{{index}}
		</view>
		<view class="cu-bar bg-white solid-bottom">
			<view class="action">
				<text class="cuIcon-title text-orange"></text> 默认
			</view>
		</view>
		<scroll-view scroll-x class="bg-white nav" scroll-with-animation :scroll-left="scrollLeft">
			<view class="cu-item" :class="index==TabCur?'text-green cur':''" v-for="(item,index) in 10" :key="index" @tap="tabSelect" :data-id="index">
				Tab{{index}}
			</view>
		</scroll-view>

		<view class="cu-bar bg-white margin-top solid-bottom">
			<view class="action">
				<text class="cuIcon-title text-orange"></text> 居中
			</view>
		</view>
		<scroll-view scroll-x class="bg-white nav text-center">
			<view class="cu-item" :class="index==TabCur?'text-blue cur':''" v-for="(item,index) in 3" :key="index" @tap="tabSelect" :data-id="index">
				Tab{{index}}
			</view>
		</scroll-view>

		<view class="cu-bar bg-white margin-top solid-bottom">
			<view class="action">
				<text class="cuIcon-title text-orange"></text> 平分
			</view>
		</view>
		<scroll-view scroll-x class="bg-white nav">
			<view class="flex text-center">
				<view class="cu-item flex-sub" :class="index==TabCur?'text-orange cur':''" v-for="(item,index) in 4" :key="index" @tap="tabSelect" :data-id="index">
					Tab{{index}}
				</view>
			</view>
		</scroll-view>
		<view class="cu-bar bg-white margin-top solid-bottom">
			<view class="action">
				<text class="cuIcon-title text-orange"></text> 背景
			</view>
		</view>
		<scroll-view scroll-x class="bg-red nav text-center">
			<view class="cu-item" :class="index==TabCur?'text-white cur':''" v-for="(item,index) in 3" :key="index" @tap="tabSelect" :data-id="index">
				Tab{{index}}
			</view>
		</scroll-view>
		<view class="cu-bar bg-white margin-top solid-bottom">
			<view class="action">
				<text class="cuIcon-title text-orange"></text> 图标
			</view>
		</view>
		<scroll-view scroll-x class="bg-green nav text-center">
			<view class="cu-item" :class="0==TabCur?'text-white cur':''" @tap="tabSelect" data-id="0">
				<text class="cuIcon-camerafill"></text> 数码
			</view>
			<view class="cu-item" :class="1==TabCur?'text-white cur':''" @tap="tabSelect" data-id="1">
				<text class="cuIcon-upstagefill"></text> 排行榜
			</view>
			<view class="cu-item" :class="2==TabCur?'text-white cur':''" @tap="tabSelect" data-id="2">
				<text class="cuIcon-clothesfill"></text> 皮肤
			</view>
		</scroll-view>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				TabCur: 0,
				scrollLeft: 0
			};
		},
		methods: {
			tabSelect(e) {
				this.TabCur = e.currentTarget.dataset.id;
				this.scrollLeft = (e.currentTarget.dataset.id - 1) * 60
			}
		}
	}
</script>

vue3+ts改造

首先我们先来看一下原作者大佬的组件设计

添加图片注释,不超过 140 字(可选)

需求整理

从原组件改造整理了如下需求

  1. 支持不同的布局
  • 默认布局
  • 居中布局
  • 平分布局
  1. 支持不同的背景样式
  • 默认背景
  • 自定义背景颜色
  1. 支持图标和背景色的标签项

初步实现

tabs组件


<!-- Tabs.vue -->
<template>
  <div :class="isCard !== false ? 'is-card' : ''">
    <div
      class="nav flex"
      :class="[, `bg-${bg}`, `text-${text}`]"
      :style="getFlex"
    >
      <div
        class="cu-item"
        v-for="(tab, index) in tabs"
        :key="index"
        @click="selectTab(index, tab)"
        :class="[activeTab === index ? 'cur text-blue' : '']"
      >
        <i
          v-if="tab.icon"
          :class="`cuIcon-${tab.icon} text-${
            activeTab === index ? 'blue' : tab.iconColor
          }`"
        ></i>
        {{ tab.label }}
      </div>
    </div>
    <div class="tab-content"><slot :tab="tabs[activeTab]"></slot>·</div>
  </div>
</template>

<script setup lang="ts">
import { ref, provide, computed, defineEmits, watch } from "vue";

interface TabItem {
  label: string;
  icon?: string;
  iconColor?: string;
  bgColor?: string;
}

const tabs = ref<TabItem[]>([]);
const activeTab = ref(0);

const emit = defineEmits(["update:modelValue", "select"]);

const selectTab = (index: number, tab: TabItem) => {
  activeTab.value = index;
  emit("update:modelValue", index);
  emit("select", tab);
};

const addTab = (tab: TabItem) => {
  tabs.value.push(tab);
  return tabs.value.length - 1; // 返回新添加的tab的索引
};

provide("registerTab", addTab);
provide("activeTab", activeTab);

const props = withDefaults(
  defineProps<{
    modelValue: number;
    center?: boolean;
    bg?: string;
    text?: string;
    isCard?: boolean;
    mode?: "center" | "flex-start" | "space-between";
  }>(),
  {
    center: false,
    bg: "white",
    isCard: false,
    mode: "flex-start",
    modelValue: 0,
  }
);

const getFlex = computed(() => {
  if (props.center !== false) {
    return "justify-content:center;";
  }
  return `justify-content:${props.mode}`;
});

watch(
  () => props.modelValue,
  (newVal) => {
    activeTab.value = newVal;
  }
);
</script>

<script lang="ts">
export default {
  name: "TTabs",
};
</script>

<style>
.tab-content {
  padding: 10px 16px;
  background: #fff;
}
</style>


解释

  1. Tabs.vue
  • 添加了 centerbgtextmode 属性,以支持不同的布局和背景样式。
  • 使用 computed 属性 getFlex 动态计算布局样式。
  • 通过 provideinject 实现子组件与父组件之间的数据传递和状态管理。
  1. Tab.vue
  • 定义了 labeliconiconColor 属性,以支持图标和颜色的自定义。
  • 使用 inject 获取父组件提供的 registerTabactiveTab,并在组件挂载时进行注册。

通过这些改造,我们的组件能够支持不同的布局和背景样式,并且标签项可以包含图标和自定义背景色。这样就满足了原组件的所有需求。 思考 那是不是就满足了我们日常的开发需求呢,我的回答是我的是基本上满足了,但还应该增加几个常用功能 双向绑定的当前选中变量:组件联动、动态内容切换、状态同步; select 事件回调并且传回的是 tab:日志记录、业务处理、路由导航; 自定义插槽:自定义样式、复杂内容、图标和文本组合。 代码改进 为了满足上述需求,我们可以进一步改进组件,增加以下功能:

  1. 双向绑定的当前选中变量:通过 v-model 实现。
  2. select 事件回调并且传回的是 tab:在 selectTab 方法中触发事件并传递当前选中的 tab 信息。
  3. 自定义插槽:允许用户自定义标签项的内容。

改进后的 Tabs 组件

<!-- Tabs.vue -->
<template>
  <div :class="isCard !== false ? 'is-card' : ''">
    <div
      class="nav flex"
      :class="[, `bg-${bg}`, `text-${text}`]"
      :style="getFlex"
    >
      <div
        class="cu-item"
        v-for="(tab, index) in tabs"
        :key="index"
        @click="selectTab(index, tab)"
        :class="[modelValue === index ? 'cur text-blue' : '']"
      >
        <i
          v-if="tab.icon"
          :class="`cuIcon-${tab.icon} text-${
            modelValue === index ? 'blue' : tab.iconColor
          }`"
        ></i>
        {{ tab.label }}
      </div>
    </div>
    <div class="tab-content">
      <slot :tab="tabs[activeTab]"></slot>·
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, provide, computed, defineEmits, watch } from "vue";

interface TabItem {
  label: string;
  icon?: string;
  iconColor?: string;
  bgColor?: string;
}

const tabs = ref<TabItem[]>([]);
const activeTab = ref(0);

const emit = defineEmits(["update:modelValue", "select"]);

const selectTab = (index: number, tab: TabItem) => {
  activeTab.value = index;
  emit("update:modelValue", index);
  emit("select", tab);
};

const addTab = (tab: TabItem) => {
  tabs.value.push(tab);
  return tabs.value.length - 1; // 返回新添加的tab的索引
};

provide("registerTab", addTab);
provide("activeTab", activeTab);

const props = withDefaults(
  defineProps<{
    modelValue: number;
    center?: boolean;
    bg?: string;
    text?: string;
    isCard?: boolean;
    mode?: "center" | "flex-start" | "space-between";
  }>(),
  {
    center: false,
    bg: "white",
    isCard: false,
    mode: "flex-start",
  }
);

const getFlex = computed(() => {
  if (props.center !== false) {
    return "justify-content:space-between;";
  }
  return `justify-content:${props.mode}`;
});

watch(
  () => props.modelValue,
  (newVal) => {
    activeTab.value = newVal;
  }
);
</script>

<script lang="ts">
export default {
  name: "TTabs",
};
</script>

<style>
.tab-content {
  padding: 10px 16px;
  background: #fff;
}
</style>

改进后的 TabItem 组件

<!-- Tab.vue -->
<template>
  <div v-show="isActive">
    <slot></slot>
    <slot name="custom" :tab="tabData"></slot>
  </div>
</template>

<script setup lang="ts">
import { inject, ref, computed, onMounted, Ref } from "vue";

const props = withDefaults(
  defineProps<{
    label: string;
    icon?: string;
    iconColor?: string;
  }>(),
  {
    icon: "",
    iconColor: "black",
  }
);

const registerTab =
  inject<(tab: { label: string; icon?: string; iconColor?: string }) => number>(
    "registerTab"
  );
const activeTab = inject<Ref<number>>("activeTab");

const index = ref<number | null>(null);

const isActive = computed(() => {
  return activeTab?.value === index.value;
});

const tabData = computed(() => ({
  label: props.label,
  icon: props.icon,
  iconColor: props.iconColor,
}));

onMounted(() => {
  if (registerTab) {
    index.value = registerTab({
      label: props.label,
      icon: props.icon,
      iconColor: props.iconColor,
    });
    console.log(`Tab ${props.label} registered with index ${index.value}`);
  }
});
</script>

<script lang="ts">
export default {
  name: "TTab",
};
</script>

解释一下 我们在创建一个 Tabs 组件系统,其中包括 TabsTab 两个组件:

  1. Tabs 组件:负责管理多个 Tab 组件。它提供了一个导航栏,用户可以点击不同的标签来切换内容。
  2. Tab 组件:表示单个标签页的内容。每个 Tab 组件通过 labelicon 等属性来定义其显示内容。

通过 provideinject 机制,Tab 组件可以注册到 Tabs 组件中,并且 Tabs 组件可以管理和控制哪些 Tab 组件是激活状态。 provideinject provideinject 是 Vue 3 中用于跨组件通信的两个 API,特别适用于祖孙组件之间的数据传递。

  • provide:在祖先组件中使用,提供数据或方法给后代组件。
  • inject:在后代组件中使用,接收祖先组件提供的数据或方法。

在我们的例子中,Tabs 组件使用 provide 来提供 registerTabactiveTab,而 Tab 组件使用 inject 来接收这些数据。 自定义插槽 自定义插槽允许我们在组件中定义可插入的内容,并且可以传递数据给插槽内容。

  • 普通插槽:默认插槽,不需要命名。
  • 命名插槽:通过 name 属性命名,可以在使用组件时指定不同的内容插入到不同的插槽中。
  • 作用域插槽:可以传递数据给插槽内容。

跨组件自定义插槽传值 跨组件自定义插槽传值结合了 provide/inject 和作用域插槽的概念。通过 Tabs 组件提供的数据,Tab 组件可以在自定义插槽中使用这些数据。 使用示例 在使用时,确保你在自定义插槽中正确地接收传递的数据:

<template>
  <div>
    <TTitle>综合示例:双向绑定、事件回调、自定义插槽</TTitle>
    <TTabs v-model="selectedTab" @select="handleSelect">
      <TTab label="Tab 1">
        <p>Content for Tab 1</p>
      </TTab>
      <TTab label="Tab 2">
        <p>Content for Tab 2</p>
      </TTab>
      <TTab label="Tab 3" icon="rank" icon-color="red">
        <template #custom="{ tab }">
          {{ tab.label }} 自定义插槽
        </template>
      </TTab>
    </TTabs>
    <p>当前选中的标签索引:{{ selectedTab }}</p>
    <p>选中的标签信息:{{ selectedTabInfo }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const selectedTab = ref(0);
const selectedTabInfo = ref(null);

const handleSelect = (tab) => {
  selectedTabInfo.value = tab;
};
</script>

总结

本文详细介绍了如何实现和改造Tabs组件,涵盖了从基础的HTML、CSS和JavaScript实现,到在Vue 3中实现组件化,再到进一步的功能改造。通过逐步完善Tabs组件的功能,使其能够满足更多的开发需求。

果展示

选中样式制作

  • 将每个 tab 设置固定宽度。
  • 每个tab 添加相对定位,居中,行高
  • 添加伪类,伪类设置绝对定位,在底部。
  • 设置伪类的宽度为 0%(伪类会继承该元素的宽度)
  • 设置选中时候的伪类宽度为50%(视情况而定)
  • 给元素添加过渡样式

HTML代码(wxml)

			
				{{item.name}}
			

CSS(less):

		.nav-bar{
			position: relative;
			z-index: 10;
			height: 90upx;
			white-space: nowrap;
			background-color: #fbfbfb;
			
			.nav-item{
				display: inline-block;
				width: 150upx;
				height: 90upx;
				text-align: center;
				line-height: 90upx;
				font-size: 30upx;
				color: #a4a4a4;
				position: relative;
			}
			
			.current{
				color: #3f3f3f;
				font-weight: bold;
			}
		}

实现效果大致为这样的:

拓展

PS: 以上为纯CSS实现部分,如果项目 tab数量 为通过接口动态获取的,可以适当加入一些 js 计算。

JS 思路:

  • 获取当前选中的 tab 的宽度
  • 获取当前选中 tab 以及它之前全部 tab 的宽度总和。
  • 获取当前屏幕宽度
  • 判断当前选中 tab 是否超过屏幕中心点(当前选中 tab 以及它之前全部 tab 的宽度总和 - 当前选中 tab 宽度/2
  • 移动当前 tabs 到屏幕的重心点位置

大致为(以微信小程序为例):

				let width = 0; // 当前选中选项卡及它之前的选项卡之和总宽度
				let nowWidth = 0; // 当前选项卡的宽度
				//获取可滑动总宽度
				for (let i = 0; i <= index; i++) {
					let result = await this.getElSize('tab' + i);
					width += result.width;
					if(i === index){
						nowWidth = result.width;
					}
				}
				// console.log(width, nowWidth, windowWidth)
				//等待swiper动画结束再修改tabbar
				this.$nextTick(() => {
					if (width - nowWidth/2 > windowWidth / 2) {
						//如果当前项越过中心点,将其放在屏幕中心
						this.scrollLeft = width - nowWidth/2 - windowWidth / 2;
						console.log(this.scrollLeft)
					}else{
						this.scrollLeft = 0;
					}
					if(typeof e === 'object'){
						this.tabCurrentIndex = index; 
					}
					this.tabCurrentIndex = index; 
				})

ps: getElSize() 函数代码为:

			getElSize(id) { 
				return new Promise((res, rej) => {
					let el = uni.createSelectorQuery().select('#' + id);
					el.fields({
						size: true,
						scrollOffset: true,
						rect: true
					}, (data) => {
						res(data);
					}).exec();
				});
			},

这样就可以实现动态 tab 切换了: