befly-admin-ui 1.8.26 → 1.8.32

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.
@@ -1,31 +1,183 @@
1
- <template>
1
+ <template>
2
2
  <div class="section-block">
3
3
  <div class="section-header flex items-center gap-2">
4
4
  <InfoCircleIcon />
5
5
  <h2>系统概览</h2>
6
6
  </div>
7
- <div class="section-content">
8
- <div class="info-block">
9
- <div class="stats-grid">
10
- <div class="stat-box stat-primary">
11
- <MenuIcon />
12
- <div class="stat-content">
13
- <div class="stat-value">{{ permissionStats.menuCount }}</div>
14
- <div class="stat-label">菜单总数</div>
7
+
8
+ <div class="section-content overview-layout">
9
+ <div class="summary-grid">
10
+ <div class="summary-card overview-card-small stat-primary">
11
+ <div class="summary-content">
12
+ <div class="summary-value">{{ permissionStats.menuCount }}</div>
13
+ <div class="summary-label">菜单总数</div>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="summary-card overview-card-small stat-success">
18
+ <div class="summary-content">
19
+ <div class="summary-value">{{ permissionStats.apiCount }}</div>
20
+ <div class="summary-label">接口总数</div>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="summary-card overview-card-small stat-warning">
25
+ <div class="summary-content">
26
+ <div class="summary-value">{{ permissionStats.roleCount }}</div>
27
+ <div class="summary-label">角色总数</div>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="summary-card overview-card-small stat-danger">
32
+ <div class="summary-content">
33
+ <div class="summary-value">{{ realtimeStats.onlineCount }}</div>
34
+ <div class="summary-label">当前在线</div>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="summary-card overview-card-small stat-critical">
39
+ <div class="summary-content">
40
+ <div class="summary-value">{{ errorTodayCount }}</div>
41
+ <div class="summary-label">今日错误</div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="product-stats-card overview-card-large">
47
+ <div class="card-head">
48
+ <div>
49
+ <div class="card-title">统计信息</div>
50
+ </div>
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>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="product-metrics-grid">
60
+ <div v-for="item in productPeriodCards" :key="item.key" class="metric-box metric-box-period">
61
+ <div class="metric-period-label">{{ item.label }}</div>
62
+ <div class="metric-pair-row">
63
+ <span class="metric-pair-name">PV</span>
64
+ <strong class="metric-pair-value">{{ item.pv }}</strong>
65
+ </div>
66
+ <div class="metric-pair-row">
67
+ <span class="metric-pair-name">UV</span>
68
+ <strong class="metric-pair-value metric-pair-value-uv">{{ item.uv }}</strong>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="trend-grid">
75
+ <div class="trend-card overview-card-large">
76
+ <div class="card-head card-head-compact">
77
+ <div class="trend-title">
78
+ <TrendingUpIcon />
79
+ <span>过去 30 天趋势</span>
80
+ </div>
81
+ <div class="trend-legend">
82
+ <span class="legend-item legend-pv">PV</span>
83
+ <span class="legend-item legend-uv">UV</span>
84
+ </div>
85
+ </div>
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>
94
+
95
+ <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>
98
+ <div class="trend-values">
99
+ <span class="trend-value pv">{{ item.pv }}</span>
100
+ <span class="trend-value uv">{{ item.uv }}</span>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <div v-else class="card-empty">暂无近 30 天趋势数据</div>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="ua-grid">
111
+ <div class="ua-card overview-card-small">
112
+ <div class="ua-card-title">设备类型</div>
113
+ <div v-if="uaStats.deviceTypes.length > 0" class="ua-list">
114
+ <div v-for="item in uaStats.deviceTypes.slice(0, 5)" :key="`device-type-${item.name}`" class="ua-list-item">
115
+ <span class="ua-list-name">{{ formatDeviceType(item.name) }}</span>
116
+ <span class="ua-list-count">{{ item.count }}</span>
117
+ </div>
118
+ </div>
119
+ <div v-else class="ua-empty">暂无设备类型数据</div>
120
+ </div>
121
+
122
+ <div class="ua-card overview-card-small">
123
+ <div class="ua-card-title">浏览器分布</div>
124
+ <div v-if="uaStats.browsers.length > 0" class="ua-list">
125
+ <div v-for="item in uaStats.browsers.slice(0, 5)" :key="`browser-${item.name}`" class="ua-list-item">
126
+ <span class="ua-list-name">{{ item.name }}</span>
127
+ <span class="ua-list-count">{{ item.count }}</span>
128
+ </div>
129
+ </div>
130
+ <div v-else class="ua-empty">暂无浏览器数据</div>
131
+ </div>
132
+
133
+ <div class="ua-card overview-card-small">
134
+ <div class="ua-card-title">操作系统分布</div>
135
+ <div v-if="uaStats.osList.length > 0" class="ua-list">
136
+ <div v-for="item in uaStats.osList.slice(0, 5)" :key="`os-${item.name}`" class="ua-list-item">
137
+ <span class="ua-list-name">{{ item.name }}</span>
138
+ <span class="ua-list-count">{{ item.count }}</span>
15
139
  </div>
16
140
  </div>
17
- <div class="stat-box stat-success">
18
- <LinkIcon />
19
- <div class="stat-content">
20
- <div class="stat-value">{{ permissionStats.apiCount }}</div>
21
- <div class="stat-label">接口总数</div>
141
+ <div v-else class="ua-empty">暂无系统数据</div>
142
+ </div>
143
+
144
+ <div class="ua-card overview-card-small">
145
+ <div class="ua-card-title">设备厂商</div>
146
+ <div v-if="uaStats.deviceVendors.length > 0" class="ua-list">
147
+ <div v-for="item in uaStats.deviceVendors.slice(0, 5)" :key="`vendor-${item.name}`" class="ua-list-item">
148
+ <span class="ua-list-name">{{ item.name }}</span>
149
+ <span class="ua-list-count">{{ item.count }}</span>
22
150
  </div>
23
151
  </div>
24
- <div class="stat-box stat-warning">
25
- <UsergroupIcon />
26
- <div class="stat-content">
27
- <div class="stat-value">{{ permissionStats.roleCount }}</div>
28
- <div class="stat-label">角色总数</div>
152
+ <div v-else class="ua-empty">暂无厂商数据</div>
153
+ </div>
154
+
155
+ <div class="ua-card ua-card-wide overview-card-large">
156
+ <div class="ua-card-title">设备细分</div>
157
+
158
+ <div class="ua-meta-group">
159
+ <div class="ua-meta-block">
160
+ <div class="ua-meta-title">设备型号</div>
161
+ <div v-if="uaStats.deviceModels.length > 0" class="ua-chip-list">
162
+ <span v-for="item in uaStats.deviceModels.slice(0, 6)" :key="`model-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
163
+ </div>
164
+ <div v-else class="ua-empty ua-empty-inline">暂无设备型号数据</div>
165
+ </div>
166
+
167
+ <div class="ua-meta-block">
168
+ <div class="ua-meta-title">渲染引擎</div>
169
+ <div v-if="uaStats.engines.length > 0" class="ua-chip-list">
170
+ <span v-for="item in uaStats.engines.slice(0, 6)" :key="`engine-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
171
+ </div>
172
+ <div v-else class="ua-empty ua-empty-inline">暂无渲染引擎数据</div>
173
+ </div>
174
+
175
+ <div class="ua-meta-block">
176
+ <div class="ua-meta-title">CPU 架构</div>
177
+ <div v-if="uaStats.cpuArchitectures.length > 0" class="ua-chip-list">
178
+ <span v-for="item in uaStats.cpuArchitectures.slice(0, 6)" :key="`cpu-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
179
+ </div>
180
+ <div v-else class="ua-empty ua-empty-inline">暂无 CPU 架构数据</div>
29
181
  </div>
30
182
  </div>
31
183
  </div>
@@ -35,22 +187,380 @@
35
187
  </template>
36
188
 
37
189
  <script setup>
38
- import { reactive } from "vue";
39
- import { InfoCircleIcon, LinkIcon, MenuIcon, UsergroupIcon } from "tdesign-icons-vue-next";
190
+ import { computed, reactive } from "vue";
191
+ import { InfoCircleIcon, TrendingUpIcon } from "tdesign-icons-vue-next";
192
+
193
+ import { $Config } from "@/plugins/config";
40
194
  import { $Http } from "@/plugins/http";
41
195
 
42
- // 组件内部数据
43
196
  const permissionStats = reactive({
44
197
  menuCount: 0,
45
198
  apiCount: 0,
46
199
  roleCount: 0
47
200
  });
48
201
 
49
- // 获取数据
202
+ const realtimeStats = reactive({
203
+ onlineCount: 0,
204
+ todayPv: 0,
205
+ todayUv: 0,
206
+ days: []
207
+ });
208
+
209
+ const errorStats = reactive({
210
+ trend: []
211
+ });
212
+
213
+ const productInfo = reactive({
214
+ productName: String($Config.productName || "-"),
215
+ productCode: String($Config.productCode || "-"),
216
+ productVersion: String($Config.productVersion || "-")
217
+ });
218
+
219
+ const productState = reactive({
220
+ selectedKey: "",
221
+ options: []
222
+ });
223
+
224
+ const uaStats = reactive({
225
+ deviceTypes: [],
226
+ browsers: [],
227
+ browserVersions: [],
228
+ osList: [],
229
+ osVersions: [],
230
+ deviceVendors: [],
231
+ deviceModels: [],
232
+ engines: [],
233
+ cpuArchitectures: []
234
+ });
235
+
236
+ const errorTodayTrend = computed(() => sortTrendList(errorStats.trend, "bucketTime"));
237
+
238
+ const errorTodayCount = computed(() => {
239
+ let total = 0;
240
+
241
+ for (const item of errorTodayTrend.value) {
242
+ total += Number(item.count || 0);
243
+ }
244
+
245
+ return total;
246
+ });
247
+
248
+ const productOptions = computed(() => {
249
+ const list = Array.isArray(productState.options) ? productState.options.slice() : [];
250
+
251
+ if (list.length > 0) {
252
+ return list;
253
+ }
254
+
255
+ return [buildFallbackProduct()];
256
+ });
257
+
258
+ const selectedProduct = computed(() => {
259
+ const list = productOptions.value;
260
+
261
+ if (list.length === 0) {
262
+ return null;
263
+ }
264
+
265
+ const matched = list.find((item) => item.key === productState.selectedKey);
266
+ return matched || list[0];
267
+ });
268
+
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));
301
+ const selectedDaysFilled = computed(() => buildFilledTrendDays(selectedProduct.value?.days || [], 30));
302
+ const productPeriodCards = computed(() => {
303
+ const list = selectedDaysFilled.value;
304
+ const today = list[list.length - 1] || { pv: 0, uv: 0 };
305
+ const yesterday = list[list.length - 2] || { pv: 0, uv: 0 };
306
+ const dayBeforeYesterday = list[list.length - 3] || { pv: 0, uv: 0 };
307
+ const recent7 = sumTrendRange(list.slice(-7));
308
+ const recent30 = sumTrendRange(list);
309
+
310
+ return [
311
+ { key: "today", label: "今日", pv: today.pv, uv: today.uv },
312
+ { key: "yesterday", label: "昨日", pv: yesterday.pv, uv: yesterday.uv },
313
+ { key: "dayBeforeYesterday", label: "前日", pv: dayBeforeYesterday.pv, uv: dayBeforeYesterday.uv },
314
+ { key: "recent7", label: "最近7天", pv: recent7.pv, uv: recent7.uv },
315
+ { key: "recent30", label: "最近30天", pv: recent30.pv, uv: recent30.uv }
316
+ ];
317
+ });
318
+
319
+ function buildFallbackProduct() {
320
+ const days = sortTrendList(realtimeStats.days, "bucketDate");
321
+
322
+ return {
323
+ key: "all-products",
324
+ productName: productInfo.productName,
325
+ productCode: productInfo.productCode,
326
+ productVersion: productInfo.productVersion,
327
+ today: {
328
+ pv: Number(realtimeStats.todayPv || 0),
329
+ uv: Number(realtimeStats.todayUv || 0)
330
+ },
331
+ days: days,
332
+ totalPv: sumTrendField(days, "pv"),
333
+ totalUv: sumTrendField(days, "uv")
334
+ };
335
+ }
336
+
337
+ function getBucketDate(timestamp) {
338
+ return Number(
339
+ new Intl.DateTimeFormat("en-CA", {
340
+ year: "numeric",
341
+ month: "2-digit",
342
+ day: "2-digit"
343
+ })
344
+ .format(timestamp)
345
+ .replace(/[^0-9]/g, "")
346
+ );
347
+ }
348
+
349
+ function buildRecentDateList(limit) {
350
+ const list = [];
351
+
352
+ for (let i = limit - 1; i >= 0; i--) {
353
+ list.push(getBucketDate(Date.now() - i * 24 * 60 * 60 * 1000));
354
+ }
355
+
356
+ return list;
357
+ }
358
+
359
+ function buildFilledTrendDays(list, limit) {
360
+ const dateList = buildRecentDateList(limit);
361
+ const dataMap = new Map();
362
+
363
+ for (const item of list) {
364
+ dataMap.set(Number(item?.bucketDate || 0), {
365
+ bucketDate: Number(item?.bucketDate || 0),
366
+ pv: Number(item?.pv || 0),
367
+ uv: Number(item?.uv || 0)
368
+ });
369
+ }
370
+
371
+ return dateList.map((bucketDate) => {
372
+ return (
373
+ dataMap.get(bucketDate) || {
374
+ bucketDate: bucketDate,
375
+ pv: 0,
376
+ uv: 0
377
+ }
378
+ );
379
+ });
380
+ }
381
+
382
+ function sumTrendRange(list) {
383
+ let pv = 0;
384
+ let uv = 0;
385
+
386
+ for (const item of list) {
387
+ pv += Number(item?.pv || 0);
388
+ uv += Number(item?.uv || 0);
389
+ }
390
+
391
+ return {
392
+ pv: pv,
393
+ uv: uv
394
+ };
395
+ }
396
+
397
+ function ensureSelectedProduct() {
398
+ const list = productOptions.value;
399
+
400
+ if (list.length === 0) {
401
+ productState.selectedKey = "";
402
+ return;
403
+ }
404
+
405
+ if (list.some((item) => item.key === productState.selectedKey)) {
406
+ return;
407
+ }
408
+
409
+ const currentProduct = list.find((item) => item.productCode === productInfo.productCode && item.productVersion === productInfo.productVersion);
410
+ productState.selectedKey = currentProduct?.key || list[0].key;
411
+ }
412
+
413
+ function selectProduct(productKey) {
414
+ productState.selectedKey = String(productKey || "");
415
+ }
416
+
417
+ function sumTrendField(list, field) {
418
+ let total = 0;
419
+
420
+ for (const item of list) {
421
+ total += Number(item?.[field] || 0);
422
+ }
423
+
424
+ return total;
425
+ }
426
+
427
+ function sortTrendList(list, keyField) {
428
+ const result = Array.isArray(list) ? list.slice() : [];
429
+
430
+ result.sort((a, b) => Number(a?.[keyField] || 0) - Number(b?.[keyField] || 0));
431
+ return result;
432
+ }
433
+
434
+ function buildTrendPreview(list, maxCount) {
435
+ if (list.length <= maxCount) {
436
+ return list;
437
+ }
438
+
439
+ const preview = [];
440
+ const lastIndex = list.length - 1;
441
+ const step = Math.ceil(lastIndex / (maxCount - 1));
442
+
443
+ for (let i = 0; i <= lastIndex; i += step) {
444
+ preview.push(list[i]);
445
+ }
446
+
447
+ if (preview[preview.length - 1] !== list[lastIndex]) {
448
+ preview.push(list[lastIndex]);
449
+ }
450
+
451
+ return preview.slice(0, maxCount);
452
+ }
453
+
454
+ function buildTrendDots(list, field, maxValue) {
455
+ if (list.length === 0) {
456
+ return [];
457
+ }
458
+
459
+ const result = [];
460
+
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;
466
+
467
+ result.push({
468
+ bucketDate: item.bucketDate,
469
+ x: Number(x.toFixed(2)),
470
+ y: Number(y.toFixed(2))
471
+ });
472
+ }
473
+
474
+ return result;
475
+ }
476
+
477
+ function buildTrendPoints(list, field, maxValue) {
478
+ return buildTrendDots(list, field, maxValue)
479
+ .map((item) => `${item.x},${item.y}`)
480
+ .join(" ");
481
+ }
482
+
483
+ function getTrendMax(list) {
484
+ let max = 0;
485
+
486
+ for (const item of list) {
487
+ const pv = Number(item?.pv || 0);
488
+ const uv = Number(item?.uv || 0);
489
+
490
+ if (pv > max) {
491
+ max = pv;
492
+ }
493
+
494
+ if (uv > max) {
495
+ max = uv;
496
+ }
497
+ }
498
+
499
+ return max > 0 ? max : 1;
500
+ }
501
+
502
+ function formatDeviceType(deviceType) {
503
+ if (deviceType === "desktop") {
504
+ return "桌面端";
505
+ }
506
+
507
+ if (deviceType === "mobile") {
508
+ return "移动端";
509
+ }
510
+
511
+ if (deviceType === "tablet") {
512
+ return "平板";
513
+ }
514
+
515
+ if (deviceType === "smarttv") {
516
+ return "电视";
517
+ }
518
+
519
+ if (deviceType === "wearable") {
520
+ return "穿戴设备";
521
+ }
522
+
523
+ if (deviceType === "embedded") {
524
+ return "嵌入式设备";
525
+ }
526
+
527
+ return String(deviceType || "Unknown");
528
+ }
529
+
530
+ function formatBucketDate(bucketDate) {
531
+ const text = String(bucketDate || "");
532
+
533
+ if (text.length !== 8) {
534
+ return text;
535
+ }
536
+
537
+ return `${text.slice(4, 6)}-${text.slice(6, 8)}`;
538
+ }
539
+
50
540
  const fetchData = async () => {
51
541
  try {
52
- const res = await $Http("/core/dashboard/systemOverview", {}, [""]);
53
- Object.assign(permissionStats, res.data);
542
+ const [overviewRes, visitStatsRes, errorStatsRes] = await Promise.all([$Http("/core/dashboard/systemOverview", {}, [""]), $Http("/core/tongJi/visitStats", {}, [""]), $Http("/core/tongJi/errorStats", {}, [""])]);
543
+
544
+ 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 : [];
560
+ productInfo.productName = String($Config.productName || "-");
561
+ productInfo.productCode = String($Config.productCode || "-");
562
+ productInfo.productVersion = String($Config.productVersion || "-");
563
+ ensureSelectedProduct();
54
564
  } catch (_error) {
55
565
  // 静默失败:不阻断页面展示
56
566
  }
@@ -60,129 +570,528 @@ fetchData();
60
570
  </script>
61
571
 
62
572
  <style scoped lang="scss">
63
- .info-block {
64
- background: transparent;
65
- border: none;
66
- padding: 0;
67
- height: 100%;
68
-
69
- .info-header {
70
- display: flex;
71
- align-items: center;
72
- gap: 6px;
73
- padding-bottom: 8px;
74
- margin-bottom: 12px;
75
- border-bottom: 2px solid var(--primary-color);
76
-
77
- .info-title {
78
- font-size: 14px;
79
- font-weight: 600;
80
- color: var(--text-primary);
81
- }
573
+ .overview-layout {
574
+ display: flex;
575
+ flex-direction: column;
576
+ gap: 10px;
577
+ }
578
+
579
+ .overview-card-large,
580
+ .overview-card-small {
581
+ border: 1px solid rgba(0, 0, 0, 0.06);
582
+ border-radius: 10px;
583
+ background: linear-gradient(180deg, rgba(var(--primary-color-rgb), 0.018), rgba(255, 255, 255, 0.96));
584
+ box-shadow: 0 4px 14px rgba(31, 35, 41, 0.04);
585
+ min-width: 0;
586
+ }
587
+
588
+ .overview-card-large {
589
+ min-height: 252px;
590
+ padding: 12px;
591
+ }
592
+
593
+ .overview-card-small {
594
+ min-height: 94px;
595
+ padding: 10px 12px;
596
+ }
597
+
598
+ .summary-grid {
599
+ display: grid;
600
+ grid-template-columns: repeat(5, minmax(0, 1fr));
601
+ gap: 8px;
602
+ }
603
+
604
+ .summary-card {
605
+ display: flex;
606
+ align-items: flex-start;
607
+ justify-content: center;
608
+ transition: all 0.2s ease;
609
+
610
+ &:hover {
611
+ transform: translateY(-2px);
612
+ box-shadow: 0 8px 20px rgba(31, 35, 41, 0.08);
82
613
  }
614
+ }
83
615
 
84
- .info-grid-compact {
85
- display: grid;
86
- grid-template-columns: repeat(3, 1fr);
87
- gap: 10px;
88
-
89
- .info-grid-item {
90
- display: flex;
91
- justify-content: space-between;
92
- align-items: center;
93
- padding: 10px 12px;
94
- background: rgba(var(--primary-color-rgb), 0.02);
95
- border-radius: var(--border-radius-small);
96
- border: 1px solid var(--border-color);
97
- transition: all 0.2s ease;
98
-
99
- &:hover {
100
- background: rgba(var(--primary-color-rgb), 0.05);
101
- border-color: var(--primary-color);
102
- }
616
+ .summary-content {
617
+ display: flex;
618
+ flex-direction: column;
619
+ justify-content: center;
620
+ gap: 4px;
621
+ width: 100%;
622
+ min-width: 0;
623
+ }
103
624
 
104
- .label {
105
- font-size: 14px;
106
- color: var(--text-secondary);
107
- font-weight: 500;
108
- }
625
+ .summary-value,
626
+ .metric-value,
627
+ .metric-period-label,
628
+ .metric-pair-value {
629
+ font-size: 22px;
630
+ font-weight: 700;
631
+ line-height: 1.1;
632
+ color: var(--text-primary);
633
+ }
109
634
 
110
- .value {
111
- font-size: 14px;
112
- color: var(--text-primary);
113
- font-weight: 600;
635
+ .summary-value {
636
+ letter-spacing: -0.3px;
637
+ font-variant-numeric: tabular-nums;
638
+ }
114
639
 
115
- &.highlight {
116
- color: var(--primary-color);
117
- }
118
- }
119
- }
640
+ .summary-label,
641
+ .metric-label,
642
+ .trend-date,
643
+ .legend-item,
644
+ .ua-list-name,
645
+ .ua-empty,
646
+ .ua-chip,
647
+ .ua-meta-title,
648
+ .metric-pair-name {
649
+ font-size: 12px;
650
+ }
651
+
652
+ .summary-label,
653
+ .metric-label,
654
+ .legend-item,
655
+ .trend-date,
656
+ .ua-list-name,
657
+ .ua-empty,
658
+ .ua-meta-title,
659
+ .metric-pair-name {
660
+ color: var(--text-secondary);
661
+ }
662
+
663
+ .stat-primary {
664
+ .summary-value {
665
+ color: var(--primary-color);
666
+ }
667
+ }
668
+
669
+ .stat-success {
670
+ .summary-value {
671
+ color: var(--success-color);
672
+ }
673
+ }
674
+
675
+ .stat-warning {
676
+ .summary-value {
677
+ color: var(--warning-color);
678
+ }
679
+ }
680
+
681
+ .stat-danger {
682
+ .summary-value {
683
+ color: var(--error-color);
684
+ }
685
+ }
686
+
687
+ .stat-critical {
688
+ .summary-value {
689
+ color: #d54941;
690
+ }
691
+ }
692
+
693
+ .product-stats-card {
694
+ display: flex;
695
+ flex-direction: column;
696
+ gap: 12px;
697
+ }
698
+
699
+ .card-head {
700
+ display: flex;
701
+ align-items: flex-start;
702
+ justify-content: space-between;
703
+ gap: 12px;
704
+ }
705
+
706
+ .card-head-compact {
707
+ align-items: center;
708
+ }
709
+
710
+ .card-title,
711
+ .trend-title,
712
+ .ua-card-title {
713
+ display: flex;
714
+ align-items: center;
715
+ gap: 6px;
716
+ font-size: 14px;
717
+ font-weight: 700;
718
+ color: var(--text-primary);
719
+ }
720
+
721
+ .product-switch-list {
722
+ display: flex;
723
+ flex-wrap: wrap;
724
+ justify-content: flex-end;
725
+ gap: 8px;
726
+ }
727
+
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);
120
751
  }
121
752
  }
122
753
 
123
- .stats-grid {
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;
762
+ }
763
+
764
+ .product-metrics-grid {
124
765
  display: grid;
125
- grid-template-columns: repeat(3, 1fr);
766
+ grid-template-columns: repeat(5, minmax(0, 1fr));
126
767
  gap: 10px;
768
+ }
127
769
 
128
- .stat-box {
129
- background: rgba(var(--primary-color-rgb), 0.02);
130
- border: 1px solid var(--border-color);
131
- border-radius: 6px;
132
- padding: 12px;
133
- display: flex;
134
- align-items: center;
135
- gap: 10px;
136
- transition: all 0.3s;
137
-
138
- &:hover {
139
- background: rgba(var(--primary-color-rgb), 0.05);
140
- border-color: var(--primary-color);
141
- transform: translateY(-2px);
142
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
143
- }
770
+ .metric-box {
771
+ display: flex;
772
+ flex-direction: column;
773
+ justify-content: space-between;
774
+ gap: 10px;
775
+ min-height: 128px;
776
+ padding: 14px 14px 12px;
777
+ border-radius: 10px;
778
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(var(--primary-color-rgb), 0.045));
779
+ border: 1px solid rgba(var(--primary-color-rgb), 0.08);
780
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
781
+ }
144
782
 
145
- .stat-content {
146
- flex: 1;
783
+ .metric-box-period {
784
+ gap: 12px;
147
785
 
148
- .stat-value {
149
- font-size: 20px;
150
- font-weight: 700;
151
- margin-bottom: 2px;
152
- }
786
+ &:hover {
787
+ transform: translateY(-2px);
788
+ box-shadow: 0 10px 24px rgba(31, 35, 41, 0.08);
789
+ }
790
+ }
153
791
 
154
- .stat-label {
155
- font-size: 14px;
156
- color: var(--text-secondary);
157
- }
158
- }
792
+ .metric-period-label {
793
+ font-size: 12px;
794
+ font-weight: 700;
795
+ color: var(--text-secondary);
796
+ letter-spacing: 0.3px;
797
+ }
159
798
 
160
- &.stat-primary {
161
- border-color: var(--primary-color);
162
- background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.05), white);
799
+ .metric-pair-row {
800
+ display: flex;
801
+ align-items: center;
802
+ justify-content: space-between;
803
+ gap: 8px;
804
+ padding-top: 6px;
805
+ border-top: 1px dashed rgba(0, 0, 0, 0.06);
806
+ }
163
807
 
164
- .stat-value {
165
- color: var(--primary-color);
166
- }
167
- }
808
+ .metric-pair-value {
809
+ font-size: 22px;
810
+ font-weight: 700;
811
+ letter-spacing: -0.3px;
812
+ font-variant-numeric: tabular-nums;
813
+ color: #0052d9;
814
+ }
168
815
 
169
- &.stat-success {
170
- border-color: var(--success-color);
171
- background: linear-gradient(135deg, rgba(var(--success-color-rgb), 0.05), white);
816
+ .metric-pair-value-uv {
817
+ color: #2ba471;
818
+ }
172
819
 
173
- .stat-value {
174
- color: var(--success-color);
175
- }
176
- }
820
+ .trend-grid {
821
+ display: grid;
822
+ grid-template-columns: minmax(0, 1fr);
823
+ gap: 10px;
824
+ }
177
825
 
178
- &.stat-warning {
179
- border-color: var(--warning-color);
180
- background: linear-gradient(135deg, rgba(var(--warning-color-rgb), 0.05), white);
826
+ .trend-card {
827
+ display: flex;
828
+ flex-direction: column;
829
+ gap: 10px;
830
+ }
181
831
 
182
- .stat-value {
183
- color: var(--warning-color);
184
- }
185
- }
832
+ .trend-content {
833
+ display: flex;
834
+ flex-direction: column;
835
+ gap: 8px;
836
+ flex: 1;
837
+ }
838
+
839
+ .trend-legend {
840
+ display: flex;
841
+ align-items: center;
842
+ gap: 8px;
843
+ flex-wrap: wrap;
844
+ }
845
+
846
+ .legend-item {
847
+ display: inline-flex;
848
+ align-items: center;
849
+ gap: 6px;
850
+
851
+ &::before {
852
+ content: "";
853
+ width: 8px;
854
+ height: 8px;
855
+ border-radius: 999px;
856
+ display: inline-block;
857
+ }
858
+ }
859
+
860
+ .legend-pv::before {
861
+ background: #0052d9;
862
+ }
863
+
864
+ .legend-uv::before {
865
+ background: #2ba471;
866
+ }
867
+
868
+ .trend-svg {
869
+ width: 100%;
870
+ height: 148px;
871
+ display: block;
872
+ border-radius: 8px;
873
+ 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
+ background-size:
875
+ 100% 25%,
876
+ 3.333% 100%,
877
+ auto;
878
+ }
879
+
880
+ .trend-line {
881
+ fill: none;
882
+ stroke-width: 1.8;
883
+ stroke-linecap: round;
884
+ stroke-linejoin: round;
885
+ }
886
+
887
+ .trend-line-pv,
888
+ .trend-dot-pv {
889
+ stroke: #0052d9;
890
+ fill: #0052d9;
891
+ }
892
+
893
+ .trend-line-uv,
894
+ .trend-dot-uv {
895
+ stroke: #2ba471;
896
+ fill: #2ba471;
897
+ }
898
+
899
+ .trend-briefs {
900
+ display: grid;
901
+ grid-template-columns: repeat(7, minmax(0, 1fr));
902
+ gap: 6px;
903
+ }
904
+
905
+ .trend-briefs-wide {
906
+ grid-template-columns: repeat(10, minmax(0, 1fr));
907
+ }
908
+
909
+ .trend-brief-item {
910
+ min-width: 0;
911
+ padding: 6px 4px;
912
+ border-radius: 6px;
913
+ background: rgba(255, 255, 255, 0.86);
914
+ border: 1px solid rgba(0, 0, 0, 0.05);
915
+ text-align: center;
916
+ }
917
+
918
+ .trend-values {
919
+ display: flex;
920
+ flex-direction: column;
921
+ gap: 1px;
922
+ font-size: 14px;
923
+ font-weight: 700;
924
+ line-height: 1.15;
925
+ font-variant-numeric: tabular-nums;
926
+ }
927
+
928
+ .trend-value.pv {
929
+ color: #0052d9;
930
+ }
931
+
932
+ .trend-value.uv {
933
+ color: #2ba471;
934
+ }
935
+
936
+ .card-empty,
937
+ .ua-empty {
938
+ display: flex;
939
+ align-items: center;
940
+ justify-content: center;
941
+ min-height: 80px;
942
+ border-radius: 8px;
943
+ background: rgba(var(--primary-color-rgb), 0.03);
944
+ }
945
+
946
+ .ua-grid {
947
+ display: grid;
948
+ grid-template-columns: repeat(4, minmax(0, 1fr));
949
+ gap: 10px;
950
+ }
951
+
952
+ .ua-card {
953
+ display: flex;
954
+ flex-direction: column;
955
+ gap: 8px;
956
+ }
957
+
958
+ .ua-card-wide {
959
+ grid-column: span 4;
960
+ }
961
+
962
+ .ua-list {
963
+ display: flex;
964
+ flex-direction: column;
965
+ gap: 6px;
966
+ flex: 1;
967
+ }
968
+
969
+ .ua-list-item {
970
+ display: flex;
971
+ align-items: center;
972
+ justify-content: space-between;
973
+ gap: 8px;
974
+ padding: 7px 9px;
975
+ border-radius: 6px;
976
+ background: rgba(var(--primary-color-rgb), 0.04);
977
+ }
978
+
979
+ .ua-list-name {
980
+ min-width: 0;
981
+ overflow: hidden;
982
+ text-overflow: ellipsis;
983
+ white-space: nowrap;
984
+ }
985
+
986
+ .ua-list-count {
987
+ font-size: 14px;
988
+ font-weight: 700;
989
+ line-height: 1.1;
990
+ font-variant-numeric: tabular-nums;
991
+ color: var(--primary-color);
992
+ }
993
+
994
+ .ua-meta-group {
995
+ display: grid;
996
+ grid-template-columns: repeat(3, minmax(0, 1fr));
997
+ gap: 10px;
998
+ flex: 1;
999
+ }
1000
+
1001
+ .ua-meta-block {
1002
+ border-radius: 8px;
1003
+ background: rgba(var(--primary-color-rgb), 0.03);
1004
+ padding: 10px;
1005
+ }
1006
+
1007
+ .ua-chip-list {
1008
+ display: flex;
1009
+ flex-wrap: wrap;
1010
+ gap: 6px;
1011
+ }
1012
+
1013
+ .ua-chip {
1014
+ display: inline-flex;
1015
+ align-items: center;
1016
+ padding: 4px 8px;
1017
+ border-radius: 999px;
1018
+ background: rgba(var(--primary-color-rgb), 0.08);
1019
+ color: var(--primary-color);
1020
+ font-weight: 600;
1021
+ }
1022
+
1023
+ .ua-empty-inline {
1024
+ min-height: auto;
1025
+ justify-content: flex-start;
1026
+ background: transparent;
1027
+ padding: 0;
1028
+ }
1029
+
1030
+ @media (max-width: 1280px) {
1031
+ .summary-grid {
1032
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1033
+ }
1034
+
1035
+ .product-metrics-grid {
1036
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1037
+ }
1038
+
1039
+ .trend-briefs-wide {
1040
+ grid-template-columns: repeat(6, minmax(0, 1fr));
1041
+ }
1042
+
1043
+ .ua-grid {
1044
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1045
+ }
1046
+
1047
+ .ua-card-wide {
1048
+ grid-column: span 2;
1049
+ }
1050
+ }
1051
+
1052
+ @media (max-width: 960px) {
1053
+ .summary-grid,
1054
+ .ua-grid,
1055
+ .ua-meta-group {
1056
+ grid-template-columns: 1fr;
1057
+ }
1058
+
1059
+ .ua-card-wide {
1060
+ grid-column: span 1;
1061
+ }
1062
+
1063
+ .card-head,
1064
+ .card-head-compact {
1065
+ flex-direction: column;
1066
+ align-items: flex-start;
1067
+ }
1068
+
1069
+ .product-switch-list {
1070
+ justify-content: flex-start;
1071
+ }
1072
+
1073
+ .trend-briefs-wide {
1074
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1075
+ }
1076
+ }
1077
+
1078
+ @media (max-width: 640px) {
1079
+ .summary-grid,
1080
+ .product-metrics-grid,
1081
+ .trend-briefs-wide {
1082
+ grid-template-columns: 1fr;
1083
+ }
1084
+
1085
+ .overview-card-large {
1086
+ min-height: 0;
1087
+ }
1088
+
1089
+ .product-switch {
1090
+ width: 100%;
1091
+ }
1092
+
1093
+ .trend-svg {
1094
+ height: 126px;
186
1095
  }
187
1096
  }
188
1097
  </style>