首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP MySQL 三級聯動選單:AJAX 動態載入省市區/分類層級完整實作

     編輯:PHP 相關知識

三級聯動架構設計

三級聯動(如省市區、分類子分類)核心邏輯:第一級變更 → AJAX 請求第二級資料 → 更新第二級選單 → 清空第三級 → 第二級變更 → AJAX 請求第三級資料 → 更新第三級選單。本文提供資料庫設計、後端 API、前端 JS(原生 fetch + 防抖)、完整 HTML 範例。

資料庫設計:無限層級分類表

CREATE TABLE `categories` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `parent_id` INT UNSIGNED NOT NULL DEFAULT 0, -- 0 為頂層
  `name` VARCHAR(64) NOT NULL,
  `sort` INT UNSIGNED NOT NULL DEFAULT 0,
  `status` TINYINT UNSIGNED NOT NULL DEFAULT 1,
  PRIMARY KEY (`id`),
  KEY `idx_parent_status` (`parent_id`, `status`),
  KEY `idx_sort` (`sort`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 範例資料
INSERT INTO `categories` (`id`, `parent_id`, `name`, `sort`) VALUES
(1, 0, '電子產品', 1),
(2, 0, '服飾鞋包', 2),
(3, 0, '家居生活', 3),
(11, 1, '手機通訊', 1),
(12, 1, '電腦週邊', 2),
(13, 1, '影音娛樂', 3),
(111, 11, '智慧型手機', 1),
(112, 11, '功能型手機', 2),
(113, 11, '手機配件', 3),
(121, 12, '筆記型電腦', 1),
(122, 12, '桌上型電腦', 2),
(123, 12, '滑鼠鍵盤', 3);

後端 API:api/categories.php

<?php
// api/categories.php
declare(strict_types=1);

require_once __DIR__ . '/../config/bootstrap.php';

header('Content-Type: application/json; charset=utf-8');

$parentId = isset($_GET['parent_id']) ? max(0, (int)$_GET['parent_id']) : 0;

$stmt = $pdo->prepare("SELECT id, name FROM categories WHERE parent_id = ? AND status = 1 ORDER BY sort, id");
$stmt->execute([$parentId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// 統一格式:[{id, name}, ...]
echo json_encode(['success' => true, 'data' => $rows], JSON_UNESCAPED_UNICODE);

前端完整頁面:cascade.html

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>三級聯動選單示範</title>
  <style>
    .cascade-group { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end; margin-bottom: 1rem; }
    .cascade-group select { min-width: 180px; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; background: white; }
    .cascade-group select:disabled { background: #f5f5f5; color: #999; }
    .result { padding: 1rem; background: #f8f9fa; border-radius: 4px; font-family: monospace; }
  </style>
</head>
<body>
  <div class="container" style="max-width: 800px; margin: 2rem auto;">
    <h2>三級分類聯動選單</h2>
    <div class="cascade-group">
      <div>
        <label>一級分類</label>
        <select id="level1"><option value="">請選擇</option></select>
      </div>
      <div>
        <label>二級分類</label>
        <select id="level2" disabled><option value="">請先選擇一級</option></select>
      </div>
      <div>
        <label>三級分類</label>
        <select id="level3" disabled><option value="">請先選擇二級</option></select>
      </div>
    </div>
    <div class="result" id="result">尚未選擇完整分類</div>
  </div>

  <script>
  (() => {
    const API = 'api/categories.php';
    const selects = {
      1: document.getElementById('level1'),
      2: document.getElementById('level2'),
      3: document.getElementById('level3')
    };
    const resultEl = document.getElementById('result');

    // 通用載入函數
    async function loadOptions(level, parentId) {
      const select = selects[level];
      if (!select) return;

      // 顯示載入中
      select.innerHTML = '';
      select.disabled = true;

      try {
        const resp = await fetch(`${API}?parent_id=${parentId}`);
        const data = await resp.json();

        if (!data.success) throw new Error('API 錯誤');

        // 清空並填入
        select.innerHTML = '';
        data.data.forEach(item => {
          const opt = document.createElement('option');
          opt.value = item.id;
          opt.textContent = item.name;
          select.appendChild(opt);
        });

        // 啟用(若無資料則保持 disabled)
        select.disabled = data.data.length === 0;

        // 清空下一級
        if (level < 3) {
          resetLevel(level + 1);
        }
      } catch (err) {
        console.error(err);
        select.innerHTML = '';
        select.disabled = true;
      }
    }

    function resetLevel(level) {
      for (let i = level; i <= 3; i++) {
        const sel = selects[i];
        if (sel) {
          sel.innerHTML = i === level ? '' : ``;
          sel.disabled = true;
        }
      }
    }

    function updateResult() {
      const vals = [1,2,3].map(l => selects[l].value).filter(v => v);
      const texts = [1,2,3].map(l => {
        const sel = selects[l];
        return sel.value ? sel.options[sel.selectedIndex].text : null;
      }).filter(t => t);
      if (texts.length === 3) {
        resultEl.textContent = '已選擇:' + texts.join(' > ') + ' (IDs: ' + vals.join(',') + ')';
        resultEl.style.background = '#d1e7dd';
      } else {
        resultEl.textContent = '尚未選擇完整分類(目前:' + (texts.join(' > ') || '無') + ')';
        resultEl.style.background = '#f8f9fa';
      }
    }

    // 事件綁定
    selects[1].addEventListener('change', () => {
      const val = selects[1].value;
      if (val) loadOptions(2, val);
      else resetLevel(2);
      updateResult();
    });

    selects[2].addEventListener('change', () => {
      const val = selects[2].value;
      if (val) loadOptions(3, val);
      else resetLevel(3);
      updateResult();
    });

    selects[3].addEventListener('change', updateResult);

    // 初始化:載入第一級
    loadOptions(1, 0);
  })();
  </script>
</body>
</html>

進階優化:單次載入全樹(資料量小適用)

若總分類數 < 2000,可一次載入全樹,前端記憶體過濾,零 AJAX 延遲。

<?php
// api/categories_tree.php
$stmt = $pdo->query("SELECT id, parent_id, name FROM categories WHERE status=1 ORDER BY parent_id, sort");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// 建構樹狀結構
$tree = [];
$map = [];
foreach ($rows as $row) {
    $map[$row['id']] = ['id' => $row['id'], 'name' => $row['name'], 'children' => []];
}
foreach ($map as $id => &$node) {
    if ($node['id'] === 0) continue; // 無 0
    $pid = $rows[array_search($id, array_column($rows, 'id'))]['parent_id'] ?? 0;
    if ($pid === 0) {
        $tree[] = &$node;
    } elseif (isset($map[$pid])) {
        $map[$pid]['children'][] = &$node;
    }
}
echo json_encode(['success' => true, 'data' => $tree], JSON_UNESCAPED_UNICODE);

前端單次載入版 JS 片段

let allTree = [];

async function initTree() {
  const resp = await fetch('api/categories_tree.php');
  const data = await resp.json();
  allTree = data.data;
  renderLevel(1, allTree);
}

function renderLevel(level, nodes) {
  const select = document.getElementById('level' + level);
  select.innerHTML = '';
  nodes.forEach(node => {
    const opt = document.createElement('option');
    opt.value = node.id;
    opt.textContent = node.name;
    opt.dataset.children = JSON.stringify(node.children || []);
    select.appendChild(opt);
  });
  select.disabled = nodes.length === 0;
}

document.getElementById('level1').addEventListener('change', function() {
  const opt = this.options[this.selectedIndex];
  const children = opt.dataset.children ? JSON.parse(opt.dataset.children) : [];
  renderLevel(2, children);
  resetLevel(3);
});

document.getElementById('level2').addEventListener('change', function() {
  const opt = this.options[this.selectedIndex];
  const children = opt.dataset.children ? JSON.parse(opt.dataset.children) : [];
  renderLevel(3, children);
});

安全與效能清單

項目建議
SQL 注入預處理語句 (PDO::prepare)
XSS前端 textContent 而非 innerHTML,後端 json_encode
速率限制API 加入 IP 限制(如每秒 10 次)
快取第一級資料極少變動,可設定 HTTP 快取頭 Cache-Control: max-age=3600
索引parent_id + status 複合索引
無障礙aria-label, aria-describedby, 鍵盤可操作
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved