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.
@@ -8,30 +8,24 @@
8
8
  <div class="resource-compact-list">
9
9
  <div class="resource-compact-item">
10
10
  <div class="resource-compact-header">
11
- <CpuIcon />
12
11
  <span class="resource-label">CPU</span>
13
12
  <span class="resource-value">{{ systemResources.cpu.usage }}%</span>
14
13
  <span class="resource-desc">{{ systemResources.cpu.cores }}核心</span>
15
14
  </div>
16
- <TProgress :percentage="systemResources.cpu.usage" :status="getProgressColor(systemResources.cpu.usage)" />
17
15
  </div>
18
16
  <div class="resource-compact-item">
19
17
  <div class="resource-compact-header">
20
- <SystemStorageIcon />
21
18
  <span class="resource-label">内存</span>
22
19
  <span class="resource-value">{{ systemResources.memory.percentage }}%</span>
23
20
  <span class="resource-desc">{{ systemResources.memory.used }}GB / {{ systemResources.memory.total }}GB</span>
24
21
  </div>
25
- <TProgress :percentage="systemResources.memory.percentage" :status="getProgressColor(systemResources.memory.percentage)" />
26
22
  </div>
27
23
  <div class="resource-compact-item">
28
24
  <div class="resource-compact-header">
29
- <HardDiskStorageIcon />
30
25
  <span class="resource-label">磁盘</span>
31
26
  <span class="resource-value">{{ systemResources.disk.percentage }}%</span>
32
27
  <span class="resource-desc">{{ systemResources.disk.used }}GB / {{ systemResources.disk.total }}GB</span>
33
28
  </div>
34
- <TProgress :percentage="systemResources.disk.percentage" :status="getProgressColor(systemResources.disk.percentage)" />
35
29
  </div>
36
30
  </div>
37
31
  </div>
@@ -40,8 +34,7 @@
40
34
 
41
35
  <script setup>
42
36
  import { reactive } from "vue";
43
- import { Progress as TProgress } from "tdesign-vue-next";
44
- import { ChartIcon, CpuIcon, HardDiskStorageIcon, SystemStorageIcon } from "tdesign-icons-vue-next";
37
+ import { ChartIcon } from "tdesign-icons-vue-next";
45
38
  import { $Http } from "@/plugins/http";
46
39
 
47
40
  // 组件内部数据
@@ -62,48 +55,82 @@ const fetchData = async () => {
62
55
  };
63
56
 
64
57
  fetchData();
65
-
66
- // 工具函数
67
- const getProgressColor = (percentage) => {
68
- if (percentage < 50) return "success";
69
- if (percentage < 80) return "warning";
70
- return "error";
71
- };
72
58
  </script>
73
59
 
74
60
  <style scoped lang="scss">
75
61
  .resource-compact-list {
76
62
  display: grid;
77
- grid-template-columns: repeat(3, 1fr);
78
- gap: var(--spacing-md);
63
+ grid-template-columns: repeat(3, minmax(0, 1fr));
64
+ gap: 10px;
79
65
 
80
66
  .resource-compact-item {
67
+ display: flex;
68
+ flex-direction: column;
69
+ justify-content: center;
70
+ min-width: 0;
71
+ min-height: 92px;
72
+ padding: 14px;
73
+ border-radius: 8px;
74
+ border: 1px solid rgba(0, 0, 0, 0.06);
75
+ background: linear-gradient(180deg, rgba(var(--primary-color-rgb), 0.018), rgba(255, 255, 255, 0.96));
76
+ transition: all 0.2s ease;
77
+
78
+ &:hover {
79
+ background: rgba(var(--primary-color-rgb), 0.05);
80
+ border-color: var(--primary-color);
81
+ box-shadow: 0 8px 18px rgba(31, 35, 41, 0.08);
82
+ }
83
+
81
84
  .resource-compact-header {
82
85
  display: flex;
83
- align-items: center;
86
+ align-items: baseline;
87
+ flex-wrap: wrap;
84
88
  gap: 10px;
85
- margin-bottom: 8px;
89
+ row-gap: 4px;
86
90
 
87
91
  .resource-label {
88
92
  font-size: 14px;
89
93
  font-weight: 600;
90
94
  color: var(--text-secondary);
91
- min-width: 50px;
95
+ min-width: 36px;
96
+ line-height: 1.2;
92
97
  }
93
98
 
94
99
  .resource-value {
95
- font-size: 16px;
100
+ font-size: 22px;
96
101
  font-weight: 700;
102
+ line-height: 1.1;
103
+ letter-spacing: -0.3px;
104
+ font-variant-numeric: tabular-nums;
97
105
  color: var(--primary-color);
98
- min-width: 60px;
106
+ min-width: 48px;
99
107
  }
100
108
 
101
109
  .resource-desc {
102
- font-size: 14px;
110
+ font-size: 12px;
103
111
  color: var(--text-placeholder);
104
- flex: 1;
112
+ flex: 0 0 100%;
113
+ width: 100%;
114
+ margin-top: 2px;
115
+ padding-left: 0;
116
+ line-height: 1.3;
117
+ min-width: 0;
118
+ word-break: break-word;
105
119
  }
106
120
  }
107
121
  }
108
122
  }
123
+
124
+ @media (max-width: 1200px) {
125
+ .resource-compact-list {
126
+ grid-template-columns: 1fr;
127
+ }
128
+ }
129
+
130
+ @media (max-width: 640px) {
131
+ .resource-compact-list {
132
+ grid-template-columns: 1fr;
133
+ gap: var(--spacing-sm);
134
+ }
135
+ }
109
136
  </style>
@@ -31,24 +31,30 @@ import EnvironmentInfo from "./components/environmentInfo.vue";
31
31
  .dashboard-container {
32
32
  display: flex;
33
33
  flex-direction: column;
34
- gap: var(--layout-gap);
34
+ gap: 12px;
35
35
  overflow-y: auto;
36
+ overflow-x: hidden;
36
37
  height: 100%;
37
- padding: 0;
38
+ padding: 4px 0 12px;
39
+ min-width: 0;
38
40
 
39
41
  .dashboard-row {
40
- display: flex;
42
+ display: grid;
41
43
  gap: var(--layout-gap);
44
+ min-width: 0;
42
45
 
43
46
  &.full-width {
47
+ grid-template-columns: minmax(0, 1fr);
48
+
44
49
  > * {
45
- flex: 1;
50
+ min-width: 0;
46
51
  }
47
52
  }
48
53
 
49
54
  &.two-columns {
55
+ grid-template-columns: repeat(2, minmax(0, 1fr));
56
+
50
57
  > * {
51
- flex: 1;
52
58
  min-width: 0;
53
59
  }
54
60
  }
@@ -57,11 +63,49 @@ import EnvironmentInfo from "./components/environmentInfo.vue";
57
63
  // 每个组件都是独立的卡片
58
64
  :deep(.section-block) {
59
65
  background: var(--bg-color-container);
60
- border-radius: var(--card-radius);
61
- box-shadow: var(--shadow-1);
62
- padding: var(--spacing-md);
63
- border: none;
66
+ border-radius: 12px;
67
+ box-shadow: 0 8px 24px rgba(31, 35, 41, 0.05);
68
+ padding: 14px;
69
+ border: 1px solid rgba(0, 0, 0, 0.05);
64
70
  height: 100%;
71
+ min-width: 0;
72
+ overflow: hidden;
73
+ }
74
+
75
+ :deep(.section-header) {
76
+ margin-bottom: 12px;
77
+ padding-bottom: 10px;
78
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
79
+
80
+ h2 {
81
+ font-size: 14px;
82
+ font-weight: 700;
83
+ line-height: 1.2;
84
+ }
85
+ }
86
+
87
+ :deep(.section-content) {
88
+ min-width: 0;
89
+ }
90
+ }
91
+
92
+ @media (max-width: 1280px) {
93
+ .dashboard-container {
94
+ .dashboard-row {
95
+ &.two-columns {
96
+ grid-template-columns: minmax(0, 1fr);
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ @media (max-width: 768px) {
103
+ .dashboard-container {
104
+ gap: 10px;
105
+
106
+ :deep(.section-block) {
107
+ padding: 12px;
108
+ }
65
109
  }
66
110
  }
67
111
  </style>
@@ -0,0 +1,319 @@
1
+ <template>
2
+ <PageTableDetail class="page-error-log page-table" :columns="$Data.columns" :endpoints="$Data.endpoints" :table-slot-names="['errorType', 'reportTime', 'firstReportTime', 'source', 'message', 'hitCount', 'deviceType']">
3
+ <template #toolLeft="scope">
4
+ <TInput v-model="$Data.filter.keyword" clearable placeholder="搜索页面/产品/消息/类型" style="width: 220px" @enter="handleFilter(scope.reload)" @clear="handleFilter(scope.reload)">
5
+ <template #suffix-icon>
6
+ <SearchIcon />
7
+ </template>
8
+ </TInput>
9
+ <TSelect v-model="$Data.filter.errorType" clearable placeholder="错误类型" style="width: 150px" @change="handleFilter(scope.reload)">
10
+ <TOption label="Vue" value="vue" />
11
+ <TOption label="Window" value="window" />
12
+ <TOption label="Promise" value="promise" />
13
+ <TOption label="Unknown" value="unknown" />
14
+ </TSelect>
15
+ <TSelect v-model="$Data.filter.source" clearable placeholder="来源" style="width: 140px" @change="handleFilter(scope.reload)">
16
+ <TOption label="Admin" value="admin" />
17
+ </TSelect>
18
+ <TInput v-model="$Data.filter.productName" clearable placeholder="产品名称" style="width: 160px" @enter="handleFilter(scope.reload)" @clear="handleFilter(scope.reload)" />
19
+ <TInput v-model="$Data.filter.productCode" clearable placeholder="产品代号" style="width: 160px" @enter="handleFilter(scope.reload)" @clear="handleFilter(scope.reload)" />
20
+ <TInput v-model="$Data.filter.productVersion" clearable placeholder="产品版本" style="width: 160px" @enter="handleFilter(scope.reload)" @clear="handleFilter(scope.reload)" />
21
+ <TSelect v-model="$Data.filter.deviceType" clearable placeholder="设备类型" style="width: 140px" @change="handleFilter(scope.reload)">
22
+ <TOption label="桌面端" value="desktop" />
23
+ <TOption label="移动端" value="mobile" />
24
+ <TOption label="平板" value="tablet" />
25
+ <TOption label="电视" value="smarttv" />
26
+ </TSelect>
27
+ <TInput v-model="$Data.filter.browserName" clearable placeholder="浏览器" style="width: 160px" @enter="handleFilter(scope.reload)" @clear="handleFilter(scope.reload)" />
28
+ <TInput v-model="$Data.filter.osName" clearable placeholder="操作系统" style="width: 160px" @enter="handleFilter(scope.reload)" @clear="handleFilter(scope.reload)" />
29
+ </template>
30
+
31
+ <template #toolRight="scope">
32
+ <TButton shape="circle" @click="onReload(scope.reload)">
33
+ <template #icon>
34
+ <RefreshIcon />
35
+ </template>
36
+ </TButton>
37
+ </template>
38
+
39
+ <template #errorType="{ row }">
40
+ <TTag shape="round" :theme="getErrorTheme(row.errorType)" variant="light-outline">{{ formatErrorType(row.errorType) }}</TTag>
41
+ </template>
42
+
43
+ <template #reportTime="{ row }">
44
+ {{ formatTime(row.reportTime) }}
45
+ </template>
46
+
47
+ <template #firstReportTime="{ row }">
48
+ {{ formatTime(row.firstReportTime) }}
49
+ </template>
50
+
51
+ <template #source="{ row }">
52
+ <TTag shape="round" variant="light-outline">{{ row.source || "admin" }}</TTag>
53
+ </template>
54
+
55
+ <template #hitCount="{ row }">
56
+ <TTag shape="round" theme="warning" variant="light-outline">{{ row.hitCount || 1 }} 次</TTag>
57
+ </template>
58
+
59
+ <template #deviceType="{ row }">
60
+ <TTag shape="round" variant="light-outline">{{ formatDeviceType(row.deviceType) }}</TTag>
61
+ </template>
62
+
63
+ <template #message="{ row }">
64
+ <div class="message-cell">{{ row.message || "-" }}</div>
65
+ </template>
66
+
67
+ <template #detail="scope">
68
+ <DetailPanel :data="scope.row" :fields="$Data.columns">
69
+ <template #errorType="slotScope">
70
+ <TTag shape="round" :theme="getErrorTheme(slotScope.value)" variant="light-outline">{{ formatErrorType(slotScope.value) }}</TTag>
71
+ </template>
72
+ <template #reportTime="slotScope">
73
+ {{ formatTime(slotScope.value) }}
74
+ </template>
75
+ <template #firstReportTime="slotScope">
76
+ {{ formatTime(slotScope.value) }}
77
+ </template>
78
+ <template #source="slotScope">
79
+ <TTag shape="round" variant="light-outline">{{ slotScope.value || "admin" }}</TTag>
80
+ </template>
81
+ <template #hitCount="slotScope">
82
+ <TTag shape="round" theme="warning" variant="light-outline">{{ slotScope.value || 1 }} 次</TTag>
83
+ </template>
84
+ <template #deviceType="slotScope">
85
+ <TTag shape="round" variant="light-outline">{{ formatDeviceType(slotScope.value) }}</TTag>
86
+ </template>
87
+ <template #detail="slotScope">
88
+ <div class="detail-actions">
89
+ <TButton theme="primary" variant="outline" size="small" @click="handleCopyDetail(scope.row.detail)">复制详情</TButton>
90
+ <TButton theme="default" variant="outline" size="small" @click="toggleDetailExpand(scope.row.id)">
91
+ {{ isDetailExpanded(scope.row.id) ? "收起详情" : "展开详情" }}
92
+ </TButton>
93
+ </div>
94
+ <pre class="detail-content" :class="{ expanded: isDetailExpanded(scope.row.id) }">{{ formatDetail(slotScope.value) }}</pre>
95
+ </template>
96
+ </DetailPanel>
97
+ </template>
98
+ </PageTableDetail>
99
+ </template>
100
+
101
+ <script setup>
102
+ import { reactive } from "vue";
103
+ import { Button as TButton, Input as TInput, MessagePlugin, Option as TOption, Select as TSelect, Tag as TTag } from "tdesign-vue-next";
104
+ import { RefreshIcon, SearchIcon } from "tdesign-icons-vue-next";
105
+ import DetailPanel from "befly-admin-ui/components/detailPanel.vue";
106
+ import PageTableDetail from "befly-admin-ui/components/pageTableDetail.vue";
107
+ import { withDefaultColumns } from "befly-admin-ui/utils/withDefaultColumns";
108
+
109
+ const $Data = reactive({
110
+ columns: withDefaultColumns([
111
+ { colKey: "productName", title: "产品名称", fixed: "left", width: 160 },
112
+ { colKey: "productCode", title: "产品代号", width: 150 },
113
+ { colKey: "productVersion", title: "产品版本", width: 130 },
114
+ { colKey: "pageName", title: "页面名称", fixed: "left", width: 160 },
115
+ { colKey: "pagePath", title: "页面路径", width: 220, ellipsis: true },
116
+ { colKey: "errorType", title: "错误类型", width: 120 },
117
+ { colKey: "deviceType", title: "设备类型", width: 110 },
118
+ { colKey: "browserName", title: "浏览器", width: 140 },
119
+ { colKey: "osName", title: "操作系统", width: 140 },
120
+ { colKey: "hitCount", title: "次数", width: 100 },
121
+ { colKey: "message", title: "错误信息", ellipsis: true },
122
+ { colKey: "source", title: "来源", width: 100 },
123
+ { colKey: "reportTime", title: "上报时间", width: 170 },
124
+ { colKey: "firstReportTime", title: "首次上报", width: 170, detail: true },
125
+ { colKey: "bucketDate", title: "统计日期", detail: true },
126
+ { colKey: "bucketTime", title: "时间桶", detail: true },
127
+ { colKey: "browserVersion", title: "浏览器版本", detail: true },
128
+ { colKey: "osVersion", title: "系统版本", detail: true },
129
+ { colKey: "deviceVendor", title: "设备厂商", detail: true },
130
+ { colKey: "deviceModel", title: "设备型号", detail: true },
131
+ { colKey: "engineName", title: "渲染引擎", detail: true },
132
+ { colKey: "cpuArchitecture", title: "CPU架构", detail: true },
133
+ { colKey: "userAgent", title: "UA字符串", detail: true },
134
+ { colKey: "detail", title: "错误详情", detail: true }
135
+ ]),
136
+ endpoints: {
137
+ list: {
138
+ path: "/core/tongJi/errorList",
139
+ dropValues: [""],
140
+ dropKeyValue: {
141
+ keyword: [""],
142
+ errorType: [""],
143
+ source: [""],
144
+ productName: [""],
145
+ productCode: [""],
146
+ productVersion: [""],
147
+ deviceType: [""],
148
+ browserName: [""],
149
+ osName: [""]
150
+ },
151
+ buildData: () => {
152
+ return {
153
+ keyword: $Data.filter.keyword,
154
+ errorType: $Data.filter.errorType,
155
+ source: $Data.filter.source,
156
+ productName: $Data.filter.productName,
157
+ productCode: $Data.filter.productCode,
158
+ productVersion: $Data.filter.productVersion,
159
+ deviceType: $Data.filter.deviceType,
160
+ browserName: $Data.filter.browserName,
161
+ osName: $Data.filter.osName
162
+ };
163
+ }
164
+ }
165
+ },
166
+ filter: {
167
+ keyword: "",
168
+ errorType: "",
169
+ source: "",
170
+ productName: "",
171
+ productCode: "",
172
+ productVersion: "",
173
+ deviceType: "",
174
+ browserName: "",
175
+ osName: ""
176
+ },
177
+ detailExpandedId: 0
178
+ });
179
+
180
+ function handleFilter(reload) {
181
+ reload({ keepSelection: false, resetPage: true });
182
+ }
183
+
184
+ function onReload(reload) {
185
+ reload({ keepSelection: true });
186
+ }
187
+
188
+ function isDetailExpanded(id) {
189
+ return Number($Data.detailExpandedId) === Number(id || 0);
190
+ }
191
+
192
+ function toggleDetailExpand(id) {
193
+ const nextId = Number(id || 0);
194
+
195
+ if (Number($Data.detailExpandedId) === nextId) {
196
+ $Data.detailExpandedId = 0;
197
+ return;
198
+ }
199
+
200
+ $Data.detailExpandedId = nextId;
201
+ }
202
+
203
+ async function handleCopyDetail(value) {
204
+ const text = formatDetail(value);
205
+ if (text === "-") {
206
+ MessagePlugin.warning("暂无可复制的错误详情");
207
+ return;
208
+ }
209
+
210
+ try {
211
+ await navigator.clipboard.writeText(text);
212
+ MessagePlugin.success("错误详情已复制");
213
+ } catch (_error) {
214
+ MessagePlugin.error("复制失败,请检查浏览器剪贴板权限");
215
+ }
216
+ }
217
+
218
+ function getErrorTheme(errorType) {
219
+ if (errorType === "vue") {
220
+ return "danger";
221
+ }
222
+
223
+ if (errorType === "promise") {
224
+ return "warning";
225
+ }
226
+
227
+ if (errorType === "window") {
228
+ return "primary";
229
+ }
230
+
231
+ return "default";
232
+ }
233
+
234
+ function formatErrorType(errorType) {
235
+ if (errorType === "vue") {
236
+ return "Vue";
237
+ }
238
+
239
+ if (errorType === "window") {
240
+ return "Window";
241
+ }
242
+
243
+ if (errorType === "promise") {
244
+ return "Promise";
245
+ }
246
+
247
+ return errorType || "Unknown";
248
+ }
249
+
250
+ function formatDeviceType(deviceType) {
251
+ if (deviceType === "desktop") {
252
+ return "桌面端";
253
+ }
254
+
255
+ if (deviceType === "mobile") {
256
+ return "移动端";
257
+ }
258
+
259
+ if (deviceType === "tablet") {
260
+ return "平板";
261
+ }
262
+
263
+ if (deviceType === "smarttv") {
264
+ return "电视";
265
+ }
266
+
267
+ return deviceType || "Unknown";
268
+ }
269
+
270
+ function formatTime(timestamp) {
271
+ if (!timestamp) return "-";
272
+ const date = new Date(timestamp);
273
+ const year = date.getFullYear();
274
+ const month = String(date.getMonth() + 1).padStart(2, "0");
275
+ const day = String(date.getDate()).padStart(2, "0");
276
+ const hours = String(date.getHours()).padStart(2, "0");
277
+ const minutes = String(date.getMinutes()).padStart(2, "0");
278
+ const seconds = String(date.getSeconds()).padStart(2, "0");
279
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
280
+ }
281
+
282
+ function formatDetail(value) {
283
+ if (!value) {
284
+ return "-";
285
+ }
286
+
287
+ return String(value);
288
+ }
289
+ </script>
290
+
291
+ <style scoped lang="scss">
292
+ .message-cell {
293
+ overflow: hidden;
294
+ text-overflow: ellipsis;
295
+ white-space: nowrap;
296
+ }
297
+
298
+ .detail-content {
299
+ margin: 0;
300
+ padding: 8px;
301
+ background: var(--td-bg-color-container);
302
+ border-radius: 4px;
303
+ font-size: 12px;
304
+ max-height: 240px;
305
+ overflow: auto;
306
+ white-space: pre-wrap;
307
+ word-break: break-all;
308
+
309
+ &.expanded {
310
+ max-height: none;
311
+ }
312
+ }
313
+
314
+ .detail-actions {
315
+ display: flex;
316
+ gap: 8px;
317
+ margin-bottom: 8px;
318
+ }
319
+ </style>