书架效果的实现

1. 对齐目标

前端想实现一个类似的书架放置书籍的效果,目标如下:

2. 思路梳理

我们使用的技术栈:vue

实现这样的一个效果,我们需要知道以下信息:

  1. 每行可以放置多少书本?
  2. 放下所有的书本需要多少行?
  3. 需要什么样的数据结构?

我们现在一个个来思考,既然我们选择了vue来实现,秉持着数据驱动视图的理念,我们先从需要什么样的数据结构进行入手,其实很简单,只需要一个二维数组就可以了。

二维数组的第一层就是书架的每一行,二维数组的第二层就是每一行对应的书本

bash 复制代码
[
    [
        {id:1,,name:"语文课本1"},//每一行放置的课本
        {id:2,name:"语文课本2"},
    ],  
    [
        {id:3,,name:"语文课本1"},//第二行放置的课本
        {id:4,name:"语文课本2"},
    ], 
]

那么我们就可以按照这样的一个数据结构来遍历展示即可。

3. 实现步骤

3.1 界面实现

我们可以先按照我们上面已经写好的数据,来写好对应的Html和css,然后将效果渲染出来。

xml 复制代码
<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 根据真实的数据构造页面数据

我们在真实的环境下,肯定是通过接口获取到真实的后端数据,后端给我们的数据可能并不是我们想要的,我们就要对后端的数据进行构造,我们先分析下我们获取到真实的后端数据,来做一下分析。

yaml 复制代码
[
        { 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(书本总数 / 每行可以放置的书本树)

截取数组:循环书架行数,然后不停的从后端数据中去截取对应数量数据即可。

ini 复制代码
// 构造页面数据 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的影响,自动撑大宽度,导致和上一行的书本宽度不一致。效果如下:

解决方法:就是添加一些虚拟的占位元素(placeholder),我们改动一下我们的构造数据的函数。

ini 复制代码
// 构造页面数据
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即可,然后计算属性会自动执行,重新计算我们的数据。直接看最终代码。

xml 复制代码
<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>
相关推荐
GBVFtou1 天前
vue3 options模式
前端·vue.js
Jyywww1211 天前
uniapp uni.chooseImage+uni.uploadFile使用方法与详解
开发语言·javascript·uni-app
小高0071 天前
🔥🔥🔥收藏!面试常问JavaScript 中统计字符出现频率,一次弄懂!
前端·javascript·面试
我是日安1 天前
从零到一打造 Vue3 响应式系统 Day 26 - 数组长度变更处理
前端·javascript·vue.js
jason_yang1 天前
vue3中使用auto-import与cdn插件冲突问题
vue.js·vite·cdn
笔尖的记忆1 天前
【前端架和框架】react协调器reconciler工作原理
前端·javascript·面试
pepedd8641 天前
我用Kiro+Claude写了一个MCP Server,让AI真正"感知"真实环境
前端·javascript·trae
San301 天前
JavaScript 标准库完全指南:从基础到实战
前端·javascript·node.js
Never_Satisfied1 天前
在JavaScript / Node.js中,Web服务器参数处理与编码指南
前端·javascript·node.js