@liujitcn/kratos-admin-system 0.0.27 → 0.0.28

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 (24) hide show
  1. package/dist/package/declarations/src/api/system/runtime_log.d.ts +13 -0
  2. package/dist/package/declarations/src/api/system/runtime_log_sse.d.ts +8 -0
  3. package/dist/package/declarations/src/locales/generated.d.ts +116 -0
  4. package/dist/package/declarations/src/rpc/system/admin/v1/ops_monitoring.d.ts +4 -0
  5. package/dist/package/declarations/src/rpc/system/admin/v1/project_document.d.ts +4 -6
  6. package/dist/package/declarations/src/rpc/system/admin/v1/runtime_log.d.ts +96 -0
  7. package/dist/package/declarations/src/views/tool/runtime-log/index.vue.d.ts +3 -0
  8. package/dist/package/src/api/system/runtime_log.ts +80 -0
  9. package/dist/package/src/api/system/runtime_log_sse.ts +40 -0
  10. package/dist/package/src/locales/en-US.json +31 -2
  11. package/dist/package/src/locales/ja-JP.json +31 -2
  12. package/dist/package/src/locales/zh-CN.json +31 -2
  13. package/dist/package/src/locales/zh-TW.json +31 -2
  14. package/dist/package/src/rpc/system/admin/v1/ops_monitoring.ts +4 -0
  15. package/dist/package/src/rpc/system/admin/v1/project_document.ts +4 -7
  16. package/dist/package/src/rpc/system/admin/v1/runtime_log.ts +113 -0
  17. package/dist/package/src/views/tool/api-doc/index.vue +14 -6
  18. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +17 -0
  19. package/dist/package/src/views/tool/code-gen/components/CodeGenLocaleEditor.vue +96 -14
  20. package/dist/package/src/views/tool/code-gen/table/index.vue +32 -0
  21. package/dist/package/src/views/tool/ops-monitoring/index.vue +130 -24
  22. package/dist/package/src/views/tool/project-doc/index.vue +9 -4
  23. package/dist/package/src/views/tool/runtime-log/index.vue +883 -0
  24. package/package.json +2 -2
@@ -28,6 +28,8 @@ export interface ProjectDocumentListItem {
28
28
  id: string;
29
29
  /** 项目内相对路径 */
30
30
  path: string;
31
+ /** 当前语言下的文档显示文件名 */
32
+ name: string;
31
33
  /** Markdown文件更新时间,RFC3339格式 */
32
34
  updated_at: string;
33
35
  }
@@ -66,19 +68,14 @@ export interface ProjectDocument {
66
68
  project_name: string;
67
69
  /** 项目内相对路径 */
68
70
  path: string;
71
+ /** 当前语言下的文档显示文件名 */
72
+ name: string;
69
73
  /** Markdown文档内容 */
70
74
  content: string;
71
- /** 按语言标识索引的Markdown翻译内容 */
72
- locale: Map<string, string>;
73
75
  /** Markdown文件更新时间,RFC3339格式 */
74
76
  updated_at: string;
75
77
  }
76
78
 
77
- export interface ProjectDocument_LocaleEntry {
78
- key: string;
79
- value: string;
80
- }
81
-
82
79
  /** 项目文档服务 */
83
80
  export interface ProjectDocumentService {
84
81
  /** 查询项目文档树 */
@@ -0,0 +1,113 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.11.8
4
+ // protoc unknown
5
+ // source: system/admin/v1/runtime_log.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /** 历史日志文件列表查询条件。 */
10
+ export interface ListRuntimeLogFilesRequest {
11
+ }
12
+
13
+ /** 历史日志文件列表响应。 */
14
+ export interface ListRuntimeLogFilesResponse {
15
+ /** 历史日志文件列表 */
16
+ files: RuntimeLogFile[];
17
+ }
18
+
19
+ /** 历史日志文件信息。 */
20
+ export interface RuntimeLogFile {
21
+ /** 服务端签发的日志文件标识 */
22
+ file_id: string;
23
+ /** 日志文件名称 */
24
+ name: string;
25
+ /** 日志文件大小,单位字节 */
26
+ size_bytes: number;
27
+ /** 最后修改时间,RFC3339格式 */
28
+ modified_at: string;
29
+ /** 是否为Gzip压缩文件 */
30
+ is_compressed: boolean;
31
+ /** 是否为当前活动日志文件 */
32
+ is_active: boolean;
33
+ }
34
+
35
+ /** 历史日志文件读取条件。 */
36
+ export interface ReadRuntimeLogFileRequest {
37
+ /** 服务端签发的日志文件标识 */
38
+ file_id: string;
39
+ /** 向更早日志翻页的不透明游标,留空时从文件尾部读取 */
40
+ cursor: string;
41
+ /** 单次读取条数,0使用默认值,最大500 */
42
+ limit: number;
43
+ /** 日志内容搜索关键字 */
44
+ keyword: string;
45
+ /** 日志级别筛选列表 */
46
+ levels: string[];
47
+ }
48
+
49
+ /** 历史日志文件读取响应。 */
50
+ export interface ReadRuntimeLogFileResponse {
51
+ /** 按时间正序排列的日志条目 */
52
+ entries: RuntimeLogEntry[];
53
+ /** 继续读取更早日志的不透明游标 */
54
+ next_cursor: string;
55
+ /** 是否还有更早日志 */
56
+ has_more: boolean;
57
+ /** 读取期间日志文件是否已滚动或截断 */
58
+ file_changed: boolean;
59
+ }
60
+
61
+ /** 实时或历史日志条目。 */
62
+ export interface RuntimeLogEntry {
63
+ /** 当前进程内的日志序号,历史文件日志为空 */
64
+ sequence: string;
65
+ /** 日志时间 */
66
+ timestamp: string;
67
+ /** 日志级别 */
68
+ level: string;
69
+ /** 日志来源 */
70
+ source: string;
71
+ /** 日志消息 */
72
+ message: string;
73
+ /** 可直接展示的纯文本日志行 */
74
+ line: string;
75
+ /** 日志行是否因长度限制被截断 */
76
+ is_truncated: boolean;
77
+ }
78
+
79
+ /** 实时控制台开启条件。 */
80
+ export interface OpenRuntimeConsoleRequest {
81
+ /** 首次返回的最近日志条数,0使用默认值,最大500 */
82
+ backlog_limit: number;
83
+ }
84
+
85
+ /** 实时控制台开启响应。 */
86
+ export interface OpenRuntimeConsoleResponse {
87
+ /** SSE业务流标识 */
88
+ stream: string;
89
+ /** 当前用户专属SSE频道标识 */
90
+ channel_id: string;
91
+ /** 频道过期时间,RFC3339格式 */
92
+ expires_at: string;
93
+ /** 当前服务实例标识 */
94
+ instance_id: string;
95
+ /** 当前进程最新日志序号 */
96
+ latest_sequence: string;
97
+ /** 当前进程最近日志 */
98
+ entries: RuntimeLogEntry[];
99
+ }
100
+
101
+ /** 实时控制台丢失消息提示。 */
102
+ export interface RuntimeLogGap {
103
+ /** 本次丢弃的日志条数 */
104
+ dropped_count: number;
105
+ /** 发生丢弃时的最新日志序号 */
106
+ latest_sequence: string;
107
+ }
108
+
109
+ /** 历史日志文件下载条件。 */
110
+ export interface DownloadRuntimeLogFileRequest {
111
+ /** 服务端签发的日志文件标识 */
112
+ file_id: string;
113
+ }
@@ -27,15 +27,16 @@
27
27
  <script setup lang="ts">
28
28
  import SwaggerUIBundle from "swagger-ui-dist/swagger-ui-bundle.js";
29
29
  import "swagger-ui-dist/swagger-ui.css";
30
+ import { DEFAULT_LOCALE, t, useLocaleStore } from "@liujitcn/kratos-admin-core";
30
31
  import { getLocaleRequestHeaders, getRequestAccessToken } from "@liujitcn/kratos-admin-core/request";
31
32
  import { defBaseApiService } from "@liujitcn/kratos-admin-system/api/system/base_api";
32
33
  import type { OpenApiServiceOption } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_api";
33
- import { t } from "@liujitcn/kratos-admin-core";
34
34
 
35
35
  const swaggerRootRef = ref<HTMLElement>();
36
36
  const loading = ref(true);
37
37
  const documents = ref<OpenApiServiceOption[]>([]);
38
38
  const selectedDocumentKey = ref("");
39
+ const { locale } = useLocaleStore();
39
40
 
40
41
  /** 初始化携带当前登录令牌的 Swagger UI。 */
41
42
  function initializeSwaggerUI(documentKey: string) {
@@ -44,9 +45,10 @@ function initializeSwaggerUI(documentKey: string) {
44
45
 
45
46
  loading.value = true;
46
47
  swaggerRoot.replaceChildren();
48
+ const localePath = locale.value === DEFAULT_LOCALE ? "" : `/${encodeURIComponent(locale.value)}`;
47
49
  SwaggerUIBundle({
48
50
  domNode: swaggerRoot,
49
- url: `/api/docs/openapi/${encodeURIComponent(documentKey)}`,
51
+ url: `/api/docs/openapi/${encodeURIComponent(documentKey)}${localePath}`,
50
52
  deepLinking: true,
51
53
  displayOperationId: true,
52
54
  displayRequestDuration: true,
@@ -84,16 +86,18 @@ function handleDocumentChange(documentKey: string) {
84
86
 
85
87
  /** 加载可访问的 OpenAPI 文档并初始化默认文档。 */
86
88
  async function loadOpenAPIDocuments() {
89
+ loading.value = true;
87
90
  try {
88
91
  const response = await defBaseApiService.OptionOpenApiService({});
89
92
  documents.value = response.list;
90
- const defaultDocument = response.list[0];
91
- if (!defaultDocument) {
93
+ const selectedDocument = response.list.find(document => document.key === selectedDocumentKey.value) ?? response.list[0];
94
+ if (!selectedDocument) {
95
+ selectedDocumentKey.value = "";
92
96
  loading.value = false;
93
97
  return;
94
98
  }
95
- selectedDocumentKey.value = defaultDocument.key;
96
- initializeSwaggerUI(defaultDocument.key);
99
+ selectedDocumentKey.value = selectedDocument.key;
100
+ initializeSwaggerUI(selectedDocument.key);
97
101
  } catch {
98
102
  loading.value = false;
99
103
  }
@@ -103,6 +107,10 @@ onMounted(() => {
103
107
  loadOpenAPIDocuments();
104
108
  });
105
109
 
110
+ watch(locale, () => {
111
+ loadOpenAPIDocuments();
112
+ });
113
+
106
114
  onBeforeUnmount(() => {
107
115
  swaggerRootRef.value?.replaceChildren();
108
116
  });
@@ -134,6 +134,23 @@ async function handleGenerate() {
134
134
  ElMessage.warning(t("system.code.gen.table.message.disabled", { name: table.value.name }));
135
135
  return;
136
136
  }
137
+ if (missingTranslations.value.length) {
138
+ try {
139
+ await ElMessageBox.alert(
140
+ t("system.code.gen.preview.message.missing_translations", {
141
+ items: missingTranslations.value.join(t("system.code.gen.preview.value.list_separator"))
142
+ }),
143
+ t("common.title.warning"),
144
+ {
145
+ confirmButtonText: t("common.action.close"),
146
+ type: "warning"
147
+ }
148
+ );
149
+ } catch {
150
+ // 关闭提示框与点击关闭按钮语义一致。
151
+ }
152
+ return;
153
+ }
137
154
  try {
138
155
  await ElMessageBox.confirm(
139
156
  t("system.code.gen.table.dialog.generate_one", { name: table.value.name }),
@@ -17,26 +17,57 @@
17
17
  {{ t("system.code.gen.i18n.status.missing") }}
18
18
  </el-tag>
19
19
  </div>
20
- <el-input
21
- :model-value="localeValue(locale.value).comment"
22
- :placeholder="t('system.code.gen.i18n.placeholder.comment', { source: sourceComment || '-' })"
23
- maxlength="255"
24
- @update:model-value="value => updateLocale(locale.value, 'comment', value)"
25
- />
26
- <el-input
27
- v-if="showLeftTreeComment"
28
- :model-value="localeValue(locale.value).left_tree_comment"
29
- :placeholder="t('system.code.gen.i18n.placeholder.left_tree_comment', { source: sourceLeftTreeComment || '-' })"
30
- maxlength="255"
31
- @update:model-value="value => updateLocale(locale.value, 'left_tree_comment', value)"
32
- />
20
+ <div class="code-gen-locale-editor__control">
21
+ <el-input
22
+ :model-value="localeValue(locale.value).comment"
23
+ :placeholder="t('system.code.gen.i18n.placeholder.comment', { source: sourceComment || '-' })"
24
+ :disabled="isTranslating(locale.value, 'comment')"
25
+ maxlength="255"
26
+ @update:model-value="value => updateLocale(locale.value, 'comment', value)"
27
+ />
28
+ <el-button
29
+ class="code-gen-locale-editor__translate"
30
+ link
31
+ type="primary"
32
+ size="small"
33
+ :icon="Promotion"
34
+ :loading="isTranslating(locale.value, 'comment')"
35
+ :disabled="!canTranslate(locale.value, 'comment', sourceComment)"
36
+ @click="handleTranslate(locale.value, 'comment', sourceComment, locale.label)"
37
+ >
38
+ {{ t("system.base.translation.action.translate") }}
39
+ </el-button>
40
+ </div>
41
+ <div v-if="showLeftTreeComment" class="code-gen-locale-editor__control">
42
+ <el-input
43
+ :model-value="localeValue(locale.value).left_tree_comment"
44
+ :placeholder="t('system.code.gen.i18n.placeholder.left_tree_comment', { source: sourceLeftTreeComment || '-' })"
45
+ :disabled="isTranslating(locale.value, 'left_tree_comment')"
46
+ maxlength="255"
47
+ @update:model-value="value => updateLocale(locale.value, 'left_tree_comment', value)"
48
+ />
49
+ <el-button
50
+ class="code-gen-locale-editor__translate"
51
+ link
52
+ type="primary"
53
+ size="small"
54
+ :icon="Promotion"
55
+ :loading="isTranslating(locale.value, 'left_tree_comment')"
56
+ :disabled="!canTranslate(locale.value, 'left_tree_comment', sourceLeftTreeComment)"
57
+ @click="handleTranslate(locale.value, 'left_tree_comment', sourceLeftTreeComment, locale.label)"
58
+ >
59
+ {{ t("system.base.translation.action.translate") }}
60
+ </el-button>
61
+ </div>
33
62
  </div>
34
63
  </div>
35
64
  </template>
36
65
 
37
66
  <script setup lang="ts">
38
- import { computed } from "vue";
67
+ import { computed, ref } from "vue";
68
+ import { Promotion } from "@element-plus/icons-vue";
39
69
  import { t } from "@liujitcn/kratos-admin-core";
70
+ import { defBaseI18nService } from "@liujitcn/kratos-admin-system/api/system/base_i18n";
40
71
  import { loadEnabledBaseLanguages, useEnabledBaseLanguages } from "@liujitcn/kratos-admin-system/api/system/base_language";
41
72
  import type { CodeGenLocaleConfig } from "@liujitcn/kratos-admin-system/rpc/system/admin/v1/base_i18n";
42
73
 
@@ -63,6 +94,7 @@ const emit = defineEmits<{
63
94
  }>();
64
95
 
65
96
  const { languages } = useEnabledBaseLanguages();
97
+ const translatingField = ref("");
66
98
  void loadEnabledBaseLanguages();
67
99
 
68
100
  const editableLocales = computed(() =>
@@ -92,6 +124,33 @@ function updateLocale(locale: string, field: keyof CodeGenLocaleConfig, value: s
92
124
  next.set(locale, { ...localeValue(locale), [field]: value });
93
125
  emit("update:modelValue", next);
94
126
  }
127
+
128
+ /** 判断指定语言字段是否正在翻译。 */
129
+ function isTranslating(locale: string, field: keyof CodeGenLocaleConfig) {
130
+ return translatingField.value === `${locale}:${field}`;
131
+ }
132
+
133
+ /** 判断指定语言字段当前是否允许自动翻译。 */
134
+ function canTranslate(locale: string, field: keyof CodeGenLocaleConfig, source: string) {
135
+ return Boolean(source) && !localeValue(locale)[field] && !translatingField.value;
136
+ }
137
+
138
+ /** 翻译指定语言的单个代码生成描述。 */
139
+ async function handleTranslate(locale: string, field: keyof CodeGenLocaleConfig, source: string, language: string) {
140
+ if (!canTranslate(locale, field, source)) return;
141
+ translatingField.value = `${locale}:${field}`;
142
+ try {
143
+ const response = await defBaseI18nService.DraftBaseTranslation({ source, locale });
144
+ const translation = response.translations.find(item => item.locale === locale)?.translation;
145
+ if (!translation) throw new Error("translation not found");
146
+ updateLocale(locale, field, translation);
147
+ ElMessage.success(t("system.base.translation.message.translate_success", { language }));
148
+ } catch {
149
+ ElMessage.error(t("system.base.translation.message.translate_failed", { language }));
150
+ } finally {
151
+ translatingField.value = "";
152
+ }
153
+ }
95
154
  </script>
96
155
 
97
156
  <style scoped lang="scss">
@@ -115,4 +174,27 @@ function updateLocale(locale: string, field: keyof CodeGenLocaleConfig, value: s
115
174
  font-weight: 600;
116
175
  color: var(--el-text-color-primary);
117
176
  }
177
+
178
+ .code-gen-locale-editor__control {
179
+ position: relative;
180
+ display: flex;
181
+ align-items: center;
182
+ width: 100%;
183
+ }
184
+
185
+ .code-gen-locale-editor__control :deep(.el-input) {
186
+ flex: 1;
187
+ }
188
+
189
+ .code-gen-locale-editor__control :deep(.el-input__suffix) {
190
+ padding-right: 78px;
191
+ }
192
+
193
+ .code-gen-locale-editor__translate {
194
+ position: absolute;
195
+ z-index: 1;
196
+ top: 50%;
197
+ right: 8px;
198
+ transform: translateY(-50%);
199
+ }
118
200
  </style>
@@ -588,6 +588,38 @@ async function handleGenerate(selected: CodeGenGenerateTarget) {
588
588
  ElMessage.warning(t("system.code.gen.table.message.disabled", { name: disabledTable.name }));
589
589
  return;
590
590
  }
591
+ generating.value = true;
592
+ let missingTranslations: string[] = [];
593
+ try {
594
+ const previewEntries = await Promise.all(
595
+ tables.map(async table => ({
596
+ table,
597
+ preview: await defCodeGenService.PreviewCodeGen({ table_id: table.id, output_paths: undefined })
598
+ }))
599
+ );
600
+ missingTranslations = previewEntries.flatMap(({ table, preview }) =>
601
+ (preview.missing_translations ?? []).map(item => (tables.length === 1 ? item : `${table.name}: ${item}`))
602
+ );
603
+ } finally {
604
+ generating.value = false;
605
+ }
606
+ if (missingTranslations.length) {
607
+ try {
608
+ await ElMessageBox.alert(
609
+ t("system.code.gen.preview.message.missing_translations", {
610
+ items: missingTranslations.join(t("system.code.gen.preview.value.list_separator"))
611
+ }),
612
+ t("common.title.warning"),
613
+ {
614
+ confirmButtonText: t("common.action.close"),
615
+ type: "warning"
616
+ }
617
+ );
618
+ } catch {
619
+ // 关闭提示框与点击关闭按钮语义一致。
620
+ }
621
+ return;
622
+ }
591
623
  const message =
592
624
  tables.length === 1
593
625
  ? t("system.code.gen.table.dialog.generate_one", { name: tables[0].name })
@@ -189,10 +189,22 @@
189
189
  <code>{{ node.name }}</code>
190
190
  <div class="ops-node-meters">
191
191
  <div v-for="metric in node.metrics" :key="metric.label" class="ops-meter">
192
- <span>{{ metric.label }}</span
193
- ><el-progress :percentage="metric.value" :show-text="false" :color="metric.color" /><strong
194
- >{{ metric.value }}%</strong
195
- >
192
+ <div class="ops-meter-head">
193
+ <span class="ops-meter-label">{{ metric.label }}</span>
194
+ <span class="ops-meter-stats">
195
+ <span>
196
+ {{ t("system.ops_monitoring.node_metric.current") }} {{ metric.usedLabel }} /
197
+ {{ t("system.ops_monitoring.node_metric.total") }} {{ metric.totalLabel }}
198
+ </span>
199
+ <strong>{{ metric.percentageLabel }}</strong>
200
+ </span>
201
+ </div>
202
+ <el-progress
203
+ :percentage="metric.percentage"
204
+ :show-text="false"
205
+ :stroke-width="10"
206
+ :color="metric.color"
207
+ />
196
208
  </div>
197
209
  </div>
198
210
  </div>
@@ -278,12 +290,28 @@ interface EndpointItem {
278
290
  healthy: boolean;
279
291
  }
280
292
 
281
- /** 实例资源指标。 */
293
+ /** 实例资源单项指标。 */
294
+ interface NodeMetricItem {
295
+ /** 指标名称。 */
296
+ label: string;
297
+ /** 指标使用百分比。 */
298
+ percentage: number;
299
+ /** 两位小数百分比。 */
300
+ percentageLabel: string;
301
+ /** 当前已用容量。 */
302
+ usedLabel: string;
303
+ /** 总容量。 */
304
+ totalLabel: string;
305
+ /** 进度条颜色。 */
306
+ color: string;
307
+ }
308
+
309
+ /** 实例资源信息。 */
282
310
  interface NodeItem {
283
311
  /** 实例名称。 */
284
312
  name: string;
285
313
  /** 实例资源指标。 */
286
- metrics: Array<{ label: string; value: number; color: string }>;
314
+ metrics: NodeMetricItem[];
287
315
  }
288
316
 
289
317
  /** 折线图中的流量趋势点。 */
@@ -334,6 +362,12 @@ const alerts = ref<OpsAlertsResponse>();
334
362
  const windowMinutes = ref(15);
335
363
  const lastCollectedAt = ref("");
336
364
  const trendGridLines = [16, 62, 108, 154, 198];
365
+ const nodeMetricLabelKeys: Record<string, string> = {
366
+ 堆内存: "system.ops_monitoring.node_metric.heap_memory",
367
+ 内存: "system.ops_monitoring.node_metric.memory",
368
+ 硬盘: "system.ops_monitoring.node_metric.disk"
369
+ };
370
+ const byteUnits = ["B", "KB", "MB", "GB", "TB", "PB"];
337
371
  const trafficSummary = computed(() => traffic.value?.traffic);
338
372
 
339
373
  const kpiItems = computed<KpiItem[]>(() => [
@@ -579,14 +613,46 @@ function mapEndpoint(endpoint: OpsEndpoint): EndpointItem {
579
613
  function mapNode(node: OpsNode): NodeItem {
580
614
  return {
581
615
  name: node.name,
582
- metrics: (node.metrics ?? []).map(metric => ({
583
- label: metric.label,
584
- value: Math.max(0, Math.min(metric.value, 100)),
585
- color: metric.value >= 70 ? "var(--el-color-warning)" : "var(--el-color-primary)"
586
- }))
616
+ metrics: (node.metrics ?? []).map(metric => {
617
+ const percentage = Math.max(0, Math.min(metric.value, 100));
618
+ return {
619
+ label: translateNodeMetricLabel(metric.label),
620
+ percentage,
621
+ percentageLabel: `${formatFixedNumber(percentage)}%`,
622
+ usedLabel: formatBytes(metric.used_bytes),
623
+ totalLabel: formatBytes(metric.total_bytes),
624
+ color: percentage >= 70 ? "var(--el-color-warning)" : "var(--el-color-primary)"
625
+ };
626
+ })
587
627
  };
588
628
  }
589
629
 
630
+ /** 将字节数格式化为保留两位小数的可读容量。 */
631
+ function formatBytes(value: number) {
632
+ if (!Number.isFinite(value) || value < 0) return "--";
633
+ let unitIndex = 0;
634
+ let displayValue = value;
635
+ while (displayValue >= 1024 && unitIndex < byteUnits.length - 1) {
636
+ displayValue /= 1024;
637
+ unitIndex += 1;
638
+ }
639
+ return `${formatFixedNumber(displayValue)} ${byteUnits[unitIndex]}`;
640
+ }
641
+
642
+ /** 将数值格式化为固定两位小数。 */
643
+ function formatFixedNumber(value: number) {
644
+ return new Intl.NumberFormat(getCurrentLocale(), {
645
+ minimumFractionDigits: 2,
646
+ maximumFractionDigits: 2
647
+ }).format(value);
648
+ }
649
+
650
+ /** 翻译实例资源指标名称,未知指标保留后端原值。 */
651
+ function translateNodeMetricLabel(label: string) {
652
+ const localeKey = nodeMetricLabelKeys[label];
653
+ return localeKey ? t(localeKey) : label;
654
+ }
655
+
590
656
  function mapAlert(alert: OpsAlert) {
591
657
  return {
592
658
  title: alert.title,
@@ -654,6 +720,7 @@ onBeforeUnmount(stopMonitoring);
654
720
  .ops-panel,
655
721
  .ops-storage-card,
656
722
  .ops-alert-panel {
723
+ min-width: 0;
657
724
  background: var(--ops-card-bg);
658
725
  border: 1px solid var(--ops-card-border);
659
726
  border-radius: var(--admin-page-radius);
@@ -934,7 +1001,7 @@ onBeforeUnmount(stopMonitoring);
934
1001
  margin-bottom: 12px;
935
1002
  }
936
1003
  .ops-storage-grid {
937
- grid-template-columns: repeat(2, minmax(0, 1fr));
1004
+ grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.9fr);
938
1005
  }
939
1006
  .ops-storage-card {
940
1007
  min-width: 0;
@@ -1040,37 +1107,70 @@ onBeforeUnmount(stopMonitoring);
1040
1107
  .ops-node-list {
1041
1108
  display: grid;
1042
1109
  gap: 16px;
1110
+ min-width: 0;
1043
1111
  padding-top: 3px;
1044
1112
  }
1045
1113
  .ops-node-row {
1046
1114
  display: grid;
1047
- grid-template-columns: 95px minmax(0, 1fr);
1048
- gap: 10px;
1049
- align-items: center;
1115
+ grid-template-columns: minmax(110px, 0.24fr) minmax(0, 1fr);
1116
+ gap: 18px;
1117
+ align-items: start;
1118
+ min-width: 0;
1050
1119
  }
1051
1120
  .ops-node-row > code {
1121
+ overflow: hidden;
1052
1122
  font-family: var(--el-font-family-monospace);
1053
1123
  font-size: 11px;
1054
1124
  color: var(--ops-text-secondary);
1125
+ text-overflow: ellipsis;
1126
+ white-space: nowrap;
1055
1127
  }
1056
1128
  .ops-node-meters {
1057
1129
  display: grid;
1058
- gap: 7px;
1130
+ gap: 13px;
1131
+ min-width: 0;
1059
1132
  }
1060
1133
  .ops-meter {
1061
1134
  display: grid;
1062
- grid-template-columns: 34px minmax(0, 1fr) 35px;
1063
1135
  gap: 7px;
1064
- align-items: center;
1065
- font-size: 10px;
1136
+ min-width: 0;
1137
+ font-size: 11px;
1066
1138
  color: var(--ops-text-placeholder);
1067
1139
  }
1068
- .ops-meter strong {
1069
- font-size: 11px;
1070
- font-weight: 500;
1140
+ .ops-meter-head,
1141
+ .ops-meter-stats {
1142
+ display: flex;
1143
+ flex-wrap: wrap;
1144
+ gap: 4px 10px;
1145
+ min-width: 0;
1146
+ align-items: center;
1147
+ justify-content: space-between;
1148
+ }
1149
+ .ops-meter-label {
1150
+ flex: none;
1151
+ font-weight: 600;
1071
1152
  color: var(--ops-text-secondary);
1153
+ }
1154
+ .ops-meter-stats {
1155
+ flex: 1;
1156
+ justify-content: flex-end;
1072
1157
  text-align: right;
1073
1158
  }
1159
+ .ops-meter-stats > span {
1160
+ overflow-wrap: anywhere;
1161
+ }
1162
+ .ops-meter-stats strong {
1163
+ flex: none;
1164
+ font-size: 11px;
1165
+ font-weight: 650;
1166
+ color: var(--ops-text-secondary);
1167
+ font-variant-numeric: tabular-nums;
1168
+ }
1169
+ .ops-meter :deep(.el-progress),
1170
+ .ops-meter :deep(.el-progress-bar) {
1171
+ min-width: 0;
1172
+ width: 100%;
1173
+ }
1074
1174
  .ops-alert-panel {
1075
1175
  margin-bottom: 0;
1076
1176
  }
@@ -1125,6 +1225,7 @@ onBeforeUnmount(stopMonitoring);
1125
1225
  grid-template-columns: repeat(2, minmax(0, 1fr));
1126
1226
  }
1127
1227
  .ops-primary-grid,
1228
+ .ops-storage-grid,
1128
1229
  .ops-secondary-grid {
1129
1230
  grid-template-columns: 1fr;
1130
1231
  }
@@ -1159,8 +1260,13 @@ onBeforeUnmount(stopMonitoring);
1159
1260
  margin-left: 54px;
1160
1261
  }
1161
1262
  .ops-node-row {
1162
- grid-template-columns: 82px minmax(0, 1fr);
1163
- gap: 7px;
1263
+ grid-template-columns: 1fr;
1264
+ gap: 10px;
1265
+ }
1266
+ .ops-meter-stats {
1267
+ flex-basis: 100%;
1268
+ justify-content: flex-start;
1269
+ text-align: left;
1164
1270
  }
1165
1271
  .ops-alert-row {
1166
1272
  grid-template-columns: 8px minmax(0, 1fr);