You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 09:17:02