在后台管理系统的开发中,下拉多选框几乎是每个表单的标配功能。但传统的select组件往往功能单一,尤其是在处理大量动态数据时显得力不从心。这就是为什么我们需要xm-select.js这样的插件来拯救开发效率。
xm-select.js是Layui生态中一个轻量级但功能强大的下拉多选框插件。我曾在多个项目中用它替换了传统的select2,主要原因有三:首先,它完美融入Layui的视觉风格,不需要额外调整CSS;其次,它的API设计非常人性化,学习曲线平缓;最重要的是,它对异步数据加载的支持堪称完美,这在处理动态分类、权限分配等场景时特别有用。
举个例子,最近我在做一个电商后台的商品分类管理模块。分类数据量很大(超过1000条),而且需要支持多级联动。用传统方式实现这个功能,代码量至少要多出3倍,而用xm-select.js配合异步加载,不到50行代码就搞定了全部交互逻辑。
先来看最基本的HTML结构。与常规select不同,xm-select.js需要特定的容器结构:
html复制<div class="layui-form-item">
<label class="layui-form-label">商品分类</label>
<div class="layui-input-block">
<div id="category-select" class="xm-select-demo" style="width:300px;">
<input type="hidden" name="category_ids" id="category_ids">
</div>
</div>
</div>
这里有几个关键点需要注意:
最简单的静态数据渲染方式如下:
javascript复制xmSelect.render({
el: '#category-select',
data: [
{name: '手机数码', value: 1},
{name: '电脑办公', value: 2},
{name: '家用电器', value: 3}
]
});
在实际项目中,我建议至少配置以下参数:
theme.color:修改主题色与项目风格统一toolbar.show:显示顶部工具栏filterable:开启搜索过滤功能paging:数据量大时启用分页动态加载数据是xm-select.js的核心优势。下面是一个完整的异步加载示例:
javascript复制xmSelect.render({
el: '#category-select',
filterable: true,
paging: true,
pageSize: 10,
onSearch: function(keyword){
// 搜索时重新加载数据
loadCategoryData(keyword);
},
on: function(data){
// 选中变化时更新隐藏域
updateHiddenField(data.arr);
}
});
function loadCategoryData(keyword = ''){
$.ajax({
url: '/api/categories',
data: {keyword: keyword},
success: function(res){
const options = res.data.map(item => ({
name: item.name,
value: item.id,
disabled: item.status === 0 // 禁用不可用选项
}));
// 更新选项数据
xmSelect.update('#category-select', {
data: options,
autoRow: true // 自动换行显示
});
}
});
}
function updateHiddenField(selectedItems){
const ids = selectedItems.map(item => item.value);
$('#category_ids').val(ids.join(','));
}
// 初始加载
loadCategoryData();
这个实现有几个实用技巧:
当处理大量数据时(比如超过1000条),我总结了几个优化经验:
优化后的搜索函数示例:
javascript复制let cachedData = [];
let searchTimer = null;
function loadCategoryData(keyword = ''){
clearTimeout(searchTimer);
// 本地缓存优先
if(keyword === '' && cachedData.length > 0){
renderOptions(cachedData);
return;
}
searchTimer = setTimeout(() => {
$.ajax({
url: '/api/categories',
data: {keyword: keyword},
success: function(res){
if(keyword === ''){
cachedData = res.data;
}
renderOptions(res.data);
}
});
}, 300); // 300ms防抖
}
要让xm-select.js与Layui表单完美配合,需要注意以下几点:
javascript复制// 表单验证示例
form.verify({
category: function(value){
const ids = $('#category_ids').val();
if(!ids){
return '请至少选择一个分类';
}
}
});
// 表单重置处理
form.on('reset(filter-form)', function(){
xmSelect.change('#category-select', []);
});
// 禁用控制
function setSelectDisabled(disabled){
xmSelect.update('#category-select', {disabled});
}
有时我们需要处理更复杂的数据结构,比如带层级关系的分类。这时可以这样处理:
javascript复制// 后端返回的层级数据
[
{
id: 1,
name: "电子产品",
children: [
{id: 10, name: "手机"},
{id: 11, name: "电脑"}
]
}
]
// 前端处理
function processTreeData(items, parentName = ''){
return items.reduce((result, item) => {
const fullName = parentName ? `${parentName} / ${item.name}` : item.name;
result.push({
name: fullName,
value: item.id
});
if(item.children){
result.push(...processTreeData(item.children, fullName));
}
return result;
}, []);
}
这样处理后,选项会显示为"电子产品 / 手机"这样的完整路径,方便用户选择。
在实际项目中,经常需要在用户操作后更新选项列表。比如新增分类后立即出现在下拉框中:
javascript复制function addNewCategory(category){
// 更新缓存
cachedData.unshift(category);
// 更新下拉框
xmSelect.update('#category-select', {
data: processTreeData(cachedData),
autoRow: true
});
// 自动选中新添加项
xmSelect.change('#category-select', [category.id]);
}
编辑场景下需要设置默认选中项,有两种方式:
javascript复制xmSelect.render({
// ...其他配置
initValue: [1, 2, 3] // 默认选中value为1,2,3的项
});
javascript复制// 通过change方法设置
xmSelect.change('#category-select', [1, 2, 3]);
// 或者通过update方法
xmSelect.update('#category-select', {
initValue: [1, 2, 3]
});
如果默认样式不符合需求,可以通过CSS定制:
css复制/* 修改下拉框高度 */
.xm-select-body .xm-select-list {
max-height: 400px !important;
}
/* 修改选中项样式 */
.xm-select-body .xm-select-selected li {
background-color: #f2f2f2;
border-radius: 3px;
}
/* 修改搜索框样式 */
.xm-select-body .xm-select-search input {
border: 1px solid #eee;
}
注意要使用!important覆盖默认样式,因为xm-select.js的样式优先级较高。
实现省市区三级联动的典型方案:
javascript复制let provinceSelect = xmSelect.render({
el: '#province-select',
on: function(data){
if(data.arr.length > 0){
loadCities(data.arr[0].value);
}
}
});
let citySelect = xmSelect.render({
el: '#city-select',
disabled: true,
on: function(data){
if(data.arr.length > 0){
loadDistricts(data.arr[0].value);
}
}
});
function loadCities(provinceId){
$.get('/api/cities', {provinceId}, function(res){
xmSelect.update('#city-select', {
data: res.data,
disabled: false
});
xmSelect.change('#district-select', []);
});
}
在表格中使用xm-select.js实现行内编辑:
javascript复制function initTableSelect(rowId, initialValue){
const el = `#select-${rowId}`;
xmSelect.render({
el,
initValue: initialValue,
on: function(data){
saveRowChange(rowId, data.arr);
}
});
}
table.on('edit(table-filter)', function(obj){
if(obj.field === 'category'){
initTableSelect(obj.data.id, obj.value.split(','));
}
});
经过多个项目的实战,我总结了以下最佳实践:
一个封装良好的示例:
javascript复制class SmartSelect {
constructor(el, options){
this.el = el;
this.config = {
theme: {color: '#1E9FFF'},
toolbar: {show: true},
filterable: true,
...options
};
this.init();
}
init(){
this.instance = xmSelect.render({
el: this.el,
...this.config
});
}
loadData(loader){
loader().then(data => {
this.instance.update({data});
}).catch(err => {
console.error('加载选项失败', err);
});
}
destroy(){
this.instance = null;
$(this.el).html('');
}
}
// 使用示例
const categorySelect = new SmartSelect('#category-select', {
paging: true,
pageSize: 20
});
categorySelect.loadData(() => {
return $.get('/api/categories').then(res => res.data);
});
这种封装方式使代码更易维护,也方便统一处理错误和加载状态。