feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用

安全(P0)
- 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、
  ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签
  (ywxapp.framework_package_verify,默认开启)
- JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret()
  校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET
- /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验
- 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流
- TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive
- 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php)
- 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis
- paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG

工程化 / 交付
- 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本
- 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过
- 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障
- .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess

结构
- appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

+2 -8
View File
@@ -47,14 +47,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+102 -26
View File
@@ -15,6 +15,37 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
// 兼容旧键名(历史版本曾写入 '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 '<li style="padding:6px 0;border-bottom:1px dashed #eee;">'
+ '<span class="layui-badge layui-bg-cyan" style="margin-right:6px;">' + badge + '</span>'
+ $('<div>').text(it.text).html()
+ '</li>';
}).join('');
var missing = (data && data.missing_addons) || [];
var extra = missing.length
? '<div style="margin-top:12px;"><a class="layui-btn layui-btn-normal layui-btn-xs" href="market" target="_blank">'
+ '去应用市场安装缺失插件(' + $('<span>').text(missing.join('、')).html() + ')</a></div>'
: '';
layer.open({
type: 1,
title: '依赖检查未通过(' + list.length + ' 项)',
area: ['620px', 'auto'],
content: '<div style="padding:16px 18px;font-size:13px;">'
+ '<ul style="list-style:none;margin:0;padding:0;">' + rows + '</ul>' + extra + '</div>',
btn: ['知道了']
});
return true;
}
// ===== 渲染核心表格 =====
var { access_token, refresh_token } = layui.setter.getTokens();
table.render({
@@ -24,7 +55,7 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
{ field: 'title', title: '插件名称', minWidth: 180 },
{ field: 'name', title: '标识', width: 120 },
{ field: 'author', title: '开发者', width: 120 }
, { field: 'version', title: '版本号', width: 100, align: 'center' }
, { 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' } // 绑定操作栏模板
@@ -42,14 +73,8 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
}
, done: function (res, curr, count) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
// 开发模式关闭时隐藏操作列的「设计」按钮
// if (!ADDON_DEV) { $('.dev-design').remove(); }
}
@@ -79,6 +104,10 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
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);
@@ -146,22 +175,69 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
// 顶部功能按钮 + 行内操作共用
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) {
layer.prompt({
formType: 1
, title: '敏感操作,请验证口令'
}, function (value, index) {
layer.close(index);
http.delete('uninstall', { name: name })
.then((res) => {
if (res.code === 0) {
layer.close(index);
clearMenuCache(); // 插件菜单已删除,清掉侧栏菜单缓存
location.reload(); // 重新加载整页,侧栏立即刷新
} else {
layer.msg(res.msg || '操作失败');
}
});
// 卸载选项:是否备份数据 / 是否删除数据表(对应 AddonService::uninstall() 的 options)
layer.open({
type: 1,
title: '卸载插件:' + name,
area: ['460px', 'auto'],
content: '<div style="padding:16px 18px;">'
+ '<label style="display:block;margin-bottom:10px;">'
+ '<input type="checkbox" id="unin_backup" checked> 卸载前备份数据(SQL 落到 runtime/addon/backup/)'
+ '</label>'
+ '<label style="display:block;">'
+ '<input type="checkbox" id="unin_delete" checked> 同时 DROP 插件数据表(不勾选则保留数据)'
+ '</label>'
+ '<div class="layui-word-aux" style="margin-top:10px;">'
+ '提示:删除数据表后数据不可恢复,建议保持「备份数据」勾选。</div>'
+ '</div>',
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 () { // 批量卸载
@@ -206,8 +282,8 @@ layui.define(['http', 'setter', 'auth', 'upload', 'form', 'layer', 'table'], fun
layer.msg('插件安装成功!', { icon: 1 });
clearMenuCache(); // 插件菜单已变化,清掉侧栏菜单缓存
location.reload(); // 重新加载整页,侧栏立即刷新
} else {
layer.msg('安装失败: ' + res.message, { icon: 2 });
} else if (!showDependencyProblems(res)) {
layer.msg('安装失败: ' + (res.message || '未知错误'), { icon: 2 });
}
},
error: function () {
+50 -73
View File
@@ -17,8 +17,7 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
, url: "index" // 后端数据接口
, page: true
, limit: 15
//, toolbar: '#tableBar'
// , defaultToolbar: auth.renderToolbar(tableActions)
, toolbar: '#tableBar'
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', title: 'ID', width: 72, align: 'center', sort: true, fixed: 'left' }
@@ -34,12 +33,7 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
]],
done: function (res, curr, count) {
// 行内按钮按真实权限显隐(data-perm 为完整权限 key,如 admin:edit)
$('#dataTable').next('.layui-table-view').find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) {
$(this).remove();
}
});
auth.filterDom($('#dataTable').next('.layui-table-view'));
}
});
@@ -57,16 +51,20 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
var id = this.value;
var status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该管理员账号吗?', function (index) {
layui.request.post('update', { id: id, status: status }
http.post('update', { id: id, status: status }
).then(function (res) {
if (res.code === 0) {
if (res && res.code === 0) {
layer.msg(res.msg, { icon: 1 });
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg, { icon: 2 });
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
layer.close(index);
}, function () {
@@ -280,23 +278,17 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
// 提交表单
form.on('submit(adminFormSubmit)', function (formData) {
var url = data ? "update" : "create";
$.ajax({
url: url,
type: 'POST',
data: formData.field,
success: function (res) {
if (res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
layer.close(index);
adminTable.reload();
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post(url, formData.field).then(function (res) {
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
layer.close(index);
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
return false;
@@ -318,21 +310,16 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
btn: ['确定', '取消'],
title: '重置密码'
}, function (index) {
$.ajax({
url: "resetPassword",
type: 'POST',
data: { id: data.id },
success: function (res) {
layer.close(index);
if (res.code === 1) {
layer.msg(res.msg, { icon: 1 });
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post("resetPassword", { id: data.id }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1 });
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
@@ -344,23 +331,18 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
title: '删除管理员',
icon: 3
}, function (index) {
$.ajax({
url: "delete",
type: 'POST',
data: { id: data.id },
success: function (res) {
layer.close(index);
if (res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post("delete", { id: data.id }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
@@ -386,23 +368,18 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
title: '批量删除',
icon: 3
}, function (index) {
$.ajax({
url: "batchDelete",
type: 'POST',
data: { ids: ids },
success: function (res) {
layer.close(index);
if (res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post("batchDelete", { ids: ids }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
+166
View File
@@ -0,0 +1,166 @@
/**
* 操作日志(backend_log)
*
* 权限按钮用完整权限名,表格渲染后由 auth.filterDom() 移除无权限按钮。
*/
layui.define(['table', 'layer', 'form', 'laydate', 'auth', 'http', 'jquery'], function (exports) {
var table = layui.table,
layer = layui.layer,
form = layui.form,
laydate = layui.laydate,
auth = layui.auth,
http = layui.http,
$ = layui.jquery;
// 注意:http 的 baseURL 为空,URL 由浏览器按「当前页面所在目录」解析,
// 核心后台页面的目录就是 /admin/<controller>/,所以这里只写动作名(纯相对),
// 写成 'adminlog/index' 会被解析成 /admin/adminlog/adminlog/index(404)。
var FUNC = {
index: 'index',
delete: 'delete',
clean: 'clean'
};
function parse(res) {
return {
code: res.code,
msg: res.message,
count: res.count || 0,
data: res.data || []
};
}
function post(url, data, done) {
var loading = layer.load(2);
http.post(url, data).then(function (res) {
layer.close(loading);
if (res && res.code === 0) {
layer.msg(res.message || '操作成功', {icon: 1});
if (done) { done(res.data); }
} else {
layer.msg((res && res.message) || '操作失败', {icon: 2});
}
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
}
function checkedIds() {
var rows = table.checkStatus('dataTable').data || [];
var ids = [];
for (var i = 0; i < rows.length; i++) { ids.push(rows[i].id); }
return ids;
}
var obj = {
render: function () {
table.render({
elem: '#dataTable',
url: FUNC.index,
where: {},
toolbar: '#tableBar',
defaultToolbar: ['filter'],
method: 'get',
cols: [[
{type: 'checkbox', width: 50},
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'operator', title: '操作人', width: 130},
{field: 'action', title: '动作', width: 150},
{field: 'ip', title: 'IP', width: 130},
{field: 'remark', title: '备注', minWidth: 240},
{field: 'create_at_text', title: '时间', width: 170, sort: true},
{title: '操作', width: 90, toolbar: '#dataBar', fixed: 'right'}
]],
page: true,
limit: 20,
limits: [20, 50, 100, 200],
response: {statusCode: 0},
parseData: parse,
done: function () {
auth.filterDom($('#dataTable').next('.layui-table-view'));
auth.filterDom('.layui-btn[data-perm]');
}
});
},
events: function () {
laydate.render({elem: '#start', type: 'date'});
laydate.render({elem: '#end', type: 'date'});
table.on('toolbar(dataTable)', function (o) {
if (o.event === 'deleteSelected') {
var ids = checkedIds();
if (ids.length === 0) {
return layer.msg('请先勾选要删除的日志', {icon: 2});
}
layer.confirm('确定删除选中的 ' + ids.length + ' 条日志?', {title: '提示'}, function (index) {
layer.close(index);
post(FUNC.delete, {ids: ids}, function () {
table.reload('dataTable');
});
});
} else if (o.event === 'clean') {
layer.prompt({
title: '保留最近多少天的日志?',
value: 30,
formType: 0
}, function (days, index) {
days = parseInt(days, 10);
if (isNaN(days) || days < 0 || days > 3650) {
return layer.msg('请输入 0 ~ 3650 之间的天数(0=清空)', {icon: 2});
}
layer.close(index);
layer.confirm(
days > 0 ? ('确定清理 ' + days + ' 天前的日志?') : '确定清空全部日志?此操作不可恢复',
{title: '危险操作'},
function (idx) {
layer.close(idx);
post(FUNC.clean, {days: days}, function () {
table.reload('dataTable');
});
}
);
});
}
});
table.on('tool(dataTable)', function (o) {
if (o.event === 'delete') {
layer.confirm('确定删除该条日志?', {title: '提示'}, function (index) {
layer.close(index);
post(FUNC.delete, {ids: [o.data.id]}, function () {
table.reload('dataTable');
});
});
}
});
form.on('submit(data-search-btn)', function (o) {
table.reload('dataTable', {
where: {
keyword: o.field.keyword,
action: o.field.action,
start: o.field.start,
end: o.field.end
},
page: {curr: 1}
});
return false;
});
$('#btn-reset').on('click', function () {
setTimeout(function () {
table.reload('dataTable', {
where: {keyword: '', action: '', start: '', end: ''},
page: {curr: 1}
});
}, 10);
});
}
};
obj.render();
obj.events();
exports('adminlog', obj);
});
+147
View File
@@ -0,0 +1,147 @@
/**
* 附件统一管理(common_attachment)
*
* 删除走后台 Attachment::delete():先经 FileStorageService 触发 FileDelete 事件
* (OSS / FTP 等自定义存储在此接管物理删除),再软删记录。
*/
layui.define(['table', 'layer', 'form', 'auth', 'http', 'jquery'], function (exports) {
var table = layui.table,
layer = layui.layer,
form = layui.form,
auth = layui.auth,
http = layui.http,
$ = layui.jquery;
// URL 由浏览器按当前页面目录解析(/admin/attachment/),只写动作名
var FUNC = {
index: 'index',
delete: 'delete'
};
function parse(res) {
return {
code: res.code,
msg: res.message,
count: res.count || 0,
data: res.data || []
};
}
function post(url, data, done) {
var loading = layer.load(2);
http.post(url, data).then(function (res) {
layer.close(loading);
if (res && res.code === 0) {
layer.msg(res.message || '操作成功', {icon: 1});
if (done) { done(res.data); }
} else {
layer.msg((res && res.message) || '操作失败', {icon: 2});
}
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
}
function checkedIds() {
var rows = table.checkStatus('dataTable').data || [];
var ids = [];
for (var i = 0; i < rows.length; i++) { ids.push(rows[i].id); }
return ids;
}
var obj = {
render: function () {
table.render({
elem: '#dataTable',
url: FUNC.index,
where: {},
toolbar: '#tableBar',
defaultToolbar: ['filter'],
method: 'get',
cols: [[
{type: 'checkbox', width: 50},
{field: 'id', title: 'ID', width: 70, sort: true},
{title: '预览', width: 70, templet: '#thumbTpl', align: 'center'},
{field: 'original', title: '文件名', minWidth: 220},
{field: 'module', title: '模块', width: 90},
{field: 'size_text', title: '大小', width: 100},
{field: 'storage', title: '存储', width: 90},
{field: 'uid', title: '上传者', width: 90},
{field: 'create_at_text', title: '时间', width: 170, sort: true},
{title: '操作', width: 130, toolbar: '#dataBar', fixed: 'right'}
]],
page: true,
limit: 20,
limits: [20, 50, 100],
response: {statusCode: 0},
parseData: parse,
done: function () {
auth.filterDom($('#dataTable').next('.layui-table-view'));
auth.filterDom('.layui-btn[data-perm]');
}
});
},
events: function () {
table.on('toolbar(dataTable)', function (o) {
if (o.event === 'deleteSelected') {
var ids = checkedIds();
if (ids.length === 0) {
return layer.msg('请先勾选要删除的附件', {icon: 2});
}
layer.confirm(
'确定删除选中的 ' + ids.length + ' 个附件?<br><span style="color:#ff5722">会同时删除存储上的文件</span>',
{title: '提示'},
function (index) {
layer.close(index);
post(FUNC.delete, {ids: ids}, function () {
table.reload('dataTable');
setTimeout(function () { location.reload(); }, 800);
});
}
);
}
});
table.on('tool(dataTable)', function (o) {
if (o.event === 'open') {
window.open(o.data.url, '_blank');
} else if (o.event === 'delete') {
layer.confirm('确定删除该附件?会同时删除存储上的文件', {title: '提示'}, function (index) {
layer.close(index);
post(FUNC.delete, {ids: [o.data.id]}, function () {
table.reload('dataTable');
setTimeout(function () { location.reload(); }, 800);
});
});
}
});
form.on('submit(data-search-btn)', function (o) {
table.reload('dataTable', {
where: {
keyword: o.field.keyword,
module: o.field.module,
only_image: o.field.only_image ? 1 : 0
},
page: {curr: 1}
});
return false;
});
$('#btn-reset').on('click', function () {
setTimeout(function () {
table.reload('dataTable', {
where: {keyword: '', module: '', only_image: 0},
page: {curr: 1}
});
}, 10);
});
}
};
obj.render();
obj.events();
exports('attachment', obj);
});
+2 -8
View File
@@ -35,14 +35,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+6 -5
View File
@@ -8,11 +8,12 @@ layui.define(['http', 'auth', 'table', 'layer'], function (exports) {
auth.setController('articles:backend:category');
auth.render();
var MODULE = 'articles/backend.category',
FUNC = {
index: MODULE + '/index',
delete: MODULE + '/delete'
};
// URL 由浏览器按当前页面所在目录解析(页面即 /articles/backend/category/index.html),
// 所以只写动作名;带前缀会拼成 .../category/articles/backend.category/index(404)。
var FUNC = {
index: 'index',
delete: 'delete'
};
var obj = {
init: function () {
+227 -254
View File
@@ -1,10 +1,18 @@
/**
* console - homepage demo
* 控制台首页
*
* 分工:
* - 后端渲染:系统概览 card、维护按钮、框架升级按钮、统计 card、最新会员 / 最近操作日志表格
* (数据来自 Console::collectStats() 与 SystemInfo::collect())
* - 前端只做两件必须异步的事:近 7 天趋势图(ECharts)、两个 flush 类接口的按钮事件
*
* 原 layuiAdmin 模板自带的演示数据已全部清掉:球员数据雷达图、中国地图、
* 假留言列表、假活跃用户、假地区分布、模拟接口的「项目进展」表格——
* 这些户型要么无数据源,要么与真数据权限重复(运行环境表已覆盖)。
*/
layui.define('ywxapp',function (exports) {
layui.define('ywxapp', function (exports) {
var ywxapp = layui.ywxapp;
//区块轮播切换
layui.use(['ywxapp', 'carousel'], function () {
var $ = layui.$
@@ -31,265 +39,230 @@ layui.define('ywxapp',function (exports) {
});
//八卦新闻
// 近 7 天趋势:会员注册 / 后台操作(真实数据)
//
// 数据由后端渲染进 layui.stats(见 console/index.html 的 json_encode),
// 这里直接取,不再额外发请求——首页是每人的第一屏,少一个请求少一处等待。
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts;
var echartsApp = [], options = [
{
title: {
subtext: '完全实况球员数据',
textStyle: {
fontSize: 14
}
},
tooltip: {
trigger: 'axis'
},
legend: {
x: 'left',
data: ['罗纳尔多', '舍普琴科']
},
polar: [
{
indicator: [
{ text: '进攻', max: 100 },
{ text: '防守', max: 100 },
{ text: '体能', max: 100 },
{ text: '速度', max: 100 },
{ text: '力量', max: 100 },
{ text: '技巧', max: 100 }
],
radius: 130
}
],
series: [
{
type: 'radar',
center: ['50%', '50%'],
itemStyle: {
normal: {
areaStyle: {
type: 'default'
}
}
},
data: [
{ value: [97, 42, 88, 94, 90, 86], name: '舍普琴科' },
{ value: [97, 32, 74, 95, 88, 92], name: '罗纳尔多' }
]
}
]
}
]
, elemDataView = $('#LAY-index-pageone').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme);
echartsApp[index].setOption(options[index]);
window.onresize = echartsApp[index].resize;
};
//没找到DOM,终止执行
if (!elemDataView[0]) return;
var stats = layui.stats || {};
var series = stats.series || {};
renderDataView(0);
});
//访问量
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts;
var echartsApp = [], options = [
{
tooltip: {
trigger: 'axis'
},
calculable: true,
legend: {
data: ['访问量', '下载量', '平均访问量']
},
xAxis: [
{
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
}
],
yAxis: [
{
type: 'value',
name: '访问量',
axisLabel: {
formatter: '{value} 万'
}
},
{
type: 'value',
name: '下载量',
axisLabel: {
formatter: '{value} 万'
}
}
],
series: [
{
name: '访问量',
type: 'line',
data: [900, 850, 950, 1000, 1100, 1050, 1000, 1150, 1250, 1370, 1250, 1100]
},
{
name: '下载量',
type: 'line',
yAxisIndex: 1,
data: [850, 850, 800, 950, 1000, 950, 950, 1150, 1100, 1240, 1000, 950]
},
{
name: '平均访问量',
type: 'line',
data: [870, 850, 850, 950, 1050, 1000, 980, 1150, 1000, 1300, 1150, 1000]
}
]
}
]
, elemDataView = $('#LAY-index-pagetwo').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme);
echartsApp[index].setOption(options[index]);
window.onresize = echartsApp[index].resize;
};
//没找到DOM,终止执行
if (!elemDataView[0]) return;
renderDataView(0);
});
//地图
layui.use(['carousel', 'echarts'], function () {
var $ = layui.$
, carousel = layui.carousel
, echarts = layui.echarts;
var echartsApp = [], options = [
{
title: {
text: '访客地区分布',
subtext: '不完全统计'
},
tooltip: {
trigger: 'item'
},
dataRange: {
orient: 'horizontal',
min: 0,
max: 60000,
text: ['高', '低'],
splitNumber: 0
},
series: [
{
name: '访客地区分布',
type: 'map',
mapType: 'china',
selectedMode: 'multiple',
itemStyle: {
normal: { label: { show: true } },
emphasis: { label: { show: true } }
},
data: [
{ name: '西藏', value: 60 },
{ name: '青海', value: 167 },
{ name: '宁夏', value: 210 },
{ name: '海南', value: 252 },
{ name: '甘肃', value: 502 },
{ name: '贵州', value: 570 },
{ name: '新疆', value: 661 },
{ name: '云南', value: 8890 },
{ name: '重庆', value: 10010 },
{ name: '吉林', value: 5056 },
{ name: '山西', value: 2123 },
{ name: '天津', value: 9130 },
{ name: '江西', value: 10170 },
{ name: '广西', value: 6172 },
{ name: '陕西', value: 9251 },
{ name: '黑龙江', value: 5125 },
{ name: '内蒙古', value: 1435 },
{ name: '安徽', value: 9530 },
{ name: '北京', value: 51919 },
{ name: '福建', value: 3756 },
{ name: '上海', value: 59190 },
{ name: '湖北', value: 37109 },
{ name: '湖南', value: 8966 },
{ name: '四川', value: 31020 },
{ name: '辽宁', value: 7222 },
{ name: '河北', value: 3451 },
{ name: '河南', value: 9693 },
{ name: '浙江', value: 62310 },
{ name: '山东', value: 39231 },
{ name: '江苏', value: 35911 },
{ name: '广东', value: 55891 }
]
}
]
}
]
, elemDataView = $('#LAY-index-pagethree').children('div')
, renderDataView = function (index) {
echartsApp[index] = echarts.init(elemDataView[index], layui.echartsTheme);
echartsApp[index].setOption(options[index]);
window.onresize = echartsApp[index].resize;
};
//没找到DOM,终止执行
if (!elemDataView[0]) return;
renderDataView(0);
});
//项目进展
layui.use('table', function () {
var $ = layui.$
, table = layui.table;
table.render({
elem: '#LAY-index-prograss'
, url: layui.setter.paths.base + 'json/console/prograss.js' //模拟接口
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'prograss', title: '任务' }
, { field: 'time', title: '所需时间' }
, {
field: 'complete', title: '完成情况'
, templet: function (d) {
if (d.complete == '已完成') {
return '<del style="color: #16b777;">' + d.complete + '</del>'
} else if (d.complete == '进行中') {
return '<span style="color: #FFB800;">' + d.complete + '</span>'
} else {
return '<span style="color: #FF5722;">' + d.complete + '</span>'
}
}
var option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['会员注册', '后台操作']
},
grid: {
left: '3%'
, right: '4%'
, bottom: '3%'
, containLabel: true
},
xAxis: [
{
type: 'category'
, boundaryGap: false
, data: stats.dates || []
}
]]
, skin: 'line'
],
yAxis: [
{
type: 'value'
// 都是条数,不该出现 0.5 这种刻度
, minInterval: 1
}
],
series: [
{
name: '会员注册'
, type: 'line'
, smooth: true
, areaStyle: {}
, data: series.member || []
},
{
name: '后台操作'
, type: 'line'
, smooth: true
, areaStyle: {}
, data: series.log || []
}
]
};
var elemDataView = $('#LAY-index-trend').children('div');
//没找到DOM,终止执行
if (!elemDataView[0]) return;
var chart = echarts.init(elemDataView[0], layui.echartsTheme);
chart.setOption(option);
window.onresize = chart.resize;
});
// 清理缓存(控制台顶部「维护」卡片)
//
// 接口:Console::clean(POST /admin/console/clean)
// 权限:console:clean,无权限时 auth.filterDom() 直接把按钮摘掉。
// 「系统信息」页与 System 控制器已下线,清理能力收进 Console(本页),
// 按钮 id 保持 sys-* 不变,只换 URL 与权限名。
layui.use(['layer', 'element', 'auth', 'http', 'jquery'], function () {
var $ = layui.$
, layer = layui.layer
, http = layui.http
, auth = layui.auth;
auth.filterDom('.layui-btn[data-perm]');
function clean(type, tip) {
layer.confirm(tip, {title: '提示'}, function (index) {
layer.close(index);
var loading = layer.load(2);
http.post('/admin/console/clean', {type: type}).then(function (res) {
layer.close(loading);
if (res && res.code === 0) {
layer.msg(res.message || '清理完成', {icon: 1});
setTimeout(function () { location.reload(); }, 800);
} else {
layer.msg((res && res.message) || '清理失败', {icon: 2});
}
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
});
}
$('#sys-clean-cache').on('click', function () {
clean('cache', '确定清理数据缓存(runtime/cache)?');
});
$('#sys-clean-temp').on('click', function () {
clean('temp', '确定清理模板编译缓存(runtime/temp)?');
});
$('#sys-clean-all').on('click', function () {
clean('all', '确定全量清理(数据缓存 + 模板缓存 + 路由缓存)?');
});
});
console.log('进入控制台',layui.ywxapp);
//回复留言
ywxapp.events.replyNote = function (othis) {
var nid = othis.data('id');
layer.prompt({
title: '回复留言 ID:' + nid
, formType: 2
}, function (value, index) {
//这里可以请求 Ajax
//…
layer.msg('得到:' + value);
layer.close(index);
// 框架升级(与「系统管理 → 框架升级」同源)
//
// 接口:/admin/framework/check(GET 检测)、/admin/framework/upgrade(POST 执行)
// 权限:framework:check / framework:upgrade,无权限时 auth.filterDom() 移除按钮。
// 本页目录是 /admin/console/,所以 URL 必须写绝对路径。
layui.use(['layer', 'element', 'auth', 'http', 'jquery'], function () {
var $ = layui.$
, layer = layui.layer
, http = layui.http
, auth = layui.auth;
auth.filterDom('.layui-btn[data-perm]');
var pkgType = 'auto';
function setDisabled($el, dis) {
if (dis) {
$el.addClass('layui-btn-disabled').prop('disabled', true);
} else {
$el.removeClass('layui-btn-disabled').prop('disabled', false);
}
}
function status(html) {
$('#fw-status').html(html);
}
function loading(text) {
status('<i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate"></i> ' + text);
}
$('#fw-pkg-group').on('click', '.fw-pkg', function () {
if ($(this).prop('disabled')) return;
pkgType = $(this).data('type');
$('#fw-pkg-group .fw-pkg').removeClass('layui-btn-normal').addClass('layui-btn-primary');
$(this).removeClass('layui-btn-primary').addClass('layui-btn-normal');
});
};
function check() {
loading('正在检查...');
// 控制台是首屏:检测限 8 秒,中心站不可达时不拖住整页
http.get('/admin/framework/check', {}, {timeout: 8000}).then(function (res) {
if (!res || res.code !== 0) {
status('<span style="color:#dc2626;">' + ((res && res.message) || '检查失败') + '</span>');
return;
}
var d = res.data;
if (d.current) {
$('#fw-current').text(d.current);
}
if (!d.has_update) {
status('<span style="color:#16a34a;">✓ 已是最新版本(' + d.current + ')</span>');
$('#fw-upgrade').hide();
$('#fw-pkg-box').hide();
return;
}
var html = '发现新版本:<b>' + d.latest + '</b>'
+ ' <span class="layui-badge layui-bg-orange">自动模式将使用'
+ (d.use_patch ? '增量补丁' : '整包') + '</span>';
if (d.use_patch && d.patch_from) html += '(基于 ' + d.patch_from + ')';
if (d.title) html += '(' + d.title + ')';
if (d.error) html += '<br><span style="color:#dc2626;">' + d.error + '</span>';
html += '<br>更新内容:<pre style="margin-top:6px;">' + (d.changelog || '') + '</pre>';
status(html);
if (d.error) {
$('#fw-upgrade').hide();
$('#fw-pkg-box').hide();
return;
}
$('#fw-upgrade').show();
$('#fw-pkg-box').show();
pkgType = 'auto';
$('#fw-pkg-group .fw-pkg').removeClass('layui-btn-normal').addClass('layui-btn-primary');
$('#fw-pkg-group .fw-pkg[data-type=auto]').removeClass('layui-btn-primary').addClass('layui-btn-normal');
setDisabled($('#fw-pkg-group .fw-pkg[data-type=full]'), !d.has_full);
setDisabled($('#fw-pkg-group .fw-pkg[data-type=patch]'), !d.use_patch);
var tips = [];
if (!d.has_full) tips.push('当前无完整包可用');
if (!d.use_patch) tips.push('当前版本(' + (d.current || '') + ')不支持增量小包,需基于 ' + (d.patch_from || '?') + ')');
$('#fw-pkg-tip').text(tips.length ? '提示:' + tips.join(';') + ',建议选「自动」' : '可手动选择完整包或增量小包');
}).catch(function (err) {
status('<span style="color:#dc2626;">请求失败:' + (err && err.message ? err.message : '网络异常') + '</span>');
});
}
$('#fw-check').on('click', check);
$('#fw-upgrade').on('click', function () {
var label = {auto: '自动模式', full: '完整包', patch: '增量小包'}[pkgType] || pkgType;
layer.confirm('将以【' + label + '】升级,将备份当前核心并覆盖,确定继续?', {title: '提示'}, function (index) {
layer.close(index);
loading('正在下载并升级...');
// 下载 + 备份 + 覆盖可能很久,放宽到 10 分钟,别在半途被超时打断
http.post('/admin/framework/upgrade', {type: pkgType}, {timeout: 600000}).then(function (res) {
if (res && res.code === 0) {
status('<span style="color:#16a34a;">✓ ' + res.message + '</span>');
$('#fw-upgrade').hide();
$('#fw-pkg-box').hide();
if (res.data && res.data.version) {
$('#fw-current').text(res.data.version);
}
} else {
status('<span style="color:#dc2626;">' + ((res && res.message) || '升级失败') + '</span>');
}
}).catch(function (err) {
status('<span style="color:#dc2626;">请求失败:' + (err && err.message ? err.message : '网络异常') + '</span>');
});
});
});
check();
});
exports('console', {})
});
});
+211
View File
@@ -0,0 +1,211 @@
/**
* 数据库备份 / 还原
*
* 权限按钮用完整权限名(database:backup / database:restore / database:delete / database:optimize),
* 表格渲染后由 auth.filterDom() 移除无权限按钮。
*/
layui.define(['table', 'layer', 'form', 'util', 'element', 'auth', 'http', 'jquery'], function (exports) {
var table = layui.table,
layer = layui.layer,
form = layui.form,
auth = layui.auth,
http = layui.http,
$ = layui.jquery;
// 注意:http 的 baseURL 为空,URL 由浏览器按「当前页面所在目录」解析,
// 核心后台页面的目录就是 /admin/<controller>/,所以这里只写动作名(纯相对),
// 写成 'database/index' 会被解析成 /admin/database/database/index(404)。
var FUNC = {
index: 'index',
backup: 'backup',
recovery: 'recovery',
delete: 'delete',
optimize: 'optimize',
download: 'download'
};
/**
* 统一的响应处理:Result 返回 {code, message, data, count}
*/
function parse(res) {
return {
code: res.code,
msg: res.message,
count: res.count || 0,
data: res.data || []
};
}
/**
* 提交型请求
*/
function post(url, data, okTip, done) {
var loading = layer.load(2);
http.post(url, data).then(function (res) {
layer.close(loading);
if (res && res.code === 0) {
layer.msg(okTip || res.message || '操作成功', {icon: 1});
if (done) { done(res.data); }
} else {
layer.msg((res && res.message) || '操作失败', {icon: 2});
}
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
}
/**
* 取当前勾选的表名
*/
function checkedTables() {
var rows = table.checkStatus('tableList').data || [];
var names = [];
for (var i = 0; i < rows.length; i++) {
names.push(rows[i].name);
}
return names;
}
var obj = {
renderTable: function () {
table.render({
elem: '#tableList',
url: FUNC.index,
where: {type: 'table'},
toolbar: '#tableBar',
defaultToolbar: ['filter'],
method: 'get',
cols: [[
{type: 'checkbox', width: 50},
{field: 'name', title: '表名', minWidth: 220},
{field: 'comment', title: '备注', minWidth: 160},
{field: 'engine', title: '引擎', width: 100},
{field: 'rows', title: '行数', width: 110, sort: true},
{field: 'size_text', title: '大小', width: 110}
]],
page: true,
limit: 15,
limits: [15, 30, 50, 100],
response: {statusCode: 0},
parseData: parse,
done: function () {
var view = $('#tableList').next('.layui-table-view');
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
},
renderBackup: function () {
table.render({
elem: '#backupList',
url: FUNC.index,
where: {type: 'backup'},
toolbar: '#backupBar',
method: 'get',
cols: [[
{field: 'name', title: '备份名', minWidth: 220},
{field: 'parts', title: '分卷', width: 80},
{field: 'size_text', title: '大小', width: 110},
{field: 'time_text', title: '时间', width: 180, sort: true},
{title: '操作', width: 190, templet: '#backupAction', fixed: 'right'}
]],
page: true,
limit: 15,
response: {statusCode: 0},
parseData: parse,
done: function () {
var view = $('#backupList').next('.layui-table-view');
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
},
events: function () {
// 数据表工具条
table.on('toolbar(tableList)', function (o) {
if (o.event === 'backupSelected' || o.event === 'backupAll') {
var names = o.event === 'backupAll' ? [] : checkedTables();
if (o.event === 'backupSelected' && names.length === 0) {
return layer.msg('请先勾选要备份的表', {icon: 2});
}
layer.confirm(
o.event === 'backupAll' ? '确认备份全部数据表?' : '确认备份选中的 ' + names.length + ' 张表?',
{title: '备份确认'},
function (index) {
layer.close(index);
post(FUNC.backup, {tables: names, part_size: 2097152}, '备份完成', function () {
table.reload('backupList');
});
}
);
} else if (o.event === 'optimize') {
var opts = checkedTables();
if (opts.length === 0) {
return layer.msg('请先勾选要优化的表', {icon: 2});
}
post(FUNC.optimize, {tables: opts}, '优化完成', function () {
table.reload('tableList');
});
}
});
// 备份记录工具条
table.on('toolbar(backupList)', function (o) {
if (o.event === 'reload') {
table.reload('backupList');
}
});
// 备份记录行操作
table.on('tool(backupList)', function (o) {
if (o.event === 'download') {
window.open(FUNC.download + '?name=' + encodeURIComponent(o.data.name));
} else if (o.event === 'recovery') {
layer.confirm(
'还原会覆盖当前同名表的数据,确定继续?<br><span style="color:#ff5722">建议先做一次新备份</span>',
{title: '危险操作', btn: ['确定还原', '取消']},
function (index) {
layer.close(index);
post(FUNC.recovery, {name: o.data.name}, '还原完成', function () {
table.reload('tableList');
});
}
);
} else if (o.event === 'delete') {
layer.confirm('确定删除该备份?', {title: '提示'}, function (index) {
layer.close(index);
post(FUNC.delete, {name: o.data.name}, '已删除', function () {
table.reload('backupList');
});
});
}
});
// 搜索
form.on('submit(data-search-btn)', function (o) {
table.reload('tableList', {where: {keyword: o.field.keyword}, page: {curr: 1}});
return false;
});
$('#btn-reset').on('click', function () {
setTimeout(function () {
table.reload('tableList', {where: {keyword: ''}, page: {curr: 1}});
}, 10);
});
// 切 tab 时修正列宽(隐藏时渲染的表格宽度会算错)
layui.element.on('tab(dbTab)', function () {
table.resize('tableList');
table.resize('backupList');
});
}
};
obj.renderTable();
obj.renderBackup();
obj.events();
exports('database', obj);
});
+408
View File
@@ -0,0 +1,408 @@
/**
* 数据表管理:列表 / 可视化建表 / 查看结构 / 优化 / 删表
*
* 与 database.js 同构:URL 只写动作名(当前页面目录即 /admin/datatable/),
* 权限按钮用完整权限名,渲染后由 auth.filterDom() 移除无权限按钮。
*/
layui.define(['table', 'layer', 'form', 'element', 'auth', 'http', 'jquery'], function (exports) {
var table = layui.table,
layer = layui.layer,
form = layui.form,
element = layui.element,
auth = layui.auth,
http = layui.http,
$ = layui.jquery;
var FUNC = {
index: 'index',
structure: 'structure',
preview: 'preview',
create: 'create',
drop: 'drop',
optimize: 'optimize',
export: 'export'
};
// 与后端 Datatable::TYPES 保持一致
var TYPES = [
'tinyint', 'smallint', 'mediumint', 'int', 'bigint',
'decimal', 'float', 'double',
'char', 'varchar', 'text', 'mediumtext', 'longtext',
'date', 'datetime', 'timestamp', 'time', 'year',
'json', 'blob', 'longblob'
];
function parse(res) {
return {
code: res.code,
msg: res.message,
count: res.count || 0,
data: res.data || []
};
}
function post(url, data, okTip, done) {
var loading = layer.load(2);
http.post(url, data).then(function (res) {
layer.close(loading);
if (res && res.code === 0) {
layer.msg(okTip || res.message || '操作成功', {icon: 1});
if (done) { done(res.data); }
} else {
layer.msg((res && res.message) || '操作失败', {icon: 2});
}
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
}
function checkedTables() {
var rows = table.checkStatus('tableList').data || [];
var names = [];
for (var i = 0; i < rows.length; i++) {
names.push(rows[i].name);
}
return names;
}
/* ====================== 建表弹窗 ====================== */
/**
* 单行字段
*/
function fieldRow(data) {
data = data || {};
var opts = '';
for (var i = 0; i < TYPES.length; i++) {
opts += '<option value="' + TYPES[i] + '"' +
(data.type === TYPES[i] ? ' selected' : '') + '>' + TYPES[i] + '</option>';
}
return '' +
'<div class="layui-form-item field-row" style="margin-bottom:4px">' +
' <div class="layui-inline" style="width:120px"><input type="text" name="name" value="' + (data.name || '') + '" placeholder="字段名" class="layui-input layui-input-sm"></div>' +
' <div class="layui-inline" style="width:120px"><select name="type">' + opts + '</select></div>' +
' <div class="layui-inline" style="width:70px"><input type="text" name="length" value="' + (data.length || '') + '" placeholder="长度" class="layui-input"></div>' +
' <div class="layui-inline" style="width:100px"><input type="text" name="default" value="' + (data.default || '') + '" placeholder="默认值" class="layui-input"></div>' +
' <div class="layui-inline" style="width:130px"><input type="text" name="comment" value="' + (data.comment || '') + '" placeholder="注释" class="layui-input"></div>' +
' <div class="layui-inline" style="width:74px"><input type="checkbox" name="unsigned" value="1" title="无符号"' + (data.unsigned ? ' checked' : '') + '></div>' +
' <div class="layui-inline" style="width:60px"><input type="checkbox" name="nullable" value="1" title="可空"' + (data.nullable ? ' checked' : '') + '></div>' +
' <div class="layui-inline" style="width:100px"><input type="checkbox" name="autoinc" value="1" title="自增"' + (data.autoinc ? ' checked' : '') + '></div>' +
' <div class="layui-inline" style="width:60px"><input type="checkbox" name="pk" value="1" title="主键"' + (data.pk ? ' checked' : '') + '></div>' +
' <div class="layui-inline" style="width:90px"><select name="index">' +
' <option value="">无索引</option>' +
' <option value="index"' + (data.index === 'index' ? ' selected' : '') + '>普通</option>' +
' <option value="unique"' + (data.index === 'unique' ? ' selected' : '') + '>唯一</option>' +
' </select></div>' +
' <button type="button" class="layui-btn layui-btn-xs layui-btn-danger" data-act="remove-field">删</button>' +
'</div>';
}
function addRow(data) {
$('#fieldRows').append(fieldRow(data));
form.render(null, 'create-form');
}
/**
* 收集表单里的字段定义
*/
function collect() {
var base = form.val('create-form');
var fields = [];
$('#fieldRows .field-row').each(function () {
var r = $(this);
fields.push({
name: $.trim(r.find('input[name=name]').val() || ''),
type: r.find('select[name=type]').val(),
length: $.trim(r.find('input[name=length]').val() || ''),
default: $.trim(r.find('input[name=default]').val() || ''),
comment: $.trim(r.find('input[name=comment]').val() || ''),
unsigned: r.find('input[name=unsigned]').is(':checked') ? 1 : 0,
nullable: r.find('input[name=nullable]').is(':checked') ? 1 : 0,
autoinc: r.find('input[name=autoinc]').is(':checked') ? 1 : 0,
pk: r.find('input[name=pk]').is(':checked') ? 1 : 0,
index: r.find('select[name=index]').val()
});
});
return {
table: $.trim(base.table || ''),
comment: $.trim(base.comment || ''),
engine: base.engine,
charset: base.charset,
collation: '',
fields: fields
};
}
function openCreate() {
var idx = layer.open({
type: 1,
title: '新建数据表',
area: ['1080px', '640px'],
content: $('#createTpl').html(),
success: function () {
form.render(null, 'create-form');
$('#fieldRows').html('');
addRow({name: 'id', type: 'bigint', unsigned: 1, autoinc: 1, pk: 1, comment: 'ID'});
addRow({name: 'name', type: 'varchar', length: '100', default: '', comment: '名称'});
addRow({name: 'create_at', type: 'int', length: '', default: '0', comment: '创建时间'});
element.render();
}
});
$(document).off('click', '#btn-add-field').on('click', '#btn-add-field', function () {
addRow({type: 'varchar', length: '255'});
});
$(document).off('click', '#btn-add-id').on('click', '#btn-add-id', function () {
addRow({name: 'id', type: 'bigint', unsigned: 1, autoinc: 1, pk: 1, comment: 'ID'});
});
$(document).off('click', '[data-act=remove-field]').on('click', '[data-act=remove-field]', function () {
if ($('#fieldRows .field-row').length <= 1) {
return layer.msg('至少保留一个字段', {icon: 2});
}
$(this).closest('.field-row').remove();
});
// 生成预览(不执行)
$(document).off('click', '#btn-preview').on('click', '#btn-preview', function () {
var payload = collect();
if (payload.table === '') {
return layer.msg('请填写表名', {icon: 2});
}
post(FUNC.preview, payload, '', function (d) {
$('#sqlPreview').val(d.sql);
layer.msg('已生成,请确认后执行', {icon: 1});
});
});
// 执行建表
$(document).off('click', '#btn-exec').on('click', '#btn-exec', function () {
var payload = collect();
if (payload.table === '') {
return layer.msg('请填写表名', {icon: 2});
}
layer.confirm(
'确定创建表 <b>' + payload.table + '</b>?<br><span class="layui-word-aux">建议先「生成 SQL 预览」确认无误</span>',
{title: '建表确认'},
function (i) {
layer.close(i);
post(FUNC.create, payload, '创建成功', function (d) {
layer.close(idx);
layer.alert(
'表:<b>' + d.table + '</b><br>下一步生成 CRUD:<br><code>' + d.command + '</code>',
{title: '建表完成', btn: ['知道了']}
);
table.reload('tableList');
});
}
);
});
}
/* ====================== 结构弹窗 ====================== */
function openStructure(name) {
http.get(FUNC.structure, {table: name}).then(function (res) {
if (!res || res.code !== 0) {
return layer.msg((res && res.message) || '读取失败', {icon: 2});
}
var d = res.data;
var html = '';
for (var i = 0; i < d.columns.length; i++) {
var c = d.columns[i];
html += '<tr><td>' + c.field + '</td><td>' + c.type + '</td><td>' + c.null + '</td>' +
'<td>' + (c.default === null ? 'NULL' : c.default) + '</td>' +
'<td>' + (c.key || '-') + '</td><td>' + (c.extra || '-') + '</td><td>' + c.comment + '</td></tr>';
}
layer.open({
type: 1,
title: '表结构:' + name,
area: ['900px', '580px'],
content: $('#structureTpl').html(),
success: function () {
$('#columnBody').html(html || '<tr><td colspan="7">无字段</td></tr>');
$('#createSqlText').val(d.sql);
element.render();
}
});
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
}
/* ====================== 导出安装 SQL ====================== */
/**
* 取选中表的 CREATE TABLE(__PREFIX__ 形式),弹窗展示 + 复制 / 下载
*/
function openExport(names) {
if (!names.length) {
return layer.msg('请先勾选要导出的表', {icon: 2});
}
var loading = layer.load(2);
http.post(FUNC.export, {tables: names}).then(function (res) {
layer.close(loading);
if (!res || res.code !== 0) {
return layer.msg((res && res.message) || '导出失败', {icon: 2});
}
var d = res.data;
var rows = '';
for (var i = 0; i < d.items.length; i++) {
var it = d.items[i];
var badge = it.registered
? '<span class="layui-badge layui-bg-green">已归档</span>'
: '<span class="layui-badge layui-bg-orange">未归档</span>';
rows += '<tr><td><code>' + it.short + '</code></td><td>' + badge + '</td>' +
'<td>' + (it.target ? '<code>' + it.target + '</code>' : '<span class="layui-word-aux">需人工指定</span>') + '</td></tr>';
}
layer.open({
type: 1,
title: '导出安装 SQL(' + d.items.length + ' 张表)',
area: ['860px', '640px'],
content: $('#exportTpl').html(),
success: function () {
$('#exportBody').html(rows);
$('#exportSqlText').val(d.text);
$('#exportSummary').html(
(d.unregistered && d.unregistered.length
? '其中未归档:' + d.unregistered.join('、') + ',请确认写入哪个 install.sql。'
: '所选表均已登记,导出可用于核对结构是否漂移。')
);
}
});
$(document).off('click', '#btn-copy-sql').on('click', '#btn-copy-sql', function () {
var el = document.getElementById('exportSqlText');
el.select();
try {
document.execCommand('copy');
layer.msg('已复制', {icon: 1});
} catch (e) {
layer.msg('复制失败,请手动选中复制', {icon: 2});
}
});
$(document).off('click', '#btn-download-sql').on('click', '#btn-download-sql', function () {
var blob = new Blob([d.text], {type: 'text/sql;charset=utf-8'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'install_' + new Date().getTime() + '.sql';
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
});
}).catch(function (err) {
layer.close(loading);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), {icon: 2});
});
}
/* ====================== 删表 ====================== */
function dropTable(name) {
layer.prompt({
title: '危险:删除表不可恢复,请输入表名确认',
formType: 0,
value: ''
}, function (val, i) {
layer.close(i);
if (val !== name) {
return layer.msg('输入的表名不一致,已取消', {icon: 2});
}
post(FUNC.drop, {table: name, confirm: val}, '已删除', function () {
table.reload('tableList');
});
});
}
var obj = {
renderTable: function () {
table.render({
elem: '#tableList',
url: FUNC.index,
toolbar: '#tableBar',
defaultToolbar: ['filter'],
method: 'get',
cols: [[
{type: 'checkbox', width: 50},
{field: 'name', title: '表名', minWidth: 240},
{field: 'comment', title: '备注', minWidth: 160},
{field: 'engine', title: '引擎', width: 100},
{field: 'registered', title: '归档', width: 110, templet: function (d) {
return d.registered
? '<span class="layui-badge layui-bg-green">已归档</span>'
: '<span class="layui-badge layui-bg-orange" title="不在任何 install.sql,全新安装/自愈都不会建这张表">未归档</span>';
}},
{field: 'target', title: 'install.sql', minWidth: 170, templet: function (d) {
return d.target ? '<code>' + d.target + '</code>' : '<span class="layui-word-aux">—</span>';
}},
{field: 'rows', title: '行数', width: 110, sort: true},
{field: 'size_text', title: '大小', width: 110, sort: true},
{field: 'collation', title: '字符集', width: 160},
{title: '操作', width: 170, templet: '#tableAction', fixed: 'right'}
]],
page: true,
limit: 15,
limits: [15, 30, 50, 100],
response: {statusCode: 0},
parseData: parse,
done: function () {
var view = $('#tableList').next('.layui-table-view');
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
},
events: function () {
table.on('toolbar(tableList)', function (o) {
if (o.event === 'create') {
openCreate();
} else if (o.event === 'reload') {
table.reload('tableList');
} else if (o.event === 'optimize') {
var names = checkedTables();
if (!names.length) {
return layer.msg('请先勾选要优化的表', {icon: 2});
}
post(FUNC.optimize, {tables: names}, '优化完成', function () {
table.reload('tableList');
});
} else if (o.event === 'export') {
openExport(checkedTables());
}
});
table.on('tool(tableList)', function (o) {
if (o.event === 'structure') {
openStructure(o.data.name);
} else if (o.event === 'optimizeRow') {
post(FUNC.optimize, {tables: [o.data.name]}, '优化完成', function () {
table.reload('tableList');
});
} else if (o.event === 'exportRow') {
openExport([o.data.name]);
} else if (o.event === 'drop') {
dropTable(o.data.name);
}
});
form.on('submit(data-search-btn)', function (o) {
table.reload('tableList', {
where: {keyword: o.field.keyword, unregistered: o.field.unregistered || '0'},
page: {curr: 1}
});
return false;
});
$('#btn-reset').on('click', function () {
setTimeout(function () {
table.reload('tableList', {where: {keyword: '', unregistered: '0'}, page: {curr: 1}});
}, 10);
});
}
};
form.render(null, 'search-form'); // 归档筛选下拉要 layui 渲染,否则是原生 select
obj.renderTable();
obj.events();
exports('datatable', obj);
});
+2 -8
View File
@@ -24,14 +24,8 @@ layui.define(['http', 'auth'], function (exports) {
]]
, done: function (res, curr, count) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+2 -8
View File
@@ -32,14 +32,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+5
View File
@@ -130,6 +130,11 @@ layui.define(["ywxapp", "http", "route"], function (exports) {
isTop = isTop !== false;
var html = '';
$.each(data || [], function (i, item) {
// 分组标题(type=5):纯视觉分组,不可点击、不跳转
if (parseInt(item.type, 10) === 5) {
html += '<dd class="nav-group-title">' + item.title + '</dd>';
return;
}
var hasChild = item.child && item.child.length > 0;
var tag = isTop ? 'li' : 'dd';
var cls = isTop ? 'layui-nav-item' : '';
+2 -8
View File
@@ -48,14 +48,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function (res, curr, count) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+20 -12
View File
@@ -15,6 +15,24 @@ layui.define(['form', 'http'], function (exports) {
if (verifyImg) {
verifyImg.addEventListener('click', refreshVerifyCode);
}
/**
* 登录成功后自动跳转(兼容 iframe 场景)
* 原实现用 layer.alert 弹「请稍后」对话框,必须用户点「确定」(或等 15 秒倒计时)才跳转,
* 体验很差;这里改为轻提示 + 自动跳转。
* @param {string} url 目标地址
*/
function jump(url) {
layer.msg('登录成功,正在跳转…', { time: 800 });
setTimeout(function () {
if (window.self !== window.top) {
// 在 iframe 中,跳转顶级页面
window.top.location.href = url;
} else {
// 顶级窗口,直接跳转
window.location.href = url;
}
}, 600);
}
form.on('submit(dataSubmit)', function (obj) {
let data = obj.field;
layui.http.post("save", data)
@@ -26,18 +44,8 @@ layui.define(['form', 'http'], function (exports) {
} else {
layui.setter.setMenus(res.data.menus);
layui.setter.permissions(res.data.permissions);
layer.alert('请稍后!', {
time: 15000,
end: function () {
if (window.self !== window.top) {
// 在 iframe 中,跳转顶级页面
window.top.location.href = "/admin";
} else {
// 顶级窗口,直接跳转
window.location.href = "/admin";
}
}
})
// 自动跳转,无需再点「确定」
jump('/admin');
}
});
+2 -8
View File
@@ -32,14 +32,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+2 -8
View File
@@ -37,14 +37,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+11 -11
View File
@@ -52,6 +52,8 @@ layui.define(['layer', 'http', 'iconPicker', 'auth'], function (exports) {
return '<span class="layui-badge layui-bg-gray">按钮</span>';
} else if (d.type === 4) {
return '<span class="layui-badge layui-bg-gray">API</span>';
} else if (d.type === 5) {
return '<span class="layui-badge">分组标题</span>';
}
}
}, { field: 'route', title: '路由', minWidth: 200, sort: true },
@@ -67,14 +69,8 @@ layui.define(['layer', 'http', 'iconPicker', 'auth'], function (exports) {
page: true,
done: function (res, curr, count, origin) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
treeTable.on('toolbar(dataTable)', function (obj) {
@@ -115,15 +111,19 @@ layui.define(['layer', 'http', 'iconPicker', 'auth'], function (exports) {
var id = this.value;
var status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status ? '启用' : '禁用') + '该数据吗?', function (index) {
layui.request.post('update', { id: id, status: status }).then(function (res) {
if (res.code === 0) {
http.post('update', { id: id, status: status }).then(function (res) {
if (res && res.code === 0) {
layer.msg(res.msg, { icon: 1 });
table.reload('dataTable', {}, true);
} else {
layer.msg(res.msg, { icon: 2 });
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
layer.close(index);
}, function () {
+2 -8
View File
@@ -32,14 +32,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+2 -8
View File
@@ -23,14 +23,8 @@ layui.define(['http', 'auth'], function (exports) {
]]
, done: function (res, curr, count) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+2 -8
View File
@@ -72,14 +72,8 @@ layui.define(['layer', 'http', 'iconPicker', 'auth'], function (exports) {
skin: 'line',
done: function (res, curr, count, origin) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
treeTable.on('toolbar(dataTable)', function (obj) {
+44 -18
View File
@@ -9,6 +9,26 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
auth.init(_savedPerms);
auth.setController('score');
// 动作候选(核心预置 + 插件注册 + 规则表已使用),供 datalist 提示
var actionList = [];
function loadActions() {
http.get('actions').then(function (res) {
if (res.code === 0 && res.data) {
actionList = res.data;
var html = '';
layui.$.each(actionList, function (i, item) {
html += '<option value="' + item.action + '">' + item.title + (item.addon ? '(插件:' + item.addon + ')' : '') + '</option>';
});
$('#action-list').html(html);
}
}).catch(function () { });
}
loadActions();
function dateStr(v) {
return v ? layui.util.toDateString(v * 1000) : '-';
}
table.render({
elem: '#dataTable',
url: "index",
@@ -20,13 +40,15 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
cols: [[
{ type: "checkbox", width: 50 },
{ field: 'id', width: 80, title: 'ID', sort: true },
{ field: 'name', minWidth: 160, title: '规则名称' },
{ field: 'action', width: 160, title: '触发动作' },
{ field: 'title', minWidth: 160, title: '规则名称' },
{ field: 'action', width: 170, title: '触发动作' },
{ field: 'type_text', width: 90, title: '类型', templet: '#typeTpl' },
{ field: 'value', width: 90, title: '变动值', sort: true },
{ field: 'sort', width: 90, title: '排序', sort: true },
{ field: 'limit_text', width: 110, title: '限次' },
{ field: 'addon', width: 100, title: '归属', templet: '#addonTpl' },
{ field: 'sort', width: 80, title: '排序', sort: true },
{ field: 'status', width: 100, title: '状态', templet: '#statusTpl' },
{ field: 'create_time', width: 180, title: '创建时间' },
{ field: 'create_at', width: 170, title: '创建时间', templet: function (d) { return dateStr(d.create_at); } },
{ title: '操作', minWidth: 120, toolbar: '#dataBar', align: "center" }
]],
limits: [10, 15, 20, 25, 50, 100],
@@ -35,24 +57,20 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
form.on('submit(data-search-btn)', function (data) {
table.reload('dataTable', { page: { curr: 1 }, where: { name: data.field.name } });
table.reload('dataTable', { page: { curr: 1 }, where: { title: data.field.title, action: data.field.action } });
return false;
});
$('#btn-reset').on('click', function () {
$('input[name="name"]').val('');
table.reload('dataTable', { page: { curr: 1 }, where: { name: '' } });
$('input[name="title"]').val('');
$('select[name="action"]').val('');
form.render('select');
table.reload('dataTable', { page: { curr: 1 }, where: { title: '', action: '' } });
});
table.on('toolbar(dataTable)', function (elem) {
@@ -91,7 +109,15 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
title: title, content: formHtml, anim: "slideLeft", offset: "r",
btnAlign: "l", area: [window.width > 768 ? '50%' : '99%', '99%'],
shade: 0.1, shadeClose: true, btn: ['确定', '取消'],
success: function (layero, index) { callback(layero, index); form.render(); },
success: function (layero, index) {
// 重新填充动作候选(弹层是新建 DOM)
var html = '';
layui.$.each(actionList, function (i, item) {
html += '<option value="' + item.action + '">' + item.title + (item.addon ? '(插件:' + item.addon + ')' : '') + '</option>';
});
layero.find('#action-list').html(html);
callback(layero, index); form.render();
},
yes: function (index, layero) {
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
done(layero, index, elem); layui.off('submit(wxapp-form)', 'from'); return false;
@@ -145,9 +171,9 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'name', title: '规则名称', minWidth: 160, fixed: 'left' },
{ field: 'title', title: '规则名称', minWidth: 160, fixed: 'left' },
{ field: 'sort', title: '排序', width: 80 },
{ field: 'delete_at', title: '删除时间', width: 160 },
{ field: 'delete_at', title: '删除时间', width: 170, templet: function (d) { return dateStr(d.delete_at); } },
{ fixed: "right", title: "操作", width: 140, align: "center", toolbar: "#dataRecybinBarTpl" }
]],
page: true
-179
View File
@@ -1,179 +0,0 @@
layui.define(['form', 'table', 'http', 'auth'], function (exports) {
var $ = layui.jquery,
form = layui.form,
table = layui.table,
layer = layui.layer,
http = layui.http, laytpl = layui.laytpl
, auth = layui.auth;
var _savedPerms = (layui.data('backend').permission) || [];
auth.init(_savedPerms);
auth.setController('shop');
table.render({
elem: '#dataTable',
url: "index",
toolbar: '#tableBar',
defaultToolbar: ['filter', 'exports', 'print'],
parseData: function (res) {
return { code: res.code === 0 ? 0 : 1, msg: res.message || '', count: res.count || 0, data: res.data || [] };
},
cols: [[
{ type: "checkbox", width: 50 },
{ field: 'id', width: 80, title: 'ID', sort: true },
{ field: 'title', minWidth: 160, title: '商品名称' },
{ field: 'sort', width: 100, title: '排序', sort: true },
{ field: 'status', width: 100, title: '状态', templet: '#statusTpl' },
{ field: 'create_time', width: 180, title: '创建时间' },
{ title: '操作', minWidth: 150, toolbar: '#dataBar', align: "center" }
]],
limits: [10, 15, 20, 25, 50, 100],
limit: 15,
page: true,
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
}
});
form.on('submit(data-search-btn)', function (data) {
table.reload('dataTable', { page: { curr: 1 }, where: { title: data.field.title } });
return false;
});
$('#btn-reset').on('click', function () {
$('input[name=\"title\"]').val('');
table.reload('dataTable', { page: { curr: 1 }, where: { title: '' } });
});
table.on('toolbar(dataTable)', function (elem) {
switch (elem.event) {
case 'dataCreate': active.dataCreate({}); break;
case 'dataDelete':
var check = table.checkStatus('dataTable').data;
active.dataDelete(check.map(function (i) { return i.id; }));
break;
case 'dataRecybin': active.dataRecybin(); break;
}
});
table.on('tool(dataTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'update': active.dataEdit(data); break;
case 'delete': active.dataDelete([data.id]); break;
}
});
form.on('switch(statusSwitch)', function (obj) {
var status = obj.elem.checked ? 1 : 0;
http.put('update', { id: this.value, status: status })
.then(function (res) {
if (res.code === 0) { layer.msg('操作成功'); }
else { layer.msg(res.msg || '操作失败'); }
table.reload('dataTable', {}, true);
})
.catch(function () { table.reload('dataTable', {}, true); });
});
var dataFromFun = function (data, callback, done) {
var title = data.id ? '编辑电子商务' : '添加电子商务';
var formHtml = laytpl($('#dataFormTpl').html()).render(data || {});
layer.open({
title: title, content: formHtml, anim: "slideLeft", offset: "r",
btnAlign: "l", area: [window.width > 768 ? '50%' : '99%', '99%'],
shade: 0.1, shadeClose: true, btn: ['确定', '取消'],
success: function (layero, index) { callback(layero, index); form.render(); },
yes: function (index, layero) {
window.layui.form.on('submit(wxapp-form-submit)', function (elem) {
done(layero, index, elem); layui.off('submit(wxapp-form)', 'from'); return false;
});
layero.contents().find("#wxapp-form-submit").trigger('click');
}
});
};
var active = {
dataDelete: function (ids, force) {
if (ids.length === 0) { return layer.msg('请选择数据'); }
layer.confirm('确定删除吗?' + (force ? '(彻底删除,不可恢复)' : '(可到回收站恢复)'), function (index) {
layer.close(index);
http.delete('delete', { ids: ids.join(','), force: force ? 1 : 0 })
.then(function (res) { if (res.code === 0) { table.reload('dataTable', {}, true); } else { layer.msg(res.msg || '操作失败'); } });
});
},
dataCreate: function (data) {
dataFromFun(data, function () {}, function (layero, index, elem) {
var field = elem.field;
if (field.status === undefined) field.status = 0;
field.status = field.status ? 1 : 0;
http.post('save', field).then(function (res) {
if (res.code === 0) { layer.close(index); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
});
},
dataEdit: function (data) {
dataFromFun(data, function () {}, function (layero, index, elem) {
var field = elem.field;
field.status = field.status ? 1 : 0;
http.put('update', field).then(function (res) {
if (res.code === 0) { layer.close(index); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
});
},
dataRecybin: function () {
var formHtml = laytpl($('#dataRecybinTpl').html()).render({});
layer.open({
title: '资源回收站', content: formHtml, anim: "slideLeft", offset: "r",
area: ['50%', '99%'], shade: 0.1, shadeClose: true,
success: function () {
table.render({
elem: '#dataRecybinTable', url: "recyclebin", height: 'full-120',
parseData: function (res) { return { code: res.code === 0 ? 0 : 1, msg: res.message || '', count: res.count || 0, data: res.data || [] }; },
cols: [[
{ type: 'checkbox', fixed: 'left' },
{ field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left' },
{ field: 'title', title: '商品名称', minWidth: 160, fixed: 'left' },
{ field: 'sort', title: '排序', width: 80 },
{ field: 'delete_at', title: '删除时间', width: 160 },
{ fixed: "right", title: "操作", width: 140, align: "center", toolbar: "#dataRecybinBarTpl" }
]],
page: true
});
}
});
},
dataRestore: function (data) {
http.put("restore", { ids: data.id }).then(function (res) {
if (res.code === 0) { layer.msg('恢复成功'); table.reload('dataRecybinTable', {}, true); table.reload('dataTable', {}, true); }
else { layer.msg(res.msg || '恢复失败'); }
});
},
dataForceDelete: function (data) {
layer.confirm('彻底删除后不可恢复,确定吗?', function (index) {
layer.close(index);
http.delete('delete', { ids: data.id, force: 1 }).then(function (res) {
if (res.code === 0) { layer.msg('已彻底删除'); table.reload('dataRecybinTable', {}, true); }
else { layer.msg(res.msg || '操作失败'); }
});
});
}
};
table.on('tool(dataRecybinTable)', function (elem) {
var data = elem.data;
switch (elem.event) {
case 'restore': active.dataRestore(data); break;
case 'forcedelete': active.dataForceDelete(data); break;
}
});
exports('shop', {});
});
+2 -8
View File
@@ -32,14 +32,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+104 -37
View File
@@ -14,35 +14,71 @@ layui.define(['form', 'table', 'http', 'auth', 'laydate'], function (exports) {
laydate.render({ elem: '#dateInput' });
// 汇总 + 趋势(纯 CSS 迷你条形图,不依赖 echarts)
http.get('stats').then(function (res) {
if (res.code !== 0) { return; }
var summary = (res.data && res.data.summary) || [];
var trend = (res.data && res.data.trend) || { dates: [], series: [] };
var $body = $('#summaryBody').empty();
if (summary.length === 0) {
$body.append('<tr><td colspan="6" style="text-align:center;color:#999;">暂无蜘蛛来访记录</td></tr>');
}
var seriesMap = {};
(trend.series || []).forEach(function (s) { seriesMap[s.name] = s.data; });
summary.forEach(function (row) {
var line = seriesMap[row.label] || [];
var max = Math.max.apply(null, line.concat([1]));
var bars = line.map(function (v, i) {
var h = v > 0 ? Math.max(2, Math.round(v / max * 28)) : 1;
return '<span title="' + (trend.dates[i] || '') + ': ' + v + '" style="display:inline-block;width:5px;margin-right:1px;height:' + h + 'px;background:' + (v > 0 ? '#1E9FFF' : '#e2e2e2') + ';vertical-align:bottom;"></span>';
}).join('');
$body.append('<tr>'
+ '<td>' + row.label + ' <span style="color:#999;">(' + row.spider + ')</span></td>'
+ '<td>' + row.today + '</td>'
+ '<td>' + row.week + '</td>'
+ '<td>' + row.month + '</td>'
+ '<td>' + row.total + '</td>'
+ '<td><div style="height:30px;line-height:30px;">' + bars + '</div></td>'
+ '</tr>');
$('#spiderSelect').append('<option value="' + row.spider + '">' + row.label + '</option>');
function loadStats() {
http.get('stats').then(function (res) {
if (res.code !== 0) { return; }
var summary = (res.data && res.data.summary) || [];
var trend = (res.data && res.data.trend) || { dates: [], series: [] };
var $body = $('#summaryBody').empty();
// DNS 熔断提示
if (res.data && res.data.dns_down) {
$('#dnsWarn').show().html('DNS 查询连续失败,真伪校验已临时降级(5 分钟后自动恢复)。请检查服务器 DNS 配置或 /etc/resolv.conf。');
} else {
$('#dnsWarn').hide();
}
if (summary.length === 0) {
$body.append('<tr><td colspan="8" style="text-align:center;color:#999;">暂无蜘蛛来访记录</td></tr>');
return;
}
var seriesMap = {};
(trend.series || []).forEach(function (s) { seriesMap[s.name] = s.data; });
// 蜘蛛下拉(去重,避免重复追加)
var $sel = $('#spiderSelect');
if ($sel.find('option').length <= 1) {
summary.forEach(function (row) {
$sel.append('<option value="' + row.spider + '">' + row.label + '</option>');
});
form.render('select');
}
summary.forEach(function (row) {
var line = seriesMap[row.label] || [];
var max = Math.max.apply(null, line.concat([1]));
var bars = line.map(function (v, i) {
var h = v > 0 ? Math.max(2, Math.round(v / max * 28)) : 1;
return '<span title="' + (trend.dates[i] || '') + ': ' + v + '" style="display:inline-block;width:5px;margin-right:1px;height:' + h + 'px;background:' + (v > 0 ? '#1E9FFF' : '#e2e2e2') + ';vertical-align:bottom;"></span>';
}).join('');
// 不支持 rDNS 校验的蜘蛛,真伪列显示占位说明
var realCell, fakeCell;
if (row.verifiable === false) {
realCell = '<span style="color:#999;" title="该蜘蛛官方不提供 rDNS,无法校验真伪">—</span>';
fakeCell = '<span style="color:#999;">—</span>';
} else {
realCell = '<span style="color:#118c26;">' + (row.real || 0) + '</span>';
fakeCell = (row.fake > 0)
? '<span style="color:#d81e06;font-weight:600;" title="伪造 UA 的抓取次数">' + row.fake + '</span>'
: '0';
}
$body.append('<tr>'
+ '<td>' + row.label + ' <span style="color:#999;">(' + row.spider + ')</span></td>'
+ '<td>' + row.today + '</td>'
+ '<td>' + row.week + '</td>'
+ '<td>' + row.month + '</td>'
+ '<td>' + row.total + '</td>'
+ '<td>' + realCell + '</td>'
+ '<td>' + fakeCell + '</td>'
+ '<td><div style="height:30px;line-height:30px;">' + bars + '</div></td>'
+ '</tr>');
});
});
form.render('select');
});
}
loadStats();
// 明细表
table.render({
@@ -61,10 +97,22 @@ layui.define(['form', 'table', 'http', 'auth', 'laydate'], function (exports) {
cols: [[
{ field: 'id', width: 90, title: 'ID', sort: true },
{ field: 'spider_text', width: 110, title: '蜘蛛' },
{ field: 'url', title: '抓取URL', minWidth: 240 },
{ field: 'verified_text', width: 100, title: '真伪', templet: function (d) {
if (d.verified === 1) {
return '<span style="color:#118c26;">真蜘蛛</span>';
}
if (d.verified === 2) {
return '<span style="color:#d81e06;font-weight:600;" title="' + (d.rdns || '无 rDNS 记录') + '">假蜘蛛</span>';
}
return '<span style="color:#999;" title="' + (d.verifiable ? '尚未校验或 DNS 暂不可用' : '该蜘蛛不支持 rDNS 校验') + '">待验证</span>';
}},
{ field: 'rdns', width: 220, title: 'rDNS', templet: function (d) {
return d.rdns ? '<span style="font-size:12px;">' + d.rdns + '</span>' : '<span style="color:#ccc;">—</span>';
}},
{ field: 'url', title: '抓取URL', minWidth: 220 },
{ field: 'ip', width: 140, title: '来源IP' },
{ field: 'app', width: 100, title: '应用' },
{ field: 'http_code', width: 90, title: '状态码' },
{ field: 'app', width: 90, title: '应用' },
{ field: 'http_code', width: 80, title: '状态码' },
{ field: 'time_text', width: 170, title: '抓取时间' }
]],
limits: [15, 30, 50, 100],
@@ -73,10 +121,7 @@ layui.define(['form', 'table', 'http', 'auth', 'laydate'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
}
});
@@ -89,12 +134,34 @@ layui.define(['form', 'table', 'http', 'auth', 'laydate'], function (exports) {
$('#btn-reset').on('click', function () {
$('input[name="ip"],input[name="url"],input[name="date"]').val('');
$('#spiderSelect').val('');
$('#verifiedSelect').val('');
form.render('select');
table.reload('dataTable', { page: { curr: 1 }, where: { spider: '', ip: '', url: '', date: '' } });
table.reload('dataTable', { page: { curr: 1 }, where: { spider: '', verified: '', ip: '', url: '', date: '' } });
});
// 工具条:清理旧日志
// 工具条:重新校验 / 清理旧日志
table.on('toolbar(dataTable)', function (elem) {
if (elem.event === 'dataRecheck') {
layer.confirm('将对历史上「待验证」的蜘蛛记录重新做 rDNS 双向校验(每 IP 一次 DNS 查询,最多 200 个 IP),可能耗时数秒。是否继续?', {
title: '重新校验真伪',
btn: ['开始校验', '取消']
}, function (index) {
layer.close(index);
var loadIndex = layer.load(2, { shade: [0.3, '#000'] });
http.post('recheck', { limit: 200 }).then(function (res) {
layer.close(loadIndex);
if (res.code === 0) {
layer.msg(res.message || '校验完成', { icon: 1, time: 5000 });
table.reload('dataTable', {}, true);
loadStats();
} else {
layer.msg(res.msg || res.message || '校验失败', { icon: 2 });
}
});
});
return;
}
if (elem.event === 'dataClear') {
layer.prompt({ title: '保留最近多少天的明细?(聚合报表不受影响)', value: '30' }, function (value, index) {
layer.close(index);
+2 -8
View File
@@ -34,14 +34,8 @@ layui.define(['form', 'table', 'http', 'auth'], function (exports) {
skin: 'line',
done: function () {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
+43 -70
View File
@@ -34,14 +34,8 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
]],
done: function (res, curr, count) {
var view = $('#dataTable').next('.layui-table-view');
view.find('[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
$('.layui-btn[data-perm]').each(function () {
var perm = $(this).attr('data-perm');
if (perm && !auth.has(perm)) { $(this).remove(); }
});
auth.filterDom(view);
auth.filterDom('.layui-btn[data-perm]');
}
});
form.on('submit(searchBtn)', function (data) {
@@ -274,23 +268,17 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
// 提交表单
form.on('submit(adminFormSubmit)', function (formData) {
var url = data ? 'update' : 'create';
$.ajax({
url: url,
type: 'POST',
data: formData.field,
success: function (res) {
if (res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
layer.close(index);
adminTable.reload();
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post(url, formData.field).then(function (res) {
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
layer.close(index);
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
return false;
@@ -312,21 +300,16 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
btn: ['确定', '取消'],
title: '重置密码'
}, function (index) {
$.ajax({
url: 'resetPassword',
type: 'POST',
data: { id: data.uid },
success: function (res) {
layer.close(index);
if (res.code === 1) {
layer.msg(res.msg, { icon: 1 });
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post('resetPassword', { id: data.uid }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1 });
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
@@ -338,23 +321,18 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
title: '删除管理员',
icon: 3
}, function (index) {
$.ajax({
url: 'delete',
type: 'POST',
data: { uid: data.uid },
success: function (res) {
layer.close(index);
if (res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post('delete', { uid: data.uid }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}
@@ -380,23 +358,18 @@ layui.define(['http', 'layer', 'laydate', 'util', 'auth'], function (exports) {
title: '批量删除',
icon: 3
}, function (index) {
$.ajax({
url: 'batchDelete',
type: 'POST',
data: { ids: ids },
success: function (res) {
layer.close(index);
if (res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg(res.msg, { icon: 2 });
}
},
error: function () {
layer.msg('操作失败', { icon: 2 });
http.post('batchDelete', { ids: ids }).then(function (res) {
layer.close(index);
if (res && res.code === 1) {
layer.msg(res.msg, { icon: 1, time: 1000 }, function () {
adminTable.reload();
});
} else {
layer.msg((res && res.msg) || '操作失败', { icon: 2 });
}
}).catch(function (err) {
layer.close(index);
layer.msg('请求失败:' + (err && err.message ? err.message : '网络异常'), { icon: 2 });
});
});
}