弹窗双列表交互功能实现文档(基于 Vue2 + Element UI)
核心功能:弹窗基础交互、双列表勾选/删除联动、加工件扫码检索、搜索分页、领取数量校验、一键提交、主列表刷新,无多余第三方依赖,仅需适配项目接口即可复用。
本文档详细阐述了基于 Vue2、Element UI 实现的功能方案。方案支持弹窗交互、双列表勾选联动、扫码快速检索、数量校验、分页查询及接口提交,交互流畅、适配性强,可直接集成到项目中使用。
核心功能:弹窗基础交互、双列表勾选/删除联动、加工件扫码检索、搜索分页、领取数量校验、一键提交、主列表刷新,适配项目现有技术栈,无多余第三方依赖,仅需适配接口即可复用。
二、技术栈与前置要求
- Vue2:构建前端组件,管理页面数据与交互逻辑;
- Element UI:提供弹窗、表格、表单、分页等基础UI组件,实现页面布局与交互;
2.1 技术栈
-
Vue2:构建前端组件,管理页面数据与交互逻辑;
-
Element UI:提供弹窗、表格、表单、分页等基础 UI 组件,实现页面布局与交互;
二、技术栈与前置要求
2.1 技术栈
-
Vue2:构建前端组件,管理页面数据与交互逻辑,实现组件复用;
-
Element UI:提供弹窗、表格、表单、分页等基础 UI 组件,实现页面布局与核心交互;
-
Avue:简化表单渲染,用于弹窗顶部领取基础信息展示(禁用编辑,仅作展示);
-
原生 JavaScript:处理列表联动、扫码交互、数据校验、接口请求回调,配合 Vue 生命周期实现功能闭环。
2.2 前置要求
-
项目已集成 Vue2、Element UI、Avue 组件库,且全局注册完毕;
-
项目已封装$http 网络请求方法(基于 axios),支持 post 请求及响应拦截;
-
已引入自定义扫码组件 PadScanInput,支持 v-model 绑定及@input 事件触发;
-
后端提供对应接口(库存查询、加工件列表查询、领料提交),返回格式统一(code、result、message);
-
浏览器支持:兼容 Chrome、Firefox、Edge 等现代浏览器,适配 PC 端及 PAD 端操作。
三、完整实现代码
以下代码为完整可运行版本,复制保存为 dialog.vue 文件,引入项目即可使用,仅需修改接口地址及字段映射即可适配实际需求。
<template>
<el-dialog
:visible.sync="scanDialog.visible"
width="1200px"
:close-on-click-modal="false"
:append-to-body="true"
>
<!– 弹窗标题区:展示领取基础信息 –>
<div slot="title">
<div class="receive-form">
<div class="receive-form-title">领取数量</div>
<avue-form :option="receiveOption" v-model="receiveForm"></avue-form>
</div>
</div>
<!– 弹窗内容区:搜索表单 + 双列表 –>
<div class="dialog-content">
<!– 搜索表单 –>
<div class="search-form">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="工单编号">
<el-input
v-model="searchForm.workOrderNo"
placeholder="请输入工单编号"
></el-input>
</el-form-item>
<el-form-item label="加工编码">
<PadScanInput
v-model="searchForm.code"
placeholder="请输入加工编码"
@input="handleScan"
></PadScanInput>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<!– 双列表布局:左侧可选列表 + 右侧已选列表 –>
<div class="list-container">
<!– 左侧可选列表(带分页) –>
<div class="left-list">
<el-table
:data="leftTableData"
border
height="400"
@select="handleSelection"
@select-all="handleSelectionAll"
ref="leftTable"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column
prop="code"
label="加工编码"
width="200"
></el-table-column>
<el-table-column
prop="stockName"
label="仓库"
min-width="100"
show-overflow-tooltip
></el-table-column>
<el-table-column
prop="rackName"
label="货位"
min-width="100"
show-overflow-tooltip
></el-table-column>
</el-table>
<!– 分页组件 –>
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 30, 50]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next"
:total="pagination.total"
></el-pagination>
</div>
</div>
<!– 右侧已选列表 –>
<div class="right-list">
<el-table :data="rightTableData" border height="400">
<el-table-column
prop="code"
label="加工编码"
width="200"
></el-table-column>
<el-table-column
prop="stockName"
label="仓库"
min-width="100"
show-overflow-tooltip
></el-table-column>
<el-table-column
prop="rackName"
label="货位"
min-width="100"
show-overflow-tooltip
></el-table-column>
<el-table-column label="操作" width="60">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!– 弹窗底部操作按钮 –>
<div class="dialog-footer" slot="footer">
<el-button @click="handleCancel">取 消</el-button>
<el-button @click="handleConfirmDialog" type="primary">确 定</el-button>
</div>
</el-dialog>
</template>
<script>
export default {
name: "Dialog",
data() {
return {
// 基础查询参数
query: {
label: "领取部件",
},
// 提交接口核心数据
submitData: {
workOrderPartsId: "",
workProductCodeRecordIds: [],
},
// Avue表单配置(展示领取基础信息,禁用编辑)
receiveOption: {
labelWidth: 100,
column: [
{
label: "商品编码",
prop: "materialNo",
span: 8,
},
{
label: "商品名称",
prop: "productName",
span: 8,
},
{
label: "需要领取数量",
prop: "waitReceiveNum",
span: 8,
},
],
disabled: true, // 禁用表单编辑
menuBtn: false, // 隐藏Avue自带操作按钮
},
// 领取基础信息表单数据(从主列表带入)
receiveForm: {
waitReceiveNum: 0,
materialNo: "",
productName: "",
partId: "",
},
workOrderNo: "",
// 主列表数据(弹窗外部的部件列表)
tableData: [],
tableHead: [
{ label: "商品名称", prop: "productName" },
{ label: "商品编码", prop: "materialNo" },
{ label: "需要领取数量", prop: "waitReceiveNum" },
{ label: "库存数量", prop: "stockNum" },
],
// 弹窗显示控制
scanDialog: {
visible: false,
batchDataList: [], // 批量数据列表(备用)
},
// 搜索表单数据
searchForm: {
workOrderNo: "",
code: "",
},
// 左侧可选列表数据
leftTableData: [],
// 右侧已选列表数据
rightTableData: [],
// 分页参数
pagination: {
currentPage: 1,
pageSize: 20,
total: 0,
},
};
},
methods: {
/**
* 打开领取弹窗
* @param {Object} row – 主列表行数据(包含领取基础信息)
*/
openScanDialog(row) {
// 重置已选/可选列表
this.rightTableData = [];
this.leftTableData = [];
// 赋值领取基础信息
this.receiveForm = row;
// 获取左侧可选列表数据
this.getLeftTableData();
// 显示弹窗
this.scanDialog.visible = true;
},
/**
* 扫码/输入加工件编码触发的处理逻辑
* @param {String} value – 扫码/输入的编码
*/
async handleScan(value) {
if (!value) return;
// 赋值到搜索表单
this.searchForm.code = value;
// 获取左侧列表数据
await this.getLeftTableData();
// 找到扫描到的项并自动选中、添加到右侧已选列表
const scannedItem = this.leftTableData.find(
(item) => item.code === value
);
if (scannedItem) {
// 避免重复添加
const exists = this.rightTableData.some(
(item) => item.id === scannedItem.id
);
if (!exists) {
this.rightTableData.push(scannedItem);
}
// 延迟更新选中状态(确保DOM渲染完成)
this.$nextTick(() => {
this.$refs.leftTable.toggleRowSelection(scannedItem, true);
});
}
},
/**
* 获取主列表数据(弹窗外部的部件列表)
*/
getTableData() {
this.$http({
url: `XXXXX`,
method: "post",
data: {
workOrderNo: this.workOrderNo,
},
}).then((res) => {
this.tableData = res.result.map((item) => {
return {
…item,
productType: item.productType,
};
});
// 初始化批量数据列表(备用)
this.scanDialog.batchDataList = res.result.map((item) => {
return {
partId: item.partId,
batchDetailList: [],
};
});
});
},
/**
* 点击查询按钮触发的搜索逻辑
*/
handleSearch() {
// 重置页码为第一页
this.pagination.currentPage = 1;
// 重新获取左侧列表数据
this.getLeftTableData();
},
/**
* 重置搜索表单
*/
resetSearch() {
// 清空搜索条件
this.searchForm = {
workOrderNo: "",
code: "",
};
// 重新查询
this.handleSearch();
},
/**
* 获取左侧可选列表数据(核心接口)
* @returns {Promise} – 接口请求Promise
*/
getLeftTableData() {
return this.$http({
url: "XXXXX",
method: "post",
data: {
pageNum: this.pagination.currentPage,
pageSize: this.pagination.pageSize,
workOrderNo: this.searchForm.workOrderNo,
code: this.searchForm.code,
materialNo: this.receiveForm.materialNo, // 关联当前领取商品编码
},
}).then((res) => {
this.leftTableData = res.result.list || [];
this.pagination.total = res.result.total || 0;
// 回显已选状态(分页切换后保持选中)
this.$nextTick(() => {
this.leftTableData.forEach((item) => {
this.rightTableData.forEach((rightItem) => {
if (item.id === rightItem.id) {
this.$refs.leftTable.toggleRowSelection(item, true);
}
});
});
});
});
},
/**
* 单行选择事件(左侧列表勾选/取消)
* @param {Array} selection – 选中列表
* @param {Object} row – 当前行数据
*/
handleSelection(selection, row) {
const findIndex = this.rightTableData.findIndex(
(item) => item.id === row.id
);
if (findIndex === -1) {
// 未选中,添加到已选列表
this.rightTableData.push(row);
} else {
// 已选中,从已选列表移除
this.rightTableData.splice(findIndex, 1);
}
},
/**
* 全选/取消全选事件
* @param {Array} selection – 选中列表
*/
handleSelectionAll(selection) {
if (selection.length === 0) {
// 取消全选:清空已选列表
this.leftTableData.forEach((row) => {
const findIndex = this.rightTableData.findIndex(
(item) => item.id === row.id
);
if (findIndex >= 0) {
this.rightTableData.splice(findIndex, 1);
}
});
} else {
// 全选:批量添加到已选列表(去重)
selection.forEach((row) => {
const findIndex = this.rightTableData.findIndex(
(item) => item.id === row.id
);
if (findIndex === -1) {
this.rightTableData.push(row);
}
});
}
},
/**
* 删除已选行(右侧列表)
* @param {Object} row – 已选行数据
*/
handleDelete(row) {
const index = this.rightTableData.findIndex((item) => item.id === row.id);
if (index === -1) return;
// 取消左侧对应行的勾选
this.leftTableData.forEach((item) => {
if (item.id === row.id) {
this.$refs.leftTable.toggleRowSelection(item, false);
}
});
// 从已选列表移除
this.rightTableData.splice(index, 1);
},
/**
* 分页条数变化
* @param {Number} val – 每页条数
*/
handleSizeChange(val) {
this.pagination.pageSize = val;
this.getLeftTableData();
},
/**
* 页码变化
* @param {Number} val – 当前页码
*/
handleCurrentChange(val) {
this.pagination.currentPage = val;
this.getLeftTableData();
},
/**
* 取消弹窗
*/
handleCancel() {
this.scanDialog.visible = false;
},
/**
* 确定领取提交
*/
handleConfirmDialog() {
try {
this.$loading();
// 提交领料接口
this.$http({
url: "XXXXXXXX",
method: "post",
data: {
workOrderPartsId: this.receiveForm.partId,
workProductCodeRecordIds: this.rightTableData.map(
(item) => item.id
),
},
}).then((res) => {
if (res.code === "Z000") {
this.$message.success("领取成功");
this.scanDialog.visible = false;
this.getTableData(); // 刷新主列表
} else {
this.$message.error(res.message);
}
});
} catch (error) {
console.error("领料提交失败:", error);
} finally {
this.$clearLoading();
}
},
},
};
</script>
<style scoped>
/* 领取表单标题样式 */
.receive-form-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
/* 搜索表单间距与样式 */
.search-form {
margin-bottom: 20px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
}
/* 双列表布局 */
.list-container {
display: flex;
gap: 20px;
}
.left-list,
.right-list {
flex: 1;
}
/* 分页容器样式 */
.pagination-container {
margin-top: 10px;
text-align: right;
}
/* 弹窗底部按钮区 */
.dialog-footer {
text-align: center;
}
</style>
四、核心功能实现说明
4.1 双列表联动交互
通过 Element UI Table 的选择事件,实现左侧可选列表与右侧已选列表的双向联动,确保勾选/删除操作同步:
-
单行勾选:监听@select 事件,左侧勾选时将行数据添加到右侧已选列表,取消勾选时从右侧移除;
-
全选操作:监听@select-all 事件,全选时批量添加左侧数据到右侧(去重),取消全选时清空右侧列表;
-
删除联动:右侧列表点击删除时,同步取消左侧对应行的勾选状态,确保数据一致性;
-
分页回显:切换分页后,通过$nextTick 回显已选状态,避免分页后选中丢失。
4.2 辅助功能
-
结果反馈:提交成功后关闭弹窗、刷新主列表;失败时提示错误信息,保持弹窗打开。
-
接口提交:校验通过后,调用领料接口,提交 workOrderPartsId(商品 ID)和 workProductCodeRecordIds(已选加工件 ID 数组);
| 双列表勾选/删除不同步 | 1. 行数据无唯一 id 字段;2. $nextTick 未生效;3. 事件监听异常 | 1. 确保所有列表数据包含唯一 id 字段;2. 检查$nextTick 执行时机,确保 DOM 渲染完成;3. 打开浏览器控制台,查看是否有事件绑定报错。 |
| 扫码后未自动选中/添加 | 1. 扫码编码与加工件 code 字段不匹配;2. PadScanInput 组件未正确引入;3. 表格 ref 未定义 | 1. 检查扫码值与 leftTableData 中 code 字段格式一致;2. 确认 PadScanInput 组件已全局注册;3. 确保左侧表格 ref=“leftTable”。 |
| 分页后已选状态丢失 | 分页切换后未回显已选状态,getLeftTableData 中回显逻辑缺失 | 检查 getLeftTableData 方法中,接口请求成功后是否有回显已选状态的逻辑(代码中已实现,无需修改,确保字段匹配即可)。 |
| 提交接口报错 | 1. 提交参数格式错误;2. 接口地址错误;3. 权限不足 | 1. 检查 workProductCodeRecordIds 是否为数组格式;2. 确认接口地址正确;3. 联系后端配置接口权限,查看接口返回错误信息。 |
| 弹窗打开后无数据 | 1. 主列表行数据未正确带入;2. getLeftTableData 接口未调用;3. 接口返回无数据 | 1. 检查 openScanDialog 方法中 row 参数是否正确;2. 确认 openScanDialog 中已调用 getLeftTableData;3. 检查接口请求参数,测试接口是否能正常返回数据。 |
- 库存校验:领料前调用库存接口,校验加工件库存数量,避免超库存领取;
六、常见问题与解决方案
-
操作日志:添加操作日志记录,记录领料人、领料时间、领料数量及加工件信息,便于追溯;
-
提示信息:修改$message 中的提示文本,适配项目业务话术。
-
已选数量显示:在弹窗顶部添加已选数量实时显示,方便用户查看当前选中数量与应领数量的差值;
-
分页参数:修改 pagination 中的 pageSizes、initialPage、pageSize,适配数据量需求;
-
导出功能:支持右侧已选列表导出 Excel,便于后续统计与核对。
网硕互联帮助中心



![[特殊字符] 纯前端M3U8视频处理工具:在线播放、录制与转换的一站式解决方案-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/02/20260131225721-697e88d1ece9e-220x150.png)

评论前必须登录!
注册