当前位置:   article > 正文

Vue3标签页(Tabs)_vue tabs标签页

vue tabs标签页

可自定义设置以下属性:

  • 标签页数组(tabPages),类型:Array<{key: string|number, tab: string, content?: string | slot, disabled?: boolean}>,默认 []

  • 标签是否居中展示(centered),类型:boolean,默认 false

  • 标签页大小(size),类型:'small' | 'middle' | 'large',默认 'middle'

  • 标签页的样式(type),类型:'line' | 'card',默认 'line'

  • tabs 之前的间隙大小(gutter),单位px,类型:number,默认 undefined

  • 当前激活 tab 面板的 key(v-model:activeKey),类型:string | number,默认 ''

效果如下图:在线预览

 注:组件引用方法 import { rafTimeout } from '../utils' 请参考以下博客: 

使用requestAnimationFrame模拟实现setTimeout和setInterval_theMuseCatcher的博客-CSDN博客使用requestAnimationFrame模拟实现setTimeout和setInterval!icon-default.png?t=N7T8https://blog.csdn.net/Dandrose/article/details/130167061

其中引入使用了以下工具函数:

①创建标签页组件Tabs.vue:

  1. <script setup lang="ts">
  2. import { ref, watch, onMounted, computed } from 'vue'
  3. import { useResizeObserver, rafTimeout, cancelRaf } from '../utils'
  4. interface Tab {
  5. key: string | number // 对应 activeKey
  6. tab: string // 标签页显示文字
  7. content?: string // 标签页内容 string | slot
  8. disabled?: boolean // 禁用对应标签页
  9. }
  10. interface Props {
  11. tabPages?: Tab[] // 标签页数组
  12. centered?: boolean // 标签是否居中展示
  13. size?: 'small' | 'middle' | 'large' // 标签页大小
  14. type?: 'line' | 'card' // 标签页的样式
  15. gutter?: number // tabs 之前的间隙大小,单位 px
  16. activeKey?: string | number // v-model 当前激活 tab 面板的 key
  17. }
  18. const props = withDefaults(defineProps<Props>(), {
  19. tabPages: () => [],
  20. centered: false,
  21. size: 'middle',
  22. type: 'line',
  23. gutter: undefined,
  24. activeKey: ''
  25. })
  26. const tabs = ref() // 所有 tabs 的 ref 模板引用
  27. const left = ref(0)
  28. const width = ref(0)
  29. const wrap = ref()
  30. const wrapWidth = ref()
  31. const nav = ref()
  32. const navWidth = ref()
  33. const rafId = ref()
  34. const showWheel = ref(false) // 导航是否有滚动
  35. const scrollMax = ref(0) // 最大滚动距离
  36. const scrollLeft = ref(0) // 滚动距离
  37. const activeIndex = computed(() => {
  38. return props.tabPages.findIndex((page) => page.key === props.activeKey)
  39. })
  40. watch(
  41. () => props.activeKey,
  42. () => {
  43. getBarDisplay()
  44. },
  45. {
  46. flush: 'post'
  47. }
  48. )
  49. useResizeObserver([wrap, nav], () => {
  50. getNavWidth()
  51. })
  52. onMounted(() => {
  53. getNavWidth()
  54. })
  55. const emits = defineEmits(['update:activeKey', 'change'])
  56. const transition = ref(false)
  57. function getBarDisplay() {
  58. const el = tabs.value[activeIndex.value]
  59. if (el) {
  60. left.value = el.offsetLeft
  61. width.value = el.offsetWidth
  62. if (showWheel.value) {
  63. if (left.value < scrollLeft.value) {
  64. transition.value = true
  65. scrollLeft.value = left.value
  66. rafId.value && cancelRaf(rafId.value)
  67. rafId.value = rafTimeout(() => {
  68. transition.value = false
  69. }, 150)
  70. }
  71. const targetScroll = left.value + width.value - wrapWidth.value
  72. if (targetScroll > scrollLeft.value) {
  73. transition.value = true
  74. scrollLeft.value = targetScroll
  75. rafId.value && cancelRaf(rafId.value)
  76. rafId.value = rafTimeout(() => {
  77. transition.value = false
  78. }, 150)
  79. }
  80. }
  81. } else {
  82. left.value = 0
  83. width.value = 0
  84. }
  85. }
  86. function getNavWidth() {
  87. wrapWidth.value = wrap.value.offsetWidth
  88. navWidth.value = nav.value.offsetWidth
  89. if (navWidth.value > wrapWidth.value) {
  90. showWheel.value = true
  91. scrollMax.value = navWidth.value - wrapWidth.value
  92. scrollLeft.value = scrollMax.value
  93. } else {
  94. showWheel.value = false
  95. scrollLeft.value = 0
  96. }
  97. getBarDisplay()
  98. }
  99. function onTab(key: string | number) {
  100. emits('update:activeKey', key)
  101. emits('change', key)
  102. }
  103. /*
  104. (触摸板滑动也会触发)监听滚轮事件,结合 transform: translate(${scrollLeft}px, 0) 模拟滚动效果
  105. 参考文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Element/wheel_event
  106. WheelEvent:
  107. 事件属性:
  108. WheelEvent.deltaX 只读:返回一个浮点数(double),表示水平方向的滚动量。
  109. WheelEvent.deltaY 只读:返回一个浮点数(double),表示垂直方向的滚动量。
  110. WheelEvent.deltaZ 只读:返回一个浮点数(double)表示 z 轴方向的滚动量。
  111. WheelEvent.deltaMode 只读:返回一个无符号长整型数(unsigned long),表示 delta* 值滚动量的单位。
  112. */
  113. function onWheel(e: WheelEvent) {
  114. if (e.deltaX !== 0) {
  115. // 防止标签页处触摸板上下滚动不生效
  116. // e.preventDefault() // 禁止浏览器捕获触摸板滑动事件
  117. const scrollX = e.deltaX * 1 // 滚轮的横向滚动量
  118. if (scrollLeft.value + scrollX > scrollMax.value) {
  119. scrollLeft.value = scrollMax.value
  120. } else if (scrollLeft.value + scrollX < 0) {
  121. scrollLeft.value = 0
  122. } else {
  123. scrollLeft.value += scrollX
  124. }
  125. }
  126. }
  127. </script>
  128. <template>
  129. <div class="m-tabs">
  130. <div class="m-tabs-nav">
  131. <div
  132. ref="wrap"
  133. class="m-tabs-nav-wrap"
  134. :class="{
  135. 'tabs-center': centered,
  136. 'before-shadow-active': showWheel && scrollLeft > 0,
  137. 'after-shadow-active': showWheel && scrollLeft < scrollMax
  138. }"
  139. >
  140. <div
  141. ref="nav"
  142. :class="['m-tabs-nav-list', { transition: transition }]"
  143. @wheel.stop.prevent="showWheel ? onWheel($event) : () => false"
  144. :style="`transform: translate(${-scrollLeft}px, 0)`"
  145. >
  146. <div
  147. ref="tabs"
  148. class="u-tab"
  149. :class="[
  150. `u-tab-${size}`,
  151. { 'u-tab-card': type === 'card', 'u-tab-disabled': page.disabled },
  152. { 'u-tab-line-active': activeKey === page.key && type === 'line' },
  153. { 'u-tab-card-active': activeKey === page.key && type === 'card' }
  154. ]"
  155. :style="`margin-left: ${index !== 0 ? gutter : null}px;`"
  156. @click="page.disabled ? () => false : onTab(page.key)"
  157. v-for="(page, index) in tabPages"
  158. :key="index"
  159. >
  160. {{ page.tab }}
  161. </div>
  162. <div
  163. class="u-tab-bar"
  164. :class="{ 'u-card-hidden': type === 'card' }"
  165. :style="`left: ${left}px; width: ${width}px;`"
  166. ></div>
  167. </div>
  168. </div>
  169. </div>
  170. <div class="m-tabs-page">
  171. <div class="m-tabs-content" v-show="activeKey === page.key" v-for="page in tabPages" :key="page.key">
  172. <slot :name="page.key">{{ page.content }}</slot>
  173. </div>
  174. </div>
  175. </div>
  176. </template>
  177. <style lang="less" scoped>
  178. .m-tabs {
  179. display: flex;
  180. color: rgba(0, 0, 0, 0.88);
  181. line-height: 1.5714285714285714;
  182. flex-direction: column; // 子元素将垂直显示,正如一个列一样。
  183. .m-tabs-nav {
  184. position: relative;
  185. display: flex;
  186. flex: none;
  187. align-items: center;
  188. margin: 0 0 16px 0;
  189. &::before {
  190. position: absolute;
  191. right: 0;
  192. left: 0;
  193. bottom: 0;
  194. border-bottom: 1px solid rgba(5, 5, 5, 0.06);
  195. content: '';
  196. }
  197. .m-tabs-nav-wrap {
  198. position: relative;
  199. display: flex;
  200. flex: auto;
  201. align-self: stretch;
  202. overflow: hidden;
  203. white-space: nowrap;
  204. transform: translate(0);
  205. .shadow {
  206. position: absolute;
  207. z-index: 1;
  208. opacity: 0;
  209. transition: opacity 0.3s;
  210. content: '';
  211. pointer-events: none;
  212. top: 0;
  213. bottom: 0;
  214. width: 32px;
  215. }
  216. &::before {
  217. .shadow();
  218. left: 0;
  219. box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.08);
  220. }
  221. &::after {
  222. .shadow();
  223. right: 0;
  224. box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.08);
  225. }
  226. .transition {
  227. transition: all 0.15s;
  228. }
  229. .m-tabs-nav-list {
  230. position: relative;
  231. display: flex;
  232. .u-tab {
  233. position: relative;
  234. display: inline-flex;
  235. align-items: center;
  236. padding: 12px 0;
  237. font-size: 14px;
  238. background: transparent;
  239. border: 0;
  240. outline: none;
  241. cursor: pointer;
  242. transition: all 0.3s;
  243. &:not(:first-child) {
  244. margin-left: 32px;
  245. }
  246. &:hover {
  247. color: @themeColor;
  248. }
  249. }
  250. .u-tab-small {
  251. font-size: 14px;
  252. padding: 8px 0;
  253. }
  254. .u-tab-large {
  255. font-size: 16px;
  256. padding: 16px 0;
  257. }
  258. .u-tab-card {
  259. border-radius: 8px 8px 0 0;
  260. padding: 8px 16px;
  261. background: rgba(0, 0, 0, 0.02);
  262. border: 1px solid rgba(5, 5, 5, 0.06);
  263. transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  264. &:not(:first-child) {
  265. margin-left: 2px;
  266. }
  267. }
  268. .u-tab-line-active {
  269. color: @themeColor;
  270. text-shadow: 0 0 0.25px currentcolor;
  271. }
  272. .u-tab-card-active {
  273. border-bottom-color: #ffffff;
  274. color: @themeColor;
  275. background: #ffffff;
  276. text-shadow: 0 0 0.25px currentcolor;
  277. }
  278. .u-tab-disabled {
  279. color: rgba(0, 0, 0, 0.25);
  280. cursor: not-allowed;
  281. &:hover {
  282. color: rgba(0, 0, 0, 0.25);
  283. }
  284. }
  285. .u-tab-bar {
  286. position: absolute;
  287. background: @themeColor;
  288. pointer-events: none;
  289. height: 2px;
  290. transition:
  291. width 0.3s,
  292. left 0.3s,
  293. right 0.3s;
  294. bottom: 0;
  295. }
  296. .u-card-hidden {
  297. visibility: hidden;
  298. }
  299. }
  300. }
  301. .tabs-center {
  302. justify-content: center;
  303. }
  304. .before-shadow-active {
  305. &::before {
  306. opacity: 1;
  307. }
  308. }
  309. .after-shadow-active {
  310. &::after {
  311. opacity: 1;
  312. }
  313. }
  314. }
  315. .m-tabs-page {
  316. font-size: 14px;
  317. flex: auto;
  318. min-width: 0;
  319. min-height: 0;
  320. .m-tabs-content {
  321. position: relative;
  322. width: 100%;
  323. height: 100%;
  324. }
  325. }
  326. }
  327. </style>

②在要使用的页面引入:

其中引入使用了 Vue3单选按钮(Radio) 组件

  1. <script setup lang="ts">
  2. import Tabs from './Tabs.vue'
  3. import { ref, watchEffect } from 'vue'
  4. const tabPages = ref([
  5. {
  6. key: '1',
  7. tab: 'Tab 1',
  8. content: 'Content of Tab Pane 1'
  9. },
  10. {
  11. key: '2',
  12. tab: 'Tab 2',
  13. content: 'Content of Tab Pane 2'
  14. },
  15. {
  16. key: '3',
  17. tab: 'Tab 3',
  18. content: 'Content of Tab Pane 3'
  19. },
  20. {
  21. key: '4',
  22. tab: 'Tab 4',
  23. content: 'Content of Tab Pane 4'
  24. },
  25. {
  26. key: '5',
  27. tab: 'Tab 5',
  28. content: 'Content of Tab Pane 5'
  29. },
  30. {
  31. key: '6',
  32. tab: 'Tab 6',
  33. content: 'Content of Tab Pane 6'
  34. }
  35. ])
  36. const tabPagesDisabled = ref([
  37. {
  38. key: '1',
  39. tab: 'Tab 1',
  40. content: 'Content of Tab Pane 1'
  41. },
  42. {
  43. key: '2',
  44. tab: 'Tab 2',
  45. content: 'Content of Tab Pane 2'
  46. },
  47. {
  48. key: '3',
  49. tab: 'Tab 3',
  50. disabled: true,
  51. content: 'Content of Tab Pane 3'
  52. },
  53. {
  54. key: '4',
  55. tab: 'Tab 4',
  56. content: 'Content of Tab Pane 4'
  57. },
  58. {
  59. key: '5',
  60. tab: 'Tab 5',
  61. content: 'Content of Tab Pane 5'
  62. },
  63. {
  64. key: '6',
  65. tab: 'Tab 6',
  66. content: 'Content of Tab Pane 6'
  67. }
  68. ])
  69. const activeKey = ref('1')
  70. watchEffect(() => { // 回调立即执行一次,同时会自动跟踪回调中所依赖的所有响应式依赖
  71. console.log('activeKey:', activeKey.value)
  72. })
  73. const options = ref([
  74. {
  75. label: 'Small',
  76. value: 'small'
  77. },
  78. {
  79. label: 'Middle',
  80. value: 'middle'
  81. },
  82. {
  83. label: 'Large',
  84. value: 'large'
  85. }
  86. ])
  87. const size = ref('middle')
  88. function onChange (key: string|number) {
  89. console.log('key:', key)
  90. }
  91. </script>
  92. <template>
  93. <div>
  94. <h1>Tabs 标签页</h1>
  95. <h2 class="mt30 mb10">基本使用</h2>
  96. <Tabs
  97. :tab-pages="tabPages"
  98. v-model:active-key="activeKey"
  99. @change="onChange" />
  100. <h2 class="mt30 mb10">卡片式标签页</h2>
  101. <Tabs
  102. type="card"
  103. :tab-pages="tabPages"
  104. v-model:active-key="activeKey"
  105. @change="onChange" />
  106. <h2 class="mt30 mb10">禁用某一项</h2>
  107. <Tabs
  108. :tab-pages="tabPagesDisabled"
  109. v-model:active-key="activeKey"
  110. @change="onChange" />
  111. <br/>
  112. <Tabs
  113. type="card"
  114. :tab-pages="tabPagesDisabled"
  115. v-model:active-key="activeKey"
  116. @change="onChange" />
  117. <h2 class="mt30 mb10">居中展示</h2>
  118. <Tabs
  119. centered
  120. :tab-pages="tabPages"
  121. v-model:active-key="activeKey"
  122. @change="onChange" />
  123. <br/>
  124. <Tabs
  125. centered
  126. type="card"
  127. :tab-pages="tabPages"
  128. v-model:active-key="activeKey"
  129. @change="onChange" />
  130. <h2 class="mt30 mb10">左右滑动,容纳更多标签</h2>
  131. <Tabs
  132. style="width: 320px;"
  133. :tab-pages="tabPages"
  134. v-model:active-key="activeKey"
  135. @change="onChange" />
  136. <br/>
  137. <Tabs
  138. style="width: 320px;"
  139. type="card"
  140. :tab-pages="tabPages"
  141. v-model:active-key="activeKey"
  142. @change="onChange" />
  143. <h2 class="mt30 mb10">三种尺寸</h2>
  144. <Radio :options="options" v-model:value="size" button />
  145. <br/>
  146. <Tabs
  147. :size="size"
  148. :tab-pages="tabPages"
  149. v-model:active-key="activeKey"
  150. @change="onChange" />
  151. <br/>
  152. <Tabs
  153. type="card"
  154. :size="size"
  155. :tab-pages="tabPages"
  156. v-model:active-key="activeKey"
  157. @change="onChange" />
  158. <h2 class="mt30 mb10">自定义内容</h2>
  159. <Tabs
  160. :tab-pages="tabPages"
  161. v-model:active-key="activeKey"
  162. @change="onChange">
  163. <template #1>
  164. <p>key: 1 的 slot 内容</p>
  165. </template>
  166. <template #2>
  167. <p>key: 2 的 slot 内容</p>
  168. </template>
  169. <template #3>
  170. <p>key: 3 的 slot 内容</p>
  171. </template>
  172. </Tabs>
  173. </div>
  174. </template>
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/903203
推荐阅读
相关标签
  

闽ICP备14008679号