befly-admin-ui 1.10.18 → 1.11.0

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.
Files changed (39) hide show
  1. package/README.md +2 -1
  2. package/components/detailPanel.vue +0 -1
  3. package/components/pageDialog.vue +0 -1
  4. package/components/pageTableDetail.vue +1 -1
  5. package/layouts/default.vue +1 -2
  6. package/package.json +2 -2
  7. package/styles/dashboardCard.scss +228 -0
  8. package/views/config/dict/components/edit.vue +1 -1
  9. package/views/config/dict/index.vue +1 -2
  10. package/views/config/dictType/components/edit.vue +1 -1
  11. package/views/config/dictType/index.vue +0 -2
  12. package/views/config/system/components/edit.vue +1 -1
  13. package/views/config/system/index.vue +0 -2
  14. package/views/index/components/environmentInfo.vue +65 -81
  15. package/views/index/components/projectStats.vue +392 -0
  16. package/views/index/components/serviceStatus.vue +28 -105
  17. package/views/index/components/systemOverview.vue +41 -1157
  18. package/views/index/components/systemResources.vue +35 -94
  19. package/views/index/index.vue +10 -7
  20. package/views/log/email/index.vue +1 -2
  21. package/views/log/error/index.vue +1 -2
  22. package/views/log/login/index.vue +0 -1
  23. package/views/log/operate/index.vue +0 -1
  24. package/views/login_1/index.vue +1 -2
  25. package/views/people/admin/components/edit.vue +0 -6
  26. package/views/people/admin/index.vue +0 -2
  27. package/views/permission/api/index.vue +1 -2
  28. package/views/permission/menu/index.vue +1 -1
  29. package/views/permission/role/components/api.vue +1 -2
  30. package/views/permission/role/components/edit.vue +0 -6
  31. package/views/permission/role/components/menu.vue +1 -2
  32. package/views/permission/role/index.vue +0 -2
  33. package/views/resource/gallery/index.vue +1 -2
  34. package/views/index/components/addonList.vue +0 -140
  35. package/views/index/components/operationLogs.vue +0 -120
  36. package/views/index/components/performanceMetrics.vue +0 -223
  37. package/views/index/components/quickActions.vue +0 -30
  38. package/views/index/components/systemNotifications.vue +0 -139
  39. package/views/index/components/userInfo.vue +0 -188
@@ -5,196 +5,59 @@
5
5
  <h2>系统概览</h2>
6
6
  </div>
7
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">{{ $Data.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">{{ $Data.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">{{ $Data.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">{{ $Data.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">{{ $Computed.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-select-wrap">
53
- <TSelect :model-value="$Data.productState.selectedKey" class="product-select" placeholder="请选择产品" @change="$Method.selectProduct">
54
- <TOption v-for="item in $Computed.productOptions" :key="item.key" :value="item.key" :label="item.productName" />
55
- </TSelect>
56
- </div>
57
- </div>
58
-
59
- <div class="product-metrics-grid">
60
- <div v-for="item in $Computed.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="$Computed.selectedDaysHasData" class="trend-content">
88
- <div class="trend-bars">
89
- <div v-for="item in $Computed.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="`${$Method.formatReportDate(item.reportDate)} PV ${item.pv}`"></div>
92
- <div class="trend-bar trend-bar-uv" :style="{ height: item.uvHeight }" :title="`${$Method.formatReportDate(item.reportDate)} UV ${item.uv}`"></div>
93
- </div>
94
- <div class="trend-bar-label">{{ $Method.formatReportDate(item.reportDate) }}</div>
95
- </div>
96
- </div>
97
-
98
- <div class="trend-briefs trend-briefs-wide">
99
- <div v-for="item in $Computed.selectedDaysTrendPreview" :key="item.reportDate" class="trend-brief-item">
100
- <div class="trend-date">{{ $Method.formatReportDate(item.reportDate) }}</div>
101
- <div class="trend-values">
102
- <span class="trend-value pv">{{ item.pv }}</span>
103
- <span class="trend-value uv">{{ item.uv }}</span>
104
- </div>
8
+ <div class="section-content">
9
+ <t-row :gutter="[16, 16]" class="dashboard-card-row">
10
+ <t-col :span="2" class="dashboard-card-col">
11
+ <div class="dashboard-card">
12
+ <div class="dashboard-card-content">
13
+ <div class="dashboard-card-label">菜单总数</div>
14
+ <div class="dashboard-card-value-row">
15
+ <div class="dashboard-card-main dashboard-card-main--metric dashboard-card-main--primary">{{ $Data.permissionStats.menuCount }}</div>
105
16
  </div>
106
17
  </div>
107
18
  </div>
108
-
109
- <div v-else class="card-empty">暂无近 30 天趋势数据</div>
110
- </div>
111
- </div>
112
-
113
- <div class="ua-grid">
114
- <div class="ua-card overview-card-small">
115
- <div class="ua-card-title">设备类型</div>
116
- <div v-if="$Data.uaStats.deviceTypes.length > 0" class="ua-list">
117
- <div v-for="item in $Data.uaStats.deviceTypes.slice(0, 5)" :key="`device-type-${item.name}`" class="ua-list-item">
118
- <span class="ua-list-name">{{ $Method.formatDeviceType(item.name) }}</span>
119
- <span class="ua-list-count">{{ item.count }}</span>
120
- </div>
121
- </div>
122
- <div v-else class="ua-empty">暂无设备类型数据</div>
123
- </div>
124
-
125
- <div class="ua-card overview-card-small">
126
- <div class="ua-card-title">浏览器分布</div>
127
- <div v-if="$Data.uaStats.browsers.length > 0" class="ua-list">
128
- <div v-for="item in $Data.uaStats.browsers.slice(0, 5)" :key="`browser-${item.name}`" class="ua-list-item">
129
- <span class="ua-list-name">{{ item.name }}</span>
130
- <span class="ua-list-count">{{ item.count }}</span>
131
- </div>
132
- </div>
133
- <div v-else class="ua-empty">暂无浏览器数据</div>
134
- </div>
135
-
136
- <div class="ua-card overview-card-small">
137
- <div class="ua-card-title">操作系统分布</div>
138
- <div v-if="$Data.uaStats.osList.length > 0" class="ua-list">
139
- <div v-for="item in $Data.uaStats.osList.slice(0, 5)" :key="`os-${item.name}`" class="ua-list-item">
140
- <span class="ua-list-name">{{ item.name }}</span>
141
- <span class="ua-list-count">{{ item.count }}</span>
142
- </div>
143
- </div>
144
- <div v-else class="ua-empty">暂无系统数据</div>
145
- </div>
146
-
147
- <div class="ua-card overview-card-small">
148
- <div class="ua-card-title">设备厂商</div>
149
- <div v-if="$Data.uaStats.deviceVendors.length > 0" class="ua-list">
150
- <div v-for="item in $Data.uaStats.deviceVendors.slice(0, 5)" :key="`vendor-${item.name}`" class="ua-list-item">
151
- <span class="ua-list-name">{{ item.name }}</span>
152
- <span class="ua-list-count">{{ item.count }}</span>
153
- </div>
154
- </div>
155
- <div v-else class="ua-empty">暂无厂商数据</div>
156
- </div>
157
-
158
- <div class="ua-card ua-card-wide overview-card-large">
159
- <div class="ua-card-title">设备细分</div>
160
-
161
- <div class="ua-meta-group">
162
- <div class="ua-meta-block">
163
- <div class="ua-meta-title">设备型号</div>
164
- <div v-if="$Data.uaStats.deviceModels.length > 0" class="ua-chip-list">
165
- <span v-for="item in $Data.uaStats.deviceModels.slice(0, 6)" :key="`model-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
19
+ </t-col>
20
+
21
+ <t-col :span="2" class="dashboard-card-col">
22
+ <div class="dashboard-card">
23
+ <div class="dashboard-card-content">
24
+ <div class="dashboard-card-label">接口总数</div>
25
+ <div class="dashboard-card-value-row">
26
+ <div class="dashboard-card-main dashboard-card-main--metric dashboard-card-main--success">{{ $Data.permissionStats.apiCount }}</div>
166
27
  </div>
167
- <div v-else class="ua-empty ua-empty-inline">暂无设备型号数据</div>
168
28
  </div>
169
-
170
- <div class="ua-meta-block">
171
- <div class="ua-meta-title">渲染引擎</div>
172
- <div v-if="$Data.uaStats.engines.length > 0" class="ua-chip-list">
173
- <span v-for="item in $Data.uaStats.engines.slice(0, 6)" :key="`engine-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
29
+ </div>
30
+ </t-col>
31
+
32
+ <t-col :span="2" class="dashboard-card-col">
33
+ <div class="dashboard-card">
34
+ <div class="dashboard-card-content">
35
+ <div class="dashboard-card-label">角色总数</div>
36
+ <div class="dashboard-card-value-row">
37
+ <div class="dashboard-card-main dashboard-card-main--metric dashboard-card-main--warning">{{ $Data.permissionStats.roleCount }}</div>
174
38
  </div>
175
- <div v-else class="ua-empty ua-empty-inline">暂无渲染引擎数据</div>
176
39
  </div>
177
-
178
- <div class="ua-meta-block">
179
- <div class="ua-meta-title">CPU 架构</div>
180
- <div v-if="$Data.uaStats.cpuArchitectures.length > 0" class="ua-chip-list">
181
- <span v-for="item in $Data.uaStats.cpuArchitectures.slice(0, 6)" :key="`cpu-${item.name}`" class="ua-chip">{{ item.name }} · {{ item.count }}</span>
40
+ </div>
41
+ </t-col>
42
+
43
+ <t-col :span="2" class="dashboard-card-col">
44
+ <div class="dashboard-card">
45
+ <div class="dashboard-card-content">
46
+ <div class="dashboard-card-label">今日错误</div>
47
+ <div class="dashboard-card-value-row">
48
+ <div class="dashboard-card-main dashboard-card-main--metric dashboard-card-main--critical">{{ $Computed.errorTodayCount }}</div>
182
49
  </div>
183
- <div v-else class="ua-empty ua-empty-inline">暂无 CPU 架构数据</div>
184
50
  </div>
185
51
  </div>
186
- </div>
187
- </div>
52
+ </t-col>
53
+ </t-row>
188
54
  </div>
189
55
  </div>
190
56
  </template>
191
57
 
192
58
  <script setup>
193
- import { InfoCircleIcon, TrendingUpIcon } from "tdesign-icons-vue-next";
194
- import { Option as TOption, Select as TSelect } from "tdesign-vue-next";
195
59
  import { computed, reactive } from "vue";
196
60
 
197
- import { $Config } from "@/plugins/config.js";
198
61
  import { $Http } from "@/plugins/http.js";
199
62
 
200
63
  const $Data = reactive({
@@ -203,398 +66,18 @@ const $Data = reactive({
203
66
  apiCount: 0,
204
67
  roleCount: 0
205
68
  },
206
- realtimeStats: {
207
- onlineCount: 0,
208
- todayPv: 0,
209
- todayUv: 0,
210
- days: []
211
- },
212
69
  errorStats: {
213
70
  trend: []
214
- },
215
- productInfo: {
216
- productName: String($Config.productName || "-"),
217
- productCode: String($Config.productCode || "-"),
218
- productVersion: String($Config.productVersion || "-")
219
- },
220
- productState: {
221
- selectedKey: "",
222
- options: [],
223
- loading: false
224
- },
225
- uaStats: {
226
- deviceTypes: [],
227
- browsers: [],
228
- browserVersions: [],
229
- osList: [],
230
- osVersions: [],
231
- deviceVendors: [],
232
- deviceModels: [],
233
- engines: [],
234
- cpuArchitectures: []
235
71
  }
236
72
  });
237
73
 
238
74
  const $Method = {
239
- buildFallbackProduct: function () {
240
- const days = $Method.sortTrendList($Data.realtimeStats.days, "reportDate");
241
-
242
- return {
243
- key: "all-products",
244
- productName: "全部产品",
245
- productCode: $Data.productInfo.productCode,
246
- productVersion: $Data.productInfo.productVersion,
247
- today: {
248
- pv: Number($Data.realtimeStats.todayPv || 0),
249
- uv: Number($Data.realtimeStats.todayUv || 0)
250
- },
251
- days: days,
252
- totalPv: $Method.sumTrendField(days, "pv"),
253
- totalUv: $Method.sumTrendField(days, "uv")
254
- };
255
- },
256
- getReportDateNumber: function (timestamp) {
257
- return Number(
258
- new Intl.DateTimeFormat("en-CA", {
259
- year: "numeric",
260
- month: "2-digit",
261
- day: "2-digit"
262
- })
263
- .format(timestamp)
264
- .replace(/[^0-9]/g, "")
265
- );
266
- },
267
- buildRecentReportDateList: function (limit) {
268
- const list = [];
269
-
270
- for (let i = limit - 1; i >= 0; i--) {
271
- list.push($Method.getReportDateNumber(Date.now() - i * 24 * 60 * 60 * 1000));
272
- }
273
-
274
- return list;
275
- },
276
- buildFilledTrendDays: function (list, limit) {
277
- const dateList = $Method.buildRecentReportDateList(limit);
278
- const dataMap = new Map();
279
-
280
- for (const item of list) {
281
- dataMap.set(Number(item?.reportDate || 0), {
282
- reportDate: Number(item?.reportDate || 0),
283
- pv: Number(item?.pv || 0),
284
- uv: Number(item?.uv || 0)
285
- });
286
- }
287
-
288
- return dateList.map((reportDate) => {
289
- return (
290
- dataMap.get(reportDate) || {
291
- reportDate: reportDate,
292
- pv: 0,
293
- uv: 0
294
- }
295
- );
296
- });
297
- },
298
- sumTrendRange: function (list) {
299
- let pv = 0;
300
- let uv = 0;
301
-
302
- for (const item of list) {
303
- pv += Number(item?.pv || 0);
304
- uv += Number(item?.uv || 0);
305
- }
306
-
307
- return {
308
- pv: pv,
309
- uv: uv
310
- };
311
- },
312
- ensureSelectedProduct: function () {
313
- const list = $Computed.productOptions;
314
-
315
- if (list.length === 0) {
316
- $Data.productState.selectedKey = "";
317
- return;
318
- }
319
-
320
- if (list.some((item) => item.key === $Data.productState.selectedKey)) {
321
- return;
322
- }
323
-
324
- const currentProduct = list.find((item) => item.productName === $Data.productInfo.productName);
325
- $Data.productState.selectedKey = currentProduct?.key || "all-products";
326
- },
327
- selectProduct: function (productKey) {
328
- $Data.productState.selectedKey = String(productKey || "");
329
- $Method.fetchProductStats($Data.productState.selectedKey);
330
- },
331
- getSelectedProductName: function (selectedKey) {
332
- if (!selectedKey || selectedKey === "all-products") {
333
- return "";
334
- }
335
-
336
- const selected = $Data.productState.options.find((item) => item.key === selectedKey);
337
-
338
- if (selected?.productName) {
339
- return String(selected.productName);
340
- }
341
-
342
- return String(selectedKey);
343
- },
344
- applyOnlineStatsData: function (onlineStatsData) {
345
- $Data.realtimeStats.onlineCount = Number(onlineStatsData?.onlineCount || 0);
346
- $Data.realtimeStats.todayPv = Number(onlineStatsData?.today?.pv || 0);
347
- $Data.realtimeStats.todayUv = Number(onlineStatsData?.today?.uv || 0);
348
- $Data.realtimeStats.days = Array.isArray(onlineStatsData?.days) ? onlineStatsData.days : [];
349
- },
350
- applyInfoStatsData: function (infoStatsData) {
351
- $Data.uaStats.deviceTypes = Array.isArray(infoStatsData?.today?.deviceTypes) ? infoStatsData.today.deviceTypes : [];
352
- $Data.uaStats.browsers = Array.isArray(infoStatsData?.today?.browsers) ? infoStatsData.today.browsers : [];
353
- $Data.uaStats.browserVersions = Array.isArray(infoStatsData?.today?.browserVersions) ? infoStatsData.today.browserVersions : [];
354
- $Data.uaStats.osList = Array.isArray(infoStatsData?.today?.osList) ? infoStatsData.today.osList : [];
355
- $Data.uaStats.osVersions = Array.isArray(infoStatsData?.today?.osVersions) ? infoStatsData.today.osVersions : [];
356
- $Data.uaStats.deviceVendors = Array.isArray(infoStatsData?.today?.deviceVendors) ? infoStatsData.today.deviceVendors : [];
357
- $Data.uaStats.deviceModels = Array.isArray(infoStatsData?.today?.deviceModels) ? infoStatsData.today.deviceModels : [];
358
- $Data.uaStats.engines = Array.isArray(infoStatsData?.today?.engines) ? infoStatsData.today.engines : [];
359
- $Data.uaStats.cpuArchitectures = Array.isArray(infoStatsData?.today?.cpuArchitectures) ? infoStatsData.today.cpuArchitectures : [];
360
- },
361
- mapProductOptions: function (onlineStatsData) {
362
- if (!Array.isArray(onlineStatsData?.products)) {
363
- return [];
364
- }
365
-
366
- return onlineStatsData.products.map((item) => {
367
- return {
368
- key: String(item?.key || item?.productName || ""),
369
- productName: String(item?.productName || "-"),
370
- today: {
371
- pv: Number(item?.today?.pv || 0),
372
- uv: Number(item?.today?.uv || 0)
373
- },
374
- week: {
375
- pv: Number(item?.week?.pv || 0),
376
- uv: Number(item?.week?.uv || 0)
377
- },
378
- month: {
379
- pv: Number(item?.month?.pv || 0),
380
- uv: Number(item?.month?.uv || 0)
381
- },
382
- days: Array.isArray(item?.days) ? item.days : [],
383
- totalPv: Number(item?.totalPv || 0),
384
- totalUv: Number(item?.totalUv || 0)
385
- };
386
- });
387
- },
388
- useSingleProductFallback: function (onlineStatsData) {
389
- const days = Array.isArray(onlineStatsData?.days) ? onlineStatsData.days : [];
390
-
391
- return [
392
- {
393
- key: $Data.productInfo.productName,
394
- productName: $Data.productInfo.productName,
395
- today: {
396
- pv: Number(onlineStatsData?.today?.pv || 0),
397
- uv: Number(onlineStatsData?.today?.uv || 0)
398
- },
399
- week: {
400
- pv: Number(onlineStatsData?.week?.pv || 0),
401
- uv: Number(onlineStatsData?.week?.uv || 0)
402
- },
403
- month: {
404
- pv: Number(onlineStatsData?.month?.pv || 0),
405
- uv: Number(onlineStatsData?.month?.uv || 0)
406
- },
407
- days: days,
408
- totalPv: $Method.sumTrendField(days, "pv"),
409
- totalUv: $Method.sumTrendField(days, "uv")
410
- }
411
- ];
412
- },
413
- applyProductOptions: function (onlineStatsData, infoStatsData) {
414
- $Data.productState.options = $Method.mapProductOptions(onlineStatsData);
415
-
416
- const onlyOneProduct = Array.isArray(infoStatsData?.month?.productNames) && infoStatsData.month.productNames.length === 1;
417
-
418
- if (onlyOneProduct && $Data.productInfo.productName !== "-") {
419
- $Data.productState.options = $Method.useSingleProductFallback(onlineStatsData);
420
- }
421
- },
422
- updateSelectedProductOption: function (selectedKey, onlineStatsData) {
423
- if (!selectedKey || selectedKey === "all-products") {
424
- return;
425
- }
426
-
427
- const selectedIndex = $Data.productState.options.findIndex((item) => item.key === selectedKey);
428
-
429
- if (selectedIndex < 0) {
430
- return;
431
- }
432
-
433
- const days = Array.isArray(onlineStatsData?.days) ? onlineStatsData.days : [];
434
- const current = $Data.productState.options[selectedIndex];
435
-
436
- $Data.productState.options[selectedIndex] = {
437
- key: current.key,
438
- productName: current.productName,
439
- today: {
440
- pv: Number(onlineStatsData?.today?.pv || 0),
441
- uv: Number(onlineStatsData?.today?.uv || 0)
442
- },
443
- week: {
444
- pv: Number(onlineStatsData?.week?.pv || 0),
445
- uv: Number(onlineStatsData?.week?.uv || 0)
446
- },
447
- month: {
448
- pv: Number(onlineStatsData?.month?.pv || 0),
449
- uv: Number(onlineStatsData?.month?.uv || 0)
450
- },
451
- days: days,
452
- totalPv: $Method.sumTrendField(days, "pv"),
453
- totalUv: $Method.sumTrendField(days, "uv")
454
- };
455
- },
456
- fetchProductStats: async function (selectedKey) {
457
- if ($Data.productState.loading) {
458
- return;
459
- }
460
-
461
- $Data.productState.loading = true;
462
-
463
- try {
464
- const productName = $Method.getSelectedProductName(selectedKey);
465
- const requestBody = productName ? { productName: productName } : {};
466
- const [onlineStatsRes, infoStatsRes] = await Promise.all([$Http("/core/tongJi/onlineStats", requestBody, [""]), $Http("/core/tongJi/infoStats", requestBody, [""])]);
467
-
468
- $Method.applyOnlineStatsData(onlineStatsRes.data);
469
- $Method.applyInfoStatsData(infoStatsRes.data);
470
- $Method.updateSelectedProductOption(selectedKey, onlineStatsRes.data);
471
- } catch (_error) {
472
- // 静默失败:不阻断页面展示
473
- } finally {
474
- $Data.productState.loading = false;
475
- }
476
- },
477
- sumTrendField: function (list, field) {
478
- let total = 0;
479
-
480
- for (const item of list) {
481
- total += Number(item?.[field] || 0);
482
- }
483
-
484
- return total;
485
- },
486
- sortTrendList: function (list, keyField) {
487
- const result = Array.isArray(list) ? list.slice() : [];
488
-
489
- result.sort((a, b) => Number(a?.[keyField] || 0) - Number(b?.[keyField] || 0));
490
- return result;
491
- },
492
- buildTrendPreview: function (list, maxCount) {
493
- if (list.length <= maxCount) {
494
- return list;
495
- }
496
-
497
- const preview = [];
498
- const usedIndex = new Set();
499
-
500
- for (let i = 0; i < maxCount; i++) {
501
- const index = Math.round((i * (list.length - 1)) / (maxCount - 1));
502
-
503
- if (usedIndex.has(index)) {
504
- continue;
505
- }
506
-
507
- usedIndex.add(index);
508
- preview.push(list[index]);
509
- }
510
-
511
- return preview;
512
- },
513
- buildTrendBars: function (list, maxValue) {
514
- const result = [];
515
-
516
- for (const item of list) {
517
- const pv = Number(item?.pv || 0);
518
- const uv = Number(item?.uv || 0);
519
-
520
- result.push({
521
- reportDate: Number(item?.reportDate || 0),
522
- pv: pv,
523
- uv: uv,
524
- pvHeight: pv > 0 ? `${Math.max((pv / maxValue) * 100, 6)}%` : "0%",
525
- uvHeight: uv > 0 ? `${Math.max((uv / maxValue) * 100, 6)}%` : "0%"
526
- });
527
- }
528
-
529
- return result;
530
- },
531
- getTrendMax: function (list) {
532
- let max = 0;
533
-
534
- for (const item of list) {
535
- const pv = Number(item?.pv || 0);
536
- const uv = Number(item?.uv || 0);
537
-
538
- if (pv > max) {
539
- max = pv;
540
- }
541
-
542
- if (uv > max) {
543
- max = uv;
544
- }
545
- }
546
-
547
- return max > 0 ? max : 1;
548
- },
549
- formatDeviceType: function (deviceType) {
550
- if (deviceType === "desktop") {
551
- return "桌面端";
552
- }
553
-
554
- if (deviceType === "mobile") {
555
- return "移动端";
556
- }
557
-
558
- if (deviceType === "tablet") {
559
- return "平板";
560
- }
561
-
562
- if (deviceType === "smarttv") {
563
- return "电视";
564
- }
565
-
566
- if (deviceType === "wearable") {
567
- return "穿戴设备";
568
- }
569
-
570
- if (deviceType === "embedded") {
571
- return "嵌入式设备";
572
- }
573
-
574
- return String(deviceType || "Unknown");
575
- },
576
- formatReportDate: function (reportDate) {
577
- const text = String(reportDate || "");
578
-
579
- if (text.length !== 8) {
580
- return text;
581
- }
582
-
583
- return `${text.slice(4, 6)}-${text.slice(6, 8)}`;
584
- },
585
75
  fetchData: async function () {
586
76
  try {
587
- const [overviewRes, onlineStatsRes, infoStatsRes, errorStatsRes] = await Promise.all([$Http("/core/dashboard/systemOverview", {}, [""]), $Http("/core/tongJi/onlineStats", {}, [""]), $Http("/core/tongJi/infoStats", {}, [""]), $Http("/core/tongJi/errorStats", {}, [""])]);
77
+ const [overviewRes, errorStatsRes] = await Promise.all([$Http("/core/dashboard/systemOverview", {}, [""]), $Http("/core/tongJi/errorStats", {}, [""])]);
588
78
 
589
79
  Object.assign($Data.permissionStats, overviewRes.data);
590
- $Method.applyOnlineStatsData(onlineStatsRes.data);
591
- $Data.productInfo.productName = String($Config.productName || "-");
592
- $Data.productInfo.productCode = String($Config.productCode || "-");
593
- $Data.productInfo.productVersion = String($Config.productVersion || "-");
594
- $Method.applyProductOptions(onlineStatsRes.data, infoStatsRes.data);
595
- $Method.applyInfoStatsData(infoStatsRes.data);
596
80
  $Data.errorStats.trend = Array.isArray(errorStatsRes.data?.trend) ? errorStatsRes.data.trend : [];
597
- $Method.ensureSelectedProduct();
598
81
  } catch (_error) {
599
82
  // 静默失败:不阻断页面展示
600
83
  }
@@ -602,72 +85,14 @@ const $Method = {
602
85
  };
603
86
 
604
87
  const $Computed = reactive({
605
- errorTodayTrend: computed(() => $Method.sortTrendList($Data.errorStats.trend, "bucketTime")),
606
88
  errorTodayCount: computed(() => {
607
89
  let total = 0;
608
90
 
609
- for (const item of $Computed.errorTodayTrend) {
91
+ for (const item of $Data.errorStats.trend) {
610
92
  total += Number(item.count || 0);
611
93
  }
612
94
 
613
95
  return total;
614
- }),
615
- productOptions: computed(() => {
616
- const list = Array.isArray($Data.productState.options) ? $Data.productState.options.slice() : [];
617
- const fallbackProduct = $Method.buildFallbackProduct();
618
-
619
- if (list.length === 0) {
620
- return [fallbackProduct];
621
- }
622
-
623
- list.sort((a, b) => {
624
- if (a.productName === $Data.productInfo.productName && b.productName !== $Data.productInfo.productName) {
625
- return -1;
626
- }
627
-
628
- if (b.productName === $Data.productInfo.productName && a.productName !== $Data.productInfo.productName) {
629
- return 1;
630
- }
631
-
632
- if (Number(b.totalPv || 0) !== Number(a.totalPv || 0)) {
633
- return Number(b.totalPv || 0) - Number(a.totalPv || 0);
634
- }
635
-
636
- return String(a.productName || "").localeCompare(String(b.productName || ""), "zh-CN");
637
- });
638
-
639
- return [fallbackProduct, ...list];
640
- }),
641
- selectedProduct: computed(() => {
642
- const list = $Computed.productOptions;
643
-
644
- if (list.length === 0) {
645
- return null;
646
- }
647
-
648
- const matched = list.find((item) => item.key === $Data.productState.selectedKey);
649
- return matched || list[0];
650
- }),
651
- selectedDaysTrend: computed(() => $Method.sortTrendList($Computed.selectedProduct?.days || [], "reportDate")),
652
- selectedDaysFilled: computed(() => $Method.buildFilledTrendDays($Computed.selectedProduct?.days || [], 30)),
653
- selectedDaysHasData: computed(() => $Computed.selectedDaysTrend.length > 0),
654
- selectedDaysTrendPreview: computed(() => $Method.buildTrendPreview($Computed.selectedDaysFilled, 10)),
655
- selectedDaysBars: computed(() => $Method.buildTrendBars($Computed.selectedDaysFilled, $Method.getTrendMax($Computed.selectedDaysFilled))),
656
- productPeriodCards: computed(() => {
657
- const list = $Computed.selectedDaysFilled;
658
- const today = list[list.length - 1] || { pv: 0, uv: 0 };
659
- const yesterday = list[list.length - 2] || { pv: 0, uv: 0 };
660
- const dayBeforeYesterday = list[list.length - 3] || { pv: 0, uv: 0 };
661
- const recent7 = $Method.sumTrendRange(list.slice(-7));
662
- const recent30 = $Method.sumTrendRange(list);
663
-
664
- return [
665
- { key: "today", label: "今日", pv: today.pv, uv: today.uv },
666
- { key: "yesterday", label: "昨日", pv: yesterday.pv, uv: yesterday.uv },
667
- { key: "dayBeforeYesterday", label: "前日", pv: dayBeforeYesterday.pv, uv: dayBeforeYesterday.uv },
668
- { key: "recent7", label: "最近7天", pv: recent7.pv, uv: recent7.uv },
669
- { key: "recent30", label: "最近30天", pv: recent30.pv, uv: recent30.uv }
670
- ];
671
96
  })
672
97
  });
673
98
 
@@ -675,550 +100,9 @@ $Method.fetchData();
675
100
  </script>
676
101
 
677
102
  <style scoped lang="scss">
678
- .overview-layout {
679
- display: flex;
680
- flex-direction: column;
681
- gap: 10px;
682
- }
683
-
684
- .overview-card-large,
685
- .overview-card-small {
686
- border: 1px solid rgba(0, 0, 0, 0.06);
687
- border-radius: 10px;
688
- background: linear-gradient(180deg, rgba(var(--primary-color-rgb), 0.018), rgba(255, 255, 255, 0.96));
689
- box-shadow: 0 4px 14px rgba(31, 35, 41, 0.04);
690
- min-width: 0;
691
- }
692
-
693
- .overview-card-large {
694
- min-height: 252px;
695
- padding: 12px;
696
- }
697
-
698
- .overview-card-small {
699
- min-height: 94px;
700
- padding: 10px 12px;
701
- }
702
-
703
- .summary-grid {
704
- display: grid;
705
- grid-template-columns: repeat(5, minmax(0, 1fr));
706
- gap: 8px;
707
- }
103
+ @use "../../../styles/dashboardCard.scss" as dashboardCard;
708
104
 
709
- .summary-card {
710
- display: flex;
711
- align-items: flex-start;
712
- justify-content: center;
713
- transition: all 0.2s ease;
714
-
715
- &:hover {
716
- transform: translateY(-2px);
717
- box-shadow: 0 8px 20px rgba(31, 35, 41, 0.08);
718
- }
719
- }
720
-
721
- .summary-content {
722
- display: flex;
723
- flex-direction: column;
724
- justify-content: center;
725
- gap: 4px;
726
- width: 100%;
727
- min-width: 0;
728
- }
729
-
730
- .summary-value,
731
- .metric-value,
732
- .metric-period-label,
733
- .metric-pair-value {
734
- font-size: 22px;
735
- font-weight: 700;
736
- line-height: 1.1;
737
- color: var(--text-primary);
738
- }
739
-
740
- .summary-value {
741
- letter-spacing: -0.3px;
742
- font-variant-numeric: tabular-nums;
743
- }
744
-
745
- .summary-label,
746
- .metric-label,
747
- .trend-date,
748
- .legend-item,
749
- .ua-list-name,
750
- .ua-empty,
751
- .ua-chip,
752
- .ua-meta-title,
753
- .metric-pair-name {
754
- font-size: 12px;
755
- }
756
-
757
- .summary-label,
758
- .metric-label,
759
- .legend-item,
760
- .trend-date,
761
- .ua-list-name,
762
- .ua-empty,
763
- .ua-meta-title,
764
- .metric-pair-name {
765
- color: var(--text-secondary);
766
- }
767
-
768
- .stat-primary {
769
- .summary-value {
770
- color: var(--primary-color);
771
- }
772
- }
773
-
774
- .stat-success {
775
- .summary-value {
776
- color: var(--success-color);
777
- }
778
- }
779
-
780
- .stat-warning {
781
- .summary-value {
782
- color: var(--warning-color);
783
- }
784
- }
785
-
786
- .stat-danger {
787
- .summary-value {
788
- color: var(--error-color);
789
- }
790
- }
791
-
792
- .stat-critical {
793
- .summary-value {
794
- color: #d54941;
795
- }
796
- }
797
-
798
- .product-stats-card {
799
- display: flex;
800
- flex-direction: column;
801
- gap: 12px;
802
- }
803
-
804
- .card-head {
805
- display: flex;
806
- align-items: flex-start;
807
- justify-content: space-between;
808
- gap: 12px;
809
- }
810
-
811
- .card-head-compact {
812
- align-items: center;
813
- }
814
-
815
- .card-title,
816
- .trend-title,
817
- .ua-card-title {
818
- display: flex;
819
- align-items: center;
820
- gap: 6px;
821
- font-size: 14px;
822
- font-weight: 700;
823
- color: var(--text-primary);
824
- }
825
-
826
- .product-select-wrap {
827
- display: flex;
828
- justify-content: flex-end;
829
- }
830
-
831
- .product-select {
832
- width: 220px;
833
- }
834
-
835
- .product-metrics-grid {
836
- display: grid;
837
- grid-template-columns: repeat(5, minmax(0, 1fr));
838
- gap: 10px;
839
- }
840
-
841
- .metric-box {
842
- display: flex;
843
- flex-direction: column;
844
- justify-content: space-between;
845
- gap: 10px;
846
- min-height: 128px;
847
- padding: 14px 14px 12px;
848
- border-radius: 10px;
849
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(var(--primary-color-rgb), 0.045));
850
- border: 1px solid rgba(var(--primary-color-rgb), 0.08);
851
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
852
- }
853
-
854
- .metric-box-period {
855
- gap: 12px;
856
-
857
- &:hover {
858
- transform: translateY(-2px);
859
- box-shadow: 0 10px 24px rgba(31, 35, 41, 0.08);
860
- }
861
- }
862
-
863
- .metric-period-label {
864
- font-size: 12px;
865
- font-weight: 700;
866
- color: var(--text-secondary);
867
- letter-spacing: 0.3px;
868
- }
869
-
870
- .metric-pair-row {
871
- display: flex;
872
- align-items: center;
873
- justify-content: space-between;
874
- gap: 8px;
875
- padding-top: 6px;
876
- border-top: 1px dashed rgba(0, 0, 0, 0.06);
877
- }
878
-
879
- .metric-pair-value {
880
- font-size: 22px;
881
- font-weight: 700;
882
- letter-spacing: -0.3px;
883
- font-variant-numeric: tabular-nums;
884
- color: #0052d9;
885
- }
886
-
887
- .metric-pair-value-uv {
888
- color: #2ba471;
889
- }
890
-
891
- .trend-grid {
892
- display: grid;
893
- grid-template-columns: minmax(0, 1fr);
894
- gap: 10px;
895
- }
896
-
897
- .trend-card {
898
- display: flex;
899
- flex-direction: column;
900
- gap: 10px;
901
- }
902
-
903
- .trend-content {
904
- display: flex;
905
- flex-direction: column;
906
- gap: 8px;
907
- flex: 1;
908
- }
909
-
910
- .trend-legend {
911
- display: flex;
912
- align-items: center;
913
- gap: 8px;
914
- flex-wrap: wrap;
915
- }
916
-
917
- .legend-item {
918
- display: inline-flex;
919
- align-items: center;
920
- gap: 6px;
921
-
922
- &::before {
923
- content: "";
924
- width: 8px;
925
- height: 8px;
926
- border-radius: 999px;
927
- display: inline-block;
928
- }
929
- }
930
-
931
- .legend-pv::before {
932
- background: #0052d9;
933
- }
934
-
935
- .legend-uv::before {
936
- background: #2ba471;
937
- }
938
-
939
- .trend-bars {
940
- display: grid;
941
- grid-template-columns: repeat(30, minmax(0, 1fr));
942
- align-items: end;
943
- gap: 0;
944
- height: 148px;
945
- padding: 10px 0 6px;
946
- border-radius: 8px;
947
- 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));
948
- background-size:
949
- 100% 25%,
950
- 3.333% 100%,
951
- auto;
952
- }
953
-
954
- .trend-bar-group {
955
- min-width: 0;
956
- height: 100%;
957
- display: flex;
958
- flex-direction: column;
959
- justify-content: flex-end;
960
- gap: 4px;
961
- padding: 0 2px;
962
- }
963
-
964
- .trend-bar-stack {
965
- flex: 1;
966
- display: flex;
967
- align-items: flex-end;
968
- justify-content: center;
969
- gap: 2px;
970
- min-height: 0;
971
- }
972
-
973
- .trend-bar {
974
- width: calc(50% - 1px);
975
- min-width: 2px;
976
- border-radius: 4px 4px 0 0;
977
- transition: all 0.2s ease;
978
-
979
- &:hover {
980
- opacity: 0.9;
981
- transform: translateY(-1px);
982
- }
983
- }
984
-
985
- .trend-bar-pv {
986
- background: linear-gradient(180deg, rgba(0, 82, 217, 0.65), #0052d9);
987
- }
988
-
989
- .trend-bar-uv {
990
- background: linear-gradient(180deg, rgba(43, 164, 113, 0.65), #2ba471);
991
- }
992
-
993
- .trend-bar-label {
994
- font-size: 10px;
995
- line-height: 1;
996
- color: var(--text-tertiary);
997
- text-align: center;
998
- font-variant-numeric: tabular-nums;
999
- transform: scale(0.92);
1000
- }
1001
-
1002
- .trend-briefs {
1003
- display: grid;
1004
- grid-template-columns: repeat(7, minmax(0, 1fr));
1005
- gap: 6px;
1006
- }
1007
-
1008
- .trend-briefs-wide {
1009
- grid-template-columns: repeat(10, minmax(0, 1fr));
1010
- }
1011
-
1012
- .trend-brief-item {
1013
- min-width: 0;
1014
- padding: 6px 4px;
1015
- border-radius: 6px;
1016
- background: rgba(255, 255, 255, 0.86);
1017
- border: 1px solid rgba(0, 0, 0, 0.05);
1018
- text-align: center;
1019
- }
1020
-
1021
- .trend-values {
1022
- display: flex;
1023
- flex-direction: column;
1024
- gap: 1px;
1025
- font-size: 14px;
1026
- font-weight: 700;
1027
- line-height: 1.15;
1028
- font-variant-numeric: tabular-nums;
1029
- }
1030
-
1031
- .trend-value.pv {
1032
- color: #0052d9;
1033
- }
1034
-
1035
- .trend-value.uv {
1036
- color: #2ba471;
1037
- }
1038
-
1039
- .card-empty,
1040
- .ua-empty {
1041
- display: flex;
1042
- align-items: center;
1043
- justify-content: center;
1044
- min-height: 80px;
1045
- border-radius: 8px;
1046
- background: rgba(var(--primary-color-rgb), 0.03);
1047
- }
1048
-
1049
- .ua-grid {
1050
- display: grid;
1051
- grid-template-columns: repeat(4, minmax(0, 1fr));
1052
- gap: 10px;
1053
- }
1054
-
1055
- .ua-card {
1056
- display: flex;
1057
- flex-direction: column;
1058
- gap: 8px;
1059
- }
1060
-
1061
- .ua-card-wide {
1062
- grid-column: span 4;
1063
- }
1064
-
1065
- .ua-list {
1066
- display: flex;
1067
- flex-direction: column;
1068
- gap: 6px;
1069
- flex: 1;
1070
- }
1071
-
1072
- .ua-list-item {
1073
- display: flex;
1074
- align-items: center;
1075
- justify-content: space-between;
1076
- gap: 8px;
1077
- padding: 7px 9px;
1078
- border-radius: 6px;
1079
- background: rgba(var(--primary-color-rgb), 0.04);
1080
- }
1081
-
1082
- .ua-list-name {
1083
- min-width: 0;
1084
- overflow: hidden;
1085
- text-overflow: ellipsis;
1086
- white-space: nowrap;
1087
- }
1088
-
1089
- .ua-list-count {
1090
- font-size: 14px;
1091
- font-weight: 700;
1092
- line-height: 1.1;
1093
- font-variant-numeric: tabular-nums;
1094
- color: var(--primary-color);
1095
- }
1096
-
1097
- .ua-meta-group {
1098
- display: grid;
1099
- grid-template-columns: repeat(3, minmax(0, 1fr));
1100
- gap: 10px;
1101
- flex: 1;
1102
- }
1103
-
1104
- .ua-meta-block {
1105
- border-radius: 8px;
1106
- background: rgba(var(--primary-color-rgb), 0.03);
1107
- padding: 10px;
1108
- }
1109
-
1110
- .ua-chip-list {
1111
- display: flex;
1112
- flex-wrap: wrap;
1113
- gap: 6px;
1114
- }
1115
-
1116
- .ua-chip {
1117
- display: inline-flex;
1118
- align-items: center;
1119
- padding: 4px 8px;
1120
- border-radius: 999px;
1121
- background: rgba(var(--primary-color-rgb), 0.08);
1122
- color: var(--primary-color);
1123
- font-weight: 600;
1124
- }
1125
-
1126
- .ua-empty-inline {
1127
- min-height: auto;
1128
- justify-content: flex-start;
1129
- background: transparent;
1130
- padding: 0;
1131
- }
1132
-
1133
- @media (max-width: 1280px) {
1134
- .summary-grid {
1135
- grid-template-columns: repeat(3, minmax(0, 1fr));
1136
- }
1137
-
1138
- .product-metrics-grid {
1139
- grid-template-columns: repeat(3, minmax(0, 1fr));
1140
- }
1141
-
1142
- .trend-briefs-wide {
1143
- grid-template-columns: repeat(6, minmax(0, 1fr));
1144
- }
1145
-
1146
- .trend-bars {
1147
- grid-template-columns: repeat(15, minmax(0, 1fr));
1148
- row-gap: 6px;
1149
- height: auto;
1150
- }
1151
-
1152
- .trend-bar-group:nth-child(n + 16) {
1153
- display: none;
1154
- }
1155
-
1156
- .ua-grid {
1157
- grid-template-columns: repeat(2, minmax(0, 1fr));
1158
- }
1159
-
1160
- .ua-card-wide {
1161
- grid-column: span 2;
1162
- }
1163
- }
1164
-
1165
- @media (max-width: 960px) {
1166
- .summary-grid,
1167
- .ua-grid,
1168
- .ua-meta-group {
1169
- grid-template-columns: 1fr;
1170
- }
1171
-
1172
- .ua-card-wide {
1173
- grid-column: span 1;
1174
- }
1175
-
1176
- .card-head,
1177
- .card-head-compact {
1178
- flex-direction: column;
1179
- align-items: flex-start;
1180
- }
1181
-
1182
- .product-select-wrap {
1183
- width: 100%;
1184
- justify-content: flex-start;
1185
- }
1186
-
1187
- .trend-briefs-wide {
1188
- grid-template-columns: repeat(4, minmax(0, 1fr));
1189
- }
1190
-
1191
- .trend-bars {
1192
- grid-template-columns: repeat(10, minmax(0, 1fr));
1193
- }
1194
-
1195
- .trend-bar-group:nth-child(n + 11) {
1196
- display: none;
1197
- }
1198
- }
1199
-
1200
- @media (max-width: 640px) {
1201
- .summary-grid,
1202
- .product-metrics-grid,
1203
- .trend-briefs-wide {
1204
- grid-template-columns: 1fr;
1205
- }
1206
-
1207
- .overview-card-large {
1208
- min-height: 0;
1209
- }
1210
-
1211
- .product-select {
1212
- width: 100%;
1213
- }
1214
-
1215
- .trend-bars {
1216
- grid-template-columns: repeat(7, minmax(0, 1fr));
1217
- height: 126px;
1218
- }
1219
-
1220
- .trend-bar-group:nth-child(n + 8) {
1221
- display: none;
1222
- }
105
+ .dashboard-card-row {
106
+ @include dashboardCard.dashboardCardStandard();
1223
107
  }
1224
108
  </style>