befly-admin-ui 1.8.32 → 1.8.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jsconfig.json +0 -1
- package/package.json +2 -2
- package/views/index/components/systemOverview.vue +134 -87
- package/views/log/error/index.vue +12 -2
- package/views/log/visit/index.vue +0 -700
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -84,17 +84,20 @@
|
|
|
84
84
|
</div>
|
|
85
85
|
</div>
|
|
86
86
|
|
|
87
|
-
<div v-if="
|
|
88
|
-
<
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
<div v-if="selectedDaysHasData" class="trend-content">
|
|
88
|
+
<div class="trend-bars">
|
|
89
|
+
<div v-for="item in selectedDaysBars" :key="item.reportDate" class="trend-bar-group">
|
|
90
|
+
<div class="trend-bar-stack">
|
|
91
|
+
<div class="trend-bar trend-bar-pv" :style="{ height: item.pvHeight }" :title="`${formatReportDate(item.reportDate)} PV ${item.pv}`"></div>
|
|
92
|
+
<div class="trend-bar trend-bar-uv" :style="{ height: item.uvHeight }" :title="`${formatReportDate(item.reportDate)} UV ${item.uv}`"></div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="trend-bar-label">{{ formatReportDate(item.reportDate) }}</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
94
97
|
|
|
95
98
|
<div class="trend-briefs trend-briefs-wide">
|
|
96
|
-
<div v-for="item in selectedDaysTrendPreview" :key="item.
|
|
97
|
-
<div class="trend-date">{{
|
|
99
|
+
<div v-for="item in selectedDaysTrendPreview" :key="item.reportDate" class="trend-brief-item">
|
|
100
|
+
<div class="trend-date">{{ formatReportDate(item.reportDate) }}</div>
|
|
98
101
|
<div class="trend-values">
|
|
99
102
|
<span class="trend-value pv">{{ item.pv }}</span>
|
|
100
103
|
<span class="trend-value uv">{{ item.uv }}</span>
|
|
@@ -291,14 +294,11 @@ const selectedProductTotalUv = computed(() => {
|
|
|
291
294
|
return sumTrendField(selectedProduct.value?.days || [], "uv");
|
|
292
295
|
});
|
|
293
296
|
|
|
294
|
-
const selectedDaysTrend = computed(() => sortTrendList(selectedProduct.value?.days || [], "
|
|
295
|
-
const selectedDaysTrendMax = computed(() => getTrendMax(selectedDaysTrend.value));
|
|
296
|
-
const selectedDaysTrendPreview = computed(() => buildTrendPreview(selectedDaysTrend.value, 10));
|
|
297
|
-
const selectedDaysPvPoints = computed(() => buildTrendPoints(selectedDaysTrend.value, "pv", selectedDaysTrendMax.value));
|
|
298
|
-
const selectedDaysUvPoints = computed(() => buildTrendPoints(selectedDaysTrend.value, "uv", selectedDaysTrendMax.value));
|
|
299
|
-
const selectedDaysPvDots = computed(() => buildTrendDots(selectedDaysTrend.value, "pv", selectedDaysTrendMax.value));
|
|
300
|
-
const selectedDaysUvDots = computed(() => buildTrendDots(selectedDaysTrend.value, "uv", selectedDaysTrendMax.value));
|
|
297
|
+
const selectedDaysTrend = computed(() => sortTrendList(selectedProduct.value?.days || [], "reportDate"));
|
|
301
298
|
const selectedDaysFilled = computed(() => buildFilledTrendDays(selectedProduct.value?.days || [], 30));
|
|
299
|
+
const selectedDaysHasData = computed(() => selectedDaysTrend.value.length > 0);
|
|
300
|
+
const selectedDaysTrendPreview = computed(() => buildTrendPreview(selectedDaysFilled.value, 10));
|
|
301
|
+
const selectedDaysBars = computed(() => buildTrendBars(selectedDaysFilled.value, getTrendMax(selectedDaysFilled.value)));
|
|
302
302
|
const productPeriodCards = computed(() => {
|
|
303
303
|
const list = selectedDaysFilled.value;
|
|
304
304
|
const today = list[list.length - 1] || { pv: 0, uv: 0 };
|
|
@@ -317,7 +317,7 @@ const productPeriodCards = computed(() => {
|
|
|
317
317
|
});
|
|
318
318
|
|
|
319
319
|
function buildFallbackProduct() {
|
|
320
|
-
const days = sortTrendList(realtimeStats.days, "
|
|
320
|
+
const days = sortTrendList(realtimeStats.days, "reportDate");
|
|
321
321
|
|
|
322
322
|
return {
|
|
323
323
|
key: "all-products",
|
|
@@ -334,7 +334,7 @@ function buildFallbackProduct() {
|
|
|
334
334
|
};
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
function
|
|
337
|
+
function getReportDateNumber(timestamp) {
|
|
338
338
|
return Number(
|
|
339
339
|
new Intl.DateTimeFormat("en-CA", {
|
|
340
340
|
year: "numeric",
|
|
@@ -346,32 +346,32 @@ function getBucketDate(timestamp) {
|
|
|
346
346
|
);
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
function
|
|
349
|
+
function buildRecentReportDateList(limit) {
|
|
350
350
|
const list = [];
|
|
351
351
|
|
|
352
352
|
for (let i = limit - 1; i >= 0; i--) {
|
|
353
|
-
list.push(
|
|
353
|
+
list.push(getReportDateNumber(Date.now() - i * 24 * 60 * 60 * 1000));
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
return list;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
function buildFilledTrendDays(list, limit) {
|
|
360
|
-
const dateList =
|
|
360
|
+
const dateList = buildRecentReportDateList(limit);
|
|
361
361
|
const dataMap = new Map();
|
|
362
362
|
|
|
363
363
|
for (const item of list) {
|
|
364
|
-
dataMap.set(Number(item?.
|
|
365
|
-
|
|
364
|
+
dataMap.set(Number(item?.reportDate || 0), {
|
|
365
|
+
reportDate: Number(item?.reportDate || 0),
|
|
366
366
|
pv: Number(item?.pv || 0),
|
|
367
367
|
uv: Number(item?.uv || 0)
|
|
368
368
|
});
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
return dateList.map((
|
|
371
|
+
return dateList.map((reportDate) => {
|
|
372
372
|
return (
|
|
373
|
-
dataMap.get(
|
|
374
|
-
|
|
373
|
+
dataMap.get(reportDate) || {
|
|
374
|
+
reportDate: reportDate,
|
|
375
375
|
pv: 0,
|
|
376
376
|
uv: 0
|
|
377
377
|
}
|
|
@@ -437,49 +437,41 @@ function buildTrendPreview(list, maxCount) {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
const preview = [];
|
|
440
|
-
const
|
|
441
|
-
const step = Math.ceil(lastIndex / (maxCount - 1));
|
|
440
|
+
const usedIndex = new Set();
|
|
442
441
|
|
|
443
|
-
for (let i = 0; i
|
|
444
|
-
|
|
445
|
-
|
|
442
|
+
for (let i = 0; i < maxCount; i++) {
|
|
443
|
+
const index = Math.round((i * (list.length - 1)) / (maxCount - 1));
|
|
444
|
+
|
|
445
|
+
if (usedIndex.has(index)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
446
448
|
|
|
447
|
-
|
|
448
|
-
preview.push(list[
|
|
449
|
+
usedIndex.add(index);
|
|
450
|
+
preview.push(list[index]);
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
return preview
|
|
453
|
+
return preview;
|
|
452
454
|
}
|
|
453
455
|
|
|
454
|
-
function
|
|
455
|
-
if (list.length === 0) {
|
|
456
|
-
return [];
|
|
457
|
-
}
|
|
458
|
-
|
|
456
|
+
function buildTrendBars(list, maxValue) {
|
|
459
457
|
const result = [];
|
|
460
458
|
|
|
461
|
-
for (
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
const x = list.length === 1 ? 50 : (i * 100) / (list.length - 1);
|
|
465
|
-
const y = 54 - (value / maxValue) * 42;
|
|
459
|
+
for (const item of list) {
|
|
460
|
+
const pv = Number(item?.pv || 0);
|
|
461
|
+
const uv = Number(item?.uv || 0);
|
|
466
462
|
|
|
467
463
|
result.push({
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
464
|
+
reportDate: Number(item?.reportDate || 0),
|
|
465
|
+
pv: pv,
|
|
466
|
+
uv: uv,
|
|
467
|
+
pvHeight: pv > 0 ? `${Math.max((pv / maxValue) * 100, 6)}%` : "0%",
|
|
468
|
+
uvHeight: uv > 0 ? `${Math.max((uv / maxValue) * 100, 6)}%` : "0%"
|
|
471
469
|
});
|
|
472
470
|
}
|
|
473
471
|
|
|
474
472
|
return result;
|
|
475
473
|
}
|
|
476
474
|
|
|
477
|
-
function buildTrendPoints(list, field, maxValue) {
|
|
478
|
-
return buildTrendDots(list, field, maxValue)
|
|
479
|
-
.map((item) => `${item.x},${item.y}`)
|
|
480
|
-
.join(" ");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
475
|
function getTrendMax(list) {
|
|
484
476
|
let max = 0;
|
|
485
477
|
|
|
@@ -527,8 +519,8 @@ function formatDeviceType(deviceType) {
|
|
|
527
519
|
return String(deviceType || "Unknown");
|
|
528
520
|
}
|
|
529
521
|
|
|
530
|
-
function
|
|
531
|
-
const text = String(
|
|
522
|
+
function formatReportDate(reportDate) {
|
|
523
|
+
const text = String(reportDate || "");
|
|
532
524
|
|
|
533
525
|
if (text.length !== 8) {
|
|
534
526
|
return text;
|
|
@@ -539,23 +531,23 @@ function formatBucketDate(bucketDate) {
|
|
|
539
531
|
|
|
540
532
|
const fetchData = async () => {
|
|
541
533
|
try {
|
|
542
|
-
const [overviewRes,
|
|
534
|
+
const [overviewRes, onlineStatsRes, infoStatsRes, errorStatsRes] = await Promise.all([$Http("/core/dashboard/systemOverview", {}, [""]), $Http("/core/tongJi/onlineStats", {}, [""]), $Http("/core/tongJi/infoStats", {}, [""]), $Http("/core/tongJi/errorStats", {}, [""])]);
|
|
543
535
|
|
|
544
536
|
Object.assign(permissionStats, overviewRes.data);
|
|
545
|
-
realtimeStats.onlineCount = Number(
|
|
546
|
-
realtimeStats.todayPv = Number(
|
|
547
|
-
realtimeStats.todayUv = Number(
|
|
548
|
-
realtimeStats.days = Array.isArray(
|
|
549
|
-
productState.options =
|
|
550
|
-
uaStats.deviceTypes = Array.isArray(
|
|
551
|
-
uaStats.browsers = Array.isArray(
|
|
552
|
-
uaStats.browserVersions = Array.isArray(
|
|
553
|
-
uaStats.osList = Array.isArray(
|
|
554
|
-
uaStats.osVersions = Array.isArray(
|
|
555
|
-
uaStats.deviceVendors = Array.isArray(
|
|
556
|
-
uaStats.deviceModels = Array.isArray(
|
|
557
|
-
uaStats.engines = Array.isArray(
|
|
558
|
-
uaStats.cpuArchitectures = Array.isArray(
|
|
537
|
+
realtimeStats.onlineCount = Number(onlineStatsRes.data?.onlineCount || 0);
|
|
538
|
+
realtimeStats.todayPv = Number(onlineStatsRes.data?.today?.pv || 0);
|
|
539
|
+
realtimeStats.todayUv = Number(onlineStatsRes.data?.today?.uv || 0);
|
|
540
|
+
realtimeStats.days = Array.isArray(onlineStatsRes.data?.days) ? onlineStatsRes.data.days : [];
|
|
541
|
+
productState.options = [];
|
|
542
|
+
uaStats.deviceTypes = Array.isArray(infoStatsRes.data?.today?.deviceTypes) ? infoStatsRes.data.today.deviceTypes : [];
|
|
543
|
+
uaStats.browsers = Array.isArray(infoStatsRes.data?.today?.browsers) ? infoStatsRes.data.today.browsers : [];
|
|
544
|
+
uaStats.browserVersions = Array.isArray(infoStatsRes.data?.today?.browserVersions) ? infoStatsRes.data.today.browserVersions : [];
|
|
545
|
+
uaStats.osList = Array.isArray(infoStatsRes.data?.today?.osList) ? infoStatsRes.data.today.osList : [];
|
|
546
|
+
uaStats.osVersions = Array.isArray(infoStatsRes.data?.today?.osVersions) ? infoStatsRes.data.today.osVersions : [];
|
|
547
|
+
uaStats.deviceVendors = Array.isArray(infoStatsRes.data?.today?.deviceVendors) ? infoStatsRes.data.today.deviceVendors : [];
|
|
548
|
+
uaStats.deviceModels = Array.isArray(infoStatsRes.data?.today?.deviceModels) ? infoStatsRes.data.today.deviceModels : [];
|
|
549
|
+
uaStats.engines = Array.isArray(infoStatsRes.data?.today?.engines) ? infoStatsRes.data.today.engines : [];
|
|
550
|
+
uaStats.cpuArchitectures = Array.isArray(infoStatsRes.data?.today?.cpuArchitectures) ? infoStatsRes.data.today.cpuArchitectures : [];
|
|
559
551
|
errorStats.trend = Array.isArray(errorStatsRes.data?.trend) ? errorStatsRes.data.trend : [];
|
|
560
552
|
productInfo.productName = String($Config.productName || "-");
|
|
561
553
|
productInfo.productCode = String($Config.productCode || "-");
|
|
@@ -865,10 +857,13 @@ fetchData();
|
|
|
865
857
|
background: #2ba471;
|
|
866
858
|
}
|
|
867
859
|
|
|
868
|
-
.trend-
|
|
869
|
-
|
|
860
|
+
.trend-bars {
|
|
861
|
+
display: grid;
|
|
862
|
+
grid-template-columns: repeat(30, minmax(0, 1fr));
|
|
863
|
+
align-items: end;
|
|
864
|
+
gap: 0;
|
|
870
865
|
height: 148px;
|
|
871
|
-
|
|
866
|
+
padding: 10px 0 6px;
|
|
872
867
|
border-radius: 8px;
|
|
873
868
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 1px, transparent 1px), linear-gradient(to right, rgba(0, 0, 0, 0.03) 1px, transparent 1px), linear-gradient(180deg, rgba(var(--primary-color-rgb), 0.025), rgba(var(--primary-color-rgb), 0.06));
|
|
874
869
|
background-size:
|
|
@@ -877,23 +872,52 @@ fetchData();
|
|
|
877
872
|
auto;
|
|
878
873
|
}
|
|
879
874
|
|
|
880
|
-
.trend-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
875
|
+
.trend-bar-group {
|
|
876
|
+
min-width: 0;
|
|
877
|
+
height: 100%;
|
|
878
|
+
display: flex;
|
|
879
|
+
flex-direction: column;
|
|
880
|
+
justify-content: flex-end;
|
|
881
|
+
gap: 4px;
|
|
882
|
+
padding: 0 2px;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.trend-bar-stack {
|
|
886
|
+
flex: 1;
|
|
887
|
+
display: flex;
|
|
888
|
+
align-items: flex-end;
|
|
889
|
+
justify-content: center;
|
|
890
|
+
gap: 2px;
|
|
891
|
+
min-height: 0;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.trend-bar {
|
|
895
|
+
width: calc(50% - 1px);
|
|
896
|
+
min-width: 2px;
|
|
897
|
+
border-radius: 4px 4px 0 0;
|
|
898
|
+
transition: all 0.2s ease;
|
|
899
|
+
|
|
900
|
+
&:hover {
|
|
901
|
+
opacity: 0.9;
|
|
902
|
+
transform: translateY(-1px);
|
|
903
|
+
}
|
|
885
904
|
}
|
|
886
905
|
|
|
887
|
-
.trend-
|
|
888
|
-
|
|
889
|
-
stroke: #0052d9;
|
|
890
|
-
fill: #0052d9;
|
|
906
|
+
.trend-bar-pv {
|
|
907
|
+
background: linear-gradient(180deg, rgba(0, 82, 217, 0.65), #0052d9);
|
|
891
908
|
}
|
|
892
909
|
|
|
893
|
-
.trend-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
910
|
+
.trend-bar-uv {
|
|
911
|
+
background: linear-gradient(180deg, rgba(43, 164, 113, 0.65), #2ba471);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.trend-bar-label {
|
|
915
|
+
font-size: 10px;
|
|
916
|
+
line-height: 1;
|
|
917
|
+
color: var(--text-tertiary);
|
|
918
|
+
text-align: center;
|
|
919
|
+
font-variant-numeric: tabular-nums;
|
|
920
|
+
transform: scale(0.92);
|
|
897
921
|
}
|
|
898
922
|
|
|
899
923
|
.trend-briefs {
|
|
@@ -1040,6 +1064,16 @@ fetchData();
|
|
|
1040
1064
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
1041
1065
|
}
|
|
1042
1066
|
|
|
1067
|
+
.trend-bars {
|
|
1068
|
+
grid-template-columns: repeat(15, minmax(0, 1fr));
|
|
1069
|
+
row-gap: 6px;
|
|
1070
|
+
height: auto;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.trend-bar-group:nth-child(n + 16) {
|
|
1074
|
+
display: none;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1043
1077
|
.ua-grid {
|
|
1044
1078
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1045
1079
|
}
|
|
@@ -1073,6 +1107,14 @@ fetchData();
|
|
|
1073
1107
|
.trend-briefs-wide {
|
|
1074
1108
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1075
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
.trend-bars {
|
|
1112
|
+
grid-template-columns: repeat(10, minmax(0, 1fr));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.trend-bar-group:nth-child(n + 11) {
|
|
1116
|
+
display: none;
|
|
1117
|
+
}
|
|
1076
1118
|
}
|
|
1077
1119
|
|
|
1078
1120
|
@media (max-width: 640px) {
|
|
@@ -1090,8 +1132,13 @@ fetchData();
|
|
|
1090
1132
|
width: 100%;
|
|
1091
1133
|
}
|
|
1092
1134
|
|
|
1093
|
-
.trend-
|
|
1135
|
+
.trend-bars {
|
|
1136
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
1094
1137
|
height: 126px;
|
|
1095
1138
|
}
|
|
1139
|
+
|
|
1140
|
+
.trend-bar-group:nth-child(n + 8) {
|
|
1141
|
+
display: none;
|
|
1142
|
+
}
|
|
1096
1143
|
}
|
|
1097
1144
|
</style>
|
|
@@ -268,8 +268,18 @@ function formatDeviceType(deviceType) {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
function formatTime(timestamp) {
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
const value = Number(timestamp || 0);
|
|
272
|
+
|
|
273
|
+
if (!value) {
|
|
274
|
+
return "-";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const date = new Date(value);
|
|
278
|
+
|
|
279
|
+
if (Number.isNaN(date.getTime())) {
|
|
280
|
+
return "-";
|
|
281
|
+
}
|
|
282
|
+
|
|
273
283
|
const year = date.getFullYear();
|
|
274
284
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
275
285
|
const day = String(date.getDate()).padStart(2, "0");
|
|
@@ -1,700 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="page-visit-stats">
|
|
3
|
-
<div class="page-header">
|
|
4
|
-
<div>
|
|
5
|
-
<div class="page-title">访问统计</div>
|
|
6
|
-
<div class="page-desc">查看访问趋势、在线人数与 UA 设备明细分布</div>
|
|
7
|
-
</div>
|
|
8
|
-
<TButton theme="primary" @click="fetchData">刷新统计</TButton>
|
|
9
|
-
</div>
|
|
10
|
-
|
|
11
|
-
<div class="summary-grid">
|
|
12
|
-
<div class="summary-card">
|
|
13
|
-
<div class="summary-label">产品名称</div>
|
|
14
|
-
<div class="summary-value primary">{{ $Config.productName }}</div>
|
|
15
|
-
</div>
|
|
16
|
-
<div class="summary-card">
|
|
17
|
-
<div class="summary-label">产品代号</div>
|
|
18
|
-
<div class="summary-value info">{{ $Config.productCode }}</div>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="summary-card">
|
|
21
|
-
<div class="summary-label">产品版本</div>
|
|
22
|
-
<div class="summary-value success">{{ $Config.productVersion }}</div>
|
|
23
|
-
</div>
|
|
24
|
-
<div class="summary-card">
|
|
25
|
-
<div class="summary-label">当前在线</div>
|
|
26
|
-
<div class="summary-value primary">{{ $Data.onlineCount }}</div>
|
|
27
|
-
</div>
|
|
28
|
-
<div class="summary-card">
|
|
29
|
-
<div class="summary-label">今日访问</div>
|
|
30
|
-
<div class="summary-value info">{{ $Data.todayPv }}</div>
|
|
31
|
-
</div>
|
|
32
|
-
<div class="summary-card">
|
|
33
|
-
<div class="summary-label">今日人数</div>
|
|
34
|
-
<div class="summary-value success">{{ $Data.todayUv }}</div>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="summary-card">
|
|
37
|
-
<div class="summary-label">主设备类型</div>
|
|
38
|
-
<div class="summary-value warning">{{ getTopUaLabel($Data.uaStats.deviceTypes, formatDeviceType) }}</div>
|
|
39
|
-
</div>
|
|
40
|
-
<div class="summary-card">
|
|
41
|
-
<div class="summary-label">主浏览器</div>
|
|
42
|
-
<div class="summary-value primary">{{ getTopUaLabel($Data.uaStats.browsers) }}</div>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="summary-card">
|
|
45
|
-
<div class="summary-label">主系统</div>
|
|
46
|
-
<div class="summary-value danger">{{ getTopUaLabel($Data.uaStats.osList) }}</div>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div class="trend-grid">
|
|
51
|
-
<div class="trend-card">
|
|
52
|
-
<div class="card-title">今日 30 分钟访问趋势</div>
|
|
53
|
-
<div v-if="todayTrend.length > 0" class="trend-chart-wrap">
|
|
54
|
-
<svg viewBox="0 0 100 60" preserveAspectRatio="none" class="trend-svg">
|
|
55
|
-
<polyline class="trend-line trend-line-pv" :points="todayPvPoints" />
|
|
56
|
-
<polyline class="trend-line trend-line-uv" :points="todayUvPoints" />
|
|
57
|
-
<circle v-for="item in todayPvDots" :key="`pv-${item.bucketTime}`" class="trend-dot trend-dot-pv" :cx="item.x" :cy="item.y" r="1.2" />
|
|
58
|
-
<circle v-for="item in todayUvDots" :key="`uv-${item.bucketTime}`" class="trend-dot trend-dot-uv" :cx="item.x" :cy="item.y" r="1.2" />
|
|
59
|
-
</svg>
|
|
60
|
-
<div class="trend-mini-list">
|
|
61
|
-
<div v-for="item in todayTrendPreview" :key="`preview-${item.bucketTime}`" class="trend-mini-item">
|
|
62
|
-
<div class="trend-mini-time">{{ formatBucketTime(item.bucketTime) }}</div>
|
|
63
|
-
<div class="trend-mini-values">
|
|
64
|
-
<span class="pv">PV {{ item.pv }}</span>
|
|
65
|
-
<span class="uv">UV {{ item.uv }}</span>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
<div v-else class="empty-block">暂无今日趋势数据</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<div class="trend-card">
|
|
74
|
-
<div class="card-title">近 7 天访问趋势</div>
|
|
75
|
-
<div v-if="trendDays.length > 0" class="trend-chart-wrap">
|
|
76
|
-
<svg viewBox="0 0 100 60" preserveAspectRatio="none" class="trend-svg">
|
|
77
|
-
<polyline class="trend-line trend-line-pv" :points="pvPoints" />
|
|
78
|
-
<polyline class="trend-line trend-line-uv" :points="uvPoints" />
|
|
79
|
-
<circle v-for="item in pvDots" :key="`days-pv-${item.bucketDate}`" class="trend-dot trend-dot-pv" :cx="item.x" :cy="item.y" r="1.2" />
|
|
80
|
-
<circle v-for="item in uvDots" :key="`days-uv-${item.bucketDate}`" class="trend-dot trend-dot-uv" :cx="item.x" :cy="item.y" r="1.2" />
|
|
81
|
-
</svg>
|
|
82
|
-
<div class="trend-mini-list trend-mini-list-days">
|
|
83
|
-
<div v-for="item in trendDays" :key="`day-${item.bucketDate}`" class="trend-mini-item">
|
|
84
|
-
<div class="trend-mini-time">{{ formatBucketDate(item.bucketDate) }}</div>
|
|
85
|
-
<div class="trend-mini-values">
|
|
86
|
-
<span class="pv">PV {{ item.pv }}</span>
|
|
87
|
-
<span class="uv">UV {{ item.uv }}</span>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
<div v-else class="empty-block">暂无近 7 天趋势数据</div>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div class="ua-grid">
|
|
97
|
-
<div class="ua-card">
|
|
98
|
-
<div class="card-title">设备类型</div>
|
|
99
|
-
<div v-if="$Data.uaStats.deviceTypes.length > 0" class="ua-list">
|
|
100
|
-
<div v-for="item in $Data.uaStats.deviceTypes" :key="`type-${item.name}`" class="ua-row">
|
|
101
|
-
<span>{{ formatDeviceType(item.name) }}</span>
|
|
102
|
-
<strong>{{ item.count }}</strong>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<div v-else class="empty-inline">暂无设备类型数据</div>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div class="ua-card">
|
|
109
|
-
<div class="card-title">浏览器</div>
|
|
110
|
-
<div v-if="$Data.uaStats.browsers.length > 0" class="ua-list">
|
|
111
|
-
<div v-for="item in $Data.uaStats.browsers" :key="`browser-${item.name}`" class="ua-row">
|
|
112
|
-
<span>{{ item.name }}</span>
|
|
113
|
-
<strong>{{ item.count }}</strong>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
<div v-else class="empty-inline">暂无浏览器数据</div>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div class="ua-card">
|
|
120
|
-
<div class="card-title">操作系统</div>
|
|
121
|
-
<div v-if="$Data.uaStats.osList.length > 0" class="ua-list">
|
|
122
|
-
<div v-for="item in $Data.uaStats.osList" :key="`os-${item.name}`" class="ua-row">
|
|
123
|
-
<span>{{ item.name }}</span>
|
|
124
|
-
<strong>{{ item.count }}</strong>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
<div v-else class="empty-inline">暂无操作系统数据</div>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div class="ua-card">
|
|
131
|
-
<div class="card-title">浏览器版本</div>
|
|
132
|
-
<div v-if="$Data.uaStats.browserVersions.length > 0" class="ua-chip-list">
|
|
133
|
-
<span v-for="item in $Data.uaStats.browserVersions" :key="`browser-version-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
|
|
134
|
-
</div>
|
|
135
|
-
<div v-else class="empty-inline">暂无浏览器版本数据</div>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<div class="ua-card">
|
|
139
|
-
<div class="card-title">系统版本</div>
|
|
140
|
-
<div v-if="$Data.uaStats.osVersions.length > 0" class="ua-chip-list">
|
|
141
|
-
<span v-for="item in $Data.uaStats.osVersions" :key="`os-version-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
|
|
142
|
-
</div>
|
|
143
|
-
<div v-else class="empty-inline">暂无系统版本数据</div>
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<div class="ua-card">
|
|
147
|
-
<div class="card-title">设备厂商</div>
|
|
148
|
-
<div v-if="$Data.uaStats.deviceVendors.length > 0" class="ua-chip-list">
|
|
149
|
-
<span v-for="item in $Data.uaStats.deviceVendors" :key="`vendor-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
|
|
150
|
-
</div>
|
|
151
|
-
<div v-else class="empty-inline">暂无设备厂商数据</div>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<div class="ua-card ua-card-wide">
|
|
155
|
-
<div class="card-title">设备明细</div>
|
|
156
|
-
<div class="detail-grid">
|
|
157
|
-
<div class="detail-block">
|
|
158
|
-
<div class="detail-title">设备型号</div>
|
|
159
|
-
<div v-if="$Data.uaStats.deviceModels.length > 0" class="ua-chip-list">
|
|
160
|
-
<span v-for="item in $Data.uaStats.deviceModels" :key="`model-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
|
|
161
|
-
</div>
|
|
162
|
-
<div v-else class="empty-inline">暂无设备型号数据</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<div class="detail-block">
|
|
166
|
-
<div class="detail-title">渲染引擎</div>
|
|
167
|
-
<div v-if="$Data.uaStats.engines.length > 0" class="ua-chip-list">
|
|
168
|
-
<span v-for="item in $Data.uaStats.engines" :key="`engine-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
|
|
169
|
-
</div>
|
|
170
|
-
<div v-else class="empty-inline">暂无渲染引擎数据</div>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
<div class="detail-block">
|
|
174
|
-
<div class="detail-title">CPU 架构</div>
|
|
175
|
-
<div v-if="$Data.uaStats.cpuArchitectures.length > 0" class="ua-chip-list">
|
|
176
|
-
<span v-for="item in $Data.uaStats.cpuArchitectures" :key="`cpu-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
|
|
177
|
-
</div>
|
|
178
|
-
<div v-else class="empty-inline">暂无 CPU 架构数据</div>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
</template>
|
|
185
|
-
|
|
186
|
-
<script setup>
|
|
187
|
-
import { computed, reactive } from "vue";
|
|
188
|
-
import { Button as TButton } from "tdesign-vue-next";
|
|
189
|
-
import { $Http } from "@/plugins/http";
|
|
190
|
-
import { $Config } from "@/plugins/config";
|
|
191
|
-
|
|
192
|
-
const $Data = reactive({
|
|
193
|
-
onlineCount: 0,
|
|
194
|
-
todayPv: 0,
|
|
195
|
-
todayUv: 0,
|
|
196
|
-
days: [],
|
|
197
|
-
trend: [],
|
|
198
|
-
uaStats: {
|
|
199
|
-
deviceTypes: [],
|
|
200
|
-
browsers: [],
|
|
201
|
-
browserVersions: [],
|
|
202
|
-
osList: [],
|
|
203
|
-
osVersions: [],
|
|
204
|
-
deviceVendors: [],
|
|
205
|
-
deviceModels: [],
|
|
206
|
-
engines: [],
|
|
207
|
-
cpuArchitectures: []
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
function getRecentDays(list, dayCount = 7) {
|
|
212
|
-
const map = new Map();
|
|
213
|
-
const now = new Date();
|
|
214
|
-
|
|
215
|
-
for (const item of Array.isArray(list) ? list : []) {
|
|
216
|
-
const bucketDate = Number(item.bucketDate || 0);
|
|
217
|
-
if (!bucketDate) {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
map.set(bucketDate, {
|
|
222
|
-
bucketDate: bucketDate,
|
|
223
|
-
pv: Number(item.pv || 0),
|
|
224
|
-
uv: Number(item.uv || 0)
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const days = [];
|
|
229
|
-
for (let i = dayCount - 1; i >= 0; i--) {
|
|
230
|
-
const date = new Date(now);
|
|
231
|
-
date.setHours(0, 0, 0, 0);
|
|
232
|
-
date.setDate(date.getDate() - i);
|
|
233
|
-
const bucketDate = Number(`${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}${String(date.getDate()).padStart(2, "0")}`);
|
|
234
|
-
|
|
235
|
-
days.push(
|
|
236
|
-
map.get(bucketDate) || {
|
|
237
|
-
bucketDate: bucketDate,
|
|
238
|
-
pv: 0,
|
|
239
|
-
uv: 0
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return days;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const todayTrend = computed(() => {
|
|
248
|
-
const list = Array.isArray($Data.trend) ? $Data.trend.slice() : [];
|
|
249
|
-
list.sort((a, b) => Number(a.bucketTime || 0) - Number(b.bucketTime || 0));
|
|
250
|
-
return list;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const trendDays = computed(() => {
|
|
254
|
-
return getRecentDays($Data.days);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const todayTrendPreview = computed(() => {
|
|
258
|
-
const list = todayTrend.value;
|
|
259
|
-
if (list.length <= 6) {
|
|
260
|
-
return list;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const preview = [];
|
|
264
|
-
const lastIndex = list.length - 1;
|
|
265
|
-
const step = Math.ceil(lastIndex / 5);
|
|
266
|
-
|
|
267
|
-
for (let i = 0; i <= lastIndex; i += step) {
|
|
268
|
-
preview.push(list[i]);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (preview[preview.length - 1]?.bucketTime !== list[lastIndex].bucketTime) {
|
|
272
|
-
preview.push(list[lastIndex]);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return preview;
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const todayTrendMax = computed(() => getTrendMax(todayTrend.value));
|
|
279
|
-
const trendMax = computed(() => getTrendMax(trendDays.value));
|
|
280
|
-
|
|
281
|
-
const todayPvPoints = computed(() => buildTrendPoints(todayTrend.value, "pv", "bucketTime", todayTrendMax.value));
|
|
282
|
-
const todayUvPoints = computed(() => buildTrendPoints(todayTrend.value, "uv", "bucketTime", todayTrendMax.value));
|
|
283
|
-
const todayPvDots = computed(() => buildTrendDots(todayTrend.value, "pv", "bucketTime", todayTrendMax.value));
|
|
284
|
-
const todayUvDots = computed(() => buildTrendDots(todayTrend.value, "uv", "bucketTime", todayTrendMax.value));
|
|
285
|
-
const pvPoints = computed(() => buildTrendPoints(trendDays.value, "pv", "bucketDate", trendMax.value));
|
|
286
|
-
const uvPoints = computed(() => buildTrendPoints(trendDays.value, "uv", "bucketDate", trendMax.value));
|
|
287
|
-
const pvDots = computed(() => buildTrendDots(trendDays.value, "pv", "bucketDate", trendMax.value));
|
|
288
|
-
const uvDots = computed(() => buildTrendDots(trendDays.value, "uv", "bucketDate", trendMax.value));
|
|
289
|
-
|
|
290
|
-
function buildTrendDots(list, field, keyField, maxValue) {
|
|
291
|
-
if (list.length === 0) {
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const dots = [];
|
|
296
|
-
for (let i = 0; i < list.length; i++) {
|
|
297
|
-
const item = list[i];
|
|
298
|
-
const value = Number(item[field] || 0);
|
|
299
|
-
const x = list.length === 1 ? 50 : (i * 100) / (list.length - 1);
|
|
300
|
-
const y = 54 - (value / maxValue) * 42;
|
|
301
|
-
|
|
302
|
-
dots.push({
|
|
303
|
-
bucketDate: item.bucketDate,
|
|
304
|
-
bucketTime: item.bucketTime,
|
|
305
|
-
key: item[keyField],
|
|
306
|
-
x: Number(x.toFixed(2)),
|
|
307
|
-
y: Number(y.toFixed(2))
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return dots;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function buildTrendPoints(list, field, keyField, maxValue) {
|
|
315
|
-
const dots = buildTrendDots(list, field, keyField, maxValue);
|
|
316
|
-
return dots.map((item) => `${item.x},${item.y}`).join(" ");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function getTrendMax(list, fieldA = "pv", fieldB = "uv") {
|
|
320
|
-
let max = 0;
|
|
321
|
-
|
|
322
|
-
for (const item of list) {
|
|
323
|
-
const valueA = Number(item[fieldA] || 0);
|
|
324
|
-
const valueB = Number(item[fieldB] || 0);
|
|
325
|
-
|
|
326
|
-
if (valueA > max) {
|
|
327
|
-
max = valueA;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (valueB > max) {
|
|
331
|
-
max = valueB;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return max > 0 ? max : 1;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function formatBucketDate(bucketDate) {
|
|
339
|
-
const text = String(bucketDate || "");
|
|
340
|
-
if (text.length !== 8) {
|
|
341
|
-
return text;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return `${text.slice(4, 6)}-${text.slice(6, 8)}`;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function formatBucketTime(bucketTime) {
|
|
348
|
-
const date = new Date(Number(bucketTime || 0));
|
|
349
|
-
if (Number.isNaN(date.getTime())) {
|
|
350
|
-
return "--:--";
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function formatDeviceType(deviceType) {
|
|
357
|
-
if (deviceType === "desktop") {
|
|
358
|
-
return "桌面端";
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (deviceType === "mobile") {
|
|
362
|
-
return "移动端";
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (deviceType === "tablet") {
|
|
366
|
-
return "平板";
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (deviceType === "smarttv") {
|
|
370
|
-
return "电视";
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (deviceType === "wearable") {
|
|
374
|
-
return "穿戴设备";
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (deviceType === "embedded") {
|
|
378
|
-
return "嵌入式设备";
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return String(deviceType || "Unknown");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function getTopUaLabel(list, formatter) {
|
|
385
|
-
const top = Array.isArray(list) && list.length > 0 ? list[0] : null;
|
|
386
|
-
if (!top) {
|
|
387
|
-
return "-";
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (typeof formatter === "function") {
|
|
391
|
-
return formatter(top.name);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return top.name;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function fetchData() {
|
|
398
|
-
try {
|
|
399
|
-
const result = await $Http("/core/tongJi/visitStats", {}, [""]);
|
|
400
|
-
$Data.onlineCount = Number(result.data?.onlineCount || 0);
|
|
401
|
-
$Data.todayPv = Number(result.data?.today?.pv || 0);
|
|
402
|
-
$Data.todayUv = Number(result.data?.today?.uv || 0);
|
|
403
|
-
$Data.days = Array.isArray(result.data?.days) ? result.data.days : [];
|
|
404
|
-
$Data.trend = Array.isArray(result.data?.trend) ? result.data.trend : [];
|
|
405
|
-
$Data.uaStats.deviceTypes = Array.isArray(result.data?.uaStats?.deviceTypes) ? result.data.uaStats.deviceTypes : [];
|
|
406
|
-
$Data.uaStats.browsers = Array.isArray(result.data?.uaStats?.browsers) ? result.data.uaStats.browsers : [];
|
|
407
|
-
$Data.uaStats.browserVersions = Array.isArray(result.data?.uaStats?.browserVersions) ? result.data.uaStats.browserVersions : [];
|
|
408
|
-
$Data.uaStats.osList = Array.isArray(result.data?.uaStats?.osList) ? result.data.uaStats.osList : [];
|
|
409
|
-
$Data.uaStats.osVersions = Array.isArray(result.data?.uaStats?.osVersions) ? result.data.uaStats.osVersions : [];
|
|
410
|
-
$Data.uaStats.deviceVendors = Array.isArray(result.data?.uaStats?.deviceVendors) ? result.data.uaStats.deviceVendors : [];
|
|
411
|
-
$Data.uaStats.deviceModels = Array.isArray(result.data?.uaStats?.deviceModels) ? result.data.uaStats.deviceModels : [];
|
|
412
|
-
$Data.uaStats.engines = Array.isArray(result.data?.uaStats?.engines) ? result.data.uaStats.engines : [];
|
|
413
|
-
$Data.uaStats.cpuArchitectures = Array.isArray(result.data?.uaStats?.cpuArchitectures) ? result.data.uaStats.cpuArchitectures : [];
|
|
414
|
-
} catch (_error) {
|
|
415
|
-
// 静默失败
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
fetchData();
|
|
420
|
-
</script>
|
|
421
|
-
|
|
422
|
-
<style scoped lang="scss">
|
|
423
|
-
.page-visit-stats {
|
|
424
|
-
display: flex;
|
|
425
|
-
flex-direction: column;
|
|
426
|
-
gap: 16px;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
.page-header {
|
|
430
|
-
display: flex;
|
|
431
|
-
align-items: center;
|
|
432
|
-
justify-content: space-between;
|
|
433
|
-
gap: 12px;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
.page-title {
|
|
437
|
-
font-size: 22px;
|
|
438
|
-
font-weight: 700;
|
|
439
|
-
color: var(--text-primary);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
.page-desc {
|
|
443
|
-
margin-top: 4px;
|
|
444
|
-
font-size: 13px;
|
|
445
|
-
color: var(--text-secondary);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
.summary-grid {
|
|
449
|
-
display: grid;
|
|
450
|
-
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
451
|
-
gap: 12px;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
.summary-card,
|
|
455
|
-
.trend-card,
|
|
456
|
-
.ua-card {
|
|
457
|
-
border: 1px solid var(--border-color);
|
|
458
|
-
border-radius: 8px;
|
|
459
|
-
background: linear-gradient(180deg, rgba(var(--primary-color-rgb), 0.03), white);
|
|
460
|
-
padding: 14px;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
.summary-label {
|
|
464
|
-
font-size: 13px;
|
|
465
|
-
color: var(--text-secondary);
|
|
466
|
-
margin-bottom: 6px;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
.summary-value {
|
|
470
|
-
font-size: 24px;
|
|
471
|
-
font-weight: 700;
|
|
472
|
-
color: var(--text-primary);
|
|
473
|
-
|
|
474
|
-
&.primary {
|
|
475
|
-
color: var(--primary-color);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
&.info {
|
|
479
|
-
color: var(--brand-color);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
&.success {
|
|
483
|
-
color: var(--success-color);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
&.warning {
|
|
487
|
-
color: var(--warning-color);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
&.danger {
|
|
491
|
-
color: var(--error-color);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
.trend-grid {
|
|
496
|
-
display: grid;
|
|
497
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
498
|
-
gap: 12px;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
.card-title {
|
|
502
|
-
font-size: 14px;
|
|
503
|
-
font-weight: 600;
|
|
504
|
-
color: var(--text-primary);
|
|
505
|
-
margin-bottom: 10px;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
.trend-chart-wrap {
|
|
509
|
-
display: flex;
|
|
510
|
-
flex-direction: column;
|
|
511
|
-
gap: 10px;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
.trend-svg {
|
|
515
|
-
width: 100%;
|
|
516
|
-
height: 220px;
|
|
517
|
-
display: block;
|
|
518
|
-
border-radius: 8px;
|
|
519
|
-
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 1px, transparent 1px), linear-gradient(to right, rgba(0, 0, 0, 0.03) 1px, transparent 1px), linear-gradient(180deg, rgba(var(--primary-color-rgb), 0.02), rgba(var(--primary-color-rgb), 0.05));
|
|
520
|
-
background-size:
|
|
521
|
-
100% 25%,
|
|
522
|
-
14.285% 100%,
|
|
523
|
-
auto;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
.trend-line {
|
|
527
|
-
fill: none;
|
|
528
|
-
stroke-width: 1.8;
|
|
529
|
-
stroke-linecap: round;
|
|
530
|
-
stroke-linejoin: round;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
.trend-line-pv {
|
|
534
|
-
stroke: #0052d9;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
.trend-line-uv {
|
|
538
|
-
stroke: #2ba471;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
.trend-dot-pv {
|
|
542
|
-
fill: #0052d9;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
.trend-dot-uv {
|
|
546
|
-
fill: #2ba471;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
.trend-mini-list {
|
|
550
|
-
display: grid;
|
|
551
|
-
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
552
|
-
gap: 8px;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
.trend-mini-list-days {
|
|
556
|
-
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
.trend-mini-item {
|
|
560
|
-
padding: 8px 6px;
|
|
561
|
-
border-radius: 6px;
|
|
562
|
-
background: rgba(255, 255, 255, 0.85);
|
|
563
|
-
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
564
|
-
text-align: center;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
.trend-mini-time {
|
|
568
|
-
font-size: 12px;
|
|
569
|
-
color: var(--text-secondary);
|
|
570
|
-
margin-bottom: 4px;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
.trend-mini-values {
|
|
574
|
-
display: flex;
|
|
575
|
-
flex-direction: column;
|
|
576
|
-
gap: 2px;
|
|
577
|
-
font-size: 12px;
|
|
578
|
-
font-weight: 600;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
.trend-mini-values .pv {
|
|
582
|
-
color: #0052d9;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
.trend-mini-values .uv {
|
|
586
|
-
color: #2ba471;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
.ua-grid {
|
|
590
|
-
display: grid;
|
|
591
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
592
|
-
gap: 12px;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
.ua-card-wide {
|
|
596
|
-
grid-column: span 3;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
.ua-list {
|
|
600
|
-
display: flex;
|
|
601
|
-
flex-direction: column;
|
|
602
|
-
gap: 8px;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
.ua-row {
|
|
606
|
-
display: flex;
|
|
607
|
-
align-items: center;
|
|
608
|
-
justify-content: space-between;
|
|
609
|
-
gap: 12px;
|
|
610
|
-
padding: 8px 10px;
|
|
611
|
-
border-radius: 6px;
|
|
612
|
-
background: rgba(var(--primary-color-rgb), 0.04);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
.ua-chip-list {
|
|
616
|
-
display: flex;
|
|
617
|
-
flex-wrap: wrap;
|
|
618
|
-
gap: 8px;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
.ua-chip {
|
|
622
|
-
display: inline-flex;
|
|
623
|
-
align-items: center;
|
|
624
|
-
padding: 4px 10px;
|
|
625
|
-
border-radius: 999px;
|
|
626
|
-
background: rgba(var(--primary-color-rgb), 0.08);
|
|
627
|
-
color: var(--primary-color);
|
|
628
|
-
font-size: 12px;
|
|
629
|
-
font-weight: 600;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
.detail-grid {
|
|
633
|
-
display: grid;
|
|
634
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
635
|
-
gap: 12px;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
.detail-block {
|
|
639
|
-
border-radius: 6px;
|
|
640
|
-
background: rgba(var(--primary-color-rgb), 0.03);
|
|
641
|
-
padding: 10px;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
.detail-title {
|
|
645
|
-
font-size: 13px;
|
|
646
|
-
font-weight: 600;
|
|
647
|
-
color: var(--text-primary);
|
|
648
|
-
margin-bottom: 8px;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
.empty-block,
|
|
652
|
-
.empty-inline {
|
|
653
|
-
display: flex;
|
|
654
|
-
align-items: center;
|
|
655
|
-
justify-content: center;
|
|
656
|
-
min-height: 96px;
|
|
657
|
-
border-radius: 6px;
|
|
658
|
-
background: rgba(var(--primary-color-rgb), 0.03);
|
|
659
|
-
color: var(--text-secondary);
|
|
660
|
-
font-size: 13px;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
.empty-inline {
|
|
664
|
-
min-height: 72px;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
@media (max-width: 1200px) {
|
|
668
|
-
.summary-grid {
|
|
669
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
@media (max-width: 960px) {
|
|
674
|
-
.trend-grid,
|
|
675
|
-
.ua-grid,
|
|
676
|
-
.detail-grid {
|
|
677
|
-
grid-template-columns: 1fr;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
.ua-card-wide {
|
|
681
|
-
grid-column: span 1;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
@media (max-width: 640px) {
|
|
686
|
-
.page-header {
|
|
687
|
-
flex-direction: column;
|
|
688
|
-
align-items: flex-start;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
.summary-grid {
|
|
692
|
-
grid-template-columns: 1fr;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
.trend-mini-list,
|
|
696
|
-
.trend-mini-list-days {
|
|
697
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
</style>
|