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

Java 實現二級聯動下拉選單:JSP + Servlet + AJAX 完整範例

     編輯:Java 相關知識

前言

二級聯動下拉選單是 Web 表單常見需求:選擇「省份」後動態載入對應的「城市」。本文提供基於 Java Web (JSP + Servlet + AJAX) 的完整實作,包含前端邏輯、後端介面、資料結構設計,並說明現代化改造方向。

整體架構

┌─────────────┐     HTTP GET      ┌─────────────┐
│  瀏覽器      │ ───────────────▶  │  Tomcat     │
│  (JSP/JS)   │  ?provinceId=1    │  (Servlet)  │
│             │ ◀───────────────  │             │
└─────────────┘     XML/JSON      └─────────────┘
       ▲                                  │
       │         城市列表                  │
       └──────────────────────────────────┘

後端實作

1. 實體類別


public class Region {
    private String id;
    private String name;
    private String parentId;
    
    // 建構子、Getter/Setter、toString()
}

2. 資料服務層(模擬資料庫)


@Component
public class RegionService {
    private final Map<String, List<Region>> childrenCache = new ConcurrentHashMap<>();
    private final Map<String, Region> allRegions = new ConcurrentHashMap<>();

    public RegionService() {
        initMockData();
    }

    private void initMockData() {
        // 省份
        addRegion("1", "北京市", "0");
        addRegion("2", "天津市", "0");
        addRegion("3", "河北省", "0");
        addRegion("4", "山西省", "0");
        
        // 城市
        addRegion("101", "北京市", "1");
        addRegion("102", "天津市", "2");
        addRegion("103", "石家莊市", "3");
        addRegion("104", "唐山市", "3");
        addRegion("105", "秦皇島市", "3");
        addRegion("106", "太原市", "4");
        addRegion("107", "大同市", "4");
    }

    private void addRegion(String id, String name, String parentId) {
        Region r = new Region(id, name, parentId);
        allRegions.put(id, r);
        childrenCache.computeIfAbsent(parentId, k -> new ArrayList<>()).add(r);
    }

    public List<Region> getProvinces() {
        return childrenCache.getOrDefault("0", Collections.emptyList());
    }

    public List<Region> getCitiesByProvince(String provinceId) {
        return childrenCache.getOrDefault(provinceId, Collections.emptyList());
    }
}

3. Servlet 控制器


@WebServlet("/api/regions")
public class RegionServlet extends HttpServlet {
    private final RegionService regionService = new RegionService();
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        String parentId = req.getParameter("parentId");
        if (parentId == null || parentId.isBlank()) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少 parentId 參數");
            return;
        }

        List<Region> regions;
        if ("0".equals(parentId)) {
            regions = regionService.getProvinces();
        } else {
            regions = regionService.getCitiesByProvince(parentId);
        }

        resp.setContentType("application/json;charset=UTF-8");
        resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        mapper.writeValue(resp.getWriter(), regions);
    }
}

前端實作

1. JSP 頁面


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>二級聯動下拉選單示例</title>
    <style>
        .form-group { margin-bottom: 1rem; }
        label { display: inline-block; width: 80px; font-weight: bold; }
        select { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; min-width: 200px; }
        .loading { color: #666; font-style: italic; }
        .error { color: red; }
    </style>
</head>
<body>
    <div class="container" style="max-width: 600px; margin: 2rem auto; padding: 1rem;">
        <h2>地址選擇 - 二級聯動示例</h2>
        
        <div class="form-group">
            <label>省份:</label>
            <select id="provinceSelect">
                <option value="">-- 請選擇省份 --</option>
            </select>
        </div>

        <div class="form-group">
            <label>城市:</label>
            <select id="citySelect" disabled>
                <option value="">-- 請先選擇省份 --</option>
            </select>
            <span id="cityStatus" class="loading"></span>
        </div>

        <div class="form-group">
            <label>結果:</label>
            <div id="result">尚未選擇</div>
        </div>
    </div>

    <script>
        // 配置
        const API_BASE = '/api/regions';
        
        // DOM 元素
        const provinceSelect = document.getElementById('provinceSelect');
        const citySelect = document.getElementById('citySelect');
        const cityStatus = document.getElementById('cityStatus');
        const resultDiv = document.getElementById('result');

        // 頁面載入:獲取省份列表
        document.addEventListener('DOMContentLoaded', loadProvinces);

        async function loadProvinces() {
            try {
                const response = await fetch(`${API_BASE}?parentId=0`);
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const provinces = await response.json();
                renderOptions(provinceSelect, provinces, '請選擇省份');
            } catch (e) {
                showError('載入省份失敗: ' + e.message);
            }
        }

        // 省份變更事件
        provinceSelect.addEventListener('change', async function() {
            const provinceId = this.value;
            citySelect.disabled = true;
            citySelect.innerHTML = '<option value="">-- 請選擇城市 --</option>';
            cityStatus.textContent = '';
            resultDiv.textContent = '尚未選擇';

            if (!provinceId) return;

            cityStatus.textContent = '載入中...';
            cityStatus.className = 'loading';

            try {
                const response = await fetch(`${API_BASE}?parentId=${encodeURIComponent(provinceId)}`);
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const cities = await response.json();
                
                if (cities.length === 0) {
                    cityStatus.textContent = '該省份暫無城市資料';
                    cityStatus.className = 'error';
                } else {
                    renderOptions(citySelect, cities, '請選擇城市');
                    citySelect.disabled = false;
                    cityStatus.textContent = '';
                }
            } catch (e) {
                cityStatus.textContent = '載入失敗: ' + e.message;
                cityStatus.className = 'error';
            }
        });

        // 城市變更事件
        citySelect.addEventListener('change', function() {
            const provinceText = provinceSelect.options[provinceSelect.selectedIndex].text;
            const cityText = this.options[this.selectedIndex].text;
            if (this.value) {
                resultDiv.textContent = `已選擇: ${provinceText} - ${cityText} (ID: ${this.value})`;
            } else {
                resultDiv.textContent = '尚未選擇城市';
            }
        });

        // 工具函數
        function renderOptions(selectEl, data, placeholder) {
            selectEl.innerHTML = ``;
            data.forEach(item => {
                const opt = document.createElement('option');
                opt.value = item.id;
                opt.textContent = item.name;
                selectEl.appendChild(opt);
            });
        }

        function showError(msg) {
            console.error(msg);
            alert(msg);
        }
    </script>
</body>
</html>

現代化改造方案

方案一:Spring Boot + REST + Vue/React


// Controller
@RestController
@RequestMapping("/api/regions")
public class RegionController {
    @GetMapping
    public List<Region> getRegions(@RequestParam String parentId) {
        return regionService.getChildren(parentId);
    }
}

// Vue 3 組件
<script setup>
import { ref, onMounted } from 'vue'

const provinces = ref([])
const cities = ref([])
const selectedProvince = ref('')
const selectedCity = ref('')

async function loadProvinces() {
  const res = await fetch('/api/regions?parentId=0')
  provinces.value = await res.json()
}

async function loadCities() {
  if (!selectedProvince.value) {
    cities.value = []
    return
  }
  const res = await fetch(`/api/regions?parentId=${selectedProvince.value}`)
  cities.value = await res.json()
}

onMounted(loadProvinces)
</script>

<template>
  <select v-model="selectedProvince" @change="loadCities">
    <option value="">請選擇省份</option>
    <option v-for="p in provinces" :key="p.id" :value="p.id">{{ p.name }}</option>
  </select>
  
  <select v-model="selectedCity" :disabled="!cities.length">
    <option value="">請選擇城市</option>
    <option v-for="c in cities" :key="c.id" :value="c.id">{{ c.name }}</option>
  </select>
</template>

方案二:前端靜態資料(適合較少變動的行政區劃)


// regions.json - 建構時打包
{
  "provinces": [
    {"id": "1", "name": "北京市", "cities": [
      {"id": "101", "name": "北京市"}
    ]},
    {"id": "3", "name": "河北省", "cities": [
      {"id": "103", "name": "石家莊市"},
      {"id": "104", "name": "唐山市"}
    ]}
  ]
}

// 純前端邏輯,零後端請求
const data = await import('./regions.json')
const provinces = data.provinces

function onProvinceChange(provinceId) {
  const province = provinces.find(p => p.id === provinceId)
  cities.value = province?.cities || []
}

資料庫設計建議


CREATE TABLE sys_region (
    id          VARCHAR(32) PRIMARY KEY COMMENT '區劃代碼',
    name        VARCHAR(100) NOT NULL COMMENT '名稱',
    parent_id   VARCHAR(32) NOT NULL COMMENT '父級ID,頂級為 0',
    level       TINYINT NOT NULL COMMENT '層級:1省 2市 3區/縣',
    sort_order  INT DEFAULT 0 COMMENT '排序',
    status      TINYINT DEFAULT 1 COMMENT '狀態:1啟用 0停用',
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at  DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_parent (parent_id),
    INDEX idx_level (level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行政區劃表';

進階功能擴展

功能實作要點
三級/多級聯動遞歸獲取子級,前端動態生成 Select
搜尋/過濾Select2、Choices.js 或自帶 filter 屬性
懶加載展開時才請求下一級,減少首屏請求
關鍵字聯想輸入時模糊匹配,下拉顯示建議
多選支援select multiple + 標籤顯示
資料快取Redis 快取熱門省份,本地 localStorage 快取
國際化i18n 資源檔,支援中英多語言

常見問題與解決

Q: 城市下拉顯示亂碼?
A: 確保 Servlet 設定 `resp.setCharacterEncoding("UTF-8")`,JSP 頁面 ``,資料庫連線 `characterEncoding=utf8`。
Q: 切換省份後城市殘留舊值?
A: 省份 change 事件中必須 `citySelect.innerHTML = ...` 重置,並 `disabled = true`。
Q: 表單提交時城市值為空?
A: 檢查 `name` 屬性是否正確,城市 select 未被 disabled(disabled 的表單欄位不會提交)。
Q: 大量資料載入緩慢?
A: 使用分頁/虛擬滾動、啟用 GZIP 壓縮、CDN 加速靜態資源、前端快取不變動資料。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved