304 lines
10 KiB
PHP
304 lines
10 KiB
PHP
@extends('admin.content')
|
||
|
||
@section('title', '仪表盘')
|
||
|
||
@section('content')
|
||
@php $portal = $portal ?? 'admin'; @endphp
|
||
<style>
|
||
.dash-grid { display: grid; gap: 12px; margin-bottom: 12px; }
|
||
.dash-grid-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||
.dash-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
@media screen and (max-width: 992px) {
|
||
.dash-grid-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.dash-grid-2 { grid-template-columns: 1fr; }
|
||
}
|
||
.dash-tile {
|
||
background: #fff;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 2px;
|
||
min-height: 108px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.dash-tile-hd {
|
||
padding: 10px 14px 0;
|
||
font-size: 13px;
|
||
color: #666;
|
||
line-height: 1.4;
|
||
}
|
||
.dash-tile-bd {
|
||
padding: 8px 14px 12px;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
.dash-metric { font-size: 26px; line-height: 1.25; color: #333; word-break: break-all; }
|
||
.dash-metric-hl { color: #009688; font-weight: 600; }
|
||
.dash-metric-sep { color: #bbb; margin: 0 4px; font-weight: 400; }
|
||
.dash-metric-total { color: #333; }
|
||
.dash-metric-caption {
|
||
display: block;
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
line-height: 1.3;
|
||
color: #999;
|
||
font-weight: 400;
|
||
}
|
||
.dash-section-label {
|
||
margin: 4px 0 8px;
|
||
font-size: 13px;
|
||
color: #999;
|
||
}
|
||
.dash-filter .layui-form-item { margin-bottom: 0; }
|
||
.dash-filter .layui-inline {
|
||
margin-right: 10px;
|
||
margin-bottom: 0;
|
||
}
|
||
.dash-filter .layui-form-label {
|
||
width: 72px;
|
||
padding: 9px 10px 9px 0;
|
||
text-align: left;
|
||
box-sizing: border-box;
|
||
}
|
||
.dash-filter .layui-input-block {
|
||
margin-left: 72px;
|
||
min-height: 36px;
|
||
}
|
||
.dash-filter .dash-date-input {
|
||
width: 210px;
|
||
}
|
||
.dash-filter .dash-date-shortcuts {
|
||
margin-left: 0;
|
||
margin-right: 4px;
|
||
}
|
||
.dash-filter .dash-date-shortcuts .layui-btn {
|
||
margin: 0 4px 0 0;
|
||
height: 36px;
|
||
line-height: 36px;
|
||
padding: 0 12px;
|
||
}
|
||
.dash-filter .dash-channel-block {
|
||
width: 170px;
|
||
min-width: 170px !important;
|
||
}
|
||
.dash-filter .dash-agent-block {
|
||
width: 150px;
|
||
min-width: 150px !important;
|
||
}
|
||
</style>
|
||
<div class="layui-card">
|
||
<form class="layui-form layui-card-header layuiadmin-card-header-auto dash-filter" lay-filter="LAY-dash-search">
|
||
<div class="layui-form-item">
|
||
<div class="layui-inline">
|
||
<label class="layui-form-label">时间</label>
|
||
<div class="layui-input-block dash-date-input">
|
||
<input type="text" name="date_range" id="LAY-dash-date-range" placeholder="开始日期 - 结束日期" autocomplete="off" class="layui-input">
|
||
</div>
|
||
</div>
|
||
<div class="layui-inline dash-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="month">本月</button>
|
||
</div>
|
||
@include('admin.partials.filter_channel_select', ['channelBlockClass' => 'dash-channel-block'])
|
||
@include('admin.partials.filter_agent_select', ['agentBlockClass' => 'dash-agent-block'])
|
||
<div class="layui-inline">
|
||
<button class="layui-btn" lay-submit lay-filter="LAY-dash-search">查询</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
<div class="layui-card-body">
|
||
<div class="dash-section-label">设备与资产</div>
|
||
<div class="dash-grid dash-grid-5">
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">总设备数</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric" id="dash-total">—</div>
|
||
<span class="dash-metric-caption">有钱包设备/总设备(区间内新增)</span>
|
||
</div>
|
||
</div>
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">新增设备</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric dash-metric-hl" id="dash-new">—</div>
|
||
</div>
|
||
</div>
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">活跃设备</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric" id="dash-active">—</div>
|
||
</div>
|
||
</div>
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">助记词</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric dash-metric-hl" id="dash-mnemonics">—</div>
|
||
</div>
|
||
</div>
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">钱包地址</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric dash-metric-hl" id="dash-addresses">—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dash-section-label">余额 / 交易额<span style="margin-left:8px;font-size:12px;">新增地址余额 · 成功转出 <span id="dash-tx-count">0</span> 笔</span></div>
|
||
<div class="dash-grid dash-grid-5">
|
||
@foreach (['usdt' => 'USDT', 'trx' => 'TRX', 'btc' => 'BTC', 'eth' => 'ETH', 'bnb' => 'BNB'] as $coin => $label)
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">{{ $label }}</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric" id="dash-coin-{{ $coin }}">—</div>
|
||
<span class="dash-metric-caption">余额/转出</span>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</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>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric" id="dash-pv">—</div>
|
||
<span class="dash-metric-caption">有效 PV/总 PV</span>
|
||
</div>
|
||
</div>
|
||
<div class="dash-tile">
|
||
<div class="dash-tile-hd">页面 UV</div>
|
||
<div class="dash-tile-bd">
|
||
<div class="dash-metric" id="dash-uv">—</div>
|
||
<span class="dash-metric-caption">有效 UV/总 UV</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layui-word-aux" id="dash-range" style="margin-top:8px;"></div>
|
||
</div>
|
||
</div>
|
||
@endsection
|
||
|
||
@push('scripts')
|
||
<script>
|
||
layui.use(['form', 'laydate', 'layer'], function () {
|
||
var form = layui.form;
|
||
var laydate = layui.laydate;
|
||
var layer = layui.layer;
|
||
var $ = layui.$;
|
||
var dataUrl = @json(route(($portal ?? 'admin').'.dashboard.data'));
|
||
|
||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||
function fmtDate(d) {
|
||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
||
}
|
||
function rangeOf(kind) {
|
||
var today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
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 fmtDate(today) + ' - ' + fmtDate(today);
|
||
}
|
||
|
||
var initialRange = rangeOf('today');
|
||
$('#LAY-dash-date-range').val(initialRange);
|
||
laydate.render({
|
||
elem: '#LAY-dash-date-range',
|
||
type: 'date',
|
||
range: true,
|
||
value: initialRange,
|
||
done: function (value) {
|
||
if (value) $('#LAY-dash-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 field = form.val('LAY-dash-search') || {};
|
||
var parsed = parseRange(field.date_range || $('#LAY-dash-date-range').val());
|
||
if (!parsed) return null;
|
||
var where = Object.assign({}, field, parsed);
|
||
delete where.range;
|
||
return where;
|
||
}
|
||
|
||
function ratioHtml(effective, total) {
|
||
return '<span class="dash-metric-hl">' + effective + '</span>' +
|
||
'<span class="dash-metric-sep">/</span>' +
|
||
'<span class="dash-metric-total">' + total + '</span>';
|
||
}
|
||
|
||
function load(where) {
|
||
$.getJSON(dataUrl, where || parseRange(initialRange), function (res) {
|
||
if (!res || res.code !== 0) return;
|
||
$('#dash-total').html(ratioHtml(res.data.wallet_count || 0, res.data.total || 0));
|
||
$('#dash-new').text(res.data.new_count);
|
||
$('#dash-active').text(res.data.active_count);
|
||
$('#dash-mnemonics').text(res.data.mnemonic_count || 0);
|
||
$('#dash-addresses').text(res.data.address_count || 0);
|
||
$('#dash-pv').html(ratioHtml(res.data.effective_pv || 0, res.data.pv || 0));
|
||
$('#dash-uv').html(ratioHtml(res.data.effective_uv || 0, res.data.uv || 0));
|
||
var bal = res.data.balances || {};
|
||
var tx = res.data.transfers || {};
|
||
$('#dash-tx-count').text(res.data.transfer_count || 0);
|
||
['usdt', 'trx', 'btc', 'eth', 'bnb'].forEach(function (c) {
|
||
var left = bal[c] != null && bal[c] !== '' ? bal[c] : '0';
|
||
var right = c === 'bnb' ? '—' : (tx[c] != null && tx[c] !== '' ? tx[c] : '0');
|
||
$('#dash-coin-' + c).html(ratioHtml(left, right));
|
||
});
|
||
$('#dash-range').text('统计区间:' + res.data.from + ' ~ ' + res.data.to);
|
||
});
|
||
}
|
||
|
||
function applyRangeAndQuery(rangeText) {
|
||
$('#LAY-dash-date-range').val(rangeText);
|
||
var where = currentWhere();
|
||
if (!where) {
|
||
layer.msg('请选择有效的时间范围');
|
||
return;
|
||
}
|
||
load(where);
|
||
}
|
||
|
||
$('.dash-date-shortcuts').on('click', '[data-range]', function () {
|
||
applyRangeAndQuery(rangeOf($(this).data('range')));
|
||
});
|
||
|
||
form.on('submit(LAY-dash-search)', function () {
|
||
var where = currentWhere();
|
||
if (!where) {
|
||
layer.msg('请选择有效的时间范围');
|
||
return false;
|
||
}
|
||
load(where);
|
||
return false;
|
||
});
|
||
|
||
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
|
||
load(parseRange(initialRange));
|
||
});
|
||
</script>
|
||
@endpush
|