🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
1. 對齊目標
前端想實現一個類似的書架放置書籍的效果,目標如下:
2. 思路梳理
我們使用的技術棧:vue
實現這樣的一個效果,我們需要知道以下信息:
- 每行可以放置多少書本?
- 放下所有的書本需要多少行?
- 需要什麼樣的數據結構?
我們現在一個個來思考,既然我們選擇了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;
}
其實,這個時候,就已經實現了基本的書架功能了。
4. 附加功能優化
上面雖然已經實現了基本的書架效果,但是我們面臨以下的問題:
- 現在最後一本書距離右側空間太大,我想讓書本平分空間。
- 當用户改變瀏覽器窗口,我對應的書架寬度改變了,需要去根據屏幕更新每行放置的書本數。
1. 書本平分空間遇到的問題
對於評分空間,大家一定覺得很容易處理,直接使用flex佈局,讓每本書flex:1平分空間即可。
但是這裏我重點想説的是,如果最後一行書架的書本如果放不滿書架,那麼就會受到flex:1的影響,自動撐大寬度,導致和上一行的書本寬度不一致。效果如下:
// 構造頁面數據
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. 解決動態計算問題
動態計算的時候其實也很簡單,我們只需要獲取到當前書架的寬度,然後監聽window的resize事件,再去重新執行我們的構造數據的邏輯即可。
但是我有一個更好的方法,使用計算屬性! 我們計算屬性中依賴一下我們當前屏幕寬度的變量(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>