二級聯動下拉選單(如「省份 → 城市」、「分類 → 子分類」)是常見 UI 需求。早期常用 iframe 方案,現代開發應採用 AJAX + JSON。本文對比兩種實作,提供完整前後端程式碼與遷移指南。
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>聯動下拉 - iframe 版</title></head><body>
<form name="myfrm">
<select name="mlist" onchange="loadCities()">
<option value="">請選擇省份</option>
<option value="北京">北京</option>
<option value="通化">通化</option>
</select>
<select name="slist" id="slist" disabled><option>請先選省份</option></select>
</form>
<iframe id="frame" src="about:blank" style="display:none;"></iframe>
<script>
function loadCities() {
const province = document.myfrm.mlist.value;
const frame = document.getElementById('frame');
frame.src = 'list.php?city=' + encodeURIComponent(province);
}
function updateCities(optionsHtml) {
const slist = document.myfrm.slist;
slist.innerHTML = optionsHtml;
slist.disabled = false;
}
</script>
</body></html>
<?php
$data = [
'北京' => ['東城區','西城區','朝陽區','海淀區'],
'通化' => ['東昌區','二道江區','通化縣','輝南縣'],
];
$city = $_GET['city'] ?? '';
$cities = $data[$city] ?? [];
$options = '<option value="">請選擇城市</option>';
foreach ($cities as $c) {
$options .= "<option value=\"$c\">$c</option>";
}
// 回傳 JS 片段更新父頁面
echo "<script>parent.updateCities(" . json_encode($options) . ");</script>";
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>聯動下拉 - AJAX 版</title></head><body>
<div id="app">
<select v-model="province" @change="loadCities">
<option value="">請選擇省份</option>
<option v-for="p in provinces" :key="p" :value="p">{{ p }}</option>
</select>
<select v-model="city" :disabled="!cities.length">
<option value="">請選擇城市</option>
<option v-for="c in cities" :key="c" :value="c">{{ c }}</option>
</select>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, ref, onMounted } = Vue;
createApp({
setup() {
const province = ref('');
const city = ref('');
const cities = ref([]);
const provinces = ['北京', '通化']; // 或從 API 獲取
async function loadCities() {
if (!province.value) { cities.value = []; return; }
try {
const res = await fetch('api/cities.php?province=' + encodeURIComponent(province.value));
if (!res.ok) throw new Error('Network error');
cities.value = await res.json();
} catch (e) {
console.error(e); cities.value = [];
}
}
return { province, city, cities, provinces, loadCities };
}
}).mount('#app');
</script>
</body></html>
<?php
header('Content-Type: application/json; charset=utf-8');
$data = [
'北京' => ['東城區','西城區','朝陽區','海淀區'],
'通化' => ['東昌區','二道江區','通化縣','輝南縣'],
];
$province = $_GET['province'] ?? '';
$cities = $data[$province] ?? [];
echo json_encode($cities, JSON_UNESCAPED_UNICODE);
<?php
// api/cities.php (PDO 版)
require 'db.php'; // 返回 $pdo
$province = $_GET['province'] ?? '';
if (!$province) { echo '[]'; exit; }
$stmt = $pdo->prepare('SELECT name FROM cities WHERE province = ? ORDER BY sort');
$stmt->execute([$province]);
$cities = $stmt->fetchAll(PDO::FETCH_COLUMN);
header('Content-Type: application/json');
echo json_encode($cities, JSON_UNESCAPED_UNICODE);
| 框架 | 關鍵點 |
|---|---|
| Vue 3 | v-model 雙向綁定、@change 觸發 fetch、ref 響應式陣列 |
| React | useState 管理狀態、useEffect 監聽省份變化發請求、select onChange |
| Alpine.js | x-data, x-model, @change, fetch 輕量級 |
| 原生 JS | addEventListener('change'), fetch, innerHTML 更新 |
Map 快取已載入城市,避免重複請求debounce(300ms),下拉選單通常不需aria-busy 無障礙?province=北京&city=朝陽區,頁面載入時自動恢復狀態api/cities.php 回傳 JSONiframe、parent.updateCities 全域函數fetch / axios 非同步請求現代 AJAX 方案分離關注點、提升使用者體驗、便於測試與維護,是聯動下拉的標準實作方式。