feat: new channel
This commit is contained in:
@@ -21,11 +21,23 @@ class ChannelController extends Controller
|
|||||||
{
|
{
|
||||||
$agents = $this->isAgentPortal()
|
$agents = $this->isAgentPortal()
|
||||||
? collect()
|
? collect()
|
||||||
: User::query()->orderBy('username')->get(['id', 'username']);
|
: User::query()->orderBy('username')->get(['id', 'username', 'comment']);
|
||||||
|
|
||||||
|
$agentOptions = $agents->map(function (User $a) {
|
||||||
|
$name = trim((string) $a->username);
|
||||||
|
$comment = trim((string) ($a->comment ?? ''));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $a->id,
|
||||||
|
'username' => $name,
|
||||||
|
'label' => $comment !== '' ? $name.'/'.$comment : $name,
|
||||||
|
];
|
||||||
|
})->values();
|
||||||
|
|
||||||
return view('admin.channels.index', [
|
return view('admin.channels.index', [
|
||||||
'portal' => $this->portal(),
|
'portal' => $this->portal(),
|
||||||
'agents' => $agents,
|
'agents' => $agents,
|
||||||
|
'agentOptions' => $agentOptions,
|
||||||
'maxPerAgent' => Channel::maxPerAgent(),
|
'maxPerAgent' => Channel::maxPerAgent(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class DashboardController extends Controller
|
|||||||
public function data(Request $request)
|
public function data(Request $request)
|
||||||
{
|
{
|
||||||
$data = $this->stats->collect([
|
$data = $this->stats->collect([
|
||||||
|
'date_from' => trim((string) $request->query('date_from', '')),
|
||||||
|
'date_to' => trim((string) $request->query('date_to', '')),
|
||||||
|
'date_range' => trim((string) $request->query('date_range', '')),
|
||||||
'range' => (string) $request->query('range', '30d'),
|
'range' => (string) $request->query('range', '30d'),
|
||||||
'channel_id' => trim((string) $request->query('channel_id', '')),
|
'channel_id' => trim((string) $request->query('channel_id', '')),
|
||||||
'agent_user_id' => $this->isAgentPortal()
|
'agent_user_id' => $this->isAgentPortal()
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class DashboardStatsService
|
|||||||
/**
|
/**
|
||||||
* @param array{
|
* @param array{
|
||||||
* range?: string,
|
* range?: string,
|
||||||
|
* date_from?: string,
|
||||||
|
* date_to?: string,
|
||||||
|
* date_range?: string,
|
||||||
* channel_id?: string,
|
* channel_id?: string,
|
||||||
* channel_exact?: bool,
|
* channel_exact?: bool,
|
||||||
* agent_user_id?: ?int,
|
* agent_user_id?: ?int,
|
||||||
@@ -44,8 +47,8 @@ class DashboardStatsService
|
|||||||
*/
|
*/
|
||||||
public function collect(array $filters = []): array
|
public function collect(array $filters = []): array
|
||||||
{
|
{
|
||||||
$range = (string) ($filters['range'] ?? '30d');
|
[$from, $to] = $this->resolveBounds($filters);
|
||||||
[$from, $to] = $this->rangeBounds($range);
|
$rangeLabel = $from->toDateString().' ~ '.$to->toDateString();
|
||||||
|
|
||||||
$channelId = trim((string) ($filters['channel_id'] ?? ''));
|
$channelId = trim((string) ($filters['channel_id'] ?? ''));
|
||||||
$channelExact = (bool) ($filters['channel_exact'] ?? false);
|
$channelExact = (bool) ($filters['channel_exact'] ?? false);
|
||||||
@@ -68,10 +71,13 @@ class DashboardStatsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = (clone $base)->count();
|
// Period cohort: devices installed (created) in the selected range.
|
||||||
$walletCount = (clone $base)->where('has_wallet', Device::WALLET_YES)->count();
|
$createdInRange = (clone $base)->whereBetween('created_at', [$from, $to]);
|
||||||
$newCount = (clone $base)->whereBetween('created_at', [$from, $to])->count();
|
$total = (clone $createdInRange)->count();
|
||||||
|
$walletCount = (clone $createdInRange)->where('has_wallet', Device::WALLET_YES)->count();
|
||||||
|
$newCount = $total;
|
||||||
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
|
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
|
||||||
|
|
||||||
$mnemonicCount = $this->countDeviceChildren(
|
$mnemonicCount = $this->countDeviceChildren(
|
||||||
WalletMnemonic::query(),
|
WalletMnemonic::query(),
|
||||||
'wallet_mnemonics',
|
'wallet_mnemonics',
|
||||||
@@ -79,6 +85,8 @@ class DashboardStatsService
|
|||||||
$agentUserId,
|
$agentUserId,
|
||||||
$channelId,
|
$channelId,
|
||||||
$channelExact,
|
$channelExact,
|
||||||
|
$from,
|
||||||
|
$to,
|
||||||
);
|
);
|
||||||
$addressCount = $this->countDeviceChildren(
|
$addressCount = $this->countDeviceChildren(
|
||||||
WalletAddress::query(),
|
WalletAddress::query(),
|
||||||
@@ -87,8 +95,10 @@ class DashboardStatsService
|
|||||||
$agentUserId,
|
$agentUserId,
|
||||||
$channelId,
|
$channelId,
|
||||||
$channelExact,
|
$channelExact,
|
||||||
|
$from,
|
||||||
|
$to,
|
||||||
);
|
);
|
||||||
$balances = $this->sumBalances($agent, $agentUserId, $channelId, $channelExact);
|
$balances = $this->sumBalances($agent, $agentUserId, $channelId, $channelExact, $from, $to);
|
||||||
|
|
||||||
$visits = PageVisit::query();
|
$visits = PageVisit::query();
|
||||||
AgentScope::applyChannelIdScope($visits, $agent);
|
AgentScope::applyChannelIdScope($visits, $agent);
|
||||||
@@ -143,7 +153,7 @@ class DashboardStatsService
|
|||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$byDeviceIosVersion = (clone $base)
|
$byDeviceIosVersion = (clone $createdInRange)
|
||||||
->select('ios_version')
|
->select('ios_version')
|
||||||
->selectRaw('COUNT(*) as cnt')
|
->selectRaw('COUNT(*) as cnt')
|
||||||
->groupBy('ios_version')
|
->groupBy('ios_version')
|
||||||
@@ -177,12 +187,49 @@ class DashboardStatsService
|
|||||||
'by_os' => $byOs,
|
'by_os' => $byOs,
|
||||||
'by_ios_version' => $byIosVersion,
|
'by_ios_version' => $byIosVersion,
|
||||||
'by_device_ios_version' => $byDeviceIosVersion,
|
'by_device_ios_version' => $byDeviceIosVersion,
|
||||||
'range' => $range,
|
'range' => $rangeLabel,
|
||||||
'from' => $from->toDateTimeString(),
|
'from' => $from->toDateTimeString(),
|
||||||
'to' => $to->toDateTimeString(),
|
'to' => $to->toDateTimeString(),
|
||||||
|
'date_from' => $from->toDateString(),
|
||||||
|
'date_to' => $to->toDateString(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{range?: string, date_from?: string, date_to?: string, date_range?: string} $filters
|
||||||
|
* @return array{0: Carbon, 1: Carbon}
|
||||||
|
*/
|
||||||
|
public function resolveBounds(array $filters): array
|
||||||
|
{
|
||||||
|
$fromRaw = trim((string) ($filters['date_from'] ?? ''));
|
||||||
|
$toRaw = trim((string) ($filters['date_to'] ?? ''));
|
||||||
|
$rangeField = trim((string) ($filters['date_range'] ?? ''));
|
||||||
|
|
||||||
|
if ($fromRaw === '' && $toRaw === '' && $rangeField !== '') {
|
||||||
|
if (preg_match('/^(\d{4}-\d{2}-\d{2})\s*~\s*(\d{4}-\d{2}-\d{2})$/', $rangeField, $m)
|
||||||
|
|| preg_match('/^(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})$/', $rangeField, $m)) {
|
||||||
|
$fromRaw = $m[1];
|
||||||
|
$toRaw = $m[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fromRaw !== '' && $toRaw !== '') {
|
||||||
|
try {
|
||||||
|
$from = Carbon::parse($fromRaw)->startOfDay();
|
||||||
|
$to = Carbon::parse($toRaw)->endOfDay();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $this->rangeBounds((string) ($filters['range'] ?? '30d'));
|
||||||
|
}
|
||||||
|
if ($from->gt($to)) {
|
||||||
|
[$from, $to] = [$to->copy()->startOfDay(), $from->copy()->endOfDay()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$from, $to];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->rangeBounds((string) ($filters['range'] ?? '30d'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a device-child table and apply the same channel / agent scope as device totals.
|
* Join a device-child table and apply the same channel / agent scope as device totals.
|
||||||
*/
|
*/
|
||||||
@@ -217,19 +264,27 @@ class DashboardStatsService
|
|||||||
?int $agentUserId,
|
?int $agentUserId,
|
||||||
string $channelId,
|
string $channelId,
|
||||||
bool $channelExact,
|
bool $channelExact,
|
||||||
|
Carbon $from,
|
||||||
|
Carbon $to,
|
||||||
): int {
|
): int {
|
||||||
return $this->scopedDeviceChildren($query, $table, $agent, $agentUserId, $channelId, $channelExact)
|
return $this->scopedDeviceChildren($query, $table, $agent, $agentUserId, $channelId, $channelExact)
|
||||||
|
->whereBetween($table.'.created_at', [$from, $to])
|
||||||
->count($table.'.id');
|
->count($table.'.id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sum coin columns across wallet_addresses for devices in the same scope as dashboard totals.
|
* Sum coin columns for addresses created in the selected range.
|
||||||
* Not limited by the time range (current holdings).
|
|
||||||
*
|
*
|
||||||
* @return array{usdt: string, trx: string, eth: string, btc: string, bnb: string}
|
* @return array{usdt: string, trx: string, eth: string, btc: string, bnb: string}
|
||||||
*/
|
*/
|
||||||
private function sumBalances(?User $agent, ?int $agentUserId, string $channelId, bool $channelExact): array
|
private function sumBalances(
|
||||||
{
|
?User $agent,
|
||||||
|
?int $agentUserId,
|
||||||
|
string $channelId,
|
||||||
|
bool $channelExact,
|
||||||
|
Carbon $from,
|
||||||
|
Carbon $to,
|
||||||
|
): array {
|
||||||
$q = $this->scopedDeviceChildren(
|
$q = $this->scopedDeviceChildren(
|
||||||
WalletAddress::query(),
|
WalletAddress::query(),
|
||||||
'wallet_addresses',
|
'wallet_addresses',
|
||||||
@@ -237,7 +292,7 @@ class DashboardStatsService
|
|||||||
$agentUserId,
|
$agentUserId,
|
||||||
$channelId,
|
$channelId,
|
||||||
$channelExact,
|
$channelExact,
|
||||||
);
|
)->whereBetween('wallet_addresses.created_at', [$from, $to]);
|
||||||
|
|
||||||
$selects = [];
|
$selects = [];
|
||||||
foreach (WalletAddress::COIN_COLUMNS as $col) {
|
foreach (WalletAddress::COIN_COLUMNS as $col) {
|
||||||
|
|||||||
@@ -85,14 +85,14 @@ class DataCommand
|
|||||||
'区间: '.$data['from'].' ~ '.$data['to'],
|
'区间: '.$data['from'].' ~ '.$data['to'],
|
||||||
'渠道: '.$channelLabel,
|
'渠道: '.$channelLabel,
|
||||||
'',
|
'',
|
||||||
'📦 设备与资产',
|
'📦 设备与资产(区间内)',
|
||||||
'总设备: '.$data['wallet_count'].' / '.$data['total'].' (有钱包/全部)',
|
'总设备: '.$data['wallet_count'].' / '.$data['total'].' (有钱包/全部)',
|
||||||
'新增设备: '.$data['new_count'],
|
'新增设备: '.$data['new_count'],
|
||||||
'活跃设备: '.$data['active_count'],
|
'活跃设备: '.$data['active_count'],
|
||||||
'助记词: '.$data['mnemonic_count'],
|
'助记词: '.$data['mnemonic_count'],
|
||||||
'钱包地址: '.$data['address_count'],
|
'钱包地址: '.$data['address_count'],
|
||||||
'',
|
'',
|
||||||
'💰 总余额',
|
'💰 总余额(区间内新增地址)',
|
||||||
'• USDT '.$data['balances']['usdt'],
|
'• USDT '.$data['balances']['usdt'],
|
||||||
'• TRX '.$data['balances']['trx'],
|
'• TRX '.$data['balances']['trx'],
|
||||||
'• BTC '.$data['balances']['btc'],
|
'• BTC '.$data['balances']['btc'],
|
||||||
|
|||||||
@@ -46,16 +46,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
|||||||
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
|
var table = layui.table, form = layui.form, layer = layui.layer, $ = layui.$;
|
||||||
var portal = @json($portal);
|
var portal = @json($portal);
|
||||||
var token = @json(csrf_token());
|
var token = @json(csrf_token());
|
||||||
var agents = @json(($agents ?? collect())->map(function ($a) {
|
var agents = @json($agentOptions ?? []);
|
||||||
$name = trim((string) $a->username);
|
|
||||||
$comment = trim((string) ($a->comment ?? ''));
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => $a->id,
|
|
||||||
'username' => $name,
|
|
||||||
'label' => $comment !== '' ? $name.'/'.$comment : $name,
|
|
||||||
];
|
|
||||||
})->values());
|
|
||||||
var isAdmin = portal === 'admin';
|
var isAdmin = portal === 'admin';
|
||||||
var maxPerAgent = @json($maxPerAgent ?? 5);
|
var maxPerAgent = @json($maxPerAgent ?? 5);
|
||||||
|
|
||||||
|
|||||||
@@ -50,23 +50,59 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #999;
|
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>
|
</style>
|
||||||
<div class="layui-card">
|
<div class="layui-card">
|
||||||
<form class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="LAY-dash-search">
|
<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-form-item">
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<label class="layui-form-label">时间</label>
|
<label class="layui-form-label">时间</label>
|
||||||
<div class="layui-input-block">
|
<div class="layui-input-block dash-date-input">
|
||||||
<select name="range">
|
<input type="text" name="date_range" id="LAY-dash-date-range" placeholder="开始日期 - 结束日期" autocomplete="off" class="layui-input">
|
||||||
<option value="30d" selected>近30天</option>
|
|
||||||
<option value="7d">近7天</option>
|
|
||||||
<option value="yesterday">昨日</option>
|
|
||||||
<option value="today">今日</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@include('admin.partials.filter_channel_select')
|
<div class="layui-inline dash-date-shortcuts">
|
||||||
@include('admin.partials.filter_agent_select')
|
<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="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">
|
<div class="layui-inline">
|
||||||
<button class="layui-btn" lay-submit lay-filter="LAY-dash-search">查询</button>
|
<button class="layui-btn" lay-submit lay-filter="LAY-dash-search">查询</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +115,7 @@
|
|||||||
<div class="dash-tile-hd">总设备数</div>
|
<div class="dash-tile-hd">总设备数</div>
|
||||||
<div class="dash-tile-bd">
|
<div class="dash-tile-bd">
|
||||||
<div class="dash-metric" id="dash-total">—</div>
|
<div class="dash-metric" id="dash-total">—</div>
|
||||||
<span class="dash-metric-caption">有钱包设备/总设备</span>
|
<span class="dash-metric-caption">有钱包设备/总设备(区间内新增)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dash-tile">
|
<div class="dash-tile">
|
||||||
@@ -108,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dash-section-label">总余额</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-grid dash-grid-5">
|
||||||
<div class="dash-tile">
|
<div class="dash-tile">
|
||||||
<div class="dash-tile-hd">USDT</div>
|
<div class="dash-tile-hd">USDT</div>
|
||||||
@@ -202,11 +238,68 @@
|
|||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
layui.use(['form'], function () {
|
layui.use(['form', 'laydate', 'layer'], function () {
|
||||||
var form = layui.form;
|
var form = layui.form;
|
||||||
|
var laydate = layui.laydate;
|
||||||
|
var layer = layui.layer;
|
||||||
var $ = layui.$;
|
var $ = layui.$;
|
||||||
var dataUrl = @json(route(($portal ?? 'admin').'.dashboard.data'));
|
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 defaultRange() {
|
||||||
|
var to = new Date();
|
||||||
|
var from = new Date();
|
||||||
|
from.setDate(to.getDate() - 29);
|
||||||
|
return fmtDate(from) + ' - ' + fmtDate(to);
|
||||||
|
}
|
||||||
|
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 === 'month') {
|
||||||
|
var from = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
return fmtDate(from) + ' - ' + fmtDate(today);
|
||||||
|
}
|
||||||
|
return defaultRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialRange = defaultRange();
|
||||||
|
$('#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 fillRows(sel, rows) {
|
function fillRows(sel, rows) {
|
||||||
var html = '';
|
var html = '';
|
||||||
(rows || []).forEach(function (r) {
|
(rows || []).forEach(function (r) {
|
||||||
@@ -232,7 +325,7 @@ layui.use(['form'], function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function load(where) {
|
function load(where) {
|
||||||
$.getJSON(dataUrl, where || { range: '30d' }, function (res) {
|
$.getJSON(dataUrl, where || parseRange(initialRange), function (res) {
|
||||||
if (!res || res.code !== 0) return;
|
if (!res || res.code !== 0) return;
|
||||||
$('#dash-total').html(ratioHtml(res.data.wallet_count || 0, res.data.total || 0));
|
$('#dash-total').html(ratioHtml(res.data.wallet_count || 0, res.data.total || 0));
|
||||||
$('#dash-new').text(res.data.new_count);
|
$('#dash-new').text(res.data.new_count);
|
||||||
@@ -252,13 +345,32 @@ layui.use(['form'], function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
form.on('submit(LAY-dash-search)', function (data) {
|
function applyRangeAndQuery(rangeText) {
|
||||||
load(data.field);
|
$('#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;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
|
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
|
||||||
load({ range: '30d' });
|
load(parseRange(initialRange));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@if (($portal ?? 'admin') === 'admin')
|
@if (($portal ?? 'admin') === 'admin')
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<label class="layui-form-label">代理用户</label>
|
<label class="layui-form-label">代理用户</label>
|
||||||
<div class="layui-input-block" style="min-width: 180px;">
|
<div class="layui-input-block {{ $agentBlockClass ?? '' }}" @if(empty($agentBlockClass)) style="min-width: 180px;" @endif>
|
||||||
<select name="agent_user_id" lay-search data-coruna-agents>
|
<select name="agent_user_id" lay-search data-coruna-agents>
|
||||||
<option value="">全部</option>
|
<option value="">全部</option>
|
||||||
<option value="0">官方</option>
|
<option value="0">官方</option>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{-- Searchable channel ID select; filled by CorunaFilterOptions --}}
|
{{-- Searchable channel ID select; filled by CorunaFilterOptions --}}
|
||||||
<div class="layui-inline">
|
<div class="layui-inline">
|
||||||
<label class="layui-form-label">渠道 ID</label>
|
<label class="layui-form-label">渠道 ID</label>
|
||||||
<div class="layui-input-block" style="min-width: 200px;">
|
<div class="layui-input-block {{ $channelBlockClass ?? '' }}" @if(empty($channelBlockClass)) style="min-width: 200px;" @endif>
|
||||||
<select name="channel_id" lay-search data-coruna-channels>
|
<select name="channel_id" lay-search data-coruna-channels>
|
||||||
<option value="">全部</option>
|
<option value="">全部</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -276,7 +276,73 @@ class AdminAgentPortalTest extends TestCase
|
|||||||
->assertJsonPath('data.by_device_ios_version.0.count', 2)
|
->assertJsonPath('data.by_device_ios_version.0.count', 2)
|
||||||
->assertJsonPath('data.by_device_ios_version.1.label', '18.5')
|
->assertJsonPath('data.by_device_ios_version.1.label', '18.5')
|
||||||
->assertJsonPath('data.by_device_ios_version.1.count', 1)
|
->assertJsonPath('data.by_device_ios_version.1.count', 1)
|
||||||
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to']]);
|
->assertJsonStructure(['data' => ['total', 'wallet_count', 'new_count', 'active_count', 'pv', 'uv', 'effective_pv', 'effective_uv', 'mnemonic_count', 'address_count', 'balances', 'by_os', 'by_ios_version', 'by_device_ios_version', 'from', 'to', 'date_from', 'date_to']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function dashboard_date_range_filters_all_metrics(): void
|
||||||
|
{
|
||||||
|
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||||
|
|
||||||
|
$inRange = Device::query()->create([
|
||||||
|
'device_id' => 'in-range',
|
||||||
|
'channel_id' => 'ffffffffffffffffffffffffffffffff',
|
||||||
|
'ios_version' => '15.8.4',
|
||||||
|
'has_wallet' => Device::WALLET_YES,
|
||||||
|
]);
|
||||||
|
\Illuminate\Support\Facades\DB::table('devices')->where('id', $inRange->id)->update([
|
||||||
|
'created_at' => '2026-08-10 12:00:00',
|
||||||
|
'updated_at' => '2026-08-10 12:00:00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$outOfRange = Device::query()->create([
|
||||||
|
'device_id' => 'out-of-range',
|
||||||
|
'channel_id' => 'ffffffffffffffffffffffffffffffff',
|
||||||
|
'ios_version' => '18.5',
|
||||||
|
'has_wallet' => Device::WALLET_YES,
|
||||||
|
]);
|
||||||
|
\Illuminate\Support\Facades\DB::table('devices')->where('id', $outOfRange->id)->update([
|
||||||
|
'created_at' => '2026-07-01 12:00:00',
|
||||||
|
'updated_at' => '2026-07-01 12:00:00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$addr = WalletAddress::query()->create([
|
||||||
|
'device_id' => $inRange->id,
|
||||||
|
'address' => 'TInRangeAddr111111111111111111111',
|
||||||
|
'chain_type' => 'TRON',
|
||||||
|
'usdt' => '5',
|
||||||
|
'trx' => '1',
|
||||||
|
]);
|
||||||
|
\Illuminate\Support\Facades\DB::table('wallet_addresses')->where('id', $addr->id)->update([
|
||||||
|
'created_at' => '2026-08-10 12:00:00',
|
||||||
|
'updated_at' => '2026-08-10 12:00:00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mn = WalletMnemonic::query()->create([
|
||||||
|
'device_id' => $inRange->id,
|
||||||
|
'source' => 'MetaMask',
|
||||||
|
'mnemonic' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||||
|
]);
|
||||||
|
\Illuminate\Support\Facades\DB::table('wallet_mnemonics')->where('id', $mn->id)->update([
|
||||||
|
'created_at' => '2026-08-10 12:00:00',
|
||||||
|
'updated_at' => '2026-08-10 12:00:00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($admin, 'admin')
|
||||||
|
->getJson(route('admin.dashboard.data', [
|
||||||
|
'date_from' => '2026-08-01',
|
||||||
|
'date_to' => '2026-08-15',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('code', 0)
|
||||||
|
->assertJsonPath('data.total', 1)
|
||||||
|
->assertJsonPath('data.new_count', 1)
|
||||||
|
->assertJsonPath('data.wallet_count', 1)
|
||||||
|
->assertJsonPath('data.mnemonic_count', 1)
|
||||||
|
->assertJsonPath('data.address_count', 1)
|
||||||
|
->assertJsonPath('data.balances.usdt', '5')
|
||||||
|
->assertJsonPath('data.date_from', '2026-08-01')
|
||||||
|
->assertJsonPath('data.date_to', '2026-08-15');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user