@liujitcn/kratos-admin-system 0.0.53 → 0.0.54

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,28 +8,42 @@
8
8
  :key="metric.key"
9
9
  :class="['metric-card', `metric-card--${metric.tone}`, 'admin-page-card']"
10
10
  >
11
- <div class="metric-content">
12
- <span class="metric-label">{{ metric.label }}</span>
13
- <strong class="metric-value">{{ metric.value }}</strong>
14
- </div>
15
- <div class="metric-icon" aria-hidden="true">
16
- <el-icon><component :is="metric.icon" /></el-icon>
11
+ <div class="metric-top">
12
+ <span class="metric-label">{{ metric.label }}</span
13
+ ><span class="metric-icon"
14
+ ><el-icon><component :is="metric.icon" /></el-icon
15
+ ></span>
17
16
  </div>
17
+ <strong class="metric-value">{{ metric.value }}</strong>
18
18
  </el-card>
19
19
  </section>
20
-
21
20
  <section class="chart-grid">
22
21
  <el-card class="chart-card chart-card-wide admin-page-card">
23
- <template #header>{{ t("system.dashboard.login_trend") }}</template>
24
- <div class="chart-wrap"><ECharts :option="loginTrendOption" height="320" /></div>
22
+ <template #header
23
+ ><div class="chart-heading">
24
+ <span>{{ t("system.dashboard.login_trend") }}</span
25
+ ><small>近 7 天</small>
26
+ </div></template
27
+ >
28
+ <div class="chart-wrap"><ECharts :option="loginTrendOption" height="285" /></div>
25
29
  </el-card>
26
30
  <el-card class="chart-card admin-page-card">
27
- <template #header>{{ t("system.dashboard.login_distribution") }}</template>
28
- <div class="chart-wrap"><ECharts :option="loginDistributionOption" height="320" /></div>
31
+ <template #header
32
+ ><div class="chart-heading">
33
+ <span>{{ t("system.dashboard.login_distribution") }}</span
34
+ ><small>总计 {{ loginDistributionTotal }}</small>
35
+ </div></template
36
+ >
37
+ <div class="chart-wrap"><ECharts :option="loginDistributionOption" height="265" /></div>
29
38
  </el-card>
30
39
  <el-card class="chart-card admin-page-card">
31
- <template #header>{{ t("system.dashboard.operation_distribution") }}</template>
32
- <div class="chart-wrap"><ECharts :option="operationDistributionOption" height="320" /></div>
40
+ <template #header
41
+ ><div class="chart-heading">
42
+ <span>{{ t("system.dashboard.operation_distribution") }}</span
43
+ ><small>总计 {{ operationDistributionTotal }}</small>
44
+ </div></template
45
+ >
46
+ <div class="chart-wrap"><ECharts :option="operationDistributionOption" height="265" /></div>
33
47
  </el-card>
34
48
  </section>
35
49
  </template>
@@ -37,7 +51,7 @@
37
51
  </template>
38
52
 
39
53
  <script setup lang="ts">
40
- import { computed, onMounted, ref } from "vue";
54
+ import { computed, onBeforeUnmount, onMounted, ref } from "vue";
41
55
  import ECharts from "@liujitcn/kratos-admin-core/components/ECharts/index.vue";
42
56
  import { defBaseDashboardService } from "@liujitcn/kratos-admin-system/api/system/admin/v1/base_dashboard";
43
57
  import type {
@@ -52,25 +66,84 @@ const overview = ref<BaseDashboardOverview>();
52
66
  const trend = ref<BaseDashboardTrendPoint[]>([]);
53
67
  const loginDistribution = ref<BaseDashboardDistributionItem[]>([]);
54
68
  const operationDistribution = ref<BaseDashboardDistributionItem[]>([]);
55
-
69
+ const chartTheme = ref({
70
+ primary: "#409eff",
71
+ success: "#67c23a",
72
+ warning: "#e6a23c",
73
+ info: "#909399",
74
+ danger: "#f56c6c",
75
+ text: "#606266",
76
+ divider: "#e4e7ed",
77
+ background: "#ffffff"
78
+ });
56
79
  const metrics = computed(() => [
57
80
  { key: "users", label: t("system.dashboard.users"), value: overview.value?.user_count ?? 0, icon: User, tone: "primary" },
58
81
  { key: "roles", label: t("system.dashboard.roles"), value: overview.value?.role_count ?? 0, icon: UserFilled, tone: "success" },
59
- { key: "logins", label: t("system.dashboard.today_logins"), value: overview.value?.today_login_count ?? 0, icon: Timer, tone: "warning" },
60
- { key: "operations", label: t("system.dashboard.today_operations"), value: overview.value?.today_operation_count ?? 0, icon: Operation, tone: "danger" }
82
+ {
83
+ key: "logins",
84
+ label: t("system.dashboard.today_logins"),
85
+ value: overview.value?.today_login_count ?? 0,
86
+ icon: Timer,
87
+ tone: "warning"
88
+ },
89
+ {
90
+ key: "operations",
91
+ label: t("system.dashboard.today_operations"),
92
+ value: overview.value?.today_operation_count ?? 0,
93
+ icon: Operation,
94
+ tone: "danger"
95
+ }
61
96
  ]);
97
+ const loginDistributionTotal = computed(() => loginDistribution.value.reduce((total, item) => total + item.count, 0));
98
+ const operationDistributionTotal = computed(() => operationDistribution.value.reduce((total, item) => total + item.count, 0));
62
99
 
100
+ function readThemeColor(name: string, fallback: string) {
101
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
102
+ }
103
+ function refreshChartTheme() {
104
+ chartTheme.value = {
105
+ primary: readThemeColor("--el-color-primary", "#409eff"),
106
+ success: readThemeColor("--el-color-success", "#67c23a"),
107
+ warning: readThemeColor("--el-color-warning", "#e6a23c"),
108
+ info: readThemeColor("--el-color-info", "#909399"),
109
+ danger: readThemeColor("--el-color-danger", "#f56c6c"),
110
+ text: readThemeColor("--el-text-color-secondary", "#606266"),
111
+ divider: readThemeColor("--el-border-color-lighter", "#e4e7ed"),
112
+ background: readThemeColor("--el-bg-color", "#ffffff")
113
+ };
114
+ }
63
115
  const loginTrendOption = computed(() => ({
116
+ color: [chartTheme.value.primary],
64
117
  tooltip: { trigger: "axis" as const },
65
- grid: { left: 40, right: 20, top: 24, bottom: 32 },
66
- xAxis: { type: "category" as const, boundaryGap: false, data: trend.value.map((item) => item.date) },
67
- yAxis: { type: "value" as const, minInterval: 1 },
68
- series: [{ type: "line" as const, smooth: true, data: trend.value.map((item) => item.count), areaStyle: {} }]
118
+ grid: { left: 42, right: 20, top: 20, bottom: 28 },
119
+ xAxis: {
120
+ type: "category" as const,
121
+ boundaryGap: false,
122
+ data: trend.value.map(item => item.date),
123
+ axisLine: { lineStyle: { color: chartTheme.value.divider } },
124
+ axisLabel: { color: chartTheme.value.text }
125
+ },
126
+ yAxis: {
127
+ type: "value" as const,
128
+ minInterval: 1,
129
+ splitLine: { lineStyle: { color: chartTheme.value.divider } },
130
+ axisLabel: { color: chartTheme.value.text }
131
+ },
132
+ series: [
133
+ {
134
+ type: "line" as const,
135
+ smooth: true,
136
+ data: trend.value.map(item => item.count),
137
+ symbol: "circle",
138
+ symbolSize: 7,
139
+ lineStyle: { width: 3, color: chartTheme.value.primary },
140
+ itemStyle: { color: chartTheme.value.primary, borderWidth: 2, borderColor: chartTheme.value.background },
141
+ areaStyle: { color: chartTheme.value.primary, opacity: 0.12 }
142
+ }
143
+ ]
69
144
  }));
70
-
71
145
  const loginDistributionOption = computed(() => distributionOption(loginDistribution.value));
72
146
  const operationDistributionOption = computed(() => distributionOption(operationDistribution.value));
73
-
74
147
  async function loadDashboard() {
75
148
  loading.value = true;
76
149
  try {
@@ -85,12 +158,11 @@ async function loadDashboard() {
85
158
  loginDistribution.value = loginResponse.items ?? [];
86
159
  operationDistribution.value = operationResponse.items ?? [];
87
160
  } catch {
88
- // 请求层已展示具体错误(包括强制改密跳转),页面不再重复弹出笼统提示。
161
+ /* 请求层已展示具体错误。 */
89
162
  } finally {
90
163
  loading.value = false;
91
164
  }
92
165
  }
93
-
94
166
  /** 将审计技术枚举名转换为当前语言的展示文案。 */
95
167
  function distributionItemLabel(label: string) {
96
168
  const mappings = [
@@ -102,67 +174,109 @@ function distributionItemLabel(label: string) {
102
174
  const value = label.slice(mapping[0].length).toLowerCase();
103
175
  return value === "unspecified" ? t("common.message.unknown") : t(`${mapping[1]}${value}`);
104
176
  }
105
-
106
177
  /** 创建使用本地化图例的分布图配置。 */
107
178
  function distributionOption(items: BaseDashboardDistributionItem[]) {
108
179
  return {
180
+ color: [
181
+ chartTheme.value.primary,
182
+ chartTheme.value.success,
183
+ chartTheme.value.info,
184
+ chartTheme.value.warning,
185
+ chartTheme.value.danger
186
+ ],
109
187
  tooltip: { trigger: "item" as const },
110
- legend: { bottom: 0, type: "scroll" as const },
111
- series: [{ type: "pie" as const, radius: ["38%", "68%"] as [string, string], center: ["50%", "45%"] as [string, string], data: items.map((item) => ({ name: distributionItemLabel(item.label), value: item.count })) }]
188
+ legend: { bottom: 0, type: "scroll" as const, textStyle: { color: chartTheme.value.text } },
189
+ series: [
190
+ {
191
+ type: "pie" as const,
192
+ radius: ["55%", "76%"] as [string, string],
193
+ center: ["50%", "43%"] as [string, string],
194
+ label: { color: chartTheme.value.text },
195
+ itemStyle: { borderColor: chartTheme.value.background, borderWidth: 3 },
196
+ data: items.map(item => ({ name: distributionItemLabel(item.label), value: item.count }))
197
+ }
198
+ ]
112
199
  };
113
200
  }
114
-
115
- onMounted(loadDashboard);
201
+ onMounted(() => {
202
+ refreshChartTheme();
203
+ loadDashboard();
204
+ window.addEventListener("theme-change", refreshChartTheme);
205
+ });
206
+ onBeforeUnmount(() => window.removeEventListener("theme-change", refreshChartTheme));
116
207
  </script>
117
208
 
118
209
  <style scoped>
119
210
  .dashboard-page {
120
211
  min-width: 0;
121
212
  }
213
+ .dashboard-heading {
214
+ display: flex;
215
+ align-items: flex-end;
216
+ justify-content: space-between;
217
+ margin-bottom: 22px;
218
+ }
219
+ .dashboard-eyebrow {
220
+ display: block;
221
+ color: var(--admin-page-text-placeholder);
222
+ font-size: 11px;
223
+ letter-spacing: 0.12em;
224
+ }
225
+ .dashboard-heading h1 {
226
+ margin: 6px 0 0;
227
+ color: var(--admin-page-text-primary);
228
+ font-size: 26px;
229
+ line-height: 1.2;
230
+ }
231
+ .dashboard-updated {
232
+ color: var(--admin-page-text-placeholder);
233
+ font-size: 12px;
234
+ }
122
235
  .metric-grid,
123
236
  .chart-grid {
124
237
  display: grid;
125
- gap: 10px;
238
+ gap: 16px;
126
239
  }
127
240
  .metric-grid {
128
241
  grid-template-columns: repeat(4, minmax(0, 1fr));
129
- margin-bottom: 10px;
242
+ margin-bottom: 16px;
243
+ }
244
+ .metric-card {
245
+ position: relative;
246
+ overflow: hidden;
130
247
  }
131
248
  .metric-card :deep(.el-card__body) {
249
+ min-height: 124px;
250
+ padding: 18px 20px 16px;
251
+ }
252
+ .metric-top {
132
253
  display: flex;
133
- gap: 16px;
134
254
  align-items: center;
135
255
  justify-content: space-between;
136
- min-height: 92px;
137
- }
138
- .metric-content {
139
- display: flex;
140
- flex-direction: column;
141
- gap: 10px;
142
- min-width: 0;
143
256
  }
144
257
  .metric-label {
258
+ color: var(--admin-page-text-secondary);
145
259
  font-size: 13px;
146
- color: var(--el-text-color-secondary);
147
260
  }
148
261
  .metric-value {
149
- font-size: 28px;
262
+ display: block;
263
+ margin-top: 18px;
264
+ color: var(--admin-page-text-primary);
265
+ font-size: 32px;
150
266
  line-height: 1;
151
- color: var(--el-text-color-primary);
267
+ font-variant-numeric: tabular-nums;
152
268
  }
153
269
  .metric-icon {
154
- display: flex;
155
- flex: 0 0 48px;
156
- align-items: center;
157
- justify-content: center;
158
- width: 48px;
159
- height: 48px;
270
+ display: grid;
271
+ width: 34px;
272
+ height: 34px;
273
+ place-items: center;
274
+ border-radius: 10px;
160
275
  color: var(--metric-color);
161
276
  background: var(--metric-bg);
162
- border-radius: var(--admin-page-radius);
163
277
  }
164
278
  .metric-icon .el-icon {
165
- font-size: 24px;
279
+ font-size: 17px;
166
280
  }
167
281
  .metric-card--primary {
168
282
  --metric-color: var(--el-color-primary);
@@ -186,11 +300,45 @@ onMounted(loadDashboard);
186
300
  .chart-card-wide {
187
301
  grid-column: 1 / -1;
188
302
  }
303
+ .chart-card :deep(.el-card__header) {
304
+ padding: 17px 22px 14px;
305
+ border-bottom-color: var(--admin-page-divider);
306
+ }
307
+ .chart-card :deep(.el-card__body) {
308
+ padding: 0 20px 16px;
309
+ }
310
+ .chart-heading {
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: space-between;
314
+ }
315
+ .chart-heading span {
316
+ color: var(--admin-page-text-primary);
317
+ font-size: 16px;
318
+ font-weight: 600;
319
+ }
320
+ .chart-heading small {
321
+ color: var(--admin-page-text-placeholder);
322
+ font-size: 12px;
323
+ font-weight: 400;
324
+ }
189
325
  .chart-wrap {
190
- height: 320px;
326
+ height: 285px;
327
+ }
328
+ .chart-card:not(.chart-card-wide) .chart-wrap {
329
+ height: 265px;
191
330
  }
192
-
193
331
  @media (width <= 960px) {
332
+ .metric-grid {
333
+ grid-template-columns: repeat(2, minmax(0, 1fr));
334
+ }
335
+ }
336
+ @media (width <= 640px) {
337
+ .dashboard-heading {
338
+ align-items: flex-start;
339
+ flex-direction: column;
340
+ gap: 8px;
341
+ }
194
342
  .metric-grid,
195
343
  .chart-grid {
196
344
  grid-template-columns: 1fr;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liujitcn/kratos-admin-system",
3
3
  "private": false,
4
- "version": "0.0.53",
4
+ "version": "0.0.54",
5
5
  "type": "module",
6
6
  "description": "kratos-admin system management module",
7
7
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "sortablejs": "^1.15.2",
17
17
  "swagger-ui-dist": "^5.32.15",
18
18
  "vue-element-plus-x": "^2.0.3",
19
- "@liujitcn/kratos-admin-core": "0.0.53"
19
+ "@liujitcn/kratos-admin-core": "0.0.54"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@element-plus/icons-vue": "^2.3.1",