最近在做后台管理系统时,表单里遇到下拉框要从数据库里查出来再展示的情况,通过学习和实践,将实现方法总结如下:
前端在页面加载时用 axios 请求后端接口,拿到数据后用原生 JS 动态拼接 <option>,并在修改页实现“加载 + 回显当前值”。文中的例子以我最近在练手的学生管理系统为场景,但方法与具体业务无关,换成任何“主表带外键下拉”的场景都适用。
一、场景拆解
下拉框动态加载其实只有两种典型场景:
| 场景 | 需要做的事 | 对应页面 |
|---|---|---|
| 新增页 | 只需加载下拉选项 | 添加表单 |
| 修改页 | 加载下拉选项 + 回显当前已选的值 | 编辑表单 |
新增页只需要关注下拉框加载数据即可,修改页在新增页的基础上多了一步“回显”。
-
后端:提供一个查询接口,返回统一格式的 JSON(包含状态码、数据、提示信息)。
-
前端加载:页面加载时(
window.onload)用 axios 请求该接口,遍历返回的列表,用document.createElement("option")动态生成选项并appendChild到<select>上。 -
修改页前端回显:把当前记录的已选值传到前端,等选项加载完成后,用
select.value = 已选值选中它。
二、后端:返回统一格式的数据
1. 统一返回对象 Result
前后端分离开发中,强烈建议后端所有接口都返回统一格式,而不是直接把 List 扔回去。一个最简单的统一返回对象长这样:
public class Result {
private Integer code; // 状态码:1 表示成功,0 表示失败
private Object data; // 真正的业务数据
private String msg; // 提示信息
public static Result success(Object data) {
Result r = new Result();
r.code = 1;
r.data = data;
r.msg = "操作成功";
return r;
}
public static Result error(String msg) {
Result r = new Result();
r.code = 0;
r.data = null;
r.msg = msg;
return r;
}
// 省略 getter / setter
}
2. Controller 接口
提供一个查询接口,把数据库里的列表查出来塞进 Result.success(...) 即可:
@GetMapping("/class/selectClass")
@ResponseBody
public Result selectClass() {
List<ClassInfo> list = classService.selectAll();
return Result.success(list);
}
返回的 JSON 大致如下:
{
"code": 1,
"data": [
{ "id": 1, "name": "一班" },
{ "id": 2, "name": "二班" }
],
"msg": "操作成功"
}
注意:返回的每个对象里至少要有两个属性,一个作为
option的value(通常是主键 id),一个作为option的显示文本(通常是名称),这是后面拼接选项的关键。
三、新增页:纯加载下拉选项
1. HTML 结构
先在页面里放一个空的 <select>,只留一个占位的提示项:
<div class="form-group">
<label for="classId">所属班级</label>
<select class="form-control" id="classId" name="classId">
<option value="-1">--请选择班级--</option>
</select>
</div>
2. JS 逻辑
引入 axios,在 window.onload 里调用加载函数:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
window.onload = function () {
loadClassList();
}
function loadClassList() {
axios.get("/class/selectClass").then(function (resp) {
let result = resp.data;
if (result.code === 1) {
let classList = result.data;
let select = document.getElementById("classId");
for (let i = 0; i < classList.length; i++) {
let option = document.createElement("option");
option.value = classList[i].id;
option.text = classList[i].name + "(ID:" + classList[i].id + ")";
select.appendChild(option);
}
} else {
alert(result.msg);
}
}).catch(function (error) {
console.log(error);
alert("请求失败" + error);
});
}
</script>
核心就是这一段循环:
for (let i = 0; i < classList.length; i++) {
let option = document.createElement("option"); // 1. 创建一个 option 节点
option.value = classList[i].id; // 2. 设置提交给后端的值(主键)
option.text = classList[i].name + "(ID:" + classList[i].id + ")"; // 3. 设置用户看到的文本
select.appendChild(option); // 4. 把节点挂到 select 上
}
-
option.value:表单提交时实际发送的值,一般用主键 id,这样后端能根据 id 关联外键。 -
option.text:用户在下拉框里看到的文字。 -
appendChild:把生成好的节点追加到<select>末尾,占位的“请选择”项依然保留在最上面。
四、修改页:加载和回显当前值
修改页比新增页多一个需求:下拉框不仅要列出所有选项,还要默认选中这条记录原本所属的班级。
1. 把当前已选值传到前端
修改页通常是由后端渲染(Thymeleaf)出来的,后端已经把这条学生记录查好传过来了。我们需要把“当前班级 id”先存到一个地方,供 JS 取用。
用一个隐藏域存id:
<input type="hidden" id="cid" th:value="${student.classId}">
为什么不直接在 JS 里写
[[${student.classId}]]?也可以,但通过隐藏域取值更接近纯 JS 写法,逻辑和数据分离,迁移到纯前后端分离项目时几乎不用改 JS。
其它字段(姓名、年龄)用 Thymeleaf 回显:
<input type="text" id="name" th:value="${student.name}">
<input type="text" id="age" th:value="${student.age}">
性别这种固定选项的下拉框,用 th:selected 回显:
<select id="gender" name="gender">
<option value="男" th:selected="${student.gender == '男'}">男</option>
<option value="女" th:selected="${student.gender == '女'}">女</option>
</select>
2. 加载函数接收当前 id
window.onload 时取出隐藏域里的班级 id,作为参数传给 loadClassList:
window.onload = function () {
let cid = document.getElementById("cid").value;
loadClassList(cid);
}
3. 在回调里设置 select.value
loadClassList 的加载逻辑和新增页一模一样,只多了一行,在所有 option 都 append 完之后,设置 select.value = id:
function loadClassList(id) {
axios.get("/class/selectClass").then(function (resp) {
let result = resp.data;
if (result.code === 1) {
let classList = result.data;
let select = document.getElementById("classId");
for (let i = 0; i < classList.length; i++) {
let option = document.createElement("option");
option.value = classList[i].id;
option.text = classList[i].name + "(ID:" + classList[i].id + ")";
select.appendChild(option);
}
// 所有班级加载完后再选中当前学生的班级
select.value = id;
} else {
alert(result.msg);
}
}).catch(function (error) {
console.log(error);
alert("请求失败" + error);
});
}
五、踩坑总结
这也是我在实际操作中碰到的问题,我当时写成了这样:
function loadClassList(id) {
axios.get("/class/selectClass").then(function (resp) {
// ... append options ...
});
select.value = id;
}
下拉框正常显示,但一直只显示第一个选项。
原因:axios 请求是异步的。代码执行到 select.value = id 时,请求可能还没返回,<select> 里还只有占位项,根本没有 value 等于 id 的 option,应该把 select.value = id 放进 then 回调里,且必须放在循环 append 之后。
六、完整代码
新增页:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加信息</title>
</head>
<body>
<form>
<select id="classId" name="classId">
<option value="-1">--请选择--</option>
</select>
<button type="button" onclick="add()">添加</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
window.onload = function () {
loadClassList();
}
function loadClassList() {
axios.get("/class/selectClass").then(function (resp) {
let result = resp.data;
if (result.code === 1) {
let classList = result.data;
let select = document.getElementById("classId");
for (let i = 0; i < classList.length; i++) {
let option = document.createElement("option");
option.value = classList[i].id;
option.text = classList[i].name + "(ID:" + classList[i].id + ")";
select.appendChild(option);
}
} else {
alert(result.msg);
}
}).catch(function (error) {
console.log(error);
alert("请求失败" + error);
});
}
function add() {
let classId = document.getElementById("classId").value;
// ...组装数据、POST 提交...
}
</script>
</body>
</html>
修改页:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>修改信息</title>
</head>
<body>
<form>
<!-- 用隐藏域把当前已选值传给 JS -->
<input type="hidden" id="cid" th:value="${student.classId}">
<select id="classId" name="classId"></select>
<button type="button" th:onclick="|update(${student.id})|">修改</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
window.onload = function () {
let cid = document.getElementById("cid").value;
loadClassList(cid);
}
function loadClassList(id) {
axios.get("/class/selectClass").then(function (resp) {
let result = resp.data;
if (result.code === 1) {
let classList = result.data;
let select = document.getElementById("classId");
for (let i = 0; i < classList.length; i++) {
let option = document.createElement("option");
option.value = classList[i].id;
option.text = classList[i].name + "(ID:" + classList[i].id + ")";
select.appendChild(option);
}
// ★ 所有选项加载完后,再选中当前值(必须在回调内)
select.value = id;
} else {
alert(result.msg);
}
}).catch(function (error) {
console.log(error);
alert("请求失败" + error);
});
}
function update(id) {
let classId = document.getElementById("classId").value;
// ...组装数据、PUT 提交...
}
</script>
</body>
</html>
暂无评论内容