@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.
Files changed (81) hide show
  1. package/README.md +7 -6
  2. package/build/auto-imports.json +73 -0
  3. package/build/plugins.ts +2 -58
  4. package/dist/package/declarations/src/api/base/mfa.d.ts +26 -0
  5. package/dist/package/declarations/src/components/Dialog/FormDialog.vue.d.ts +3 -3
  6. package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +65 -0
  7. package/dist/package/declarations/src/components/Dialog/ProDialog.vue.d.ts +1 -1
  8. package/dist/package/declarations/src/components/Grid/components/GridItem.vue.d.ts +1 -1
  9. package/dist/package/declarations/src/components/Mfa/MfaRecoveryCodesDialog.vue.d.ts +19 -0
  10. package/dist/package/declarations/src/components/Mfa/MfaSetupPanel.vue.d.ts +8 -0
  11. package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +4 -4
  12. package/dist/package/declarations/src/components/ProForm/index.vue.d.ts +1 -1
  13. package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +4 -1
  14. package/dist/package/declarations/src/components/RichTextPreview/index.vue.d.ts +8 -0
  15. package/dist/package/declarations/src/components/Upload/Img.vue.d.ts +1 -1
  16. package/dist/package/declarations/src/components/Upload/Imgs.vue.d.ts +1 -1
  17. package/dist/package/declarations/src/layouts/components/LockScreen.vue.d.ts +3 -0
  18. package/dist/package/declarations/src/locales/generated.d.ts +168 -0
  19. package/dist/package/declarations/src/locales/index.d.ts +2 -0
  20. package/dist/package/declarations/src/rpc/base/v1/login.d.ts +33 -3
  21. package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +152 -0
  22. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +25 -0
  23. package/dist/package/declarations/src/rpc/base/v1/oauth_client.d.ts +23 -0
  24. package/dist/package/declarations/src/rpc/system/admin/v1/base_i18n.d.ts +10 -6
  25. package/dist/package/declarations/src/security.d.ts +2 -0
  26. package/dist/package/declarations/src/stores/interface/index.d.ts +13 -0
  27. package/dist/package/declarations/src/stores/modules/lockScreen.d.ts +20 -0
  28. package/dist/package/declarations/src/stores/modules/user.d.ts +5 -2
  29. package/dist/package/declarations/src/stores/runtime.d.ts +1 -0
  30. package/dist/package/declarations/src/utils/clipboard.d.ts +2 -0
  31. package/dist/package/declarations/src/utils/webauthn.d.ts +4 -0
  32. package/dist/package/declarations/vite.config.d.ts +2 -0
  33. package/dist/package/src/api/base/mfa.ts +114 -0
  34. package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +176 -0
  35. package/dist/package/src/components/Loading/fullScreen.ts +2 -1
  36. package/dist/package/src/components/MarkdownPreview/index.vue +21 -1
  37. package/dist/package/src/components/Mfa/MfaRecoveryCodesDialog.vue +91 -0
  38. package/dist/package/src/components/Mfa/MfaSetupPanel.vue +81 -0
  39. package/dist/package/src/components/ProForm/components/ProFormItem.vue +16 -1
  40. package/dist/package/src/components/ProForm/interface/index.ts +4 -0
  41. package/dist/package/src/components/ProTable/components/ColSetting.vue +1 -1
  42. package/dist/package/src/components/ProTable/index.vue +6 -2
  43. package/dist/package/src/components/RichTextPreview/index.vue +45 -0
  44. package/dist/package/src/config/index.ts +1 -1
  45. package/dist/package/src/layouts/LayoutClassic/index.vue +3 -1
  46. package/dist/package/src/layouts/LayoutColumns/index.vue +3 -1
  47. package/dist/package/src/layouts/LayoutTransverse/index.vue +3 -1
  48. package/dist/package/src/layouts/LayoutVertical/index.vue +9 -1
  49. package/dist/package/src/layouts/components/Header/components/Avatar.vue +23 -4
  50. package/dist/package/src/layouts/components/Header/components/InfoDialog.vue +1 -1
  51. package/dist/package/src/layouts/components/Header/components/PasswordDialog.vue +1 -1
  52. package/dist/package/src/layouts/components/LockScreen.vue +450 -0
  53. package/dist/package/src/layouts/index.vue +2 -0
  54. package/dist/package/src/layouts/indexAsync.vue +2 -0
  55. package/dist/package/src/locales/README.md +2 -1
  56. package/dist/package/src/locales/en-US.json +43 -1
  57. package/dist/package/src/locales/index.ts +16 -12
  58. package/dist/package/src/locales/ja-JP.json +43 -1
  59. package/dist/package/src/locales/zh-CN.json +43 -1
  60. package/dist/package/src/locales/zh-TW.json +43 -1
  61. package/dist/package/src/rpc/base/v1/login.ts +33 -2
  62. package/dist/package/src/rpc/base/v1/mfa.ts +180 -0
  63. package/dist/package/src/rpc/base/v1/oauth.ts +25 -0
  64. package/dist/package/src/rpc/base/v1/oauth_client.ts +33 -0
  65. package/dist/package/src/rpc/system/admin/v1/base_i18n.ts +10 -6
  66. package/dist/package/src/security.ts +2 -0
  67. package/dist/package/src/stores/interface/index.ts +14 -0
  68. package/dist/package/src/stores/modules/lockScreen.ts +78 -0
  69. package/dist/package/src/stores/modules/user.ts +16 -3
  70. package/dist/package/src/stores/runtime.ts +1 -0
  71. package/dist/package/src/utils/clipboard.ts +22 -0
  72. package/dist/package/src/utils/request.ts +26 -10
  73. package/dist/package/src/utils/webauthn.ts +90 -0
  74. package/dist/package/src/views/login/components/LoginForm.vue +237 -16
  75. package/dist/package/src/views/login/index.vue +3 -1
  76. package/dist/package/types/generated/auto-imports.d.ts +132 -129
  77. package/dist/package/types/generated/components.d.ts +4 -72
  78. package/package.json +22 -1
  79. package/types/generated/auto-imports.d.ts +132 -129
  80. package/types/generated/components.d.ts +4 -72
  81. 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";
@@ -0,0 +1,2 @@
1
+ /** 将文本复制到系统剪贴板,并兼容非安全上下文的管理端开发地址。 */
2
+ export declare function copyText(content: string): Promise<void>;
@@ -0,0 +1,4 @@
1
+ /** 执行 WebAuthn 注册 ceremony。 */
2
+ export declare function createWebAuthnCredential(optionsJson: string): Promise<string>;
3
+ /** 执行 WebAuthn 登录 ceremony。 */
4
+ export declare function getWebAuthnAssertion(optionsJson: string): Promise<string>;
@@ -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: "Loading",
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="zh-CN"
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="notData" />
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 v-if="showToolButton('setting') && columns.length" :content="t('core.table.column_setting')" placement="top">
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="notData" />
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>