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 CHANGED
@@ -2,7 +2,6 @@
2
2
  "extends": "../../jsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "target": "ES2022",
5
- "types": ["bun"],
6
5
  "strict": false,
7
6
  "exactOptionalPropertyTypes": false,
8
7
  "noImplicitReturns": false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly-admin-ui",
3
- "version": "1.8.32",
4
- "gitHead": "ba7fa7cc0534c48de2df4970d69a14853cf6b2a6",
3
+ "version": "1.8.34",
4
+ "gitHead": "c47d1e515dabe142531e0a8415979cccb2729259",
5
5
  "private": false,
6
6
  "description": "Befly - 管理后台功能组件",
7
7
  "keywords": [
@@ -49,10 +49,10 @@
49
49
  <div class="card-title">统计信息</div>
50
50
  </div>
51
51
 
52
- <div class="product-switch-list">
53
- <button v-for="item in productOptions" :key="item.key" type="button" class="product-switch" :class="{ active: item.key === selectedProduct?.key }" @click="selectProduct(item.key)">
54
- <span class="product-switch-name">{{ item.productName }}</span>
55
- </button>
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="selectedDaysTrend.length > 0" class="trend-content">
88
- <svg viewBox="0 0 100 60" preserveAspectRatio="none" class="trend-svg">
89
- <polyline class="trend-line trend-line-pv" :points="selectedDaysPvPoints" />
90
- <polyline class="trend-line trend-line-uv" :points="selectedDaysUvPoints" />
91
- <circle v-for="item in selectedDaysPvDots" :key="`day-pv-${item.bucketDate}`" class="trend-dot trend-dot-pv" :cx="item.x" :cy="item.y" r="1.2" />
92
- <circle v-for="item in selectedDaysUvDots" :key="`day-uv-${item.bucketDate}`" class="trend-dot trend-dot-uv" :cx="item.x" :cy="item.y" r="1.2" />
93
- </svg>
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.bucketDate" class="trend-brief-item">
97
- <div class="trend-date">{{ formatBucketDate(item.bucketDate) }}</div>
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 > 0) {
252
- return list;
256
+ if (list.length === 0) {
257
+ return [fallbackProduct];
253
258
  }
254
259
 
255
- return [buildFallbackProduct()];
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 selectedProductToday = computed(() => {
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, "bucketDate");
313
+ const days = sortTrendList(realtimeStats.days, "reportDate");
321
314
 
322
315
  return {
323
316
  key: "all-products",
324
- productName: productInfo.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 getBucketDate(timestamp) {
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 buildRecentDateList(limit) {
342
+ function buildRecentReportDateList(limit) {
350
343
  const list = [];
351
344
 
352
345
  for (let i = limit - 1; i >= 0; i--) {
353
- list.push(getBucketDate(Date.now() - i * 24 * 60 * 60 * 1000));
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 = buildRecentDateList(limit);
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?.bucketDate || 0), {
365
- bucketDate: Number(item?.bucketDate || 0),
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((bucketDate) => {
364
+ return dateList.map((reportDate) => {
372
365
  return (
373
- dataMap.get(bucketDate) || {
374
- bucketDate: bucketDate,
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.productCode === productInfo.productCode && item.productVersion === productInfo.productVersion);
410
- productState.selectedKey = currentProduct?.key || list[0].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 lastIndex = list.length - 1;
441
- const step = Math.ceil(lastIndex / (maxCount - 1));
433
+ const usedIndex = new Set();
442
434
 
443
- for (let i = 0; i <= lastIndex; i += step) {
444
- preview.push(list[i]);
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
- if (preview[preview.length - 1] !== list[lastIndex]) {
448
- preview.push(list[lastIndex]);
442
+ usedIndex.add(index);
443
+ preview.push(list[index]);
449
444
  }
450
445
 
451
- return preview.slice(0, maxCount);
446
+ return preview;
452
447
  }
453
448
 
454
- function buildTrendDots(list, field, maxValue) {
455
- if (list.length === 0) {
456
- return [];
457
- }
458
-
449
+ function buildTrendBars(list, maxValue) {
459
450
  const result = [];
460
451
 
461
- for (let i = 0; i < list.length; i++) {
462
- const item = list[i];
463
- const value = Number(item?.[field] || 0);
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
- bucketDate: item.bucketDate,
469
- x: Number(x.toFixed(2)),
470
- y: Number(y.toFixed(2))
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 formatBucketDate(bucketDate) {
531
- const text = String(bucketDate || "");
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, visitStatsRes, errorStatsRes] = await Promise.all([$Http("/core/dashboard/systemOverview", {}, [""]), $Http("/core/tongJi/visitStats", {}, [""]), $Http("/core/tongJi/errorStats", {}, [""])]);
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(visitStatsRes.data?.onlineCount || 0);
546
- realtimeStats.todayPv = Number(visitStatsRes.data?.today?.pv || 0);
547
- realtimeStats.todayUv = Number(visitStatsRes.data?.today?.uv || 0);
548
- realtimeStats.days = Array.isArray(visitStatsRes.data?.days) ? visitStatsRes.data.days : [];
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-switch-list {
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-switch {
729
- display: inline-flex;
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-svg {
869
- width: 100%;
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
- display: block;
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-line {
881
- fill: none;
882
- stroke-width: 1.8;
883
- stroke-linecap: round;
884
- stroke-linejoin: round;
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-line-pv,
888
- .trend-dot-pv {
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-line-uv,
894
- .trend-dot-uv {
895
- stroke: #2ba471;
896
- fill: #2ba471;
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-switch-list {
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-switch {
1136
+ .product-select {
1090
1137
  width: 100%;
1091
1138
  }
1092
1139
 
1093
- .trend-svg {
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
- if (!timestamp) return "-";
272
- const date = new Date(timestamp);
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>