博客 / 詳情

返回

書架效果的實現

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

1. 對齊目標

前端想實現一個類似的書架放置書籍的效果,目標如下:

ScreenShot_2026-02-09_171838_726

2. 思路梳理

我們使用的技術棧:vue

實現這樣的一個效果,我們需要知道以下信息:

  1. 每行可以放置多少書本?
  2. 放下所有的書本需要多少行?
  3. 需要什麼樣的數據結構?

我們現在一個個來思考,既然我們選擇了vue來實現,秉持着數據驅動視圖的理念,我們先從需要什麼樣的數據結構進行入手,其實很簡單,只需要一個二維數組就可以了。

二維數組的第一層就是書架的每一行,二維數組的第二層就是每一行對應的書本

[
    [
        {id:1,,name:"語文課本1"},//每一行放置的課本
        {id:2,name:"語文課本2"},
    ],  
    [
        {id:3,,name:"語文課本1"},//第二行放置的課本
        {id:4,name:"語文課本2"},
    ], 
]

那麼我們就可以按照這樣的一個數據結構來遍歷展示即可。

3. 實現步驟

3.1 界面實現

我們可以先按照我們上面已經寫好的數據,來寫好對應的Html和css,然後將效果渲染出來。

<template>
    <div class="shelf">
        <div class="shlef-row" v-for="(row, rowIndex) in bookData" :key="rowIndex">
            <div class="book-item" v-for="book in row" :key="book.id">
                {{ book.bookName }}
            </div>
        </div>
    </div>
</template>
​
<script setup>
import { ref } from 'vue';
​
const bookData = ref([
    [
        { id: 1, bookName: "語文課本1" },
        { id: 2, bookName: "語文課本2" },
    ],
    [
        { id: 3, bookName: "語文課本1" },//第二行放置的課本
        { id: 4, bookName: "語文課本2" },
    ]
])
</script>
​
<style>
.shelf {
    width: 1200px;
    height: auto;
    border: 1px solid #ccc;
    margin: 0 auto;
}
​
.shlef-row {
    width: 100%;
    margin: 0 0 20px 0;
    display: flex;
    border-bottom: 2px solid orange;
}
​
.shlef-row:last-child {
    margin-bottom: 0;
}
​
.book-item {
    box-sizing: border-box;
    padding: 10px;
    margin-right: 20px;
    width: 130px;
    height: 160px;
    color: #fff;
    background-color: skyblue;
}
</style>

3.2 根據真實的數據構造頁面數據

我們在真實的環境下,肯定是通過接口獲取到真實的後端數據,後端給我們的數據可能並不是我們想要的,我們就要對後端的數據進行構造,我們先分析下我們獲取到真實的後端數據,來做一下分析。

[
        { id: 1, bookName: "語文課本1" },
        { id: 2, bookName: "語文課本2" },
        { id: 3, bookName: "數學課本1" },
        { id: 4, bookName: "數學課本2" },
        { id: 5, bookName: "數學課本3" },
        { id: 6, bookName: "數學課本4" },
        { id: 7, bookName: "化學課本1" },
        { id: 8, bookName: "化學課本2" },
        { id: 9, bookName: "化學課本1" },
        { id: 10, bookName: "化學課本2" },
        { id: 11, bookName: "物理課本1" },
        { id: 12, bookName: "物理課本2" },
        { id: 13, bookName: "物理課本3" },
        { id: 14, bookName: "物理課本4" },
        { id: 15, bookName: "生物課本1" },
        { id: 16, bookName: "生物課本2" }
]

可以看出,後端的數據給我們的是一整個數組,那麼對於我們來説就需要解決以下問題:

  • 計算一行可以放置多少本書
  • 計算總共多少行

每行可以放置書本數:Math.floor(書架寬度 / 每本書實際佔據的寬度(包含margin))

總共多少行書架:Math.ceil(書本總數 / 每行可以放置的書本樹)

截取數組:循環書架行數,然後不停的從後端數據中去截取對應數量數據即可。

// 構造頁面數據 rawData:後端數據
const genBookData = (rawData) => {
    const counts = Math.floor(1200 / 150);//每行可放置書本數fam
    const rowCount = Math.ceil(rawData.length / counts);//總共有多少行
    const rowArr = [];//書架二維數組
​
    for (let i = 0; i < rowCount; i++) {
        //每次截取對應的書本,添加到二維數組
        const rowBooks = rawData.slice(i * counts, (i + 1) * counts);
        rowArr.push(rowBooks);
    }
    return rowArr;
}

其實,這個時候,就已經實現了基本的書架功能了。

ScreenShot_2026-02-09_172000_447

 

4. 附加功能優化

上面雖然已經實現了基本的書架效果,但是我們面臨以下的問題:

  • 現在最後一本書距離右側空間太大,我想讓書本平分空間。
  • 當用户改變瀏覽器窗口,我對應的書架寬度改變了,需要去根據屏幕更新每行放置的書本數。

1. 書本平分空間遇到的問題

對於評分空間,大家一定覺得很容易處理,直接使用flex佈局,讓每本書flex:1平分空間即可。

但是這裏我重點想説的是,如果最後一行書架的書本如果放不滿書架,那麼就會受到flex:1的影響,自動撐大寬度,導致和上一行的書本寬度不一致。效果如下:

ScreenShot_2026-02-09_172019_674

解決方法:就是添加一些虛擬的佔位元素(placeholder),我們改動一下我們的構造數據的函數。
// 構造頁面數據
const genBookData = (rawData) => {
    const counts = Math.floor(1200 / 150);//每行可放置書本數fam
    const rowCount = Math.ceil(rawData.length / counts);//總共有多少行
    const rowArr = [];//書架二維數組
​
    for (let i = 0; i < rowCount; i++) {
        const rowBooks = rawData.slice(i * counts, (i + 1) * counts);
        //+++
        if (i === rowCount - 1 && rowBooks.length < counts) {
            // 當這一行實際的書本數 < 每行能放置的書本數時     向二維數組中添加佔位元素
            const placeholders = Array(counts - rowBooks.length).fill().map((_, index) => ({
                id: `placeholder-${index}`,
                isPlaceholder: true
            }));
            rowArr.push([...rowBooks, ...placeholders]);
        } else {
            rowArr.push(rowBooks);
        }
    }
    return rowArr;
}

這樣就正常了,大家可以把佔位元素直接給隱藏( visibility: hidden;)即可

2. 解決動態計算問題

動態計算的時候其實也很簡單,我們只需要獲取到當前書架的寬度,然後監聽windowresize事件,再去重新執行我們的構造數據的邏輯即可。

但是我有一個更好的方法,使用計算屬性! 我們計算屬性中依賴一下我們當前屏幕寬度的變量(shelfWidth),這樣我們在改變屏幕的時候,直接更新shelfWidth即可,然後計算屬性會自動執行,重新計算我們的數據。直接看最終代碼。

<template>
    <div class="shelf" ref="shelfRef">
        <div class="shlef-row" v-for="(row, rowIndex) in bookData" :key="rowIndex">
            <div class="book-item" v-for="book in row" :key="book.id">
                {{ book.bookName }}
            </div>
        </div>
    </div>
​
    <button @click="changeWidtn">改變寬度</button>
</template>
​
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
​
const changeWidtn = () => {
    shelfWidth.value = 900;
}
​
// 請求接口的數據
const apiData = [
    { id: 1, bookName: "語文課本1" },
    { id: 2, bookName: "語文課本2" },
    { id: 3, bookName: "數學課本1" },
    { id: 4, bookName: "數學課本2" },
    { id: 5, bookName: "數學課本3" },
    { id: 6, bookName: "數學課本4" },
    { id: 7, bookName: "化學課本1" },
    { id: 8, bookName: "化學課本2" },
    { id: 9, bookName: "化學課本1" },
    { id: 10, bookName: "化學課本2" },
    { id: 11, bookName: "物理課本1" },
    { id: 12, bookName: "物理課本2" },
    { id: 13, bookName: "物理課本3" },
    { id: 14, bookName: "物理課本4" },
    { id: 15, bookName: "生物課本1" },
]
​
​
/* 書架效果 */
const shelfRef = ref(null);//書架Ref
const shelfWidth = ref(1200);//書架寬度
​
// 構造頁面數據
const bookData = computed(() => {               //頁面渲染的數據
    if (!shelfRef.value || !shelfWidth.value) {
        return []
    }
​
    const counts = Math.floor(shelfWidth.value / 150);//每行可放置書本數
    const rowCount = Math.ceil(apiData.length / counts);//總共有多少行
    const rowArr = [];//書架二維數組
​
    // 如果是最後一行且不滿,添加佔位元素,解決flex問題
    for (let i = 0; i < rowCount; i++) {
        const rowBooks = apiData.slice(i * counts, (i + 1) * counts);
​
        if (i === rowCount - 1 && rowBooks.length < counts) {
            const placeholders = Array(counts - rowBooks.length).fill().map((_, index) => ({
                id: `placeholder-${index}`,
                isPlaceholder: true,
                bookName: '佔位元素'
            }));
            rowArr.push([...rowBooks, ...placeholders]);
        } else {
            rowArr.push(rowBooks);
        }
​
    }
​
    return rowArr;
})
​
// 更新屏幕寬度
const updateShelfWidth = () => {
    shelfWidth.value = shelfRef.value.offsetWidth;
}
​
onMounted(() => {
    updateShelfWidth();//頁面加載後,更新下屏幕寬度
    window.addEventListener('resize', updateShelfWidth);
})
​
onBeforeUnmount(() => {
    window.removeEventListener('resize', updateShelfWidth);
})
</script>
​
<style>
.shelf {
    width: 1200px;
    height: auto;
    border: 1px solid #ccc;
    margin: 0 auto;
    min-width: 1000px;
}
​
.shlef-row {
    width: 100%;
    margin: 0 0 20px 0;
    display: flex;
    border-bottom: 2px solid orange;
}
​
.shlef-row:last-child {
    margin-bottom: 0;
}
​
.shlef-row .book-item:last-child {
    margin-right: 0;
}
​
.book-item {
    flex: 1;
    box-sizing: border-box;
    padding: 10px;
    margin-right: 20px;
    width: 130px;
    height: 160px;
    color: #fff;
    background-color: skyblue;
}
</style>

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.