layui 表格行级 upload 上传操作
一、背景:
根据业务需要,要求在数据表格的行级操作和表级操作实现上传文件的功能。表级上传操作使用layui自带的upload组件,通过绑定DOM元素就可以实现。而行操作却不行,因为upload的elem属性是必填项,必须要绑定DOM元素。由于行数据是动态且不确定的,经过一番研究,有了以下解决方案。
二、实现逻辑:
1、核心前提:行内上传元素的模板设计
首先,在表格操作列模板(#operationBar
)中,为每一行都预留了独立的上传相关隐藏元素,这是行内上传的基础:
<script type="text/html" id="operationBar"><div class="layui-clear-space"><!-- 操作下拉触发器 --><a class="layui-btn layui-btn-xs" lay-event="operation">操作<i class="layui-icon layui-icon-down"></i></a ><!-- 行内独立的隐藏上传元素(用class而非id,避免重复) --><input type="file" class="uploadBtn" style="display:none"> <!-- 文件选择按钮 --><button class="uploadAction" style="display:none"></button> <!-- 上传触发按钮 --></div>
</script>
每个表格行的上传元素是「独立的」,通过 class
标识(而非 id
),避免多行列元素 ID 重复导致的绑定冲突。用户看不到这些元素,仅通过下拉菜单操作间接触发。
2、完整流程拆解:从点击到上传完成
阶段 1:用户触发「上传插件」操作
- 下拉菜单触发:用户点击某一行的「操作」按钮,触发
table.on('tool(pluginRegistrTable)')
事件(表格工具事件)。 - 下拉菜单渲染:通过
dropdown.render()
渲染包含「上传插件」的下拉菜单,点击「上传插件」时进入menudata.id == 'uploadFile'
分支。
阶段 2:定位当前行的上传元素
进入「上传插件」分支后,首先要找到当前行专属的上传元素(避免操作其他行):
// $this 是当前行的「操作」按钮元素
var $uploadBtn = $this.siblings('.uploadBtn'); // 当前行的文件选择按钮
var $uploadAction = $this.siblings('.uploadAction'); // 当前行的上传触发按钮
用 siblings()
方法定位:基于当前行的「操作」按钮,找到同一父容器下的 .uploadBtn
和 .uploadAction
,确保是当前行的元素。
阶段 3:销毁旧实例 + 重新初始化上传组件
由于每行的 pluginId
不同(上传接口需要携带当前插件的 ID),必须动态初始化 upload
组件(而非页面加载时初始化):
// 1. 销毁旧实例(避免多实例冲突)
if (self.uploadInst) {self.uploadInst.destroy();
}// 2. 重新初始化upload组件(绑定当前行元素+当前pluginId)
self.uploadInst = upload.render({elem: $uploadBtn, // 绑定当前行的文件选择按钮url: '/AuxiliaryTool/AuxiliaryToolFileUploadService?pluginId=' + pluginId, // 携带当前行的pluginIdauto: false, // 关闭自动上传(需手动触发)bindAction: $uploadAction, // 绑定当前行的上传触发按钮accept: 'file', // 允许所有文件类型(可限制为zip)exts: 'zip', // 仅允许zip格式文件// 以下是回调函数,按执行顺序触发before: function(obj) { /* 上传前触发 */ },choose: function(obj) { /* 选择文件后触发 */ },done: function(res) { /* 上传成功后触发 */ },error: function(index, upload) { /* 上传失败后触发 */ }
});
阶段 4:触发文件选择 + 上传
触发文件选择:通过 $uploadBtn.click()
模拟点击隐藏的文件选择按钮,弹出系统文件选择窗口。
用户选择文件后:进入 choose
回调,执行以下操作:
choose: function(obj) {console.log('选择文件', obj); // 调试日志// 预览文件(可选,这里用于确认文件信息)obj.preview(function(index, file, result) {console.log('当前选择的文件', file); // 打印文件名、大小等信息$uploadAction.click(); // 手动触发上传(因为auto:false)});
}
阶段 5:上传过程中的回调执行
上传前(before 回调):显示加载层,提示用户 “正在上传”:
before: function(obj) {console.log('开始上传', obj);layer.load(); // 显示Layui加载层
}
上传成功(done 回调):关闭加载层,处理后端返回结果:
done: function(res) {layer.closeAll('loading'); // 关闭加载层console.log('上传结果', res); // 打印后端返回的结果if (res.Code != 0) { // 后端返回失败(假设Code=0为成功)layer.alert(res.Data || res.Msg, { icon: 2 }); // 提示失败原因return;}// 上传成功:提示+刷新表格layer.msg("上传成功!", { icon: 1 });table.reloadData('pluginRegistrTable'); // 刷新表格,显示最新状态
}
上传失败(error 回调):关闭加载层,提示错误:
error: function(index, upload) {layer.closeAll('loading');console.error('上传错误', index, upload); // 打印错误信息(方便调试)layer.alert('上传失败,请重试', { icon: 2 });
}
附完整代码:
<style>.layui-table {margin-top: -20px;}
</style><table class="layui-table" id="pluginRegistrTable" lay-filter="pluginRegistrTable" style="margin-top:2px;"></table><script type="text/html" id="IsPluginSet"><input type="checkbox" title="是|否" lay-skin="switch" disabled {{ d.IsPluginSet?`checked`:`` }}>
</script><script type="text/html" id="headToolbar"><div class="layui-btn-container layui-clear"><a class="layui-btn" lay-event="add" href=" " title="注册插件"><i class="layui-icon layui-icon-addition"></i>注册插件</a ><a class="layui-btn" lay-event="delete" href="javascript:;" title="删除插件" style="background-color:red;"><i class="layui-icon layui-icon-delete"></i>删除插件</a ></div>
</script><!-- 修复:将ID改为class,避免重复 -->
<script type="text/html" id="operationBar"><div class="layui-clear-space"><a class="layui-btn layui-btn-xs" lay-event="operation">操作<i class="layui-icon layui-icon-down"></i></a ><!-- 隐藏的上传元素(使用class而非id) --><input type="file" class="uploadBtn" style="display:none"><button class="uploadAction" style="display:none"></button></div>
</script>@section Script{<script src="~/Content/static/js/promise.js"></script><script src="~/Content/static/js/fingerprintjs.js"></script><script type="text/javascript">layui.use(['form', 'table', 'jquery', 'laypage', 'laytpl', "urp", "element", "dropdown", "openSelect", "format",'upload'], function () {var $ = layui.jquery;var form = layui.form;var table = layui.table;var urp = layui.urp;var dropdown = layui.dropdown;var format = layui.format;var upload = layui.upload;var layer = layui.layer; // 补充layer引用var module = {trData: {},uploadInst: null, // 上传实例变量init: function () {this.renderTable();this.initEvent();this.callback();},renderTable: function () {table.render({elem: '#pluginRegistrTable',method: 'post',url: '/AuxiliaryTool/GetPluginRegistrListService?pluginId=' + "" + "&parentPluginId=",height: 'full-35',toolbar: "#headToolbar",defaultToolbar: "",page: true,autoSort: false,limit: 20,limits: [10, 20, 25, 30, 50, 100, 200],cols: [[{ checkbox: true, fixed: true }, { field: 'PluginId', title: '插件Id', width: 300 }, { field: 'PluginName', title: '插件名称', width: 180, }, { field: 'PluginDescription', title: '插件说明', width: 600, }, { field: 'ReleaseStatusName', title: '发布状态', width: 180, }, { field: 'IsPluginSet', title: '插件集', width: 100, templet: "#IsPluginSet" }, { field: 'Operation', title: '操作', toolbar: '#operationBar', align: 'center', fixed: 'right', width: 120, minWidth: 120 }]],parseData: function (res) {var count = res.Data == null ? 0 : res.Data.length;return {"code": res.Code,"msg": res.Msg,"data": res.Data,"count": count,};}});}, registerPlugin: function (isReadonly, pluginId, operateType) {var title = "";if (isReadonly) {title = "查看插件详情";}else {if (operateType == "add") {title = "注册插件";} else if (operateType == "edit") {title = "编辑插件";}}urp.openWindow({href: "/AuxiliaryTool/RegistPlugin?pluginId=" + pluginId + '&isReadonly=' + isReadonly + "&operateType=" + operateType,title: title,area: ["80%", "80%"],callbackFunction: 'fn',target: 'parent',initOpt: {maxmin: false,},});}, initEvent: function () {var self = this;//行双击事件table.on('rowDouble(pluginRegistrTable)', function (obj) {var data = obj.data;var id = data.PluginId;if (id == "" || id == undefined) {return;}self.registerPlugin(true, id,"display");});// 表格上方工具栏事件table.on('toolbar(pluginRegistrTable)', function (obj) {switch (obj.event) {case 'add': {self.registerPlugin(false, "", "add");break;}case 'delete': {var checkStatus = table.checkStatus('pluginRegistrTable')var data = checkStatus.data;if (data.length <= 0) {layer.msg("当前没有选择要删除的数据,请选择数据!");return;}if (data.length > 1) {layer.msg("当前操作只能选择一条数据!");return;}var pluginId = data[0].PluginId;if (pluginId == "") {layer.alert("插件ID不能为空!", { icon: 2 });return;}urp.post("/AuxiliaryTool/DeletePluginRegistrService", { pluginId: pluginId }, function (data) {if (data.Code != 0) {layer.alert(data.Msg, { icon: 2 });return;}layer.msg(data.Msg, { icon: 1 });table.reloadData('pluginRegistrTable');});break;}};});//操作按钮事件(移到最后,确保元素已渲染)table.on('tool(pluginRegistrTable)', function (obj) {var data = obj.data;self.trData = data;var $this = $(this); // 当前操作元素if (obj.event === 'operation') {dropdown.render({elem: $this,show: true,data: [{title: '编辑插件',id: 'edit'}, {title: '查看插件',id: 'display'},{title: '上传插件',id: 'uploadFile',},{title: '发布',id: 'release'}],click: function (menudata) {var pluginId = data.PluginId;if (pluginId === "") {layer.msg("请选择数据!", { icon: 2 });return;}if (menudata.id == 'edit') {self.registerPlugin(false, pluginId, "edit");}else if (menudata.id == 'display') {self.registerPlugin(true, pluginId, "display");}else if (menudata.id == 'uploadFile') {// 修复:获取当前行的上传元素var $uploadBtn = $this.siblings('.uploadBtn');var $uploadAction = $this.siblings('.uploadAction');// 销毁旧实例,创建新实例(确保使用最新的pluginId)if (self.uploadInst) {self.uploadInst.destroy();}// 重新初始化上传组件self.uploadInst = upload.render({elem: $uploadBtn,url: '/AuxiliaryTool/AuxiliaryToolFileUploadService?pluginId=' + pluginId,auto: false,bindAction: $uploadAction,accept: 'file',exts: 'zip',// 增加调试信息before: function(obj){console.log('开始上传', obj);layer.load(); // 显示加载层},choose: function(obj){console.log('选择文件', obj);// 选择后自动触发上传obj.preview(function(index, file, result){console.log('预览文件', file);$uploadAction.click(); // 触发上传});},done: function (res) {layer.closeAll('loading'); // 关闭加载层console.log('上传结果', res);if (res.Code != 0) {layer.alert(res.Data || res.Msg, { icon: 2 });return;}layer.msg("上传成功!", { icon: 1 });table.reloadData('pluginRegistrTable');},error: function(index, upload){layer.closeAll('loading');console.error('上传错误', index, upload);layer.alert('上传失败,请重试', { icon: 2 });}});// 触发文件选择$uploadBtn.click();} else if (menudata.id == 'release') {urp.post("/AuxiliaryTool/ReleasePluginService", { pluginId: pluginId }, function (data) {if (data.Code != 0) {layer.alert(data.Msg, { icon: 2 });return;}layer.msg(data.Msg, { icon: 1 });table.reloadData('pluginRegistrTable');});}}});}});}, callback: function () {var self = this;urp.callbackFunction.fn = function (data) {self.renderTable();};}}module.init();});</script>
}