在Laravel中使用JavaScript Fetch实现多面板加载更多功能
实现Laravel多面板加载更多功能
问题背景
我在Laravel项目中,已经通过JavaScript Fetch API从接口获取分类数据,并将各分类列表渲染到对应面板中,但不知道如何给不同面板(如TopUps、Voucher)添加加载更多按钮并关联逻辑,需要实现分面板的加载更多功能。
现有代码回顾
JavaScript代码
<script type="text/javascript"> const filterProduct = document.getElementById("filterProducts"); const domain = window.location.host if (filterProduct) { // Fetch JSON data and generate HTML fetch(`http://api.${domain}/v2/categories`) .then((response) => response.json()) .then((data) => { const generateCTGameHTML = (games, targetElementId) => { const gameListHTML = games .map( (game) => ` <a class="group relative transform overflow-hidden rounded-2xl bg-murky-700 duration-300 ease-in-out hover:shadow-2xl hover:ring-2 hover:ring-primary-500 hover:ring-offset-2 hover:ring-offset-murky-900" href="order/${game?.slug}" style="outline: none;"> <img alt="${game?.title}" width="192" height="288" class="aspect-[4/6] object-cover object-center" src="${game?.image}" style="color: transparent;"> <article class="absolute inset-x-0 -bottom-10 z-10 flex transform flex-col px-3 transition-all duration-300 ease-in-out group-hover:bottom-3 sm:px-4 group-hover:sm:bottom-4"> <h2 class="truncate text-sm font-semibold text-murky-200 sm:text-base">${game?.title}</h2> <p class="truncate text-xxs text-murky-400 sm:text-xs">${game?.production}</p> </article> <div class="absolute inset-0 transform bg-gradient-to-t from-transparent transition-all duration-300 group-hover:from-murky-900"></div> </a> ` ) .join(""); const gameListElement = document.getElementById(targetElementId); gameListElement.innerHTML = gameListHTML; }; data.forEach(function(cat){ generateCTGameHTML(cat.lists, cat.panel); }); }) .catch((error) => console.log("Error:", error)); } // Drawer Menu function drawerMenu() { return { open: false, usedKeyboard: false, init() { this.$watch('open', value => { value && this.$refs.closeButton.focus() this.toggleOverlay() }) this.toggleOverlay() }, toggleOverlay() { document.body.classList[this.open ? 'add' : 'remove']('h-screen', 'overflow-hidden') } } } </script>
HTML结构
<div id="filterProducts"> @foreach ($typecategories as $item) <div class="hidden" id="{{ preg_replace('/\s+/', '', $item->name) }}panel" role="tabpanel" aria-labelledby="{{ preg_replace('/\s+/', '', $item->name) }}panel-tab"> <div id="{{ preg_replace('/\s+/', '', $item->name) }}" class="grid grid-cols-3 gap-4 sm:grid-cols-4 sm:gap-x-6 sm:gap-y-8 lg:grid-cols-5 xl:grid-cols-6"></div> </div> @endforeach </div>
API返回JSON数据
[ { "_id": "B0AACBFC143915E51DA0", "panel": "TopUps", "lists": [ { "_id": "4D9D082AA579BC083B91", "title": "Mobile Legend", "production": "Moonton", "slug": "mobile-legend", "image": "http:\/\/api.bjtopupku.test\/storage\/categories\/B2C0EEDC2F7F3F3FED99.webp" }, { "_id": "9DBEA3A115CFCBF0588F", "title": "Call Of Dutty", "production": "Garena", "slug": "call-of-dutty", "image": "http:\/\/api.bjtopupku.test\/storage\/categories\/DAD584AD18173A577DB8.webp" }, { "_id": "262C4C5EE1E3155FD336", "title": "Free Fire", "production": "Garena", "slug": "free-fire", "image": "http:\/\/api.bjtopupku.test\/storage\/categories\/2EA149D36A8F45505FD1.webp" } ] }, { "_id": "352426BF7F7B9A697148", "panel": "Voucher", "lists": [ { "_id": "9AAB33BCF8405239AAC9", "title": "TESTING KATEGORI", "production": "TESTING", "slug": "testing-kategori", "image": "http:\/\/api.bjtopupku.test\/storage\/categories\/729B1BF05BC9F54A2696.webp" }, { "_id": "AFCF4BCE0B7FE85B3578", "title": "asdsda", "production": "asdasdasd", "slug": "asdsda", "image": "http:\/\/api.bjtopupku.test\/storage\/categories\/642AF002249DEDF5DD91.webp" }, { "_id": "DD890C5739C94F809296", "title": "asddassda", "production": "sdaasdsda", "slug": "asddassda", "image": "http:\/\/api.bjtopupku.test\/storage\/categories\/C7F9F5CBD230FB7D27F2.webp" } ] } ]
实现步骤
1. 修改HTML结构,添加加载更多按钮
每个面板的列表容器下方新增加载更多按钮和加载状态容器,按钮ID与面板一一对应:
<div id="filterProducts"> @foreach ($typecategories as $item) @php $panelSlug = preg_replace('/\s+/', '', $item->name); @endphp <div class="hidden" id="{{ $panelSlug }}panel" role="tabpanel" aria-labelledby="{{ $panelSlug }}panel-tab"> <div id="{{ $panelSlug }}" class="grid grid-cols-3 gap-4 sm:grid-cols-4 sm:gap-x-6 sm:gap-y-8 lg:grid-cols-5 xl:grid-cols-6"></div> <!-- 加载更多区域 --> <div class="mt-6 flex justify-center" id="{{ $panelSlug }}-load-more-container"> <button id="{{ $panelSlug }}-load-more" class="px-6 py-2 rounded-lg bg-primary-500 text-white hover:bg-primary-600 transition-colors" data-panel="{{ $item->name }}" > 加载更多 </button> <div id="{{ $panelSlug }}-loading" class="hidden">加载中...</div> </div> </div> @endforeach </div>
2. 更新JavaScript逻辑,实现分面板加载更多
维护每个面板的独立分页状态,修改渲染函数支持内容追加,绑定加载更多事件:
<script type="text/javascript"> const filterProduct = document.getElementById("filterProducts"); const domain = window.location.host; // 存储每个面板的分页状态:当前页码、是否还有更多数据、是否正在加载 const panelPagination = {}; if (filterProduct) { // 初始化所有面板的分页状态 document.querySelectorAll('[data-panel]').forEach(btn => { const panelName = btn.dataset.panel; panelPagination[panelName] = { page: 1, hasMore: true, isLoading: false }; }); // 渲染商品列表,支持追加模式 const generateCTGameHTML = (games, targetElementId, append = false) => { const gameListHTML = games .map( (game) => ` <a class="group relative transform overflow-hidden rounded-2xl bg-murky-700 duration-300 ease-in-out hover:shadow-2xl hover:ring-2 hover:ring-primary-500 hover:ring-offset-2 hover:ring-offset-murky-900" href="order/${game?.slug}" style="outline: none;"> <img alt="${game?.title}" width="192" height="288" class="aspect-[4/6] object-cover object-center" src="${game?.image}" style="color: transparent;"> <article class="absolute inset-x-0 -bottom-10 z-10 flex transform flex-col px-3 transition-all duration-300 ease-in-out group-hover:bottom-3 sm:px-4 group-hover:sm:bottom-4"> <h2 class="truncate text-sm font-semibold text-murky-200 sm:text-base">${game?.title}</h2> <p class="truncate text-xxs text-murky-400 sm:text-xs">${game?.production}</p> </article> <div class="absolute inset-0 transform bg-gradient-to-t from-transparent transition-all duration-300 group-hover:from-murky-900"></div> </a> ` ) .join(""); const gameListElement = document.getElementById(targetElementId); if (append) { gameListElement.innerHTML += gameListHTML; } else { gameListElement.innerHTML = gameListHTML; } }; // 加载指定面板的更多数据 const loadMorePanelData = async (panelName) => { const panelState = panelPagination[panelName]; if (!panelState.hasMore || panelState.isLoading) return; panelState.isLoading = true; const panelSlug = panelName.replace(/\s+/g, ''); // 更新加载状态UI document.getElementById(`${panelSlug}-load-more`).classList.add('hidden'); document.getElementById(`${panelSlug}-loading`).classList.remove('hidden'); try { const response = await fetch(`http://api.${domain}/v2/categories?panel=${encodeURIComponent(panelName)}&page=${panelState.page}&limit=3`); const data = await response.json(); if (data.lists.length > 0) { generateCTGameHTML(data.lists, panelName, true); panelState.page += 1; panelState.hasMore = data.hasMore; } else { panelState.hasMore = false; } // 没有更多数据时隐藏加载更多容器 if (!panelState.hasMore) { document.getElementById(`${panelSlug}-load-more-container`).classList.add('hidden'); } } catch (error) { console.error(`加载${panelName}数据失败:`, error); } finally { panelState.isLoading = false; // 恢复按钮显示(如果还有更多数据) if (panelState.hasMore) { document.getElementById(`${panelSlug}-load-more`).classList.remove('hidden'); } document.getElementById(`${panelSlug}-loading`).classList.add('hidden'); } }; // 初始化加载第一页数据 const initPanelData = async () => { try { const response = await fetch(`http://api.${domain}/v2/categories?page=1&limit=3`); const data = await response.json(); data.forEach(cat => { generateCTGameHTML(cat.lists, cat.panel); // 更新面板的分页状态 panelPagination[cat.panel].hasMore = cat.hasMore ?? true; if (!panelPagination[cat.panel].hasMore) { const panelSlug = cat.panel.replace(/\s+/g, ''); document.getElementById(`${panelSlug}-load-more-container`).classList.add('hidden'); } }); } catch (error) { console.log("初始化数据加载错误:", error); } }; // 绑定所有加载更多按钮的点击事件 document.querySelectorAll('[data-panel]').forEach(btn => { btn.addEventListener('click', () => { const panelName = btn.dataset.panel; loadMorePanelData(panelName); }); }); // 执行初始化 initPanelData(); } // Drawer Menu function drawerMenu() { return { open: false, usedKeyboard: false, init() { this.$watch('open', value => { value && this.$refs.closeButton.focus() this.toggleOverlay() }) this.toggleOverlay() }, toggleOverlay() { document.body.classList[this.open ? 'add' : 'remove']('h-screen', 'overflow-hidden') } } } </script>
3. 后端API适配分页
修改Laravel的API接口,支持面板筛选、分页参数,并返回是否还有更多数据:
// 示例Laravel控制器方法 public function getCategories(Request $request) { $panel = $request->query('panel'); $page = $request->query('page', 1); $limit = $request->query('limit', 3); $query = Category::query(); if ($panel) { $query->where('panel', $panel); } $categories = $query->paginate($limit, ['*'], 'page', $page); // 返回适配前端的结构 return response()->json([ 'lists' => $categories->items(), 'hasMore' => $categories->hasMorePages(), ]); }
关键说明
- 每个面板独立维护分页状态,避免不同面板间的分页干扰
- 渲染函数新增
append参数,实现加载更多时追加内容而非覆盖原有列表 - 处理加载状态,防止重复发起请求
- 后端API必须支持分页参数和面板筛选,返回
hasMore字段标识是否还有更多数据
内容的提问来源于stack exchange,提问作者Baca Kode
相关产品推荐
相关产品推荐

