想做蜗牛的蝎 發表於 2026-2-9 17:21:00

书架效果的实现

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<h2 data-id="heading-0">1. 对齐目标</h2>
<p>前端想实现一个类似的书架放置书籍的效果,目标如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260209171848399-164465083.png" alt="ScreenShot_2026-02-09_171838_726" loading="lazy"></p>
<div>
<div>
<h2 data-id="heading-1">2. 思路梳理</h2>
<p>我们使用的技术栈:vue</p>
<p>实现这样的一个效果,我们需要知道以下信息:</p>
<ol start="0">
<li>每行可以放置多少书本?</li>
<li>放下所有的书本需要多少行?</li>
<li>需要什么样的数据结构?</li>
</ol>
<p>我们现在一个个来思考,既然我们选择了vue来实现,秉持着数据驱动视图的理念,我们先从需要什么样的数据结构进行入手,其实很简单,只需要一个二维数组就可以了。</p>
<p>二维数组的第一层就是书架的每一行,二维数组的第二层就是每一行对应的书本</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">[
&nbsp;[
&nbsp; &nbsp; &nbsp;{id:1,,name:"语文课本1"},//每一行放置的课本
&nbsp; &nbsp; &nbsp;{id:2,name:"语文课本2"},
&nbsp;], &nbsp;
&nbsp;[
&nbsp; &nbsp; &nbsp;{id:3,,name:"语文课本1"},//第二行放置的课本
&nbsp; &nbsp; &nbsp;{id:4,name:"语文课本2"},
&nbsp;],
]</pre>
</div>
<p>那么我们就可以按照这样的一个数据结构来遍历展示即可。</p>
<h2 data-id="heading-2">3. 实现步骤</h2>
<p>3.1 界面实现</p>
<p>我们可以先按照我们上面已经写好的数据,来写好对应的Html和css,然后将效果渲染出来。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&nbsp; &nbsp;&lt;div class="shelf"&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&lt;div class="shlef-row" v-for="(row, rowIndex) in bookData" :key="rowIndex"&gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&lt;div class="book-item" v-for="book in row" :key="book.id"&gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{{ book.bookName }}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&lt;/div&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&lt;/div&gt;
&nbsp; &nbsp;&lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { ref } from 'vue';

const bookData = ref([
&nbsp;[
&nbsp; &nbsp; &nbsp;{ id: 1, bookName: "语文课本1" },
&nbsp; &nbsp; &nbsp;{ id: 2, bookName: "语文课本2" },
&nbsp;],
&nbsp;[
&nbsp; &nbsp; &nbsp;{ id: 3, bookName: "语文课本1" },//第二行放置的课本
&nbsp; &nbsp; &nbsp;{ id: 4, bookName: "语文课本2" },
&nbsp;]
])
&lt;/script&gt;

&lt;style&gt;
.shelf {
&nbsp; &nbsp;width: 1200px;
&nbsp; &nbsp;height: auto;
&nbsp; &nbsp;border: 1px solid #ccc;
&nbsp; &nbsp;margin: 0 auto;
}

.shlef-row {
&nbsp; &nbsp;width: 100%;
&nbsp; &nbsp;margin: 0 0 20px 0;
&nbsp; &nbsp;display: flex;
&nbsp; &nbsp;border-bottom: 2px solid orange;
}

.shlef-row:last-child {
&nbsp; &nbsp;margin-bottom: 0;
}

.book-item {
&nbsp; &nbsp;box-sizing: border-box;
&nbsp; &nbsp;padding: 10px;
&nbsp; &nbsp;margin-right: 20px;
&nbsp; &nbsp;width: 130px;
&nbsp; &nbsp;height: 160px;
&nbsp; &nbsp;color: #fff;
&nbsp; &nbsp;background-color: skyblue;
}
&lt;/style&gt;</pre>
</div>
<p>3.2 根据真实的数据构造页面数据</p>
<p>我们在真实的环境下,肯定是通过接口获取到真实的后端数据,后端给我们的数据可能并不是我们想要的,我们就要对后端的数据进行构造,我们先分析下我们获取到真实的后端数据,来做一下分析。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">[
&nbsp; &nbsp; &nbsp;{ id: 1, bookName: "语文课本1" },
&nbsp; &nbsp; &nbsp;{ id: 2, bookName: "语文课本2" },
&nbsp; &nbsp; &nbsp;{ id: 3, bookName: "数学课本1" },
&nbsp; &nbsp; &nbsp;{ id: 4, bookName: "数学课本2" },
&nbsp; &nbsp; &nbsp;{ id: 5, bookName: "数学课本3" },
&nbsp; &nbsp; &nbsp;{ id: 6, bookName: "数学课本4" },
&nbsp; &nbsp; &nbsp;{ id: 7, bookName: "化学课本1" },
&nbsp; &nbsp; &nbsp;{ id: 8, bookName: "化学课本2" },
&nbsp; &nbsp; &nbsp;{ id: 9, bookName: "化学课本1" },
&nbsp; &nbsp; &nbsp;{ id: 10, bookName: "化学课本2" },
&nbsp; &nbsp; &nbsp;{ id: 11, bookName: "物理课本1" },
&nbsp; &nbsp; &nbsp;{ id: 12, bookName: "物理课本2" },
&nbsp; &nbsp; &nbsp;{ id: 13, bookName: "物理课本3" },
&nbsp; &nbsp; &nbsp;{ id: 14, bookName: "物理课本4" },
&nbsp; &nbsp; &nbsp;{ id: 15, bookName: "生物课本1" },
&nbsp; &nbsp; &nbsp;{ id: 16, bookName: "生物课本2" }
]</pre>
</div>
<div>
<div>
<p>可以看出,后端的数据给我们的是一整个数组,那么对于我们来说就需要解决以下问题:</p>
<ul>
<li>计算一行可以放置多少本书</li>
<li>计算总共多少行</li>
</ul>
<p>每行可以放置书本数:Math.floor(书架宽度 / 每本书实际占据的宽度(包含margin))</p>
<p>总共多少行书架:Math.ceil(书本总数 / 每行可以放置的书本树)</p>
<p>截取数组:循环书架行数,然后不停的从后端数据中去截取对应数量数据即可。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 构造页面数据 rawData:后端数据
const genBookData = (rawData) =&gt; {
&nbsp; &nbsp;const counts = Math.floor(1200 / 150);//每行可放置书本数fam
&nbsp; &nbsp;const rowCount = Math.ceil(rawData.length / counts);//总共有多少行
&nbsp; &nbsp;const rowArr = [];//书架二维数组

&nbsp; &nbsp;for (let i = 0; i &lt; rowCount; i++) {
&nbsp; &nbsp; &nbsp; &nbsp;//每次截取对应的书本,添加到二维数组
&nbsp; &nbsp; &nbsp; &nbsp;const rowBooks = rawData.slice(i * counts, (i + 1) * counts);
&nbsp; &nbsp; &nbsp; &nbsp;rowArr.push(rowBooks);
&nbsp;}
&nbsp; &nbsp;return rowArr;
}</pre>
</div>
<p>其实,这个时候,就已经实现了基本的书架功能了。</p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260209172012347-1640367544.png" alt="ScreenShot_2026-02-09_172000_447" loading="lazy"></p>
<p>&nbsp;</p>
<div>
<div>
<h2 data-id="heading-3">4. 附加功能优化</h2>
<p>上面虽然已经实现了基本的书架效果,但是我们面临以下的问题:</p>
<ul>
<li>现在最后一本书距离右侧空间太大,我想让书本平分空间。</li>
<li>当用户改变浏览器窗口,我对应的书架宽度改变了,需要去根据屏幕更新每行放置的书本数。</li>
</ul>
<p><strong>1. 书本平分空间遇到的问题</strong></p>
<p>对于评分空间,大家一定觉得很容易处理,直接使用flex布局,让每本书<code>flex:1</code>平分空间即可。</p>
<p>但是这里我重点想说的是,如果最后一行书架的书本如果放不满书架,那么就会受到<code>flex:1</code>的影响,自动撑大宽度,导致和上一行的书本宽度不一致。效果如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260209172029650-1994963191.png" alt="ScreenShot_2026-02-09_172019_674" loading="lazy"></p>
</div>
解决方法:就是添加一些虚拟的占位元素(placeholder),我们改动一下我们的构造数据的函数。<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 构造页面数据
const genBookData = (rawData) =&gt; {
&nbsp; &nbsp;const counts = Math.floor(1200 / 150);//每行可放置书本数fam
&nbsp; &nbsp;const rowCount = Math.ceil(rawData.length / counts);//总共有多少行
&nbsp; &nbsp;const rowArr = [];//书架二维数组

&nbsp; &nbsp;for (let i = 0; i &lt; rowCount; i++) {
&nbsp; &nbsp; &nbsp; &nbsp;const rowBooks = rawData.slice(i * counts, (i + 1) * counts);
&nbsp; &nbsp; &nbsp; &nbsp;//+++
&nbsp; &nbsp; &nbsp; &nbsp;if (i === rowCount - 1 &amp;&amp; rowBooks.length &lt; counts) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 当这一行实际的书本数 &lt; 每行能放置的书本数时 &nbsp; &nbsp; 向二维数组中添加占位元素
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;const placeholders = Array(counts - rowBooks.length).fill().map((_, index) =&gt; ({
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;id: `placeholder-${index}`,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;isPlaceholder: true
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;rowArr.push([...rowBooks, ...placeholders]);
&nbsp; &nbsp; &nbsp;} else {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;rowArr.push(rowBooks);
&nbsp; &nbsp; &nbsp;}
&nbsp;}
&nbsp; &nbsp;return rowArr;
}</pre>
</div>
<div>
<div>
<p>这样就正常了,大家可以把占位元素直接给隐藏( visibility: hidden;)即可</p>
<p><strong>2. 解决动态计算问题</strong></p>
<p>动态计算的时候其实也很简单,我们只需要获取到当前书架的宽度,然后监听<code>window</code>的<code>resize</code>事件,再去重新执行我们的构造数据的逻辑即可。</p>
<p>但是我有一个更好的方法,使用计算属性! 我们计算属性中依赖一下我们当前屏幕宽度的变量(shelfWidth),这样我们在改变屏幕的时候,直接更新shelfWidth即可,然后计算属性会自动执行,重新计算我们的数据。直接看最终代码。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&nbsp; &nbsp;&lt;div class="shelf" ref="shelfRef"&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&lt;div class="shlef-row" v-for="(row, rowIndex) in bookData" :key="rowIndex"&gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&lt;div class="book-item" v-for="book in row" :key="book.id"&gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{{ book.bookName }}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&lt;/div&gt;
&nbsp; &nbsp; &nbsp; &nbsp;&lt;/div&gt;
&nbsp; &nbsp;&lt;/div&gt;

&nbsp; &nbsp;&lt;button @click="changeWidtn"&gt;改变宽度&lt;/button&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';

const changeWidtn = () =&gt; {
&nbsp; &nbsp;shelfWidth.value = 900;
}

// 请求接口的数据
const apiData = [
&nbsp;{ id: 1, bookName: "语文课本1" },
&nbsp;{ id: 2, bookName: "语文课本2" },
&nbsp;{ id: 3, bookName: "数学课本1" },
&nbsp;{ id: 4, bookName: "数学课本2" },
&nbsp;{ id: 5, bookName: "数学课本3" },
&nbsp;{ id: 6, bookName: "数学课本4" },
&nbsp;{ id: 7, bookName: "化学课本1" },
&nbsp;{ id: 8, bookName: "化学课本2" },
&nbsp;{ id: 9, bookName: "化学课本1" },
&nbsp;{ id: 10, bookName: "化学课本2" },
&nbsp;{ id: 11, bookName: "物理课本1" },
&nbsp;{ id: 12, bookName: "物理课本2" },
&nbsp;{ id: 13, bookName: "物理课本3" },
&nbsp;{ id: 14, bookName: "物理课本4" },
&nbsp;{ id: 15, bookName: "生物课本1" },
]


/* 书架效果 */
const shelfRef = ref(null);//书架Ref
const shelfWidth = ref(1200);//书架宽度

// 构造页面数据
const bookData = computed(() =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; //页面渲染的数据
&nbsp; &nbsp;if (!shelfRef.value || !shelfWidth.value) {
&nbsp; &nbsp; &nbsp; &nbsp;return []
&nbsp;}

&nbsp; &nbsp;const counts = Math.floor(shelfWidth.value / 150);//每行可放置书本数
&nbsp; &nbsp;const rowCount = Math.ceil(apiData.length / counts);//总共有多少行
&nbsp; &nbsp;const rowArr = [];//书架二维数组

&nbsp; &nbsp;// 如果是最后一行且不满,添加占位元素,解决flex问题
&nbsp; &nbsp;for (let i = 0; i &lt; rowCount; i++) {
&nbsp; &nbsp; &nbsp; &nbsp;const rowBooks = apiData.slice(i * counts, (i + 1) * counts);

&nbsp; &nbsp; &nbsp; &nbsp;if (i === rowCount - 1 &amp;&amp; rowBooks.length &lt; counts) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;const placeholders = Array(counts - rowBooks.length).fill().map((_, index) =&gt; ({
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;id: `placeholder-${index}`,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;isPlaceholder: true,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;bookName: '占位元素'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;rowArr.push([...rowBooks, ...placeholders]);
&nbsp; &nbsp; &nbsp;} else {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;rowArr.push(rowBooks);
&nbsp; &nbsp; &nbsp;}

&nbsp;}

&nbsp; &nbsp;return rowArr;
})

// 更新屏幕宽度
const updateShelfWidth = () =&gt; {
&nbsp; &nbsp;shelfWidth.value = shelfRef.value.offsetWidth;
}

onMounted(() =&gt; {
&nbsp; &nbsp;updateShelfWidth();//页面加载后,更新下屏幕宽度
&nbsp; &nbsp;window.addEventListener('resize', updateShelfWidth);
})

onBeforeUnmount(() =&gt; {
&nbsp; &nbsp;window.removeEventListener('resize', updateShelfWidth);
})
&lt;/script&gt;

&lt;style&gt;
.shelf {
&nbsp; &nbsp;width: 1200px;
&nbsp; &nbsp;height: auto;
&nbsp; &nbsp;border: 1px solid #ccc;
&nbsp; &nbsp;margin: 0 auto;
&nbsp; &nbsp;min-width: 1000px;
}

.shlef-row {
&nbsp; &nbsp;width: 100%;
&nbsp; &nbsp;margin: 0 0 20px 0;
&nbsp; &nbsp;display: flex;
&nbsp; &nbsp;border-bottom: 2px solid orange;
}

.shlef-row:last-child {
&nbsp; &nbsp;margin-bottom: 0;
}

.shlef-row .book-item:last-child {
&nbsp; &nbsp;margin-right: 0;
}

.book-item {
&nbsp; &nbsp;flex: 1;
&nbsp; &nbsp;box-sizing: border-box;
&nbsp; &nbsp;padding: 10px;
&nbsp; &nbsp;margin-right: 20px;
&nbsp; &nbsp;width: 130px;
&nbsp; &nbsp;height: 160px;
&nbsp; &nbsp;color: #fff;
&nbsp; &nbsp;background-color: skyblue;
}
&lt;/style&gt;</pre>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19596342
頁: [1]
查看完整版本: 书架效果的实现