實(shí)踐積累:用Vue3簡(jiǎn)單寫(xiě)一個(gè)單行橫向滾動(dòng)組件

目錄

版權(quán)聲明:本文為博主原創(chuàng)文章俗慈,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。

  • 效果圖
  • 需求分析
  • 實(shí)現(xiàn)分析
    • 樣式展示分析
    • 變量分析
    • 方法分析
  • 實(shí)現(xiàn)步驟
      1. 實(shí)現(xiàn)模板
      1. 實(shí)現(xiàn)css
      1. 首先獲取list
      1. 頁(yè)面掛載后監(jiān)聽(tīng)groupBoxRefscroll事件并獲取當(dāng)前的滾動(dòng)位置
      1. 計(jì)算展示的寬度顯隱箭頭,當(dāng)卡片寬度大于外層寬度就展示
      1. 控制箭頭展示方向
      1. 監(jiān)聽(tīng)外層寬度改變和窗口大小改變箭頭顯隱
  • 完整代碼

效果圖

把之前完成的一個(gè)效果圖摘出來(lái)記錄一下年局,核心代碼在這里,如果項(xiàng)目中用到其他的技術(shù)咸产,也很容易改矢否。

scroll.gif

需求分析

  1. 展示數(shù)據(jù)始終一行,多余的部分可以出滾動(dòng)條脑溢,同時(shí)隱藏滾動(dòng)條樣式僵朗。
  2. 支持筆記本觸控滑動(dòng)展示
  3. 支持鼠標(biāo)點(diǎn)擊滑動(dòng),多余的時(shí)候出現(xiàn)箭頭按鈕,默認(rèn)滑動(dòng)3個(gè)卡片的位置验庙,頂頭就切換方向
  4. 當(dāng)頁(yè)面出現(xiàn)變動(dòng)的時(shí)候要監(jiān)聽(tīng)及時(shí)顯示或隱藏按鈕

實(shí)現(xiàn)分析

樣式展示分析

  • 外層控制總體組件寬度
    • 內(nèi)層箭頭區(qū)域展示顶吮,內(nèi)部使用flex布局,絕對(duì)定位到右側(cè)
      • 內(nèi)部箭頭svg圖標(biāo)壶谒,垂直居中
    • 內(nèi)層控制滾動(dòng)區(qū)域?qū)挾仍平茫瑑?nèi)部使用flex布局,控制一層展示汗菜,溢出滾動(dòng)展示,并隱藏滾動(dòng)條
      • 內(nèi)部確定卡片寬高和間距挑社,最后一個(gè)右邊距為0

變量分析

  1. 卡片 list:Array
  2. 控制箭頭顯隱 arrowShow:Boolean陨界,控制箭頭方向 direction:String
  3. 獲取滾動(dòng)位置 scrollPosition = {left: 0, top: 0}
  4. 計(jì)算寬度需要的ref:控制滾動(dòng)條層 groupBoxRef,卡片 groupCardRef

方法分析

  1. 獲取list(可以http痛阻,也可以props菌瘪,根據(jù)需求來(lái)定)
  2. 頁(yè)面掛載之后,監(jiān)聽(tīng)groupBoxRefscroll事件和窗口變化的resize事件
  3. 箭頭的顯隱判斷方法阱当,改變箭頭方向的方法
  4. 鼠標(biāo)點(diǎn)擊箭頭的方法

實(shí)現(xiàn)步驟

1. 實(shí)現(xiàn)模板

<template>
  <div class="index-group-box">
    <!-- 右邊滑動(dòng)箭頭 -->
    <div class="scrollX">
      <img src='../assets/arrow-left-bold.svg'/>
    </div>
    <!-- 卡? -->
    <div class="index-group-boxIn" ref="groupBoxRef">
      <div
        v-for="item in groupInfo"
        :key="item.id"
        ref="groupCardRef"
        class="group-card"
      >
        <div class="card-name">
          名稱(chēng)
          <span>{{ item.name }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

2. 實(shí)現(xiàn)css

<style scoped>
  .index-group-box {
    padding-right: 16px;
    position: relative;
    box-sizing: border-box;
    width: 100%;
  }  

  .scrollX {
    width: 16px;
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    background-color: #512D6D;
    display: flex;
    justify-content: center;
    align-items: center
  }

  .scrollX:hover {
    cursor: pointer;
    background-color: #65447d;
  }

  .index-group-boxIn {
    display: flex;
    scroll-behavior: smooth;
    white-space: nowrap;
    overflow-x: auto;
    flex: none;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE 10+ */
  }

  .index-group-boxIn::-webkit-scrollbar {
    display: none; /* Chrome Safari */
  }

  .group-card {
    padding: 8px 16px;
    box-sizing:border-box;
    width: 200px;
    height: 100px;
    border-radius: 4px;
    margin-right: 16px;
    flex: none;
    background: #71EFA3;
    color: #54436B;
  }

  .group-card span{
    color: #54436B;
  }

  .group-card:hover{
    background: #ACFFAD;
  }

  .group-card:nth-last-of-type(1){
    margin-right: 0px;
  }
</style>

3. 首先獲取list

<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
    name: 'scroll',
    setup() {
        const groupInfo = ref([]);
        
        // 獲取卡片列表
        const getMyGroup = async () => {
            const data = [{
                id: 1,
                name:'卡片1'
            },{
                id: 2,
                name:'卡片2'
            },{
                id: 3,
                name:'卡片3'
            },{
                id: 4,
                name:'卡片4'
            },{
                id: 5,
                name:'卡片5'
            }]
            groupInfo.value = data;
        }
        getMyGroup();
        return {
            // data
            groupInfo,
        };
    },
});
</script>

4. 頁(yè)面掛載后監(jiān)聽(tīng)groupBoxRef的scroll事件并獲取當(dāng)前的滾動(dòng)位置

// 添加reactive和onMounted
import { defineComponent, ref, reactive, onMounted } 
...
const groupBoxRef = ref(null); // 獲取外層卡?ref
const groupCardRef = ref(null); // 獲取卡?ref
const scrollPosition = reactive({
    left: 0,
    top: 0
}); // 滾動(dòng)位置
...
// 獲取scroll函數(shù)的位置
const handleScroll = e => {
    scrollPosition.left = e.target.scrollLeft;
    scrollPosition.top = e.target.scrollTop;
}

getMyGroup();

onMounted(() => {
    // 監(jiān)聽(tīng)scroll事件
    groupBoxRef.value.addEventListener('scroll', handleScroll, true);
})

return {
    // data
    groupInfo,
    // ref
    groupBoxRef,
    groupCardRef,
};

5. 計(jì)算展示的寬度顯隱箭頭俏扩,當(dāng)卡片寬度大于外層寬度就展示

  • 卡片寬度:groupCardRef.value.offsetWidth
  • 外層寬度:groupBoxRef.value.offsetWidth
  • 滾動(dòng)區(qū)域?qū)挾龋?code>卡片數(shù)量 * (卡片寬度 + 右邊距)- 最后一個(gè)右邊距
<div class="scrollX" v-if="arrowShow">
    <img src='../assets/arrow-left-bold.svg'/>
</div>
...
const arrowShow = ref(false); // 滾動(dòng)箭頭是否顯示

// 獲取卡?寬度,第?個(gè)參數(shù)是卡?個(gè)數(shù)弊添,默認(rèn)是整個(gè)數(shù)組录淡,第?個(gè)參數(shù)是剩余的margin
const getWidth = (num = groupInfo.value.length, restMargin = 16) => {
    // 如果沒(méi)有內(nèi)容就返回0
    if(!groupCardRef.value) return 0;
    return num * (groupCardRef.value.offsetWidth + 16) - restMargin;
}

// 判斷arrow是否展示
const checkArrowShow = () => {
    arrowShow.value = getWidth() > groupBoxRef.value?.offsetWidth ? true : false;
}
...
onMounted(() => {
    // 監(jiān)聽(tīng)scroll事件
    groupBoxRef.value.addEventListener('scroll', handleScroll, true);
    // 首次檢查箭頭展示
    checkArrowShow();
})

6. 控制箭頭展示方向

  • 初始朝右,橫向滾動(dòng)區(qū)域?yàn)?就朝右油坝,剩余寬度比外層寬度小就朝左
  • 剩余寬度:滾動(dòng)區(qū)域?qū)挾?- 滾動(dòng)距離
<!-- 添加點(diǎn)擊箭頭事件和箭頭方向svg -->
<div class="scrollX" @click="groupScroll" v-if="arrowShow">
    <img v-if="direction === 'left'" src='../assets/arrow-left-bold.svg'/>
    <img v-else src='../assets/arrow-right-bold.svg'/>
</div>
...
const direction = ref('right'); // 默認(rèn)項(xiàng)?組箭頭向右
...
// 改變滾動(dòng)?向
const changeArrow = (scrollLeft) => {
    // 默認(rèn)獲取scoll部分整個(gè)寬度
    const getScrollWidth = getWidth();
    // 計(jì)算得出剩余寬度
    const restWidth = getScrollWidth - scrollLeft
    if (restWidth <= groupBoxRef.value.offsetWidth) {
        direction.value = 'left'
    } else if ( scrollLeft === 0 ) {
        direction.value = 'right'
    }
}

// ?標(biāo)點(diǎn)擊滾動(dòng)
const groupScroll = async () => {
    // 計(jì)算移動(dòng)寬度嫉戚,現(xiàn)在是移動(dòng)3個(gè)卡片的數(shù)量
    const getMoveWidth = getWidth(3, 0);
    // 如果方向是右邊就+,左邊就-
    if (direction.value === 'right') {
        groupBoxRef.value.scrollLeft += getMoveWidth;
    } else {
        groupBoxRef.value.scrollLeft -= getMoveWidth;
    }
    // 滾動(dòng)需要時(shí)間才能獲取最新的距離澈圈,根據(jù)新的距離看箭頭的方向
    setTimeout(() => {
        changeArrow(groupBoxRef.value.scrollLeft);
    }, 500)
}

// 觸摸板滑動(dòng)的時(shí)候位置實(shí)時(shí)改變箭頭方向
const handleScroll = e => {
    ...
    changeArrow(scrollPosition.left);
}

return {
        
    // 新加的data
    ...
    direction,
    // ref
    ...
    // 新加的methods
    groupScroll
};

7. 監(jiān)聽(tīng)外層寬度改變和窗口大小改變箭頭顯隱

import { defineComponent, ref, reactive, onMounted, watchEffect } from 'vue';
...
watchEffect(() => {
    checkArrowShow();
})

onMounted(() => {
    ...
    // 監(jiān)聽(tīng)窗?變化事件彬檀,判斷arrow的展示
    window.addEventListener('resize', checkArrowShow, true);
})

完整代碼

<template>
    <div class="index-group-box">
        <!-- 右邊滑動(dòng)箭頭 -->
        <div class="scrollX" @click="groupScroll" v-if="arrowShow">
            <img v-if="direction === 'left'" src='../assets/arrow-left-bold.svg'/>
            <img v-else src='../assets/arrow-right-bold.svg'/>
        </div>
        <!-- 卡? -->
        <div class="index-group-boxIn" ref="groupBoxRef">
            <div
                v-for="item in groupInfo"
                :key="item.id"
                ref="groupCardRef"
                class="group-card"
            >
                <div class="card-name">
                    名稱(chēng)
                    <span>{{ item.name }}</span>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { defineComponent, ref, reactive, onMounted, watchEffect } from 'vue';
export default defineComponent({
    name: 'scroll',
    setup() {
        const groupInfo = ref([]); // 卡片list
        const direction = ref('right'); // 默認(rèn)箭頭向右
        const arrowShow = ref(false); // 滾動(dòng)箭頭是否顯示
        const groupBoxRef = ref(null); // 獲取外層卡?ref
        const groupCardRef = ref(null); // 獲取卡?ref
        const scrollPosition = reactive({
            left: 0,
            top: 0
        }); // 滾動(dòng)位置

  
        // 獲取卡片列表
        const getMyGroup = async () => {
            const data = [{
                id: 1,
                name:'卡片1'
            },{
                id: 2,
                name:'卡片2'
            },{
                id: 3,
                name:'卡片3'
            },{
                id: 4,
                name:'卡片4'
            },{
                id: 5,
                name:'卡片5'
            }]
            groupInfo.value = data;
        }
    
        // 獲取卡?寬度,第?個(gè)參數(shù)是卡?個(gè)數(shù)瞬女,默認(rèn)是整個(gè)數(shù)組窍帝,第?個(gè)參數(shù)是剩余的margin
        const getWidth = (num = groupInfo.value.length, restMargin = 16) => {
            // 如果沒(méi)有內(nèi)容就返回0
            if(!groupCardRef.value) return 0;
            return num * (groupCardRef.value.offsetWidth + 16) - restMargin;
        }
        // 改變滾動(dòng)?向
        const changeArrow = (scrollLeft) => {
            // 默認(rèn)獲取scoll部分整個(gè)寬度
            const getScrollWidth = getWidth();
            // 獲取剩余寬度
            const restWidth = getScrollWidth - scrollLeft
            if (restWidth <= groupBoxRef.value.offsetWidth) {
                direction.value = 'left'
            } else if ( scrollLeft === 0 ) {
                direction.value = 'right'
            }
        }
        // ?標(biāo)點(diǎn)擊滾動(dòng)
        const groupScroll = async () => {
            // 獲取滾動(dòng)寬度
            const getMoveWidth = getWidth(3, 0);
            if (direction.value === 'right') {
                groupBoxRef.value.scrollLeft += getMoveWidth;
            } else {
                groupBoxRef.value.scrollLeft -= getMoveWidth;
            }
            // 滾動(dòng)需要時(shí)間才能獲取最新的距離
            setTimeout(() => {
                changeArrow(groupBoxRef.value.scrollLeft);
            }, 500)
        }

        // 判斷arrow是否展示
        const checkArrowShow = () => {
            arrowShow.value = getWidth() > groupBoxRef.value?.offsetWidth ? true : false;
        }

        watchEffect(() => {
            checkArrowShow();
        })

        // 獲取scroll函數(shù)的位置
        const handleScroll = e => {
            scrollPosition.left = e.target.scrollLeft;
            scrollPosition.top = e.target.scrollTop;
            changeArrow(scrollPosition.left);
        }

        getMyGroup();

        onMounted(() => {
            // 監(jiān)聽(tīng)scroll事件
            groupBoxRef.value.addEventListener('scroll', handleScroll, true);
            // 監(jiān)聽(tīng)窗?變化事件,判斷arrow的展示
            window.addEventListener('resize', checkArrowShow, true);
            // 首次檢查箭頭展示
            checkArrowShow();
        })

        return {
            // data
            groupInfo,
            direction,
            arrowShow,
            // ref
            groupBoxRef,
            groupCardRef,
            // methods
            groupScroll
        };
    },
});
</script>
<style scoped>
.index-group-box {
    padding-right: 16px;
    position: relative;
    box-sizing: border-box;
    width: 100%;
}  

.scrollX {
    width: 16px;
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    background-color: #512D6D;
    display: flex;
    justify-content: center;
    align-items: center
}

.scrollX:hover {
    cursor: pointer;
    background-color: #65447d;
}

.index-group-boxIn {
    display: flex;
    scroll-behavior: smooth;
    white-space: nowrap;
    overflow-x: auto;
    flex: none;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE 10+ */
}

.index-group-boxIn::-webkit-scrollbar {
    display: none; /* Chrome Safari */
}

.group-card {
    padding: 8px 16px;
    box-sizing:border-box;
    width: 200px;
    height: 100px;
    border-radius: 4px;
    margin-right: 16px;
    flex: none;
    background: #71EFA3;
    color: #54436B;
}

.group-card span{
    color: #54436B;
}

.group-card:hover{
    background: #ACFFAD;
}

.group-card:nth-last-of-type(1){
    margin-right: 0px;
}
</style>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诽偷,一起剝皮案震驚了整個(gè)濱河市坤学,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渤刃,老刑警劉巖拥峦,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異卖子,居然都是意外死亡略号,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)玄柠,“玉大人突梦,你說(shuō)我怎么就攤上這事∮鹄” “怎么了宫患?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)这弧。 經(jīng)常有香客問(wèn)我娃闲,道長(zhǎng),這世上最難降的妖魔是什么匾浪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任皇帮,我火速辦了婚禮,結(jié)果婚禮上蛋辈,老公的妹妹穿的比我還像新娘属拾。我一直安慰自己,他們只是感情好冷溶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布渐白。 她就那樣靜靜地躺著,像睡著了一般逞频。 火紅的嫁衣襯著肌膚如雪纯衍。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,155評(píng)論 1 299
  • 那天虏劲,我揣著相機(jī)與錄音托酸,去河邊找鬼。 笑死柒巫,一個(gè)胖子當(dāng)著我的面吹牛励堡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堡掏,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼应结,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了泉唁?” 一聲冷哼從身側(cè)響起鹅龄,我...
    開(kāi)封第一講書(shū)人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亭畜,沒(méi)想到半個(gè)月后扮休,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拴鸵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年玷坠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜗搔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡八堡,死狀恐怖樟凄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兄渺,我是刑警寧澤缝龄,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站挂谍,受9級(jí)特大地震影響叔壤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜口叙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一百新、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧庐扫,春花似錦、人聲如沸仗哨。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)厌漂。三九已至萨醒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間苇倡,已是汗流浹背富纸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旨椒,地道東北人晓褪。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像综慎,于是被迫代替她去往敵國(guó)和親涣仿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353