@liujitcn/kratos-admin-system 0.0.52 → 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.
- package/dist/package/declarations/src/api/base/v1/sse.d.ts +1 -1
- package/dist/package/declarations/src/locales/generated.d.ts +32 -0
- package/dist/package/declarations/src/rpc/base/v1/ai_search.d.ts +26 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_redact_output_policy.d.ts +0 -1
- package/dist/package/src/api/base/v1/sse.ts +19 -12
- package/dist/package/src/components/log/LogTable.vue +25 -18
- package/dist/package/src/components/notification/Notification.vue +14 -7
- package/dist/package/src/components/tenant-project/TenantProjectManager.vue +12 -6
- package/dist/package/src/components/tenant-project-grant/ProjectGrantDialog.vue +22 -7
- package/dist/package/src/locales/en-US.json +8 -0
- package/dist/package/src/locales/ja-JP.json +8 -0
- package/dist/package/src/locales/zh-CN.json +8 -0
- package/dist/package/src/locales/zh-TW.json +8 -0
- package/dist/package/src/rpc/base/v1/ai_search.ts +37 -0
- package/dist/package/src/rpc/system/admin/v1/base_redact_output_policy.ts +0 -1
- package/dist/package/src/views/ai/chat/index.vue +16 -7
- package/dist/package/src/views/base/api-log/index.vue +1 -1
- package/dist/package/src/views/base/area/index.vue +15 -9
- package/dist/package/src/views/base/backup-management/archive-config/index.vue +22 -10
- package/dist/package/src/views/base/backup-management/backup-config/index.vue +13 -6
- package/dist/package/src/views/base/config/index.vue +38 -14
- package/dist/package/src/views/base/dashboard/index.vue +201 -53
- package/dist/package/src/views/base/dept/index.vue +31 -25
- package/dist/package/src/views/base/dict/index.vue +12 -8
- package/dist/package/src/views/base/dict/item.vue +12 -8
- package/dist/package/src/views/base/i18n-custom/index.vue +13 -7
- package/dist/package/src/views/base/job/index.vue +12 -8
- package/dist/package/src/views/base/job/log.vue +14 -9
- package/dist/package/src/views/base/language/index.vue +7 -4
- package/dist/package/src/views/base/login-policy/index.vue +22 -11
- package/dist/package/src/views/base/menu/index.vue +23 -16
- package/dist/package/src/views/base/message/index.vue +65 -21
- package/dist/package/src/views/base/message-category/index.vue +10 -4
- package/dist/package/src/views/base/oauth-client/index.vue +15 -5
- package/dist/package/src/views/base/post/index.vue +10 -4
- package/dist/package/src/views/base/redact-output-policy/index.vue +12 -5
- package/dist/package/src/views/base/redact-rule/index.vue +8 -7
- package/dist/package/src/views/base/redact-storage-policy/index.vue +32 -8
- package/dist/package/src/views/base/role/index.vue +24 -17
- package/dist/package/src/views/base/tenant/index.vue +7 -6
- package/dist/package/src/views/base/tenant-project-grant/index.vue +44 -27
- package/dist/package/src/views/base/user/components/dept-tree.vue +4 -9
- package/dist/package/src/views/base/user/index.vue +39 -28
- package/dist/package/src/views/message/inbox/index.vue +8 -3
- package/dist/package/src/views/profile/components/base.vue +1 -1
- package/dist/package/src/views/profile/index.vue +1 -1
- package/dist/package/src/views/tool/cache/index.vue +1 -1
- package/dist/package/src/views/tool/code-gen/columns/index.vue +35 -16
- package/dist/package/src/views/tool/code-gen/table/index.vue +58 -31
- package/package.json +2 -2
|
@@ -113,7 +113,9 @@ async function loadParentIdFormTreeOptions(
|
|
|
113
113
|
resolve(normalizeLazyTreeOptions((response.list ?? []) as GeneratedTreeOption[]));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
void loadFormOptions()
|
|
116
|
+
void loadFormOptions().then(options => {
|
|
117
|
+
parentIdFormOptions.value = options;
|
|
118
|
+
});
|
|
117
119
|
|
|
118
120
|
/** 行政区域表单字段配置。 */
|
|
119
121
|
const formFields = computed<ProFormField[]>(() => [
|
|
@@ -218,7 +220,7 @@ async function loadFormOptions() {
|
|
|
218
220
|
const parentIdFormResponse = await defBaseAreaService.OptionBaseArea({ parent_id: 0, lazy: true } as Parameters<
|
|
219
221
|
typeof defBaseAreaService.OptionBaseArea
|
|
220
222
|
>[0]);
|
|
221
|
-
|
|
223
|
+
return [
|
|
222
224
|
{ label: t("system.base.area.value.root"), value: 0 },
|
|
223
225
|
...normalizeLazyTreeOptions((parentIdFormResponse.list ?? []) as GeneratedTreeOption[]).filter(
|
|
224
226
|
option => Number(option.value) !== 0
|
|
@@ -231,19 +233,23 @@ async function loadFormOptions() {
|
|
|
231
233
|
*/
|
|
232
234
|
async function handleOpenDialog(id?: number) {
|
|
233
235
|
resetForm();
|
|
234
|
-
await loadFormOptions();
|
|
235
236
|
dialog.titleKey = id ? "system.base.area.action.edit" : "system.base.area.action.create";
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
await formDialogRef.value?.open({
|
|
238
|
+
load: async () => ({
|
|
239
|
+
options: await loadFormOptions(),
|
|
240
|
+
data: id ? await defBaseAreaService.GetBaseArea({ id }) : undefined
|
|
241
|
+
}),
|
|
242
|
+
commit: ({ options, data }) => {
|
|
243
|
+
parentIdFormOptions.value = options;
|
|
244
|
+
if (data) Object.assign(formData, data);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
241
247
|
}
|
|
242
248
|
/**
|
|
243
249
|
* 关闭行政区域弹窗。
|
|
244
250
|
*/
|
|
245
251
|
function handleCloseDialog() {
|
|
246
|
-
|
|
252
|
+
formDialogRef.value?.close();
|
|
247
253
|
resetForm();
|
|
248
254
|
}
|
|
249
255
|
/**
|
|
@@ -83,13 +83,21 @@ function refresh() { proTable.value?.getTableList(); }
|
|
|
83
83
|
function defaultForm(): BaseTableArchiveForm { return { id: 0, source_name: "", table_name: "", archive_mode: BaseTableArchiveMode.BASE_TABLE_ARCHIVE_MODE_INTERNAL_DATABASE, online_retention_days: 180, archive_retention_days: 3650, batch_size: 5000, delete_after_verify: false, oss_prefix: "archive", status: Status.STATUS_ENABLE }; }
|
|
84
84
|
function resetForm() { dialog.visible = false; formDialogRef.value?.resetFields(); Object.assign(formData, defaultForm()); }
|
|
85
85
|
async function openDialog(id?: number) {
|
|
86
|
-
await
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
await formDialogRef.value?.open({
|
|
87
|
+
load: async () => {
|
|
88
|
+
await loadSourceOptions();
|
|
89
|
+
const data = id ? await defBaseTableArchiveService.GetBaseTableArchive({ id }) : undefined;
|
|
90
|
+
const form = { ...defaultForm(), ...(data ?? {}) };
|
|
91
|
+
if (!form.source_name) form.source_name = String(sourceOptions.value[0]?.value ?? "");
|
|
92
|
+
const loadedTableOptions = form.source_name ? await requestTableOptions(form.source_name) : [];
|
|
93
|
+
return { form, loadedTableOptions };
|
|
94
|
+
},
|
|
95
|
+
commit: ({ form, loadedTableOptions }) => {
|
|
96
|
+
Object.assign(formData, form);
|
|
97
|
+
dialog.editing = Boolean(id);
|
|
98
|
+
tableOptions.value = loadedTableOptions;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
93
101
|
}
|
|
94
102
|
async function loadSourceOptions() {
|
|
95
103
|
if (sourceOptions.value.length || loadingSources.value) return;
|
|
@@ -107,12 +115,16 @@ async function handleSourceChange(value: string | number | boolean | undefined)
|
|
|
107
115
|
await loadTableOptions(formData.source_name);
|
|
108
116
|
}
|
|
109
117
|
async function loadTableOptions(sourceName: string) {
|
|
110
|
-
tableOptions.value =
|
|
111
|
-
|
|
118
|
+
tableOptions.value = await requestTableOptions(sourceName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 请求指定数据源下的数据表选项。 */
|
|
122
|
+
async function requestTableOptions(sourceName: string) {
|
|
123
|
+
if (!sourceName) return [];
|
|
112
124
|
loadingTables.value = true;
|
|
113
125
|
try {
|
|
114
126
|
const data = await defBaseTableSourceService.OptionBaseTable({ source_name: sourceName });
|
|
115
|
-
|
|
127
|
+
return (data.value ?? []).map(value => ({ label: value, value }));
|
|
116
128
|
} finally {
|
|
117
129
|
loadingTables.value = false;
|
|
118
130
|
}
|
|
@@ -68,12 +68,19 @@ function refresh() { proTable.value?.getTableList(); }
|
|
|
68
68
|
function defaultForm(): BaseTableBackupForm { return { id: 0, source_name: "", backup_type: BaseTableBackupType.BASE_TABLE_BACKUP_TYPE_FULL, oss_prefix: "backup/database", retention_count: 7, status: Status.STATUS_ENABLE }; }
|
|
69
69
|
function resetForm() { dialog.visible = false; formDialogRef.value?.resetFields(); Object.assign(formData, defaultForm()); }
|
|
70
70
|
async function openDialog(id?: number) {
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
await formDialogRef.value?.open({
|
|
72
|
+
load: async () => {
|
|
73
|
+
await loadSourceOptions();
|
|
74
|
+
const data = id ? await defBaseTableBackupService.GetBaseTableBackup({ id }) : undefined;
|
|
75
|
+
const form = { ...defaultForm(), ...(data ?? {}) };
|
|
76
|
+
if (!form.source_name) form.source_name = String(sourceOptions.value[0]?.value ?? "");
|
|
77
|
+
return form;
|
|
78
|
+
},
|
|
79
|
+
commit: form => {
|
|
80
|
+
Object.assign(formData, form);
|
|
81
|
+
dialog.editing = Boolean(id);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
77
84
|
}
|
|
78
85
|
async function loadSourceOptions() {
|
|
79
86
|
if (sourceOptions.value.length || loadingSources.value) return;
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
12
|
<ProDialog
|
|
13
|
+
ref="dialogRef"
|
|
13
14
|
v-model="dialog.visible"
|
|
14
15
|
class="system-config-dialog"
|
|
15
16
|
:title="t(dialog.titleKey, { resource: t('system.base.config.resource') })"
|
|
16
|
-
width="min(
|
|
17
|
+
width="min(1280px, calc(100vw - 32px))"
|
|
17
18
|
top="4vh"
|
|
18
19
|
:confirm-loading="saving"
|
|
19
20
|
@confirm="handleSubmit"
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
:rules="rules"
|
|
33
34
|
:col-span="12"
|
|
34
35
|
:gutter="24"
|
|
36
|
+
label-width="10em"
|
|
35
37
|
label-position="right"
|
|
36
38
|
scroll-to-error
|
|
37
39
|
>
|
|
@@ -96,6 +98,7 @@
|
|
|
96
98
|
:fields="localizedFormFields"
|
|
97
99
|
:col-span="12"
|
|
98
100
|
:gutter="24"
|
|
101
|
+
label-width="10em"
|
|
99
102
|
label-position="right"
|
|
100
103
|
scroll-to-error
|
|
101
104
|
/>
|
|
@@ -160,6 +163,7 @@ defineOptions({
|
|
|
160
163
|
|
|
161
164
|
const { BUTTONS } = useAuthButtons();
|
|
162
165
|
const proTable = ref<ProTableInstance>();
|
|
166
|
+
const dialogRef = ref<InstanceType<typeof ProDialog>>();
|
|
163
167
|
const basicFormRef = ref<ProFormInstance>();
|
|
164
168
|
const activeTab = ref("basic");
|
|
165
169
|
|
|
@@ -635,29 +639,48 @@ function refreshTable() {
|
|
|
635
639
|
* 打开系统配置弹窗。
|
|
636
640
|
*/
|
|
637
641
|
async function handleOpenDialog(configId?: number) {
|
|
638
|
-
await loadEnabledBaseLanguages();
|
|
639
642
|
resetForm();
|
|
640
643
|
dialog.titleKey = configId ? "common.action.edit_resource" : "common.action.create_resource";
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
644
|
+
await dialogRef.value?.open({
|
|
645
|
+
load: async () => ({
|
|
646
|
+
data: configId ? await defBaseConfigService.GetBaseConfig({ id: configId }) : undefined,
|
|
647
|
+
languages: await loadEnabledBaseLanguages()
|
|
648
|
+
}),
|
|
649
|
+
commit: ({ data }) => {
|
|
650
|
+
if (data) {
|
|
651
|
+
Object.assign(formData, data);
|
|
652
|
+
if (formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM) loadFormValue(formData.value);
|
|
653
|
+
}
|
|
654
|
+
activeTab.value = configId && formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM ? "form" : "basic";
|
|
645
655
|
}
|
|
646
|
-
}
|
|
647
|
-
activeTab.value = configId && formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM ? "form" : "basic";
|
|
648
|
-
dialog.visible = true;
|
|
656
|
+
});
|
|
649
657
|
}
|
|
650
658
|
|
|
651
659
|
/** 按注册定义创建独立模型,并用已保存的配置值覆盖默认值。 */
|
|
652
660
|
function loadFormValue(value?: string) {
|
|
653
661
|
const definition = formDefinition.value;
|
|
662
|
+
const parsed = value ? parseJSONValue(value) : {};
|
|
654
663
|
formValue.value =
|
|
655
|
-
formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM && definition
|
|
656
|
-
? Object.assign(definition.createModel(),
|
|
657
|
-
: {};
|
|
664
|
+
formData.type === BaseConfigType.BASE_CONFIG_TYPE_FORM && definition && isJSONObject(parsed)
|
|
665
|
+
? Object.assign(definition.createModel(), parsed)
|
|
666
|
+
: definition?.createModel() ?? {};
|
|
658
667
|
valueFormRef.value?.clearValidate();
|
|
659
668
|
}
|
|
660
669
|
|
|
670
|
+
/** 安全解析运行配置 JSON,脏数据回退到表单默认模型。 */
|
|
671
|
+
function parseJSONValue(value: string): unknown {
|
|
672
|
+
try {
|
|
673
|
+
return JSON.parse(value);
|
|
674
|
+
} catch {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** 判断运行配置 JSON 是否为可合并对象。 */
|
|
680
|
+
function isJSONObject(value: unknown): value is Record<string, unknown> {
|
|
681
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
682
|
+
}
|
|
683
|
+
|
|
661
684
|
/** 按字段声明恢复运行配置提交值的 JSON 类型。 */
|
|
662
685
|
function normalizeRuntimeConfigValue() {
|
|
663
686
|
if (!formDefinition.value) return;
|
|
@@ -669,7 +692,8 @@ function normalizeRuntimeConfigValue() {
|
|
|
669
692
|
} else if (field.valueType === "boolean" && typeof value === "string") {
|
|
670
693
|
setRuntimeConfigFieldValue(field.prop, value === "true");
|
|
671
694
|
} else if (field.valueType === "json" && typeof value === "string" && value !== "") {
|
|
672
|
-
|
|
695
|
+
const parsed = parseJSONValue(value);
|
|
696
|
+
if (parsed !== undefined) setRuntimeConfigFieldValue(field.prop, parsed);
|
|
673
697
|
}
|
|
674
698
|
}
|
|
675
699
|
}
|
|
@@ -690,7 +714,7 @@ function setRuntimeConfigFieldValue(prop: string, value: unknown) {
|
|
|
690
714
|
* 关闭系统配置弹窗并恢复默认表单值。
|
|
691
715
|
*/
|
|
692
716
|
function handleCloseDialog() {
|
|
693
|
-
|
|
717
|
+
dialogRef.value?.close();
|
|
694
718
|
resetForm();
|
|
695
719
|
}
|
|
696
720
|
|
|
@@ -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-
|
|
12
|
-
<span class="metric-label">{{ metric.label }}</span
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
24
|
-
|
|
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
|
|
28
|
-
|
|
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
|
|
32
|
-
|
|
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
|
-
{
|
|
60
|
-
|
|
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:
|
|
66
|
-
xAxis: {
|
|
67
|
-
|
|
68
|
-
|
|
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: [
|
|
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
|
-
|
|
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:
|
|
238
|
+
gap: 16px;
|
|
126
239
|
}
|
|
127
240
|
.metric-grid {
|
|
128
241
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
129
|
-
margin-bottom:
|
|
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
|
-
|
|
262
|
+
display: block;
|
|
263
|
+
margin-top: 18px;
|
|
264
|
+
color: var(--admin-page-text-primary);
|
|
265
|
+
font-size: 32px;
|
|
150
266
|
line-height: 1;
|
|
151
|
-
|
|
267
|
+
font-variant-numeric: tabular-nums;
|
|
152
268
|
}
|
|
153
269
|
.metric-icon {
|
|
154
|
-
display:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
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:
|
|
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;
|