@@ -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 : 10 px ;
margin - bottom : 0 ;
}
. an - filter . layui - form - label {
width : 72 px ;
padding : 9 px 10 px 9 px 0 ;
text - align : left ;
box - sizing : border - box ;
}
. an - filter . layui - input - block {
margin - left : 72 px ;
min - height : 36 px ;
}
. an - filter . an - date - input { width : 210 px ; }
. an - filter . an - date - shortcuts {
margin - left : 0 ;
margin - right : 4 px ;
}
. an - filter . an - date - shortcuts . layui - btn {
margin : 0 4 px 0 0 ;
height : 36 px ;
line - height : 36 px ;
padding : 0 12 px ;
}
. an - filter . an - agent - block {
width : 150 px ;
min - width : 150 px ! important ;
}
. an - survival {
display : flex ;
align - items : baseline ;
flex - wrap : wrap ;
gap : 10 px 16 px ;
}
. an - metric { font - size : 26 px ; line - height : 1.2 ; color : #009688; font-weight: 600; }
. an - metric - caption { font - size : 13 px ; color : #999; }
. an - pie - card . layui - card - body { padding - bottom : 8 px ; }
. an - pie { width : 100 % ; height : 240 px ; }
. an - pie - hint {
margin : 0 0 4 px ;
text - align : center ;
font - size : 12 px ;
color : #bbb;
min - height : 18 px ;
}
. an - tags { line - height : 26 px ; }
. an - tags - scroll {
max - height : 132 px ;
overflow - y : auto ;
}
. an - tag {
display : inline - block ;
margin : 0 8 px 8 px 0 ;
padding : 0 8 px ;
font - size : 12 px ;
line - height : 22 px ;
color : #cf8a00;
background : #fff7e6;
border : 1 px solid #ffe0a3;
border - radius : 3 px ;
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 , '&' )
. replace ( /</ g , '<' )
. replace ( />/ g , '>' )
. replace ( / " /g, '"');
}
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