Dashboard recent platform orders: add mini chart summary with links
This commit is contained in:
@@ -334,6 +334,109 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 仪表盘:最近平台订单(概览 mini bars)
|
||||||
|
// 说明:让“最近订单列表”也纳入数据图形化体系,但保留明细扫描行。
|
||||||
|
// 渐进增强:由 Blade 输出 data-points(label/count/href/tone),JS 渲染为可点击条目。
|
||||||
|
(function(){
|
||||||
|
var el = qs('[data-role="recent-platform-orders-mini-bars"][data-points]');
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw = el.getAttribute('data-points') || '[]';
|
||||||
|
var points = [];
|
||||||
|
try {
|
||||||
|
points = JSON.parse(raw) || [];
|
||||||
|
} catch (e) {
|
||||||
|
points = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = Number(el.getAttribute('data-total') || 0);
|
||||||
|
if (!isFinite(total) || total < 0) {
|
||||||
|
total = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!points || points.length === 0) {
|
||||||
|
el.classList.add('is-empty');
|
||||||
|
el.textContent = '暂无最近订单概览';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视觉口径:按当前 points 内最大值归一(更易读);tooltip 同时提供 count/占比。
|
||||||
|
var max = 0;
|
||||||
|
points.forEach(function(p){
|
||||||
|
var c = Number(p && p.count != null ? p.count : 0);
|
||||||
|
if (isFinite(c) && c > max) {
|
||||||
|
max = c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!max || max <= 0) {
|
||||||
|
max = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toneToGradient(tone){
|
||||||
|
if (tone === 'danger') {
|
||||||
|
return 'linear-gradient(90deg, rgba(239, 68, 68, .45), rgba(239, 68, 68, .14))';
|
||||||
|
}
|
||||||
|
if (tone === 'warn') {
|
||||||
|
return 'linear-gradient(90deg, rgba(245, 158, 11, .45), rgba(245, 158, 11, .16))';
|
||||||
|
}
|
||||||
|
if (tone === 'success') {
|
||||||
|
return 'linear-gradient(90deg, rgba(34, 197, 94, .40), rgba(34, 197, 94, .12))';
|
||||||
|
}
|
||||||
|
return 'linear-gradient(90deg, rgba(22, 119, 255, .35), rgba(22, 119, 255, .12))';
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = '';
|
||||||
|
|
||||||
|
points.forEach(function(p, idx){
|
||||||
|
var label = (p && p.label) ? String(p.label) : ('#' + String(idx + 1));
|
||||||
|
var cnt = Number(p && p.count != null ? p.count : 0);
|
||||||
|
if (!isFinite(cnt) || cnt < 0) {
|
||||||
|
cnt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var href = (p && p.href) ? String(p.href) : '';
|
||||||
|
var tone = (p && p.tone) ? String(p.tone) : '';
|
||||||
|
|
||||||
|
var row = document.createElement(href ? 'a' : 'div');
|
||||||
|
row.className = 'adm-mini-share-row' + (href ? ' adm-mini-share-row-link' : '');
|
||||||
|
if (href) {
|
||||||
|
row.setAttribute('href', href);
|
||||||
|
row.setAttribute('role', 'link');
|
||||||
|
row.setAttribute('aria-label', '进入最近订单集合:' + label);
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = document.createElement('div');
|
||||||
|
name.className = 'adm-mini-share-name';
|
||||||
|
name.textContent = label;
|
||||||
|
name.title = label;
|
||||||
|
|
||||||
|
var wrap = document.createElement('div');
|
||||||
|
wrap.className = 'adm-mini-share-bar-wrap';
|
||||||
|
|
||||||
|
var bar = document.createElement('div');
|
||||||
|
bar.className = 'adm-mini-share-bar';
|
||||||
|
bar.style.width = Math.round(Math.max(0, Math.min(1, cnt / max)) * 100) + '%';
|
||||||
|
bar.style.background = toneToGradient(tone);
|
||||||
|
wrap.appendChild(bar);
|
||||||
|
|
||||||
|
var val = document.createElement('div');
|
||||||
|
val.className = 'adm-mini-share-value';
|
||||||
|
val.textContent = String(cnt);
|
||||||
|
|
||||||
|
var pct = (total > 0) ? (cnt / total) : 0;
|
||||||
|
row.title = label + '|' + String(cnt) + ' 单|占比 ' + formatPct(pct, 1) + '%';
|
||||||
|
|
||||||
|
row.appendChild(name);
|
||||||
|
row.appendChild(wrap);
|
||||||
|
row.appendChild(val);
|
||||||
|
|
||||||
|
el.appendChild(row);
|
||||||
|
});
|
||||||
|
})();
|
||||||
// 仪表盘:迷你占比(套餐订单占比 Top5)
|
// 仪表盘:迷你占比(套餐订单占比 Top5)
|
||||||
(function () {
|
(function () {
|
||||||
var el = qs('[data-role="plan-order-share-top5-chart"][data-points]');
|
var el = qs('[data-role="plan-order-share-top5-chart"][data-points]');
|
||||||
|
|||||||
@@ -825,6 +825,117 @@
|
|||||||
<a class="muted" href="{!! $platformOrdersQuickLinks['platform_orders'] !!}">查看全部</a>
|
<a class="muted" href="{!! $platformOrdersQuickLinks['platform_orders'] !!}">查看全部</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@php
|
||||||
|
// 最近平台订单:概览 mini chart(渐进增强)
|
||||||
|
// 目标:让“最近订单列表”也纳入数据图形化体系,但不牺牲明细扫描行。
|
||||||
|
$recentPos = (array) ($recentPlatformOrders ?? []);
|
||||||
|
$recentTotal = count($recentPos);
|
||||||
|
|
||||||
|
$recentCntBmpaSuccess = 0;
|
||||||
|
$recentCntBmpaFailed = 0;
|
||||||
|
$recentCntSyncSuccess = 0;
|
||||||
|
$recentCntSyncFailed = 0;
|
||||||
|
$recentCntRenewalMissing = 0;
|
||||||
|
$recentCntRefundTrace = 0;
|
||||||
|
$recentCntReconcileMismatch = 0;
|
||||||
|
|
||||||
|
foreach ($recentPos as $rpo) {
|
||||||
|
$orderType = (string) ($rpo->order_type ?? '');
|
||||||
|
|
||||||
|
$bmpaErr = (string) (data_get($rpo, 'meta.batch_mark_paid_and_activate_error.message') ?? '');
|
||||||
|
$bmpaRunId = (string) (data_get($rpo, 'meta.batch_mark_paid_and_activate.run_id') ?? '');
|
||||||
|
if ($bmpaErr !== '') {
|
||||||
|
$recentCntBmpaFailed++;
|
||||||
|
} elseif ($bmpaRunId !== '') {
|
||||||
|
$recentCntBmpaSuccess++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncErr = (string) (data_get($rpo, 'meta.subscription_activation_error.message') ?? '');
|
||||||
|
$syncSubId = (string) (data_get($rpo, 'meta.subscription_activation.subscription_id') ?? '');
|
||||||
|
if ($syncErr !== '') {
|
||||||
|
$recentCntSyncFailed++;
|
||||||
|
} elseif ($syncSubId !== '') {
|
||||||
|
$recentCntSyncSuccess++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orderType === 'renewal' && ((int) ($rpo->site_subscription_id ?? 0) <= 0)) {
|
||||||
|
$recentCntRenewalMissing++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$refundTotal = (float) (data_get($rpo, 'refund_total') ?? 0);
|
||||||
|
if ($refundTotal > 0) {
|
||||||
|
$recentCntRefundTrace++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) (data_get($rpo, 'reconciliation_delta_row') ?? 0) != 0) {
|
||||||
|
$recentCntReconcileMismatch++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickable links: 复用 $platformOrdersQuickLinks(保证口径不漂移)
|
||||||
|
$recentMiniPoints = [
|
||||||
|
[
|
||||||
|
'key' => 'bmpa_success',
|
||||||
|
'label' => 'BMPA成功',
|
||||||
|
'count' => $recentCntBmpaSuccess,
|
||||||
|
'href' => $platformOrdersQuickLinks['bmpa_success'] ?? $platformOrdersQuickLinks['platform_orders'],
|
||||||
|
'tone' => 'success',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'bmpa_failed',
|
||||||
|
'label' => 'BMPA失败',
|
||||||
|
'count' => $recentCntBmpaFailed,
|
||||||
|
'href' => $platformOrdersQuickLinks['bmpa_failed'] ?? $platformOrdersQuickLinks['platform_orders'],
|
||||||
|
'tone' => 'danger',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'sync_success',
|
||||||
|
'label' => '同步成功',
|
||||||
|
'count' => $recentCntSyncSuccess,
|
||||||
|
'href' => \App\Support\BackUrl::withBack('/admin/platform-orders?' . \Illuminate\Support\Arr::query([
|
||||||
|
'sync_status' => 'synced',
|
||||||
|
]), $selfWithoutBack),
|
||||||
|
'tone' => 'success',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'sync_failed',
|
||||||
|
'label' => '同步失败',
|
||||||
|
'count' => $recentCntSyncFailed,
|
||||||
|
'href' => $platformOrdersQuickLinks['sync_failed'] ?? $platformOrdersQuickLinks['platform_orders'],
|
||||||
|
'tone' => 'danger',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'renewal_missing_subscription',
|
||||||
|
'label' => '续费缺订阅',
|
||||||
|
'count' => $recentCntRenewalMissing,
|
||||||
|
'href' => $platformOrdersQuickLinks['renewal_missing_subscription'] ?? $platformOrdersQuickLinks['platform_orders'],
|
||||||
|
'tone' => 'warn',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'refund_trace',
|
||||||
|
'label' => '有退款轨迹',
|
||||||
|
'count' => $recentCntRefundTrace,
|
||||||
|
'href' => \App\Support\BackUrl::withBack('/admin/platform-orders?refund_status=has', $selfWithoutBack),
|
||||||
|
'tone' => 'warn',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'reconcile_mismatch',
|
||||||
|
'label' => '对账不一致',
|
||||||
|
'count' => $recentCntReconcileMismatch,
|
||||||
|
'href' => $platformOrdersQuickLinks['reconcile_mismatch'] ?? $platformOrdersQuickLinks['platform_orders'],
|
||||||
|
'tone' => 'danger',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="adm-mini-rank" data-role="recent-platform-orders-mini-bars" data-total="{{ $recentTotal }}" data-points='@json($recentMiniPoints)'></div>
|
||||||
|
<div class="adm-mini-meta" data-role="recent-platform-orders-mini-meta">
|
||||||
|
<span class="adm-mini-meta-item">最近:<strong>{{ $recentTotal }}</strong> 单</span>
|
||||||
|
<span class="adm-mini-meta-sep">|</span>
|
||||||
|
<span class="adm-mini-meta-item">说明:点击条目进入对应治理集合(用于快速 spot-check)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table data-role="recent-platform-orders-table">
|
<table data-role="recent-platform-orders-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminDashboardRecentPlatformOrdersMiniBarsShouldRenderTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function loginAsPlatformAdmin(): void
|
||||||
|
{
|
||||||
|
$this->seed();
|
||||||
|
|
||||||
|
$this->post('/admin/login', [
|
||||||
|
'email' => 'platform.admin@demo.local',
|
||||||
|
'password' => 'Platform@123456',
|
||||||
|
])->assertRedirect('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_dashboard_should_render_recent_platform_orders_mini_bars_container(): void
|
||||||
|
{
|
||||||
|
$this->loginAsPlatformAdmin();
|
||||||
|
|
||||||
|
$html = $this->get('/admin')
|
||||||
|
->assertOk()
|
||||||
|
->getContent();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('data-role="recent-platform-orders-mini-bars"', $html);
|
||||||
|
$this->assertStringContainsString('data-role="recent-platform-orders-mini-meta"', $html);
|
||||||
|
$this->assertStringContainsString('data-points=', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user