@liujitcn/kratos-admin-core 0.0.29 → 0.0.31
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/README.md +7 -6
- package/build/auto-imports.json +73 -0
- package/build/plugins.ts +2 -58
- package/dist/package/declarations/src/api/base/mfa.d.ts +26 -0
- package/dist/package/declarations/src/components/Dialog/FormDialog.vue.d.ts +3 -3
- package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +65 -0
- package/dist/package/declarations/src/components/Dialog/ProDialog.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Grid/components/GridItem.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Mfa/MfaRecoveryCodesDialog.vue.d.ts +19 -0
- package/dist/package/declarations/src/components/Mfa/MfaSetupPanel.vue.d.ts +8 -0
- package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +4 -4
- package/dist/package/declarations/src/components/ProForm/index.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +4 -1
- package/dist/package/declarations/src/components/RichTextPreview/index.vue.d.ts +8 -0
- package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +1 -1
- package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +1 -1
- package/dist/package/declarations/src/layouts/components/LockScreen.vue.d.ts +3 -0
- package/dist/package/declarations/src/locales/generated.d.ts +168 -0
- package/dist/package/declarations/src/locales/index.d.ts +2 -0
- package/dist/package/declarations/src/rpc/base/v1/login.d.ts +33 -3
- package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +152 -0
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +25 -0
- package/dist/package/declarations/src/rpc/base/v1/oauth_client.d.ts +23 -0
- package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +10 -6
- package/dist/package/declarations/src/security.d.ts +2 -0
- package/dist/package/declarations/src/stores/interface/index.d.ts +13 -0
- package/dist/package/declarations/src/stores/modules/lockScreen.d.ts +20 -0
- package/dist/package/declarations/src/stores/modules/user.d.ts +5 -2
- package/dist/package/declarations/src/stores/runtime.d.ts +1 -0
- package/dist/package/declarations/src/utils/clipboard.d.ts +2 -0
- package/dist/package/declarations/src/utils/webauthn.d.ts +4 -0
- package/dist/package/declarations/vite.config.d.ts +2 -0
- package/dist/package/src/api/base/mfa.ts +114 -0
- package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +176 -0
- package/dist/package/src/components/Loading/fullScreen.ts +2 -1
- package/dist/package/src/components/MarkdownPreview/index.vue +21 -1
- package/dist/package/src/components/Mfa/MfaRecoveryCodesDialog.vue +91 -0
- package/dist/package/src/components/Mfa/MfaSetupPanel.vue +81 -0
- package/dist/package/src/components/ProForm/components/ProFormItem.vue +16 -1
- package/dist/package/src/components/ProForm/interface/index.ts +4 -0
- package/dist/package/src/components/ProTable/components/ColSetting.vue +1 -1
- package/dist/package/src/components/ProTable/index.vue +6 -2
- package/dist/package/src/components/RichTextPreview/index.vue +45 -0
- package/dist/package/src/config/index.ts +1 -1
- package/dist/package/src/layouts/LayoutClassic/index.vue +3 -1
- package/dist/package/src/layouts/LayoutColumns/index.vue +3 -1
- package/dist/package/src/layouts/LayoutTransverse/index.vue +3 -1
- package/dist/package/src/layouts/LayoutVertical/index.vue +9 -1
- package/dist/package/src/layouts/components/Header/components/Avatar.vue +23 -4
- package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +1 -1
- package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +1 -1
- package/dist/package/src/layouts/components/LockScreen.vue +450 -0
- package/dist/package/src/layouts/index.vue +2 -0
- package/dist/package/src/layouts/indexAsync.vue +2 -0
- package/dist/package/src/locales/README.md +2 -1
- package/dist/package/src/locales/en-US.json +43 -1
- package/dist/package/src/locales/index.ts +16 -12
- package/dist/package/src/locales/ja-JP.json +43 -1
- package/dist/package/src/locales/zh-CN.json +43 -1
- package/dist/package/src/locales/zh-TW.json +43 -1
- package/dist/package/src/rpc/base/v1/login.ts +33 -2
- package/dist/package/src/rpc/base/v1/mfa.ts +180 -0
- package/dist/package/src/rpc/base/v1/oauth.ts +25 -0
- package/dist/package/src/rpc/base/v1/oauth_client.ts +33 -0
- package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +10 -6
- package/dist/package/src/security.ts +2 -0
- package/dist/package/src/stores/interface/index.ts +14 -0
- package/dist/package/src/stores/modules/lockScreen.ts +78 -0
- package/dist/package/src/stores/modules/user.ts +16 -3
- package/dist/package/src/stores/runtime.ts +1 -0
- package/dist/package/src/utils/clipboard.ts +22 -0
- package/dist/package/src/utils/request.ts +26 -10
- package/dist/package/src/utils/webauthn.ts +90 -0
- package/dist/package/src/views/login/components/LoginForm.vue +237 -16
- package/dist/package/src/views/login/index.vue +3 -1
- package/dist/package/types/generated/auto-imports.d.ts +132 -129
- package/dist/package/types/generated/components.d.ts +4 -72
- package/package.json +22 -1
- package/types/generated/auto-imports.d.ts +132 -129
- package/types/generated/components.d.ts +4 -72
- package/vite.config.ts +3 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useAuthStore } from "./modules/auth";
|
|
2
2
|
export { useConfigStore } from "./modules/config";
|
|
3
3
|
export { useGlobalStore } from "./modules/global";
|
|
4
|
+
export { useLockScreenStore } from "./modules/lockScreen";
|
|
4
5
|
export { useTabsStore } from "./modules/tabs";
|
|
5
6
|
export { useUserStore } from "./modules/user";
|
|
@@ -6,6 +6,8 @@ export interface AdminViteConfigOptions {
|
|
|
6
6
|
optimizeDependencies?: string[];
|
|
7
7
|
/** 静态资源构建输出目录。 */
|
|
8
8
|
outputDirectory?: string;
|
|
9
|
+
/** 自动组件声明输出位置,外部宿主默认不写入依赖包。 */
|
|
10
|
+
componentDts?: string | false;
|
|
9
11
|
}
|
|
10
12
|
/** 创建管理端宿主 Vite 配置。 */
|
|
11
13
|
export declare function defineAdminViteConfig(options?: AdminViteConfigOptions): import("vite").UserConfigFnObject;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import service from "../../utils/request";
|
|
2
|
+
import type { LoginResponse } from "../../rpc/base/v1/login";
|
|
3
|
+
import type {
|
|
4
|
+
BeginMfaDisableRequest,
|
|
5
|
+
BeginMfaDisableResponse,
|
|
6
|
+
BeginMfaEnrollmentRequest,
|
|
7
|
+
BeginMfaEnrollmentResponse,
|
|
8
|
+
BeginMfaSetupRequest,
|
|
9
|
+
BeginMfaSetupResponse,
|
|
10
|
+
ConfirmMfaEnrollmentRequest,
|
|
11
|
+
ConfirmMfaEnrollmentResponse,
|
|
12
|
+
ConfirmMfaSetupRequest,
|
|
13
|
+
ConfirmMfaSetupResponse,
|
|
14
|
+
DisableMfaRequest,
|
|
15
|
+
GetMfaStatusRequest,
|
|
16
|
+
MfaService,
|
|
17
|
+
MfaStatusResponse,
|
|
18
|
+
RecoveryCodesResponse,
|
|
19
|
+
RegenerateMfaRecoveryCodesRequest,
|
|
20
|
+
VerifyMfaRequest
|
|
21
|
+
} from "../../rpc/base/v1/mfa";
|
|
22
|
+
import type { Empty } from "../../rpc/google/protobuf/empty";
|
|
23
|
+
|
|
24
|
+
const MFA_URL = "/v1/base/mfa";
|
|
25
|
+
|
|
26
|
+
/** 多因素认证公共服务。 */
|
|
27
|
+
export class MfaServiceImpl implements MfaService {
|
|
28
|
+
/** 校验登录阶段多因素认证。 */
|
|
29
|
+
VerifyMfa(request: VerifyMfaRequest): Promise<LoginResponse> {
|
|
30
|
+
return service<VerifyMfaRequest, LoginResponse>({
|
|
31
|
+
url: `${MFA_URL}/verify`,
|
|
32
|
+
method: "post",
|
|
33
|
+
data: request,
|
|
34
|
+
headers: { Authorization: "no-auth" }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 开始登录阶段的强制多因素认证绑定。 */
|
|
39
|
+
BeginMfaEnrollment(request: BeginMfaEnrollmentRequest): Promise<BeginMfaEnrollmentResponse> {
|
|
40
|
+
return service<BeginMfaEnrollmentRequest, BeginMfaEnrollmentResponse>({
|
|
41
|
+
url: `${MFA_URL}/enrollment`,
|
|
42
|
+
method: "post",
|
|
43
|
+
data: request,
|
|
44
|
+
headers: { Authorization: "no-auth" }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 确认登录阶段的强制多因素认证绑定。 */
|
|
49
|
+
ConfirmMfaEnrollment(request: ConfirmMfaEnrollmentRequest): Promise<ConfirmMfaEnrollmentResponse> {
|
|
50
|
+
return service<ConfirmMfaEnrollmentRequest, ConfirmMfaEnrollmentResponse>({
|
|
51
|
+
url: `${MFA_URL}/enrollment/confirm`,
|
|
52
|
+
method: "post",
|
|
53
|
+
data: request,
|
|
54
|
+
headers: { Authorization: "no-auth" }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 查询当前用户多因素认证状态。 */
|
|
59
|
+
GetMfaStatus(request: GetMfaStatusRequest): Promise<MfaStatusResponse> {
|
|
60
|
+
return service<GetMfaStatusRequest, MfaStatusResponse>({
|
|
61
|
+
url: MFA_URL,
|
|
62
|
+
method: "get",
|
|
63
|
+
params: request
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 开始绑定当前用户的多因素认证方式。 */
|
|
68
|
+
BeginMfaSetup(request: BeginMfaSetupRequest): Promise<BeginMfaSetupResponse> {
|
|
69
|
+
return service<BeginMfaSetupRequest, BeginMfaSetupResponse>({
|
|
70
|
+
url: `${MFA_URL}/setup`,
|
|
71
|
+
method: "post",
|
|
72
|
+
data: request
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 确认绑定当前用户的多因素认证方式。 */
|
|
77
|
+
ConfirmMfaSetup(request: ConfirmMfaSetupRequest): Promise<ConfirmMfaSetupResponse> {
|
|
78
|
+
return service<ConfirmMfaSetupRequest, ConfirmMfaSetupResponse>({
|
|
79
|
+
url: `${MFA_URL}/setup/confirm`,
|
|
80
|
+
method: "post",
|
|
81
|
+
data: request
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 开始禁用 WebAuthn 多因素认证的 Passkey 验证。 */
|
|
86
|
+
BeginMfaDisable(request: BeginMfaDisableRequest): Promise<BeginMfaDisableResponse> {
|
|
87
|
+
return service<BeginMfaDisableRequest, BeginMfaDisableResponse>({
|
|
88
|
+
url: `${MFA_URL}/disable/challenge`,
|
|
89
|
+
method: "post",
|
|
90
|
+
data: request
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 禁用当前用户的多因素认证。 */
|
|
95
|
+
DisableMfa(request: DisableMfaRequest): Promise<Empty> {
|
|
96
|
+
return service<DisableMfaRequest, Empty>({
|
|
97
|
+
url: `${MFA_URL}/disable`,
|
|
98
|
+
method: "post",
|
|
99
|
+
data: request
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 重新生成当前用户的多因素认证恢复码。 */
|
|
104
|
+
RegenerateMfaRecoveryCodes(request: RegenerateMfaRecoveryCodesRequest): Promise<RecoveryCodesResponse> {
|
|
105
|
+
return service<RegenerateMfaRecoveryCodesRequest, RecoveryCodesResponse>({
|
|
106
|
+
url: `${MFA_URL}/recovery-codes`,
|
|
107
|
+
method: "post",
|
|
108
|
+
data: request
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** defMfaService 多因素认证公共服务实例。 */
|
|
114
|
+
export const defMfaService = new MfaServiceImpl();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ProDialog
|
|
3
|
+
:model-value="modelValue"
|
|
4
|
+
:title="dialogTitle"
|
|
5
|
+
:width="width"
|
|
6
|
+
:top="top"
|
|
7
|
+
:confirm-text="confirmText"
|
|
8
|
+
:cancel-text="cancelText"
|
|
9
|
+
:confirm-loading="submitting"
|
|
10
|
+
:destroy-on-close="destroyOnClose"
|
|
11
|
+
:close-on-click-modal="closeOnClickModal"
|
|
12
|
+
:close-on-press-escape="closeOnPressEscape"
|
|
13
|
+
@update:model-value="handleVisibleChange"
|
|
14
|
+
@confirm="handleConfirm"
|
|
15
|
+
@cancel="handleCancel"
|
|
16
|
+
@close="handleClose"
|
|
17
|
+
@closed="handleClosed"
|
|
18
|
+
>
|
|
19
|
+
<p v-if="description" class="password-verify-dialog__description">{{ description }}</p>
|
|
20
|
+
<ProForm
|
|
21
|
+
ref="formRef"
|
|
22
|
+
:model="form"
|
|
23
|
+
:fields="fields"
|
|
24
|
+
:rules="rules"
|
|
25
|
+
:label-width="labelWidth"
|
|
26
|
+
@keyup.enter.prevent="handleConfirm"
|
|
27
|
+
/>
|
|
28
|
+
</ProDialog>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts" name="PasswordVerifyDialog">
|
|
32
|
+
import { computed, reactive, ref } from "vue";
|
|
33
|
+
import type { FormRules } from "element-plus";
|
|
34
|
+
import ProDialog from "../../components/Dialog/ProDialog.vue";
|
|
35
|
+
import ProForm from "../../components/ProForm/index.vue";
|
|
36
|
+
import type { ProFormField, ProFormInstance } from "../../components/ProForm/interface";
|
|
37
|
+
import { useLocaleStore } from "../../locales";
|
|
38
|
+
import { PASSWORD_CRYPTO_SCENE, encryptPassword } from "../../utils/passwordCrypto";
|
|
39
|
+
import type { PasswordCryptoScene } from "../../utils/passwordCrypto";
|
|
40
|
+
import type { PasswordCrypto } from "../../rpc/common/v1/types";
|
|
41
|
+
|
|
42
|
+
const { t } = useLocaleStore();
|
|
43
|
+
|
|
44
|
+
/** 通用密码验证弹窗属性。 */
|
|
45
|
+
interface PasswordVerifyDialogProps {
|
|
46
|
+
/** 弹窗显示状态。 */
|
|
47
|
+
modelValue: boolean;
|
|
48
|
+
/** 弹窗标题。 */
|
|
49
|
+
title?: string;
|
|
50
|
+
/** 密码输入前的可选补充说明。 */
|
|
51
|
+
description?: string;
|
|
52
|
+
/** 弹窗宽度。 */
|
|
53
|
+
width?: string | number;
|
|
54
|
+
/** 弹窗顶部距离。 */
|
|
55
|
+
top?: string;
|
|
56
|
+
/** 密码字段标题。 */
|
|
57
|
+
passwordLabel?: string;
|
|
58
|
+
/** 密码输入提示。 */
|
|
59
|
+
passwordPlaceholder?: string;
|
|
60
|
+
/** 确认按钮文案。 */
|
|
61
|
+
confirmText?: string;
|
|
62
|
+
/** 取消按钮文案。 */
|
|
63
|
+
cancelText?: string;
|
|
64
|
+
/** 外部业务提交状态。 */
|
|
65
|
+
confirmLoading?: boolean;
|
|
66
|
+
/** 密码加密场景。 */
|
|
67
|
+
scene?: PasswordCryptoScene;
|
|
68
|
+
/** 是否关闭时销毁内容。 */
|
|
69
|
+
destroyOnClose?: boolean;
|
|
70
|
+
/** 是否允许点击遮罩关闭。 */
|
|
71
|
+
closeOnClickModal?: boolean;
|
|
72
|
+
/** 是否允许按 Esc 关闭。 */
|
|
73
|
+
closeOnPressEscape?: boolean;
|
|
74
|
+
/** 表单标签宽度。 */
|
|
75
|
+
labelWidth?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
|
|
79
|
+
title: "",
|
|
80
|
+
description: "",
|
|
81
|
+
width: "500px",
|
|
82
|
+
top: "8vh",
|
|
83
|
+
passwordLabel: "",
|
|
84
|
+
passwordPlaceholder: "",
|
|
85
|
+
confirmText: "",
|
|
86
|
+
cancelText: "",
|
|
87
|
+
confirmLoading: false,
|
|
88
|
+
scene: PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
89
|
+
destroyOnClose: false,
|
|
90
|
+
closeOnClickModal: false,
|
|
91
|
+
closeOnPressEscape: true,
|
|
92
|
+
labelWidth: "auto"
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const emit = defineEmits<{
|
|
96
|
+
"update:modelValue": [value: boolean];
|
|
97
|
+
confirm: [password: PasswordCrypto];
|
|
98
|
+
cancel: [];
|
|
99
|
+
close: [];
|
|
100
|
+
closed: [];
|
|
101
|
+
}>();
|
|
102
|
+
|
|
103
|
+
const formRef = ref<ProFormInstance>();
|
|
104
|
+
const form = reactive({ password: "" });
|
|
105
|
+
const encrypting = ref(false);
|
|
106
|
+
const submitting = computed(() => props.confirmLoading || encrypting.value);
|
|
107
|
+
const dialogTitle = computed(() => props.title || t("core.password.verify_title"));
|
|
108
|
+
const passwordLabel = computed(() => props.passwordLabel || t("core.password.verify_label"));
|
|
109
|
+
const passwordPlaceholder = computed(() => props.passwordPlaceholder || t("core.password.required"));
|
|
110
|
+
const confirmText = computed(() => props.confirmText || t("common.action.confirm"));
|
|
111
|
+
const cancelText = computed(() => props.cancelText || t("common.action.cancel"));
|
|
112
|
+
|
|
113
|
+
const fields = computed<ProFormField[]>(() => [
|
|
114
|
+
{
|
|
115
|
+
prop: "password",
|
|
116
|
+
label: passwordLabel.value,
|
|
117
|
+
component: "password",
|
|
118
|
+
props: {
|
|
119
|
+
autocomplete: "current-password",
|
|
120
|
+
placeholder: passwordPlaceholder.value,
|
|
121
|
+
showPassword: true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const rules = computed<FormRules>(() => ({
|
|
127
|
+
password: [{ required: true, message: t("core.password.required"), trigger: "blur" }]
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
/** 同步弹窗显示状态到外部。 */
|
|
131
|
+
function handleVisibleChange(value: boolean) {
|
|
132
|
+
emit("update:modelValue", value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 校验密码并向业务方返回加密结果。 */
|
|
136
|
+
async function handleConfirm() {
|
|
137
|
+
if (submitting.value) return;
|
|
138
|
+
if (!(await formRef.value?.validate())) return;
|
|
139
|
+
encrypting.value = true;
|
|
140
|
+
try {
|
|
141
|
+
const password = await encryptPassword(form.password, props.scene);
|
|
142
|
+
emit("confirm", password);
|
|
143
|
+
} finally {
|
|
144
|
+
encrypting.value = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 对外透传取消事件。 */
|
|
149
|
+
function handleCancel() {
|
|
150
|
+
emit("cancel");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 对外透传关闭事件。 */
|
|
154
|
+
function handleClose() {
|
|
155
|
+
emit("close");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 关闭弹窗后清理密码和校验状态。 */
|
|
159
|
+
function handleClosed() {
|
|
160
|
+
form.password = "";
|
|
161
|
+
formRef.value?.clearValidate();
|
|
162
|
+
emit("closed");
|
|
163
|
+
}
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<style scoped lang="scss">
|
|
167
|
+
.password-verify-dialog__description {
|
|
168
|
+
margin: 0 0 16px;
|
|
169
|
+
color: var(--el-text-color-regular);
|
|
170
|
+
line-height: 1.6;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.password-verify-dialog__description + :deep(.pro-form) {
|
|
174
|
+
margin-top: 0;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ElLoading } from "element-plus";
|
|
2
|
+
import { t } from "../../locales";
|
|
2
3
|
|
|
3
4
|
/* 全局请求 loading */
|
|
4
5
|
let loadingInstance: ReturnType<typeof ElLoading.service>;
|
|
@@ -10,7 +11,7 @@ const startLoading = () => {
|
|
|
10
11
|
loadingInstance = ElLoading.service({
|
|
11
12
|
fullscreen: true,
|
|
12
13
|
lock: true,
|
|
13
|
-
text: "
|
|
14
|
+
text: t("core.loading.text"),
|
|
14
15
|
background: "rgba(0, 0, 0, 0.7)"
|
|
15
16
|
});
|
|
16
17
|
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
:theme="props.isDark ? 'dark' : 'light'"
|
|
8
8
|
preview-theme="github"
|
|
9
9
|
code-theme="github"
|
|
10
|
-
language="
|
|
10
|
+
:language="locale"
|
|
11
11
|
:show-code-row-number="false"
|
|
12
12
|
:code-foldable="false"
|
|
13
13
|
:no-mermaid="true"
|
|
@@ -26,6 +26,7 @@ import DOMPurify from "dompurify";
|
|
|
26
26
|
import hljs from "highlight.js";
|
|
27
27
|
import { config as configureMarkdownPreview, MdPreview } from "md-editor-v3";
|
|
28
28
|
import type { MdHeadingId } from "md-editor-v3";
|
|
29
|
+
import { getLocaleText, useLocaleStore } from "../../locales";
|
|
29
30
|
import "md-editor-v3/lib/preview.css";
|
|
30
31
|
|
|
31
32
|
defineOptions({
|
|
@@ -53,12 +54,31 @@ const props = withDefaults(defineProps<MarkdownPreviewProps>(), {
|
|
|
53
54
|
const generatedId = useId();
|
|
54
55
|
const resolvedId = computed(() => props.id || `core-markdown-preview-${generatedId.replaceAll(":", "")}`);
|
|
55
56
|
const markdownHeadingIdCounts = new Map<string, number>();
|
|
57
|
+
const { locale } = useLocaleStore();
|
|
56
58
|
|
|
57
59
|
configureMarkdownPreview({
|
|
58
60
|
editorExtensions: {
|
|
59
61
|
highlight: {
|
|
60
62
|
instance: hljs
|
|
61
63
|
}
|
|
64
|
+
},
|
|
65
|
+
editorConfig: {
|
|
66
|
+
languageUserDefined: {
|
|
67
|
+
"zh-TW": {
|
|
68
|
+
copyCode: {
|
|
69
|
+
text: getLocaleText("zh-TW", "core.markdown.action.copy_code"),
|
|
70
|
+
successTips: getLocaleText("zh-TW", "core.markdown.message.copy_success"),
|
|
71
|
+
failTips: getLocaleText("zh-TW", "core.markdown.message.copy_failed")
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"ja-JP": {
|
|
75
|
+
copyCode: {
|
|
76
|
+
text: getLocaleText("ja-JP", "core.markdown.action.copy_code"),
|
|
77
|
+
successTips: getLocaleText("ja-JP", "core.markdown.message.copy_success"),
|
|
78
|
+
failTips: getLocaleText("ja-JP", "core.markdown.message.copy_failed")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
62
82
|
}
|
|
63
83
|
});
|
|
64
84
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ProDialog
|
|
3
|
+
:model-value="modelValue"
|
|
4
|
+
:title="t('core.login.mfa_recovery_codes_title')"
|
|
5
|
+
width="420px"
|
|
6
|
+
:close-on-click-modal="false"
|
|
7
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
8
|
+
>
|
|
9
|
+
<div class="mfa-recovery-dialog__codes">
|
|
10
|
+
<span>{{ codesText }}</span>
|
|
11
|
+
<el-tooltip :content="t('core.login.mfa_copy_recovery_codes')" placement="top">
|
|
12
|
+
<el-button text circle :aria-label="t('core.login.mfa_copy_recovery_codes')" @click="copyRecoveryCodes">
|
|
13
|
+
<el-icon class="mfa-recovery-dialog__copy"><CopyDocument /></el-icon>
|
|
14
|
+
</el-button>
|
|
15
|
+
</el-tooltip>
|
|
16
|
+
</div>
|
|
17
|
+
<p class="mfa-recovery-dialog__warning">{{ t("core.login.mfa_recovery_codes_warning") }}</p>
|
|
18
|
+
<template #footer>
|
|
19
|
+
<el-button type="primary" @click="handleConfirm">{{ t("core.login.mfa_recovery_codes_confirm") }}</el-button>
|
|
20
|
+
</template>
|
|
21
|
+
</ProDialog>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts" name="MfaRecoveryCodesDialog">
|
|
25
|
+
import { computed } from "vue";
|
|
26
|
+
import ProDialog from "../../components/Dialog/ProDialog.vue";
|
|
27
|
+
import { useLocaleStore } from "../../locales";
|
|
28
|
+
import { copyText } from "../../utils/clipboard";
|
|
29
|
+
|
|
30
|
+
/** MFA 恢复码弹窗属性。 */
|
|
31
|
+
interface MfaRecoveryCodesDialogProps {
|
|
32
|
+
/** 弹窗显示状态。 */
|
|
33
|
+
modelValue: boolean;
|
|
34
|
+
/** 本次生成的一次性恢复码。 */
|
|
35
|
+
codes: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<MfaRecoveryCodesDialogProps>(), {
|
|
39
|
+
modelValue: false,
|
|
40
|
+
codes: () => []
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
"update:modelValue": [value: boolean];
|
|
45
|
+
confirm: [];
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
const { t } = useLocaleStore();
|
|
49
|
+
const codesText = computed(() => props.codes.join("\n"));
|
|
50
|
+
|
|
51
|
+
/** 复制当前显示的恢复码。 */
|
|
52
|
+
async function copyRecoveryCodes() {
|
|
53
|
+
await copyText(codesText.value);
|
|
54
|
+
ElMessage.success(t("core.login.mfa_recovery_codes_copied"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 关闭弹窗并通知调用方完成绑定流程。 */
|
|
58
|
+
function handleConfirm() {
|
|
59
|
+
emit("update:modelValue", false);
|
|
60
|
+
emit("confirm");
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<style scoped lang="scss">
|
|
65
|
+
.mfa-recovery-dialog__codes {
|
|
66
|
+
display: flex;
|
|
67
|
+
gap: 8px;
|
|
68
|
+
align-items: flex-start;
|
|
69
|
+
color: var(--el-text-color-regular);
|
|
70
|
+
line-height: 1.6;
|
|
71
|
+
overflow-wrap: anywhere;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.mfa-recovery-dialog__codes span {
|
|
75
|
+
flex: 1;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
white-space: pre-wrap;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mfa-recovery-dialog__copy {
|
|
81
|
+
color: var(--el-color-primary);
|
|
82
|
+
font-size: 18px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.mfa-recovery-dialog__warning {
|
|
86
|
+
margin: 8px 0 0;
|
|
87
|
+
color: var(--el-color-warning);
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
line-height: 1.6;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="uri" class="mfa-setup-panel">
|
|
3
|
+
<img v-if="qrDataUrl" class="mfa-setup-panel__qr" :src="qrDataUrl" :alt="t('core.login.mfa_setup_qr_alt')" />
|
|
4
|
+
<el-collapse class="mfa-setup-panel__uri">
|
|
5
|
+
<el-collapse-item :title="t('core.login.mfa_show_uri')" name="uri">
|
|
6
|
+
<div class="mfa-setup-panel__value">
|
|
7
|
+
<span class="mfa-setup-panel__text">{{ uri }}</span>
|
|
8
|
+
<el-tooltip :content="t('core.login.mfa_copy_uri')" placement="top">
|
|
9
|
+
<el-button text circle :aria-label="t('core.login.mfa_copy_uri')" @click.stop="copySetupUri">
|
|
10
|
+
<el-icon class="mfa-setup-panel__copy"><CopyDocument /></el-icon>
|
|
11
|
+
</el-button>
|
|
12
|
+
</el-tooltip>
|
|
13
|
+
</div>
|
|
14
|
+
</el-collapse-item>
|
|
15
|
+
</el-collapse>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts" name="MfaSetupPanel">
|
|
20
|
+
import { computed } from "vue";
|
|
21
|
+
import qrcode from "qrcode-generator";
|
|
22
|
+
import { useLocaleStore } from "../../locales";
|
|
23
|
+
import { copyText } from "../../utils/clipboard";
|
|
24
|
+
|
|
25
|
+
/** MFA TOTP 绑定面板属性。 */
|
|
26
|
+
interface MfaSetupPanelProps {
|
|
27
|
+
/** TOTP 绑定地址。 */
|
|
28
|
+
uri: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = defineProps<MfaSetupPanelProps>();
|
|
32
|
+
const { t } = useLocaleStore();
|
|
33
|
+
|
|
34
|
+
const qrDataUrl = computed(() => {
|
|
35
|
+
if (!props.uri) return "";
|
|
36
|
+
const code = qrcode(0, "M");
|
|
37
|
+
code.addData(props.uri);
|
|
38
|
+
code.make();
|
|
39
|
+
return code.createDataURL(4, 8);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** 复制 TOTP 绑定地址。 */
|
|
43
|
+
async function copySetupUri() {
|
|
44
|
+
await copyText(props.uri);
|
|
45
|
+
ElMessage.success(t("core.login.mfa_uri_copied"));
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<style scoped lang="scss">
|
|
50
|
+
.mfa-setup-panel__qr {
|
|
51
|
+
display: block;
|
|
52
|
+
width: 220px;
|
|
53
|
+
height: 220px;
|
|
54
|
+
margin: 12px auto;
|
|
55
|
+
image-rendering: pixelated;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.mfa-setup-panel__uri {
|
|
59
|
+
margin-top: 12px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mfa-setup-panel__value {
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 8px;
|
|
65
|
+
align-items: flex-start;
|
|
66
|
+
color: var(--el-text-color-regular);
|
|
67
|
+
line-height: 1.6;
|
|
68
|
+
overflow-wrap: anywhere;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mfa-setup-panel__text {
|
|
72
|
+
flex: 1;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
white-space: pre-wrap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.mfa-setup-panel__copy {
|
|
78
|
+
color: var(--el-color-primary);
|
|
79
|
+
font-size: 18px;
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
<el-input-number v-else-if="field.component === 'input-number'" v-model="fieldValue" v-bind="fieldProps" />
|
|
15
15
|
|
|
16
|
+
<el-color-picker v-else-if="field.component === 'color-picker'" v-model="fieldValue" v-bind="fieldProps" />
|
|
17
|
+
|
|
16
18
|
<el-segmented v-else-if="field.component === 'segmented'" v-model="fieldValue" :options="fieldOptions" v-bind="fieldProps" />
|
|
17
19
|
|
|
18
20
|
<el-checkbox v-else-if="field.component === 'checkbox'" v-model="fieldValue" v-bind="fieldProps">
|
|
@@ -28,7 +30,12 @@
|
|
|
28
30
|
:label="option.label"
|
|
29
31
|
:value="option.value"
|
|
30
32
|
:disabled="option.disabled"
|
|
31
|
-
|
|
33
|
+
>
|
|
34
|
+
<span class="pro-form-option">
|
|
35
|
+
<component :is="option.icon" v-if="option.icon" />
|
|
36
|
+
<span>{{ option.label }}</span>
|
|
37
|
+
</span>
|
|
38
|
+
</el-option>
|
|
32
39
|
</el-select>
|
|
33
40
|
|
|
34
41
|
<el-radio-group v-else-if="field.component === 'radio-group'" v-model="fieldValue" v-bind="fieldProps">
|
|
@@ -189,3 +196,11 @@ const multipleKvValue = computed({
|
|
|
189
196
|
set: value => setFieldValue(value)
|
|
190
197
|
});
|
|
191
198
|
</script>
|
|
199
|
+
|
|
200
|
+
<style scoped lang="scss">
|
|
201
|
+
.pro-form-option {
|
|
202
|
+
display: inline-flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: 6px;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FormItemRule } from "element-plus";
|
|
2
|
+
import type { Component } from "vue";
|
|
2
3
|
|
|
3
4
|
/** ProForm 支持的字段组件类型。 */
|
|
4
5
|
export type ProFormComponentType =
|
|
@@ -6,6 +7,7 @@ export type ProFormComponentType =
|
|
|
6
7
|
| "password"
|
|
7
8
|
| "textarea"
|
|
8
9
|
| "input-number"
|
|
10
|
+
| "color-picker"
|
|
9
11
|
| "segmented"
|
|
10
12
|
| "switch"
|
|
11
13
|
| "checkbox"
|
|
@@ -31,6 +33,8 @@ export interface ProFormOption {
|
|
|
31
33
|
label: string;
|
|
32
34
|
value: string | number | boolean;
|
|
33
35
|
disabled?: boolean;
|
|
36
|
+
/** 下拉项前置图标。 */
|
|
37
|
+
icon?: Component;
|
|
34
38
|
children?: ProFormOption[];
|
|
35
39
|
/** Element Plus 懒加载树节点是否为叶子节点。 */
|
|
36
40
|
isLeaf?: boolean;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</el-table-column>
|
|
13
13
|
<template #empty>
|
|
14
14
|
<div class="table-empty">
|
|
15
|
-
<img src="../../../assets/images/notData.png" alt="
|
|
15
|
+
<img src="../../../assets/images/notData.png" :alt="t('core.table.no_configurable_columns')" />
|
|
16
16
|
<div>{{ t("core.table.no_configurable_columns") }}</div>
|
|
17
17
|
</div>
|
|
18
18
|
</template>
|
|
@@ -41,7 +41,11 @@
|
|
|
41
41
|
<el-tooltip v-if="showToolButton('refresh')" :content="t('core.table.refresh')" placement="top">
|
|
42
42
|
<el-button class="tool-button" :icon="Refresh" circle @click="handleToolRefresh" />
|
|
43
43
|
</el-tooltip>
|
|
44
|
-
<el-tooltip
|
|
44
|
+
<el-tooltip
|
|
45
|
+
v-if="showToolButton('setting') && columns.length"
|
|
46
|
+
:content="t('core.table.column_setting')"
|
|
47
|
+
placement="top"
|
|
48
|
+
>
|
|
45
49
|
<el-button class="tool-button" :icon="Operation" circle @click="openColSetting" />
|
|
46
50
|
</el-tooltip>
|
|
47
51
|
<el-tooltip
|
|
@@ -107,7 +111,7 @@
|
|
|
107
111
|
<template #empty>
|
|
108
112
|
<div class="table-empty">
|
|
109
113
|
<slot name="empty">
|
|
110
|
-
<img src="../../assets/images/notData.png" alt="
|
|
114
|
+
<img src="../../assets/images/notData.png" :alt="t('common.message.no_data')" />
|
|
111
115
|
<div>{{ t("common.message.no_data") }}</div>
|
|
112
116
|
</slot>
|
|
113
117
|
</div>
|