一、背景:
根据业务需要,要求在数据表格的行级操作和表级操作实现上传文件 的功能。表级上传操作使用layui自带的upload组件,通过绑定DOM元素就可以实现。而行操作却不行,因为upload的elem属性是必填项,必须要绑定DOM元素。由于行数据是动态且不确定的,经过一番研究,有了以下解决方案。
二、实现逻辑:
1、核心前提:行内上传元素的模板设计
首先,在表格操作列模板(#operationBar
)中,为每一行都预留了独立的上传相关隐藏元素,这是行内上传的基础:
javascript
<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:定位当前行的上传元素
进入「上传插件」分支后,首先要找到当前行专属的上传元素(避免操作其他行):
javascript
// $this 是当前行的「操作」按钮元素
var $uploadBtn = $this.siblings('.uploadBtn'); // 当前行的文件选择按钮
var $uploadAction = $this.siblings('.uploadAction'); // 当前行的上传触发按钮
用 siblings()
方法定位:基于当前行的「操作」按钮,找到同一父容器下的 .uploadBtn
和 .uploadAction
,确保是当前行的元素。
阶段 3:销毁旧实例 + 重新初始化上传组件
由于每行的 pluginId
不同(上传接口需要携带当前插件的 ID),必须动态初始化 upload
组件(而非页面加载时初始化):
javascript
// 1. 销毁旧实例(避免多实例冲突)
if (self.uploadInst) {
self.uploadInst.destroy();
}
// 2. 重新初始化upload组件(绑定当前行元素+当前pluginId)
self.uploadInst = upload.render({
elem: $uploadBtn, // 绑定当前行的文件选择按钮
url: '/AuxiliaryTool/AuxiliaryToolFileUploadService?pluginId=' + pluginId, // 携带当前行的pluginId
auto: 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
回调,执行以下操作:
javascript
choose: function(obj) {
console.log('选择文件', obj); // 调试日志
// 预览文件(可选,这里用于确认文件信息)
obj.preview(function(index, file, result) {
console.log('当前选择的文件', file); // 打印文件名、大小等信息
$uploadAction.click(); // 手动触发上传(因为auto:false)
});
}
阶段 5:上传过程中的回调执行
上传前(before 回调):显示加载层,提示用户 "正在上传":
javascript
before: function(obj) {
console.log('开始上传', obj);
layer.load(); // 显示Layui加载层
}
上传成功(done 回调):关闭加载层,处理后端返回结果:
javascript
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 回调):关闭加载层,提示错误:
javascript
error: function(index, upload) {
layer.closeAll('loading');
console.error('上传错误', index, upload); // 打印错误信息(方便调试)
layer.alert('上传失败,请重试', { icon: 2 });
}
附完整代码:
html
<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>
}