layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], function (exports) { var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$, upload = layui.upload, http = layui.http , auth = layui.auth; var _savedPerms = (layui.data('backend').permission) || []; auth.init(_savedPerms); auth.setController('addon'); var ADDON_DEV = window.ADDON_DEV || 0; // 清空后台侧栏菜单的本地缓存(Layui data 存储于 setter.appName 下,键为 'menus')。 // 插件卸载/安装/启停会改变 admin_power 菜单,必须清掉缓存让侧栏重新向后端拉取, // 否则侧栏会一直显示(或一直缺)已变更插件的菜单。 function clearMenuCache() { var appName = (layui.setter && layui.setter.appName) || 'backend'; layui.data(appName, { key: 'menus', remove: true }); // 兼容旧键名(历史版本曾写入 'menu') layui.data(appName, { key: 'menu', remove: true }); } // 安装/升级失败的可读化:后端 AddonService::validateDependencies() 收集式校验后, // 会在 data.problems 里带上完整清单(缺哪些依赖、该装还是该升),这里逐条展示并给 // 「去应用市场安装」入口,避免只弹出一长串难以扫读的文案。 function showDependencyProblems(res) { var data = res && res.data; var list = (data && data.problems) || []; if (!list.length) { return false; } var badgeText = { addon: '插件', extension: 'PHP 扩展', framework: '框架' }; var rows = list.map(function (it) { var badge = badgeText[it.type] || '依赖'; return '
  • ' + '' + badge + '' + $('
    ').text(it.text).html() + '
  • '; }).join(''); var missing = (data && data.missing_addons) || []; var extra = missing.length ? '
    ' + '去应用市场安装缺失插件(' + $('').text(missing.join('、')).html() + ')
    ' : ''; layer.open({ type: 1, title: '依赖检查未通过(' + list.length + ' 项)', area: ['620px', 'auto'], content: '
    ' + '' + extra + '
    ', btn: ['知道了'] }); return true; } // ===== 渲染核心表格 ===== var { access_token, refresh_token } = layui.setter.getTokens(); table.render({ elem: '#dataTable' , url: "index" // 此处应填写你真实的后端接口地址 , cols: [[ { field: 'title', title: '插件名称', minWidth: 180 }, { field: 'name', title: '标识', width: 120 }, { field: 'author', title: '开发者', width: 120 } , { field: 'version', title: '版本号', width: 170, align: 'center', templet: '#versionTpl' } , { field: 'description', title: '功能描述', minWidth: 250 } , { field: 'status', title: '运行状态', width: 110, align: 'center', templet: '#switchTpl' } // 绑定开关模板 , { fixed: 'right', title: '操作', toolbar: '#operateTpl', width: 300, align: 'right' } // 绑定操作栏模板 ]] , page: true // 开启分页 , limit: 10 , limits: [10, 20, 50] , skin: 'line' // 设定表格风格为行边框风格 , response: { statusName: 'code' // 状态码字段 , statusCode: 0 // 成功状态码 , msgName: 'message' // 提示信息字段(后端返回 message) , countName: 'count' // 数据总数字段 , dataName: 'data' // 数据列表字段 } , done: function (res, curr, count) { var view = $('#dataTable').next('.layui-table-view'); auth.filterDom(view); auth.filterDom('.layui-btn[data-perm]'); // 开发模式关闭时隐藏操作列的「设计」按钮 // if (!ADDON_DEV) { $('.dev-design').remove(); } } }); // ===== 事件绑定(提前到这里:即便下方 upload/form 初始化出错,行内按钮依然可用) ===== table.on('toolbar(dataTable)', function (elem) { switch (elem.event) { case 'dataCreate': active.dataCreate({ pid: 0, type: 1 }); break; case 'dataRecybin': active.dataRecybin(); break; } }); table.on('tool(dataTable)', function (elem) { var data = elem.data; // 插件标识:优先用 name,缺失时回退到 id(后端 index 两种都返回) var addonName = data.name || data.id || ''; switch (elem.event) { case 'update': active.dataEdit(data); break; case 'config': if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; } location.href = 'setting?addon=' + encodeURIComponent(addonName); break; case 'upgrade': if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; } active.upgrade(addonName, data.latest_version || ''); break; case 'uninstall': if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; } active.uninstall(addonName); break; case 'more': var that = this; var dropdown = layui.dropdown; // 水平对齐方式 dropdown.render({ elem: that, align: 'center', // align 已配置在元素 `lay-options` 属性上 show: true,accordion : true, data: [{ title: '设计', id: 'design' }, { title: '打包', id: 'pack' }, { title: '发布', id: 'submitOfficial' }], click: function (data, othis) { switch (data.id) { case 'pack': // 触发后端 addon::pack 下载该插件的 zip 包(addon/temp/-.zip) if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; } window.location.href = 'pack?name=' + encodeURIComponent(addonName); break; case 'design': if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; } location.href = 'design?addon=' + encodeURIComponent(addonName); break; case 'submitOfficial': // 发布到官方市场:打包本机已装插件并 POST 到官方提交接口,进入待审核 if (!addonName) { layer.msg('缺少插件标识', { icon: 2 }); break; } layer.confirm('确定将「' + (data.title || addonName) + '」发布到官方市场吗?\n提交后需官方审核通过才公开上架。', function (index) { layer.close(index); var loadIdx = layer.load(1, { shade: [0.3, '#fff'] }); http.post('submitOfficial', { name: addonName }) .then(function (res) { layer.close(loadIdx); if (res.code === 0) { layer.msg(res.message || '已提交,等待官方审核', { icon: 1 }); } else { layer.msg(res.message || res.msg || '提交失败', { icon: 2 }); } }) .catch(function () { layer.close(loadIdx); layer.msg('请求失败', { icon: 2 }); }); }); break; default: break; } }, }); break; case 'default': console.warn('未处理的操作事件:', elem.event, data); } }); // 顶部功能按钮 + 行内操作共用 var active = { // 一键升级到市场最新版(version 为空时由服务端取最新版) upgrade: function (name, version) { var loadIdx = layer.load(); // 升级含下载+备份+覆盖,放宽超时到 10 分钟,避免半途被中断 http.post('upgrade', { name: name, version: version }, { timeout: 600000 }) .then(function (res) { layer.close(loadIdx); if (res.code === 0) { layer.msg(res.message || '升级成功', { icon: 1 }); clearMenuCache(); // 插件菜单可能变更 table.reload('dataTable'); } else if (!showDependencyProblems(res)) { layer.msg(res.message || '升级失败', { icon: 2 }); } }) .catch(function () { layer.close(loadIdx); layer.msg('请求失败', { icon: 2 }); }); }, uninstall: function (name) { // 卸载选项:是否备份数据 / 是否删除数据表(对应 AddonService::uninstall() 的 options) layer.open({ type: 1, title: '卸载插件:' + name, area: ['460px', 'auto'], content: '
    ' + '' + '' + '
    ' + '提示:删除数据表后数据不可恢复,建议保持「备份数据」勾选。
    ' + '
    ', btn: ['下一步', '取消'], yes: function (index) { var opts = { backup_data: document.getElementById('unin_backup').checked ? 1 : 0, delete_data: document.getElementById('unin_delete').checked ? 1 : 0 }; layer.close(index); layer.prompt({ formType: 1 , title: '敏感操作,请验证口令' }, function (value, index2) { layer.close(index2); http.delete('uninstall', { name: name, backup_data: opts.backup_data, delete_data: opts.delete_data }) .then((res) => { if (res.code === 0) { clearMenuCache(); // 插件菜单已删除,清掉侧栏菜单缓存 location.reload(); // 重新加载整页,侧栏立即刷新 } else { layer.msg(res.msg || '操作失败'); } }); } } }); }, batchDel: function () { // 批量卸载 var checkStatus = table.checkStatus('dataTable'); var data = checkStatus.data; if (data.length === 0) { layer.msg('请至少选中一项进行卸载', { icon: 0 }); return; } layer.confirm('确定要批量卸载选中的 ' + data.length + ' 个插件吗?', function (index) { layer.msg('正在批量卸载...', { icon: 1 }); table.reload('dataTable'); layer.close(index); }); }, refresh: function () { // 刷新表格 table.reload('dataTable'); layer.msg('已刷新列表', { icon: 1 }); } }; $('.layui-btn').on('click', function () { var type = $(this).data('type'); active[type] && active[type].call(this); }); console.log(layui.setter.getTokens()); var inst = upload.render({ elem: '#buttonInstall', field: 'file', url: "upload", // 上传接口地址 accept: 'file', // 只允许上传文件 // 注意:不要设置 acceptMime。该属性会让浏览器文件选择框按 MIME 过滤, // 而 zip 在本机常被识别为 application/x-zip-compressed / application/octet-stream, // 导致选择框中插件包被置灰、无法选中。仅用 exts 在选完后校验后缀即可。 exts: 'zip', // 仅允许 zip 插件包(后端 AddonService 只支持 zip) headers: { 'Authorization': `Bearer ${access_token}` }, // 鉴权 done: function (res) { // 上传成功的回调 if (res.code == 0) { layer.msg('插件安装成功!', { icon: 1 }); clearMenuCache(); // 插件菜单已变化,清掉侧栏菜单缓存 location.reload(); // 重新加载整页,侧栏立即刷新 } else if (!showDependencyProblems(res)) { layer.msg('安装失败: ' + (res.message || '未知错误'), { icon: 2 }); } }, error: function () { // 上传失败的回调 // 当上传失败时,可在此处生成「重新上传」按钮,并执行该方法重新触发上传提交 /* $('#btn').on('click', function(){ inst.upload(); }) */ } }); // ===== 监听顶部搜索表单提交 ===== form.on('submit(searchBtn)', function (data) { var field = data.field; // 获取搜索表单的值 // 重载表格,将搜索条件传给后端 table.reload('dataTable', { where: field, page: { curr: 1 } // 搜索后回到第一页 }); return false; // 阻止表单默认跳转 }); // ===== 监听状态开关(启用/停用)的切换 ===== form.on('switch(statusSwitch)', function (obj) { var name = this.value; // value 绑定的是插件名 d.id var status = obj.elem.checked ? 1 : 0; // 根据开关状态赋值 var action = status ? 'enable' : 'disable'; var that = obj.othis; http.post('toggle', { name: name, action: action }) .then(function (res) { if (res.code === 0) { layer.msg(status ? '已启用' : '已停用', { icon: 1 }); clearMenuCache(); // 菜单可见性已变化,清掉侧栏菜单缓存(下次导航即刷新) } else { layer.msg(res.message || '操作失败', { icon: 2 }); that.find('input').prop('checked', !status); form.render(); } }) .catch(function () { layer.msg('请求失败', { icon: 2 }); that.find('input').prop('checked', !status); form.render(); }); }); exports('addon', {}); // 输出模块接口 });