befly-admin-ui 1.8.32 → 1.8.34
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 +214 -162
- package/views/log/error/index.vue +12 -2
- package/views/log/visit/index.vue +0 -700
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
<div class="card-title">统计信息</div>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
|
-
<div class="product-
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
</
|
|
52
|
+
<div class="product-select-wrap">
|
|
53
|
+
<TSelect :model-value="productState.selectedKey" class="product-select" placeholder="请选择产品" @change="selectProduct">
|
|
54
|
+
<TOption v-for="item in productOptions" :key="item.key" :value="item.key" :label="item.productName" />
|
|
55
|
+
</TSelect>
|
|
56
56
|
</div>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
@@ -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>
|
|
@@ -188,6 +191,7 @@
|
|
|
188
191
|
|
|
189
192
|
<script setup>
|
|
190
193
|
import { computed, reactive } from "vue";
|
|
194
|
+
import { Option as TOption, Select as TSelect } from "tdesign-vue-next";
|
|
191
195
|
import { InfoCircleIcon, TrendingUpIcon } from "tdesign-icons-vue-next";
|
|
192
196
|
|
|
193
197
|
import { $Config } from "@/plugins/config";
|
|
@@ -247,12 +251,29 @@ const errorTodayCount = computed(() => {
|
|
|
247
251
|
|
|
248
252
|
const productOptions = computed(() => {
|
|
249
253
|
const list = Array.isArray(productState.options) ? productState.options.slice() : [];
|
|
254
|
+
const fallbackProduct = buildFallbackProduct();
|
|
250
255
|
|
|
251
|
-
if (list.length
|
|
252
|
-
return
|
|
256
|
+
if (list.length === 0) {
|
|
257
|
+
return [fallbackProduct];
|
|
253
258
|
}
|
|
254
259
|
|
|
255
|
-
|
|
260
|
+
list.sort((a, b) => {
|
|
261
|
+
if (a.productName === productInfo.productName && b.productName !== productInfo.productName) {
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (b.productName === productInfo.productName && a.productName !== productInfo.productName) {
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (Number(b.totalPv || 0) !== Number(a.totalPv || 0)) {
|
|
270
|
+
return Number(b.totalPv || 0) - Number(a.totalPv || 0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return String(a.productName || "").localeCompare(String(b.productName || ""), "zh-CN");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return [fallbackProduct, ...list];
|
|
256
277
|
});
|
|
257
278
|
|
|
258
279
|
const selectedProduct = computed(() => {
|
|
@@ -266,39 +287,11 @@ const selectedProduct = computed(() => {
|
|
|
266
287
|
return matched || list[0];
|
|
267
288
|
});
|
|
268
289
|
|
|
269
|
-
const
|
|
270
|
-
return (
|
|
271
|
-
selectedProduct.value?.today || {
|
|
272
|
-
pv: 0,
|
|
273
|
-
uv: 0
|
|
274
|
-
}
|
|
275
|
-
);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
const selectedProductTotalPv = computed(() => {
|
|
279
|
-
if (selectedProduct.value?.totalPv !== undefined) {
|
|
280
|
-
return Number(selectedProduct.value.totalPv || 0);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return sumTrendField(selectedProduct.value?.days || [], "pv");
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const selectedProductTotalUv = computed(() => {
|
|
287
|
-
if (selectedProduct.value?.totalUv !== undefined) {
|
|
288
|
-
return Number(selectedProduct.value.totalUv || 0);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return sumTrendField(selectedProduct.value?.days || [], "uv");
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const selectedDaysTrend = computed(() => sortTrendList(selectedProduct.value?.days || [], "bucketDate"));
|
|
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));
|
|
290
|
+
const selectedDaysTrend = computed(() => sortTrendList(selectedProduct.value?.days || [], "reportDate"));
|
|
301
291
|
const selectedDaysFilled = computed(() => buildFilledTrendDays(selectedProduct.value?.days || [], 30));
|
|
292
|
+
const selectedDaysHasData = computed(() => selectedDaysTrend.value.length > 0);
|
|
293
|
+
const selectedDaysTrendPreview = computed(() => buildTrendPreview(selectedDaysFilled.value, 10));
|
|
294
|
+
const selectedDaysBars = computed(() => buildTrendBars(selectedDaysFilled.value, getTrendMax(selectedDaysFilled.value)));
|
|
302
295
|
const productPeriodCards = computed(() => {
|
|
303
296
|
const list = selectedDaysFilled.value;
|
|
304
297
|
const today = list[list.length - 1] || { pv: 0, uv: 0 };
|
|
@@ -317,11 +310,11 @@ const productPeriodCards = computed(() => {
|
|
|
317
310
|
});
|
|
318
311
|
|
|
319
312
|
function buildFallbackProduct() {
|
|
320
|
-
const days = sortTrendList(realtimeStats.days, "
|
|
313
|
+
const days = sortTrendList(realtimeStats.days, "reportDate");
|
|
321
314
|
|
|
322
315
|
return {
|
|
323
316
|
key: "all-products",
|
|
324
|
-
productName:
|
|
317
|
+
productName: "全部产品",
|
|
325
318
|
productCode: productInfo.productCode,
|
|
326
319
|
productVersion: productInfo.productVersion,
|
|
327
320
|
today: {
|
|
@@ -334,7 +327,7 @@ function buildFallbackProduct() {
|
|
|
334
327
|
};
|
|
335
328
|
}
|
|
336
329
|
|
|
337
|
-
function
|
|
330
|
+
function getReportDateNumber(timestamp) {
|
|
338
331
|
return Number(
|
|
339
332
|
new Intl.DateTimeFormat("en-CA", {
|
|
340
333
|
year: "numeric",
|
|
@@ -346,32 +339,32 @@ function getBucketDate(timestamp) {
|
|
|
346
339
|
);
|
|
347
340
|
}
|
|
348
341
|
|
|
349
|
-
function
|
|
342
|
+
function buildRecentReportDateList(limit) {
|
|
350
343
|
const list = [];
|
|
351
344
|
|
|
352
345
|
for (let i = limit - 1; i >= 0; i--) {
|
|
353
|
-
list.push(
|
|
346
|
+
list.push(getReportDateNumber(Date.now() - i * 24 * 60 * 60 * 1000));
|
|
354
347
|
}
|
|
355
348
|
|
|
356
349
|
return list;
|
|
357
350
|
}
|
|
358
351
|
|
|
359
352
|
function buildFilledTrendDays(list, limit) {
|
|
360
|
-
const dateList =
|
|
353
|
+
const dateList = buildRecentReportDateList(limit);
|
|
361
354
|
const dataMap = new Map();
|
|
362
355
|
|
|
363
356
|
for (const item of list) {
|
|
364
|
-
dataMap.set(Number(item?.
|
|
365
|
-
|
|
357
|
+
dataMap.set(Number(item?.reportDate || 0), {
|
|
358
|
+
reportDate: Number(item?.reportDate || 0),
|
|
366
359
|
pv: Number(item?.pv || 0),
|
|
367
360
|
uv: Number(item?.uv || 0)
|
|
368
361
|
});
|
|
369
362
|
}
|
|
370
363
|
|
|
371
|
-
return dateList.map((
|
|
364
|
+
return dateList.map((reportDate) => {
|
|
372
365
|
return (
|
|
373
|
-
dataMap.get(
|
|
374
|
-
|
|
366
|
+
dataMap.get(reportDate) || {
|
|
367
|
+
reportDate: reportDate,
|
|
375
368
|
pv: 0,
|
|
376
369
|
uv: 0
|
|
377
370
|
}
|
|
@@ -406,8 +399,8 @@ function ensureSelectedProduct() {
|
|
|
406
399
|
return;
|
|
407
400
|
}
|
|
408
401
|
|
|
409
|
-
const currentProduct = list.find((item) => item.
|
|
410
|
-
productState.selectedKey = currentProduct?.key ||
|
|
402
|
+
const currentProduct = list.find((item) => item.productName === productInfo.productName);
|
|
403
|
+
productState.selectedKey = currentProduct?.key || "all-products";
|
|
411
404
|
}
|
|
412
405
|
|
|
413
406
|
function selectProduct(productKey) {
|
|
@@ -437,49 +430,41 @@ function buildTrendPreview(list, maxCount) {
|
|
|
437
430
|
}
|
|
438
431
|
|
|
439
432
|
const preview = [];
|
|
440
|
-
const
|
|
441
|
-
const step = Math.ceil(lastIndex / (maxCount - 1));
|
|
433
|
+
const usedIndex = new Set();
|
|
442
434
|
|
|
443
|
-
for (let i = 0; i
|
|
444
|
-
|
|
445
|
-
|
|
435
|
+
for (let i = 0; i < maxCount; i++) {
|
|
436
|
+
const index = Math.round((i * (list.length - 1)) / (maxCount - 1));
|
|
437
|
+
|
|
438
|
+
if (usedIndex.has(index)) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
446
441
|
|
|
447
|
-
|
|
448
|
-
preview.push(list[
|
|
442
|
+
usedIndex.add(index);
|
|
443
|
+
preview.push(list[index]);
|
|
449
444
|
}
|
|
450
445
|
|
|
451
|
-
return preview
|
|
446
|
+
return preview;
|
|
452
447
|
}
|
|
453
448
|
|
|
454
|
-
function
|
|
455
|
-
if (list.length === 0) {
|
|
456
|
-
return [];
|
|
457
|
-
}
|
|
458
|
-
|
|
449
|
+
function buildTrendBars(list, maxValue) {
|
|
459
450
|
const result = [];
|
|
460
451
|
|
|
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;
|
|
452
|
+
for (const item of list) {
|
|
453
|
+
const pv = Number(item?.pv || 0);
|
|
454
|
+
const uv = Number(item?.uv || 0);
|
|
466
455
|
|
|
467
456
|
result.push({
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
457
|
+
reportDate: Number(item?.reportDate || 0),
|
|
458
|
+
pv: pv,
|
|
459
|
+
uv: uv,
|
|
460
|
+
pvHeight: pv > 0 ? `${Math.max((pv / maxValue) * 100, 6)}%` : "0%",
|
|
461
|
+
uvHeight: uv > 0 ? `${Math.max((uv / maxValue) * 100, 6)}%` : "0%"
|
|
471
462
|
});
|
|
472
463
|
}
|
|
473
464
|
|
|
474
465
|
return result;
|
|
475
466
|
}
|
|
476
467
|
|
|
477
|
-
function buildTrendPoints(list, field, maxValue) {
|
|
478
|
-
return buildTrendDots(list, field, maxValue)
|
|
479
|
-
.map((item) => `${item.x},${item.y}`)
|
|
480
|
-
.join(" ");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
468
|
function getTrendMax(list) {
|
|
484
469
|
let max = 0;
|
|
485
470
|
|
|
@@ -527,8 +512,8 @@ function formatDeviceType(deviceType) {
|
|
|
527
512
|
return String(deviceType || "Unknown");
|
|
528
513
|
}
|
|
529
514
|
|
|
530
|
-
function
|
|
531
|
-
const text = String(
|
|
515
|
+
function formatReportDate(reportDate) {
|
|
516
|
+
const text = String(reportDate || "");
|
|
532
517
|
|
|
533
518
|
if (text.length !== 8) {
|
|
534
519
|
return text;
|
|
@@ -539,27 +524,72 @@ function formatBucketDate(bucketDate) {
|
|
|
539
524
|
|
|
540
525
|
const fetchData = async () => {
|
|
541
526
|
try {
|
|
542
|
-
const [overviewRes,
|
|
527
|
+
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
528
|
|
|
544
529
|
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 = Array.isArray(visitStatsRes.data?.products) ? visitStatsRes.data.products : [];
|
|
550
|
-
uaStats.deviceTypes = Array.isArray(visitStatsRes.data?.uaStats?.deviceTypes) ? visitStatsRes.data.uaStats.deviceTypes : [];
|
|
551
|
-
uaStats.browsers = Array.isArray(visitStatsRes.data?.uaStats?.browsers) ? visitStatsRes.data.uaStats.browsers : [];
|
|
552
|
-
uaStats.browserVersions = Array.isArray(visitStatsRes.data?.uaStats?.browserVersions) ? visitStatsRes.data.uaStats.browserVersions : [];
|
|
553
|
-
uaStats.osList = Array.isArray(visitStatsRes.data?.uaStats?.osList) ? visitStatsRes.data.uaStats.osList : [];
|
|
554
|
-
uaStats.osVersions = Array.isArray(visitStatsRes.data?.uaStats?.osVersions) ? visitStatsRes.data.uaStats.osVersions : [];
|
|
555
|
-
uaStats.deviceVendors = Array.isArray(visitStatsRes.data?.uaStats?.deviceVendors) ? visitStatsRes.data.uaStats.deviceVendors : [];
|
|
556
|
-
uaStats.deviceModels = Array.isArray(visitStatsRes.data?.uaStats?.deviceModels) ? visitStatsRes.data.uaStats.deviceModels : [];
|
|
557
|
-
uaStats.engines = Array.isArray(visitStatsRes.data?.uaStats?.engines) ? visitStatsRes.data.uaStats.engines : [];
|
|
558
|
-
uaStats.cpuArchitectures = Array.isArray(visitStatsRes.data?.uaStats?.cpuArchitectures) ? visitStatsRes.data.uaStats.cpuArchitectures : [];
|
|
559
|
-
errorStats.trend = Array.isArray(errorStatsRes.data?.trend) ? errorStatsRes.data.trend : [];
|
|
530
|
+
realtimeStats.onlineCount = Number(onlineStatsRes.data?.onlineCount || 0);
|
|
531
|
+
realtimeStats.todayPv = Number(onlineStatsRes.data?.today?.pv || 0);
|
|
532
|
+
realtimeStats.todayUv = Number(onlineStatsRes.data?.today?.uv || 0);
|
|
533
|
+
realtimeStats.days = Array.isArray(onlineStatsRes.data?.days) ? onlineStatsRes.data.days : [];
|
|
560
534
|
productInfo.productName = String($Config.productName || "-");
|
|
561
535
|
productInfo.productCode = String($Config.productCode || "-");
|
|
562
536
|
productInfo.productVersion = String($Config.productVersion || "-");
|
|
537
|
+
productState.options = Array.isArray(onlineStatsRes.data?.products)
|
|
538
|
+
? onlineStatsRes.data.products.map((item) => {
|
|
539
|
+
return {
|
|
540
|
+
key: String(item?.key || item?.productName || ""),
|
|
541
|
+
productName: String(item?.productName || "-"),
|
|
542
|
+
today: {
|
|
543
|
+
pv: Number(item?.today?.pv || 0),
|
|
544
|
+
uv: Number(item?.today?.uv || 0)
|
|
545
|
+
},
|
|
546
|
+
week: {
|
|
547
|
+
pv: Number(item?.week?.pv || 0),
|
|
548
|
+
uv: Number(item?.week?.uv || 0)
|
|
549
|
+
},
|
|
550
|
+
month: {
|
|
551
|
+
pv: Number(item?.month?.pv || 0),
|
|
552
|
+
uv: Number(item?.month?.uv || 0)
|
|
553
|
+
},
|
|
554
|
+
days: Array.isArray(item?.days) ? item.days : [],
|
|
555
|
+
totalPv: Number(item?.totalPv || 0),
|
|
556
|
+
totalUv: Number(item?.totalUv || 0)
|
|
557
|
+
};
|
|
558
|
+
})
|
|
559
|
+
: [];
|
|
560
|
+
if (Array.isArray(infoStatsRes.data?.month?.productNames) && infoStatsRes.data.month.productNames.length === 1 && productInfo.productName !== "-") {
|
|
561
|
+
productState.options = [
|
|
562
|
+
{
|
|
563
|
+
key: productInfo.productName,
|
|
564
|
+
productName: productInfo.productName,
|
|
565
|
+
today: {
|
|
566
|
+
pv: realtimeStats.todayPv,
|
|
567
|
+
uv: realtimeStats.todayUv
|
|
568
|
+
},
|
|
569
|
+
week: {
|
|
570
|
+
pv: Number(onlineStatsRes.data?.week?.pv || 0),
|
|
571
|
+
uv: Number(onlineStatsRes.data?.week?.uv || 0)
|
|
572
|
+
},
|
|
573
|
+
month: {
|
|
574
|
+
pv: Number(onlineStatsRes.data?.month?.pv || 0),
|
|
575
|
+
uv: Number(onlineStatsRes.data?.month?.uv || 0)
|
|
576
|
+
},
|
|
577
|
+
days: realtimeStats.days,
|
|
578
|
+
totalPv: sumTrendField(realtimeStats.days, "pv"),
|
|
579
|
+
totalUv: sumTrendField(realtimeStats.days, "uv")
|
|
580
|
+
}
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
uaStats.deviceTypes = Array.isArray(infoStatsRes.data?.today?.deviceTypes) ? infoStatsRes.data.today.deviceTypes : [];
|
|
584
|
+
uaStats.browsers = Array.isArray(infoStatsRes.data?.today?.browsers) ? infoStatsRes.data.today.browsers : [];
|
|
585
|
+
uaStats.browserVersions = Array.isArray(infoStatsRes.data?.today?.browserVersions) ? infoStatsRes.data.today.browserVersions : [];
|
|
586
|
+
uaStats.osList = Array.isArray(infoStatsRes.data?.today?.osList) ? infoStatsRes.data.today.osList : [];
|
|
587
|
+
uaStats.osVersions = Array.isArray(infoStatsRes.data?.today?.osVersions) ? infoStatsRes.data.today.osVersions : [];
|
|
588
|
+
uaStats.deviceVendors = Array.isArray(infoStatsRes.data?.today?.deviceVendors) ? infoStatsRes.data.today.deviceVendors : [];
|
|
589
|
+
uaStats.deviceModels = Array.isArray(infoStatsRes.data?.today?.deviceModels) ? infoStatsRes.data.today.deviceModels : [];
|
|
590
|
+
uaStats.engines = Array.isArray(infoStatsRes.data?.today?.engines) ? infoStatsRes.data.today.engines : [];
|
|
591
|
+
uaStats.cpuArchitectures = Array.isArray(infoStatsRes.data?.today?.cpuArchitectures) ? infoStatsRes.data.today.cpuArchitectures : [];
|
|
592
|
+
errorStats.trend = Array.isArray(errorStatsRes.data?.trend) ? errorStatsRes.data.trend : [];
|
|
563
593
|
ensureSelectedProduct();
|
|
564
594
|
} catch (_error) {
|
|
565
595
|
// 静默失败:不阻断页面展示
|
|
@@ -718,47 +748,13 @@ fetchData();
|
|
|
718
748
|
color: var(--text-primary);
|
|
719
749
|
}
|
|
720
750
|
|
|
721
|
-
.product-
|
|
751
|
+
.product-select-wrap {
|
|
722
752
|
display: flex;
|
|
723
|
-
flex-wrap: wrap;
|
|
724
753
|
justify-content: flex-end;
|
|
725
|
-
gap: 8px;
|
|
726
754
|
}
|
|
727
755
|
|
|
728
|
-
.product-
|
|
729
|
-
|
|
730
|
-
align-items: center;
|
|
731
|
-
justify-content: center;
|
|
732
|
-
min-width: 0;
|
|
733
|
-
min-height: 36px;
|
|
734
|
-
padding: 0 12px;
|
|
735
|
-
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
736
|
-
border-radius: 8px;
|
|
737
|
-
background: rgba(255, 255, 255, 0.88);
|
|
738
|
-
color: var(--text-primary);
|
|
739
|
-
cursor: pointer;
|
|
740
|
-
transition: all 0.2s ease;
|
|
741
|
-
|
|
742
|
-
&:hover {
|
|
743
|
-
border-color: rgba(var(--primary-color-rgb), 0.3);
|
|
744
|
-
background: rgba(var(--primary-color-rgb), 0.05);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
&.active {
|
|
748
|
-
border-color: rgba(var(--primary-color-rgb), 0.45);
|
|
749
|
-
background: rgba(var(--primary-color-rgb), 0.08);
|
|
750
|
-
box-shadow: inset 0 0 0 1px rgba(var(--primary-color-rgb), 0.08);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
.product-switch-name {
|
|
755
|
-
max-width: 160px;
|
|
756
|
-
overflow: hidden;
|
|
757
|
-
text-overflow: ellipsis;
|
|
758
|
-
white-space: nowrap;
|
|
759
|
-
font-size: 12px;
|
|
760
|
-
font-weight: 600;
|
|
761
|
-
letter-spacing: 0.2px;
|
|
756
|
+
.product-select {
|
|
757
|
+
width: 220px;
|
|
762
758
|
}
|
|
763
759
|
|
|
764
760
|
.product-metrics-grid {
|
|
@@ -865,10 +861,13 @@ fetchData();
|
|
|
865
861
|
background: #2ba471;
|
|
866
862
|
}
|
|
867
863
|
|
|
868
|
-
.trend-
|
|
869
|
-
|
|
864
|
+
.trend-bars {
|
|
865
|
+
display: grid;
|
|
866
|
+
grid-template-columns: repeat(30, minmax(0, 1fr));
|
|
867
|
+
align-items: end;
|
|
868
|
+
gap: 0;
|
|
870
869
|
height: 148px;
|
|
871
|
-
|
|
870
|
+
padding: 10px 0 6px;
|
|
872
871
|
border-radius: 8px;
|
|
873
872
|
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
873
|
background-size:
|
|
@@ -877,23 +876,52 @@ fetchData();
|
|
|
877
876
|
auto;
|
|
878
877
|
}
|
|
879
878
|
|
|
880
|
-
.trend-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
879
|
+
.trend-bar-group {
|
|
880
|
+
min-width: 0;
|
|
881
|
+
height: 100%;
|
|
882
|
+
display: flex;
|
|
883
|
+
flex-direction: column;
|
|
884
|
+
justify-content: flex-end;
|
|
885
|
+
gap: 4px;
|
|
886
|
+
padding: 0 2px;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.trend-bar-stack {
|
|
890
|
+
flex: 1;
|
|
891
|
+
display: flex;
|
|
892
|
+
align-items: flex-end;
|
|
893
|
+
justify-content: center;
|
|
894
|
+
gap: 2px;
|
|
895
|
+
min-height: 0;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.trend-bar {
|
|
899
|
+
width: calc(50% - 1px);
|
|
900
|
+
min-width: 2px;
|
|
901
|
+
border-radius: 4px 4px 0 0;
|
|
902
|
+
transition: all 0.2s ease;
|
|
903
|
+
|
|
904
|
+
&:hover {
|
|
905
|
+
opacity: 0.9;
|
|
906
|
+
transform: translateY(-1px);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.trend-bar-pv {
|
|
911
|
+
background: linear-gradient(180deg, rgba(0, 82, 217, 0.65), #0052d9);
|
|
885
912
|
}
|
|
886
913
|
|
|
887
|
-
.trend-
|
|
888
|
-
|
|
889
|
-
stroke: #0052d9;
|
|
890
|
-
fill: #0052d9;
|
|
914
|
+
.trend-bar-uv {
|
|
915
|
+
background: linear-gradient(180deg, rgba(43, 164, 113, 0.65), #2ba471);
|
|
891
916
|
}
|
|
892
917
|
|
|
893
|
-
.trend-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
918
|
+
.trend-bar-label {
|
|
919
|
+
font-size: 10px;
|
|
920
|
+
line-height: 1;
|
|
921
|
+
color: var(--text-tertiary);
|
|
922
|
+
text-align: center;
|
|
923
|
+
font-variant-numeric: tabular-nums;
|
|
924
|
+
transform: scale(0.92);
|
|
897
925
|
}
|
|
898
926
|
|
|
899
927
|
.trend-briefs {
|
|
@@ -1040,6 +1068,16 @@ fetchData();
|
|
|
1040
1068
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
1041
1069
|
}
|
|
1042
1070
|
|
|
1071
|
+
.trend-bars {
|
|
1072
|
+
grid-template-columns: repeat(15, minmax(0, 1fr));
|
|
1073
|
+
row-gap: 6px;
|
|
1074
|
+
height: auto;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.trend-bar-group:nth-child(n + 16) {
|
|
1078
|
+
display: none;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1043
1081
|
.ua-grid {
|
|
1044
1082
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1045
1083
|
}
|
|
@@ -1066,13 +1104,22 @@ fetchData();
|
|
|
1066
1104
|
align-items: flex-start;
|
|
1067
1105
|
}
|
|
1068
1106
|
|
|
1069
|
-
.product-
|
|
1107
|
+
.product-select-wrap {
|
|
1108
|
+
width: 100%;
|
|
1070
1109
|
justify-content: flex-start;
|
|
1071
1110
|
}
|
|
1072
1111
|
|
|
1073
1112
|
.trend-briefs-wide {
|
|
1074
1113
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1075
1114
|
}
|
|
1115
|
+
|
|
1116
|
+
.trend-bars {
|
|
1117
|
+
grid-template-columns: repeat(10, minmax(0, 1fr));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.trend-bar-group:nth-child(n + 11) {
|
|
1121
|
+
display: none;
|
|
1122
|
+
}
|
|
1076
1123
|
}
|
|
1077
1124
|
|
|
1078
1125
|
@media (max-width: 640px) {
|
|
@@ -1086,12 +1133,17 @@ fetchData();
|
|
|
1086
1133
|
min-height: 0;
|
|
1087
1134
|
}
|
|
1088
1135
|
|
|
1089
|
-
.product-
|
|
1136
|
+
.product-select {
|
|
1090
1137
|
width: 100%;
|
|
1091
1138
|
}
|
|
1092
1139
|
|
|
1093
|
-
.trend-
|
|
1140
|
+
.trend-bars {
|
|
1141
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
1094
1142
|
height: 126px;
|
|
1095
1143
|
}
|
|
1144
|
+
|
|
1145
|
+
.trend-bar-group:nth-child(n + 8) {
|
|
1146
|
+
display: none;
|
|
1147
|
+
}
|
|
1096
1148
|
}
|
|
1097
1149
|
</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>
|