| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- <template>
- <view class="v-tabs">
- <scroll-view :id="getDomId" :scroll-x="scroll" :scroll-left="scroll ? scrollLeft : 0"
- :scroll-with-animation="scroll" :style="{ position: fixed ? 'fixed' : 'relative' }">
- <view class="v-tabs__container" :style="{
- display: scroll ? 'inline-flex' : 'flex',
- whiteSpace: scroll ? 'nowrap' : 'normal',
- background: bgColor,
- padding
- }">
- <view :class="['v-tabs__container-item', { disabled: !!v.disabled }, { active: current == i }]"
- v-for="(v, i) in tabs" :key="i" :style="{
- color: current == i ? activeColor : color,
- fontSize: current == i ? fontSize : fontSize,
- fontWeight: bold && current == i ? 'bold' : '',
- justifyContent: !scroll ? 'center' : '',
- flex: scroll ? '' : 1,
- padding: paddingItem
- }" @click="change(i,'tab')">
- <view class="column-c">
- <image class="wh-60 m-tb10 r-100" :class="current == i?'double-border':''"
- style="display: block;" :src="v.icon" mode=""></image>
- <slot :row="v" :index="i">{{ field ? v[field] : v }}</slot>
- </view>
- </view>
- <template v-if="!!tabs.length">
- <view v-if="lineSlot" class="lineSlot"
- :class="['v-tabs__container-line', { animation: lineAnimation }]" :style="{
- transform: `translate3d(${lineLeft}px, 0, 0)`
- }">
- <slot name="line" />
- </view>
- <view class="column-c" v-else-if="!pills"
- :class="['v-tabs__container-line', { animation: lineAnimation }]" :style="{
- width: lineWidth + 'px',
- height: '30rpx',
- borderRadius: lineRadius,
- transform: `translate3d(${lineLeft}px, 0, 0)`
- }">
- <view class="triangle"></view>
- </view>
- <view v-else :class="['v-tabs__container-pills', { animation: lineAnimation }]" :style="{
- background: pillsColor,
- borderRadius: pillsBorderRadius,
- width: currentWidth + 'px',
- transform: `translate3d(${pillsLeft}px, 0, 0)`,
- height
- }" />
- </template>
- </view>
- </scroll-view>
- <!-- fixed 的站位高度 -->
- <view class="v-tabs__placeholder" :style="{ height: fixed ? height : '0', padding }"></view>
- <view class="v-tabs__content" v-if="tabsSwiper">
- <swiper class="activity-swiper" v-if="!swiperType" :current="value" :duration="duration"
- :circular="circular" @change="onSwiperChange"
- :style="{ height: contentHeight ? contentHeight : `calc(100vh - ${height} - ${paddingBottom})`, paddingBottom: paddingBottom, paddingTop: paddingTop}">
- <swiper-item class="activity-swiper-item" v-for="(item,index) in tabs" :key="item.key"
- :item-id="item.key" :style="{ paddingBottom: 0, paddingTop: 0 }">
- <scroll-view scroll-y class="scroll-list" @scrolltolower="onBottom" :style="{ height: '100%' }">
- <slot :index="index" name="swiperContent" />
- </scroll-view>
- </swiper-item>
- </swiper>
- <view class="data_list" @touchstart="touchStart" @touchend="touchEnd" :animation="animationData" v-else>
- <view class="data_list_content" v-for="(item,index) in tabs" :key="index">
- <scroll-view scroll-y class="scroll-list" @scrolltolower="onBottom"
- :style="{ height: contentHeight ? contentHeight : `calc(100vh - ${height} - ${paddingBottom})`, paddingBottom: paddingBottom, paddingTop: paddingTop}">
- <slot :index="index" name="swiperContent" />
- </scroll-view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import {
- startMicroTask
- } from './utils'
- import props from './props'
- /**
- * v-tabs
- * @property {Number} value 选中的下标
- * @property {Array} tabs tabs 列表
- * @property {String} bgColor = '#fff' 背景颜色
- * @property {String} color = '#333' 默认颜色
- * @property {String} activeColor = '#2979ff' 选中文字颜色
- * @property {String} fontSize = '28rpx' 默认文字大小
- * @property {String} activeFontSize = '28rpx' 选中文字大小
- * @property {Boolean} bold = [true | false] 选中文字是否加粗
- * @property {Boolean} scroll = [true | false] 是否滚动
- * @property {String} height = '60rpx' tab 的高度
- * @property {String} lineHeight = '10rpx' 下划线的高度
- * @property {String} lineColor = '#2979ff' 下划线的颜色
- * @property {Number} lineScale = 0.5 下划线的宽度缩放比例
- * @property {String} lineRadius = '10rpx' 下划线圆角
- * @property {Boolean} pills = [true | false] 是否胶囊样式
- * @property {String} pillsColor = '#2979ff' 胶囊背景色
- * @property {String} pillsBorderRadius = '10rpx' 胶囊圆角大小
- * @property {String} field 如果是对象,显示的键名
- * @property {Boolean} fixed = [true | false] 是否固定
- * @property {String} paddingItem = '0 22rpx' 选项的边距
- * @property {Boolean} lineAnimation = [true | false] 下划线是否有动画
- * @property {Number} zIndex = 1993 默认层级
- * @property {Boolean} lineSlot true 是否自定义底部滑块
- * @property {Boolean} tabsSwiper false 是否使用swiper
- * @property {Boolean} swiperType false swiper类型
- * @property {Boolean} circular false 是否采用衔接滑动,即播放到末尾后重新回到开头
- * @property {String} paddingBottom 0rpx swiper下边padding
- * @property {String} paddingTop 0rpx swiper上边padding
- * @property {Number} duration 500 swiper滑动动画时长
- * @property {String} contentHeight '' swiper内容高度
- *
- * @event {Function(current)} change 改变标签触发
- */
- export default {
- name: 'kTabsSwiper',
- props,
- data() {
- return {
- lineWidth: 30,
- currentWidth: 0, // 当前选项的宽度
- lineLeft: 0, // 滑块距离左侧的位置
- pillsLeft: 0, // 胶囊距离左侧的位置
- scrollLeft: 0, // 距离左边的位置
- container: {
- width: 0,
- height: 0,
- left: 0,
- right: 0
- }, // 容器的宽高,左右距离
- current: 0, // 当前选中项
- scrollWidth: 0, // 可以滚动的宽度
- lineSlotWidth: 0, //自定义底部的宽度
- startX: 0,
- startY: 0,
- animationData: {}, // 动画
- }
- },
- computed: {
- getDomId() {
- const len = 16
- const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
- const maxPos = $chars.length
- let pwd = ''
- for (let i = 0; i < len; i++) {
- pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
- }
- return `xfjpeter_${pwd}`
- }
- },
- watch: {
- value: {
- immediate: true,
- handler(newVal) {
- this.current = newVal
- this.$nextTick(this.update)
- }
- },
- },
- methods: {
- initScroll() {
- this.scrollY = new Array(10).fill(0);
- },
- touchStart(event) {
- this.startX = event.touches[0].pageX;
- this.startY = event.touches[0].pageY;
- },
- touchEnd(event) {
- let deltaX = event.changedTouches[0].pageX - this.startX;
- let deltaY = event.changedTouches[0].pageY - this.startY;
- if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 60) {
- if (deltaX < 0) { //往左
- if (this.current == this.tabs.length - 1) {
- if (this.circular) {
- this.change(0);
- } else {
- return
- }
- } else {
- this.change(this.current * 1 + 1);
- }
- this.animate();
- } else if (deltaX > 0) { //往右
- if (this.current == 0) {
- if (this.circular) {
- this.change(this.tabs.length - 1);
- } else {
- return
- }
- } else {
- this.change(this.current * 1 - 1);
- }
- this.animate();
- } else { // 挪动距离0
- }
- } else {
- }
- },
- onSwiperChange(e) {
- this.change(e.detail.current);
- },
- onBottom() {
- this.$emit("load", this.current)
- },
- animate() {
- this.animation.translateX(-this.swiperWidthPx * this.current + "px").step({
- duration: this.duration
- })
- this.animationData = this.animation.export()
- setTimeout(() => {
- this.animationData = {}
- }, this.duration)
- },
- // 切换事件
- change(index, source) {
- const isDisabled = !!this.tabs[index].disabled
- if (this.current !== index && !isDisabled) {
- this.current = index
- this.$emit('input', index)
- this.$emit('change', index)
- }
- //如果是tab切换
- if (this.swiperType && source == 'tab') {
- this.animate();
- }
- },
- createQueryHandler() {
- const query = uni
- .createSelectorQuery()
- // #ifndef MP-ALIPAY
- .in(this)
- // #endif
- return query
- },
- update() {
- const _this = this
- startMicroTask(() => {
- // 没有列表的时候,不执行
- if (!this.tabs.length) return
- _this
- .createQueryHandler()
- .select(`#${this.getDomId}`)
- .boundingClientRect(data => {
- const {
- width,
- height,
- left,
- right
- } = data || {}
- // 获取容器的相关属性
- this.container = {
- width,
- height,
- left,
- right: right - width
- }
- _this.lineScrollWidth();
- _this.calcScrollWidth()
- _this.setScrollLeft()
- _this.setLine()
- if (this.swiperType) {
- _this.getSwiperWidth();
- _this.initScroll();
- // 创建动画实例
- _this.animation = uni.createAnimation({
- timingFunction: 'ease',
- duration: 120
- })
- }
- })
- .exec()
- })
- },
- getSwiperWidth() {
- this.createQueryHandler()
- .select(`.v-tabs__content`)
- .boundingClientRect(data => {
- if (!data) return;
- this.swiperWidthPx = data.width;
- })
- .exec()
- },
- // 计算可以滚动的宽度
- calcScrollWidth(callback) {
- const view = this.createQueryHandler().select(`#${this.getDomId}`)
- view.fields({
- scrollOffset: true
- })
- view
- .scrollOffset(res => {
- if (typeof callback === 'function') {
- callback(res)
- } else {
- // 获取滚动条的宽度
- this.scrollWidth = res.scrollWidth
- }
- })
- .exec()
- },
- // 设置滚动条滚动的进度
- setScrollLeft() {
- this.calcScrollWidth(res => {
- // 动态读取 scrollLeft
- let scrollLeft = res.scrollLeft
- this.createQueryHandler()
- .select(`#${this.getDomId} .v-tabs__container-item.active`)
- .boundingClientRect(data => {
- if (!data) return
- // 除开当前选项外容器的一半宽度
- let curHalfWidth = (this.container.width - data.width) / 2
- let scrollDiff = this.scrollWidth - this.container.width
- // 在原有滚动条的基础上 + (当前元素距离左侧的距离 - 计算的一半宽度) - 容器的外边距之类的
- scrollLeft += data.left - curHalfWidth - this.container.left
- // 已经滚动在左侧了
- if (scrollLeft < 0) scrollLeft = 0
- // 已经超出右侧了
- else if (scrollLeft > scrollDiff) scrollLeft = scrollDiff
- this.scrollLeft = scrollLeft
- })
- .exec()
- })
- },
- lineScrollWidth() {
- this.createQueryHandler()
- .select(`#${this.getDomId} .lineSlot`)
- .boundingClientRect(data => {
- if (!data) return;
- this.lineSlotWidth = data.width;
- })
- .exec()
- },
- setLine() {
- this.calcScrollWidth(res => {
- const scrollLeft = res.scrollLeft
- this.createQueryHandler()
- .select(`#${this.getDomId} .v-tabs__container-item.active`)
- .boundingClientRect(data => {
- if (!data) return
- if (this.lineSlot) {
- this.lineWidth = data.width * this.lineScale
- this.lineLeft = scrollLeft + data.left + data.width / 2 - this.lineSlotWidth /
- 2 - this.container.left
- } else if (this.pills) {
- this.currentWidth = data.width
- this.pillsLeft = scrollLeft + data.left - this.container.left
- } else {
- this.lineWidth = data.width * this.lineScale
- this.lineLeft = scrollLeft + data.left + (data.width - data.width * this
- .lineScale) / 2 - this.container.left
- }
- })
- .exec()
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .v-tabs {
- width: 100%;
- box-sizing: border-box;
- overflow: hidden;
- border-radius: 10px;
- padding: 0 20rpx;
- background: #fff;
- /* #ifdef H5 */
- ::-webkit-scrollbar {
- display: none;
- }
- /* #endif */
- &__container {
- min-width: 100%;
- position: relative;
- display: inline-flex;
- align-items: center;
- white-space: nowrap;
- overflow: hidden;
- padding: 20rpx !important;
- border-radius: 10px;
- background: #fff;
- &-item {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- height: 100%;
- position: relative;
- z-index: 10;
- transition: all 0.3s;
- white-space: nowrap;
- padding: 10rpx 40rpx !important;
- &.disabled {
- opacity: 0.5;
- color: #999;
- }
- }
- &-line {
- position: absolute;
- left: 0;
- bottom: -20rpx;
- }
- &-pills {
- position: absolute;
- z-index: 9;
- }
- &-line,
- &-pills {
- &.animation {
- transition: all 0.3s linear;
- }
- }
- }
- }
- .data_list {
- display: flex;
- &_content {
- flex-shrink: 0;
- width: 100%;
- }
- }
- .triangle {
- width: 20rpx;
- height: 20rpx;
- background: #10B261;
- border-radius: 50%;
- }
- .double-border {
- box-shadow: 0 0 0 6rpx #fff, 0 0 0 12rpx #10B261;
- }
- </style>
|