fix: link old

This commit is contained in:
hashbro
2026-09-14 01:09:01 +08:00
parent b49755c900
commit d5ae0bf6ff
29 changed files with 1584 additions and 212 deletions
@@ -0,0 +1,428 @@
@extends('admin.content')
@section('title', '数据分析')
@section('content')
@php $portal = $portal ?? 'admin'; @endphp
<style>
.an-filter .layui-form-item { margin-bottom: 0; }
.an-filter .layui-inline {
margin-right: 10px;
margin-bottom: 0;
}
.an-filter .layui-form-label {
width: 72px;
padding: 9px 10px 9px 0;
text-align: left;
box-sizing: border-box;
}
.an-filter .layui-input-block {
margin-left: 72px;
min-height: 36px;
}
.an-filter .an-date-input { width: 210px; }
.an-filter .an-date-shortcuts {
margin-left: 0;
margin-right: 4px;
}
.an-filter .an-date-shortcuts .layui-btn {
margin: 0 4px 0 0;
height: 36px;
line-height: 36px;
padding: 0 12px;
}
.an-filter .an-agent-block {
width: 150px;
min-width: 150px !important;
}
.an-survival {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 10px 16px;
}
.an-metric { font-size: 26px; line-height: 1.2; color: #009688; font-weight: 600; }
.an-metric-caption { font-size: 13px; color: #999; }
.an-pie-card .layui-card-body { padding-bottom: 8px; }
.an-pie { width: 100%; height: 240px; }
.an-pie-hint {
margin: 0 0 4px;
text-align: center;
font-size: 12px;
color: #bbb;
min-height: 18px;
}
.an-tags { line-height: 26px; }
.an-tags-scroll {
max-height: 132px;
overflow-y: auto;
}
.an-tag {
display: inline-block;
margin: 0 8px 8px 0;
padding: 0 8px;
font-size: 12px;
line-height: 22px;
color: #cf8a00;
background: #fff7e6;
border: 1px solid #ffe0a3;
border-radius: 3px;
vertical-align: middle;
}
.an-tags-empty { color: #bbb; font-size: 13px; }
</style>
<div class="layui-card">
<form class="layui-form layui-card-header layuiadmin-card-header-auto an-filter" lay-filter="LAY-an-search">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">时间</label>
<div class="layui-input-block an-date-input">
<input type="text" name="date_range" id="LAY-an-date-range" placeholder="开始日期 - 结束日期" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline an-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' => 'an-agent-block'])
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="LAY-an-search">查询</button>
</div>
</div>
</form>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-row layui-col-space15">
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">设备存活时长均值</div>
<div class="layui-card-body">
<div class="an-survival">
<div class="an-metric" id="an-survival">—</div>
<span class="an-metric-caption" id="an-survival-caption">区间内新增设备 · 创建到最后更新</span>
</div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">有钱包设备存活时长均值</div>
<div class="layui-card-body">
<div class="an-survival">
<div class="an-metric" id="an-survival-wallet">—</div>
<span class="an-metric-caption" id="an-survival-wallet-caption">区间内新增有钱包设备 · 创建到最后更新</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">有钱包设备存活时长均值</div>
<div class="layui-card-body">
<div class="an-survival">
<div class="an-metric" id="an-survival-wallet">—</div>
<span class="an-metric-caption" id="an-survival-wallet-caption">区间内新增有钱包设备 · 创建到最后更新</span>
</div>
</div>
</div>
</div>
</div>
<div class="layui-row layui-col-space15">
<div class="layui-col-md4">
<div class="layui-card an-pie-card">
<div class="layui-card-header">访问 · 按系统</div>
<div class="layui-card-body">
<div id="an-os-chart" class="an-pie"></div>
<p class="an-pie-hint" id="an-os-hint"></p>
</div>
</div>
</div>
<div class="layui-col-md4">
<div class="layui-card an-pie-card">
<div class="layui-card-header">访问 · iOS 版本</div>
<div class="layui-card-body">
<div id="an-ios-chart" class="an-pie"></div>
<p class="an-pie-hint" id="an-ios-hint"></p>
</div>
</div>
</div>
<div class="layui-col-md4">
<div class="layui-card an-pie-card">
<div class="layui-card-header">受控设备 · 版本</div>
<div class="layui-card-body">
<div id="an-device-chart" class="an-pie"></div>
<p class="an-pie-hint" id="an-device-hint"></p>
</div>
</div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">访问版本明细 <span class="layui-word-aux" id="an-ios-count"></span></div>
<div class="layui-card-body">
<div class="an-tags an-tags-scroll" id="an-by-ios-version"><span class="an-tags-empty">—</span></div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">受控设备版本明细 <span class="layui-word-aux" id="an-device-count"></span></div>
<div class="layui-card-body">
<div class="an-tags an-tags-scroll" id="an-by-device-ios"><span class="an-tags-empty">—</span></div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">各版本受控比例(受控设备 / 有效 UV)</div>
<div class="layui-card-body">
<div class="an-tags an-tags-scroll" id="an-control-version"><span class="an-tags-empty">—</span></div>
</div>
</div>
<div class="layui-word-aux" id="an-range" style="margin:0 0 12px;"></div>
@endsection
@push('scripts')
<script>
var adminStatic = @json(rtrim(asset('static/layuiadmin'), '/').'/');
layui.extend({
echarts: '{/}' + adminStatic + 'lib/extend/echarts',
echartsTheme: '{/}' + adminStatic + 'lib/extend/echartsTheme'
});
layui.use(['form', 'laydate', 'layer', 'echarts'], function () {
var form = layui.form;
var laydate = layui.laydate;
var layer = layui.layer;
var $ = layui.$;
var echartsLib = layui.echarts;
var dataUrl = @json(route(($portal ?? 'admin').'.reports.analytics.data'));
var charts = {};
var PIE_MAX = 7;
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 === '30d') {
var from30 = new Date(today);
from30.setDate(today.getDate() - 29);
return fmtDate(from30) + ' - ' + 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('30d');
$('#LAY-an-date-range').val(initialRange);
laydate.render({
elem: '#LAY-an-date-range',
type: 'date',
range: true,
value: initialRange,
done: function (value) {
if (value) $('#LAY-an-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-an-search') || {};
var parsed = parseRange(field.date_range || $('#LAY-an-date-range').val());
if (!parsed) return null;
return Object.assign({}, field, parsed);
}
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function fmtPct(pct) {
if (pct === undefined || pct === null) return '—';
return pct + '%';
}
function roundPct(part, total) {
return total > 0 ? Math.round(part / total * 1000) / 10 : 0;
}
function fillTags(sel, rows, countKey) {
var html = '';
(rows || []).forEach(function (r) {
html += '<span class="an-tag">' + esc(r.label || '—')
+ '(' + esc(r[countKey]) + '/' + esc(fmtPct(r.pct)) + ')</span>';
});
$(sel).html(html || '<span class="an-tags-empty">暂无</span>');
}
function fillControl(rows) {
var html = '';
(rows || []).forEach(function (r) {
var uv = r.effective_uv === undefined || r.effective_uv === null ? '—' : r.effective_uv;
html += '<span class="an-tag">' + esc(r.label || '—')
+ '(' + esc(r.devices) + '/' + esc(uv) + ' · ' + esc(fmtPct(r.pct)) + ')</span>';
});
$('#an-control-version').html(html || '<span class="an-tags-empty">暂无</span>');
}
function topPieRows(rows, valueKey) {
var sorted = (rows || []).filter(function (r) {
return Number(r[valueKey]) > 0;
}).slice().sort(function (a, b) {
return Number(b[valueKey]) - Number(a[valueKey]);
});
if (sorted.length <= PIE_MAX) return { rows: sorted, hidden: 0 };
var head = sorted.slice(0, PIE_MAX - 1);
var rest = sorted.slice(PIE_MAX - 1);
var otherVal = 0;
var total = 0;
sorted.forEach(function (r) { total += Number(r[valueKey]) || 0; });
rest.forEach(function (r) { otherVal += Number(r[valueKey]) || 0; });
var other = { label: '其他', pct: roundPct(otherVal, total) };
other[valueKey] = otherVal;
head.push(other);
return { rows: head, hidden: rest.length };
}
function renderPie(id, hintId, rows, valueKey, seriesName) {
var el = document.getElementById(id);
if (!el || !echartsLib || !echartsLib.init) return;
var sliced = topPieRows(rows, valueKey);
var items = [];
var names = [];
sliced.rows.forEach(function (r) {
var value = Number(r[valueKey]) || 0;
if (value <= 0) return;
var name = r.label || '—';
items.push({ name: name, value: value });
names.push(name);
});
if (!charts[id]) {
charts[id] = echartsLib.init(el, layui.echartsTheme);
window.addEventListener('resize', function () { charts[id].resize(); });
}
var chart = charts[id];
chart.resize();
chart.setOption({
tooltip: { trigger: 'item', formatter: '{b}:{c}({d}%)' },
legend: {
orient: 'horizontal',
x: 'center',
y: 'bottom',
itemWidth: 10,
itemHeight: 10,
data: names
},
calculable: false,
series: [{
name: seriesName,
type: 'pie',
radius: ['48%', '70%'],
center: ['50%', '44%'],
itemStyle: {
normal: {
label: { show: false },
labelLine: { show: false }
}
},
data: items
}]
}, true);
var hint = document.getElementById(hintId);
if (hint) {
hint.textContent = sliced.hidden > 0
? ('图中前 ' + (PIE_MAX - 1) + ' 项 + 其他(另有 ' + sliced.hidden + ' 个版本)')
: '';
}
}
function paint(data) {
var s = data.survival || {};
$('#an-survival').text(s.avg_label || '—');
$('#an-survival-caption').text('区间内新增 ' + (s.devices || 0) + ' 台 · 创建到最后更新');
var sw = data.survival_wallet || {};
$('#an-survival-wallet').text(sw.avg_label || '—');
$('#an-survival-wallet-caption').text('区间内新增 ' + (sw.devices || 0) + ' 台有钱包 · 创建到最后更新');
var sw = data.survival_wallet || {};
$('#an-survival-wallet').text(sw.avg_label || '—');
$('#an-survival-wallet-caption').text('区间内新增 ' + (sw.devices || 0) + ' 台有钱包 · 创建到最后更新');
renderPie('an-os-chart', 'an-os-hint', data.by_os, 'uv', '系统');
renderPie('an-ios-chart', 'an-ios-hint', data.by_ios_version, 'uv', 'iOS 版本');
renderPie('an-device-chart', 'an-device-hint', data.by_device_ios_version, 'count', '受控版本');
fillTags('#an-by-ios-version', data.by_ios_version, 'uv');
fillTags('#an-by-device-ios', data.by_device_ios_version, 'count');
fillControl(data.control_by_version);
var iosN = (data.by_ios_version || []).length;
var deviceN = (data.by_device_ios_version || []).length;
$('#an-ios-count').text(iosN ? ('· ' + iosN + ' 个') : '');
$('#an-device-count').text(deviceN ? ('· ' + deviceN + ' 个') : '');
var computed = data.computed_at ? (' · 汇总于 ' + data.computed_at) : '';
$('#an-range').text('统计区间:' + data.from + ' ~ ' + data.to + '(按日去重后加总,今日约 15 分钟刷新)' + computed);
}
function load(where) {
var query = where || parseRange(initialRange);
$.getJSON(dataUrl, query, function (res) {
if (!res || res.code !== 0) return;
paint(res.data);
});
}
function applyRangeAndQuery(rangeText) {
$('#LAY-an-date-range').val(rangeText);
var where = currentWhere();
if (!where) {
layer.msg('请选择有效的时间范围');
return;
}
load(where);
}
$('.an-date-shortcuts').on('click', '[data-range]', function () {
applyRangeAndQuery(rangeOf($(this).data('range')));
});
form.on('submit(LAY-an-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