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.
@@ -0,0 +1,700 @@
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>