@liujitcn/kratos-admin-core 0.0.65 → 0.0.67
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/login.d.ts +1 -3
- package/dist/package/declarations/src/api/base/v1/secret_crypto.d.ts +9 -0
- package/dist/package/declarations/src/components/CodeBlock/index.vue.d.ts +15 -0
- package/dist/package/declarations/src/components/Dialog/PasswordVerifyDialog.vue.d.ts +3 -7
- package/dist/package/declarations/src/components/ProForm/components/ProFormItem.vue.d.ts +6 -6
- package/dist/package/declarations/src/components/ProForm/components/SecretInput.vue.d.ts +23 -0
- package/dist/package/declarations/src/components/ProForm/interface/index.d.ts +14 -1
- package/dist/package/declarations/src/components/YamlEditor/index.vue.d.ts +7 -1
- package/dist/package/declarations/src/locales/generated.d.ts +388 -0
- package/dist/package/declarations/src/rpc/base/v1/login.d.ts +2 -39
- package/dist/package/declarations/src/rpc/base/v1/mfa.d.ts +3 -3
- package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +2 -2
- package/dist/package/declarations/src/rpc/base/v1/secret_crypto.d.ts +37 -0
- package/dist/package/declarations/src/rpc/common/v1/types.d.ts +4 -4
- package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +3 -3
- package/dist/package/declarations/src/security.d.ts +1 -1
- package/dist/package/declarations/src/utils/secretCrypto.d.ts +19 -0
- package/dist/package/declarations/src/views/demo/components/DemoDetailDrawer.vue.d.ts +15 -0
- package/dist/package/declarations/src/views/demo/components/DemoFormDialog.vue.d.ts +69 -0
- package/dist/package/declarations/src/views/demo/demo-data.d.ts +145 -0
- package/dist/package/declarations/src/views/demo/table/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/demo/tree-grid/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/demo/tree-table/index.vue.d.ts +3 -0
- package/dist/package/src/api/base/v1/login.ts +0 -12
- package/dist/package/src/api/base/v1/secret_crypto.ts +35 -0
- package/dist/package/src/components/CodeBlock/index.vue +156 -0
- package/dist/package/src/components/Dialog/PasswordVerifyDialog.vue +4 -8
- package/dist/package/src/components/ProForm/components/ProFormItem.vue +29 -2
- package/dist/package/src/components/ProForm/components/SecretInput.vue +117 -0
- package/dist/package/src/components/ProForm/interface/index.ts +16 -0
- package/dist/package/src/components/YamlEditor/index.vue +103 -7
- package/dist/package/src/locales/README.md +1 -1
- package/dist/package/src/locales/en-US.json +98 -1
- package/dist/package/src/locales/ja-JP.json +98 -1
- package/dist/package/src/locales/zh-CN.json +98 -1
- package/dist/package/src/locales/zh-TW.json +98 -1
- package/dist/package/src/modules/kratosAdmin.ts +11 -1
- package/dist/package/src/rpc/base/v1/login.ts +2 -42
- package/dist/package/src/rpc/base/v1/mfa.ts +3 -3
- package/dist/package/src/rpc/base/v1/oauth.ts +2 -2
- package/dist/package/src/rpc/base/v1/secret_crypto.ts +49 -0
- package/dist/package/src/rpc/common/v1/types.ts +4 -4
- package/dist/package/src/rpc/system/admin/v1/auth.ts +3 -3
- package/dist/package/src/security.ts +1 -1
- package/dist/package/src/utils/index.ts +2 -1
- package/dist/package/src/utils/request.ts +5 -4
- package/dist/package/src/utils/{passwordCrypto.ts → secretCrypto.ts} +112 -50
- package/dist/package/src/views/demo/components/DemoDetailDrawer.vue +108 -0
- package/dist/package/src/views/demo/components/DemoFormDialog.vue +470 -0
- package/dist/package/src/views/demo/demo-data.ts +279 -0
- package/dist/package/src/views/demo/table/index.vue +391 -0
- package/dist/package/src/views/demo/tree-grid/index.vue +272 -0
- package/dist/package/src/views/demo/tree-table/index.vue +346 -0
- package/dist/package/src/views/login/components/LoginForm.vue +2 -2
- package/dist/package/types/generated/components.d.ts +4 -1
- package/package.json +14 -2
- package/src/runtime-messages.ts +39 -0
- package/types/generated/components.d.ts +4 -1
- package/dist/package/declarations/src/utils/passwordCrypto.d.ts +0 -6
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="core-code-block" :style="{ '--core-code-block-max-height': props.maxHeight }">
|
|
3
|
+
<button
|
|
4
|
+
class="core-code-block__copy"
|
|
5
|
+
type="button"
|
|
6
|
+
:title="t('core.markdown.action.copy_code')"
|
|
7
|
+
:aria-label="t('core.markdown.action.copy_code')"
|
|
8
|
+
@click="copyCode"
|
|
9
|
+
>
|
|
10
|
+
<el-icon>
|
|
11
|
+
<Select v-if="copied" />
|
|
12
|
+
<CopyDocument v-else />
|
|
13
|
+
</el-icon>
|
|
14
|
+
</button>
|
|
15
|
+
<pre class="core-code-block__pre"><code class="hljs" v-html="highlightedHtml"></code></pre>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, ref } from "vue";
|
|
21
|
+
import { CopyDocument, Select } from "@element-plus/icons-vue";
|
|
22
|
+
import hljs from "highlight.js";
|
|
23
|
+
import { t } from "../../locales";
|
|
24
|
+
import { copyText } from "../../utils/clipboard";
|
|
25
|
+
|
|
26
|
+
defineOptions({ name: "CoreCodeBlock" });
|
|
27
|
+
|
|
28
|
+
/** 代码块组件属性。 */
|
|
29
|
+
interface CodeBlockProps {
|
|
30
|
+
/** 代码原文。 */
|
|
31
|
+
code: string;
|
|
32
|
+
/** 高亮语言,缺省时自动检测。 */
|
|
33
|
+
language?: string;
|
|
34
|
+
/** 代码区最大高度。 */
|
|
35
|
+
maxHeight?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const props = withDefaults(defineProps<CodeBlockProps>(), {
|
|
39
|
+
language: "",
|
|
40
|
+
maxHeight: "420px"
|
|
41
|
+
});
|
|
42
|
+
const copied = ref(false);
|
|
43
|
+
let copiedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
44
|
+
|
|
45
|
+
const highlightedHtml = computed(() => {
|
|
46
|
+
const code = props.code ?? "";
|
|
47
|
+
if (props.language && hljs.getLanguage(props.language)) {
|
|
48
|
+
return hljs.highlight(code, { language: props.language, ignoreIllegals: true }).value;
|
|
49
|
+
}
|
|
50
|
+
return hljs.highlightAuto(code).value;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** 复制代码原文并短暂反馈复制结果。 */
|
|
54
|
+
async function copyCode() {
|
|
55
|
+
try {
|
|
56
|
+
await copyText(props.code ?? "");
|
|
57
|
+
} catch {
|
|
58
|
+
ElMessage.error(t("core.markdown.message.copy_failed"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
ElMessage.success(t("core.clipboard.success"));
|
|
62
|
+
copied.value = true;
|
|
63
|
+
clearTimeout(copiedTimer);
|
|
64
|
+
copiedTimer = setTimeout(() => {
|
|
65
|
+
copied.value = false;
|
|
66
|
+
}, 2000);
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<style scoped lang="scss">
|
|
71
|
+
.core-code-block {
|
|
72
|
+
position: relative;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
background: var(--admin-page-card-bg-muted);
|
|
75
|
+
border: 1px solid var(--admin-page-card-border-soft);
|
|
76
|
+
border-radius: var(--admin-page-radius);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.core-code-block__copy {
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 8px;
|
|
82
|
+
right: 8px;
|
|
83
|
+
z-index: 1;
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
width: 26px;
|
|
88
|
+
height: 26px;
|
|
89
|
+
padding: 0;
|
|
90
|
+
color: var(--admin-page-text-secondary);
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
background: var(--admin-page-card-bg);
|
|
93
|
+
border: 1px solid var(--admin-page-card-border-soft);
|
|
94
|
+
border-radius: var(--admin-page-radius);
|
|
95
|
+
box-shadow: var(--admin-page-shadow);
|
|
96
|
+
opacity: 0;
|
|
97
|
+
transition: opacity 0.15s ease;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.core-code-block:hover .core-code-block__copy,
|
|
101
|
+
.core-code-block__copy:focus-visible {
|
|
102
|
+
opacity: 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.core-code-block__pre {
|
|
106
|
+
max-height: var(--core-code-block-max-height);
|
|
107
|
+
margin: 0;
|
|
108
|
+
overflow: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.core-code-block__pre code.hljs {
|
|
112
|
+
display: block;
|
|
113
|
+
padding: 10px 12px;
|
|
114
|
+
margin: 0;
|
|
115
|
+
font-size: 13px;
|
|
116
|
+
line-height: 1.6;
|
|
117
|
+
color: var(--admin-page-text-primary);
|
|
118
|
+
white-space: pre-wrap;
|
|
119
|
+
word-break: break-word;
|
|
120
|
+
background: transparent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.core-code-block :deep(.hljs-comment),
|
|
124
|
+
.core-code-block :deep(.hljs-quote) {
|
|
125
|
+
font-style: italic;
|
|
126
|
+
color: var(--admin-page-text-secondary);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.core-code-block :deep(.hljs-keyword),
|
|
130
|
+
.core-code-block :deep(.hljs-selector-tag),
|
|
131
|
+
.core-code-block :deep(.hljs-built_in),
|
|
132
|
+
.core-code-block :deep(.hljs-name) {
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
color: var(--el-color-primary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.core-code-block :deep(.hljs-string),
|
|
138
|
+
.core-code-block :deep(.hljs-attr),
|
|
139
|
+
.core-code-block :deep(.hljs-symbol),
|
|
140
|
+
.core-code-block :deep(.hljs-bullet) {
|
|
141
|
+
color: var(--el-color-success);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.core-code-block :deep(.hljs-number),
|
|
145
|
+
.core-code-block :deep(.hljs-literal),
|
|
146
|
+
.core-code-block :deep(.hljs-variable),
|
|
147
|
+
.core-code-block :deep(.hljs-template-variable) {
|
|
148
|
+
color: var(--el-color-warning);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.core-code-block :deep(.hljs-title),
|
|
152
|
+
.core-code-block :deep(.hljs-section),
|
|
153
|
+
.core-code-block :deep(.hljs-type) {
|
|
154
|
+
color: var(--el-color-danger);
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -36,9 +36,8 @@ import ProDialog from "../../components/Dialog/ProDialog.vue";
|
|
|
36
36
|
import ProForm from "../../components/ProForm/index.vue";
|
|
37
37
|
import type { ProFormField, ProFormInstance, ProFormLabelPosition } from "../../components/ProForm/interface";
|
|
38
38
|
import { useLocaleStore } from "../../locales";
|
|
39
|
-
import {
|
|
40
|
-
import type {
|
|
41
|
-
import type { PasswordCrypto } from "../../rpc/common/v1/types";
|
|
39
|
+
import { encryptPassword } from "../../utils/secretCrypto";
|
|
40
|
+
import type { SecretCrypto } from "../../rpc/common/v1/types";
|
|
42
41
|
|
|
43
42
|
const { t } = useLocaleStore();
|
|
44
43
|
|
|
@@ -64,8 +63,6 @@ interface PasswordVerifyDialogProps {
|
|
|
64
63
|
cancelText?: string;
|
|
65
64
|
/** 外部业务提交状态。 */
|
|
66
65
|
confirmLoading?: boolean;
|
|
67
|
-
/** 密码加密场景。 */
|
|
68
|
-
scene?: PasswordCryptoScene;
|
|
69
66
|
/** 是否关闭时销毁内容。 */
|
|
70
67
|
destroyOnClose?: boolean;
|
|
71
68
|
/** 是否允许点击遮罩关闭。 */
|
|
@@ -88,7 +85,6 @@ const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
|
|
|
88
85
|
confirmText: "",
|
|
89
86
|
cancelText: "",
|
|
90
87
|
confirmLoading: false,
|
|
91
|
-
scene: PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
92
88
|
destroyOnClose: false,
|
|
93
89
|
closeOnClickModal: false,
|
|
94
90
|
closeOnPressEscape: true,
|
|
@@ -98,7 +94,7 @@ const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
|
|
|
98
94
|
|
|
99
95
|
const emit = defineEmits<{
|
|
100
96
|
"update:modelValue": [value: boolean];
|
|
101
|
-
confirm: [password:
|
|
97
|
+
confirm: [password: SecretCrypto];
|
|
102
98
|
cancel: [];
|
|
103
99
|
close: [];
|
|
104
100
|
closed: [];
|
|
@@ -142,7 +138,7 @@ async function handleConfirm() {
|
|
|
142
138
|
if (!(await formRef.value?.validate())) return;
|
|
143
139
|
encrypting.value = true;
|
|
144
140
|
try {
|
|
145
|
-
const password = await encryptPassword(form.password
|
|
141
|
+
const password = await encryptPassword(form.password);
|
|
146
142
|
emit("confirm", password);
|
|
147
143
|
} finally {
|
|
148
144
|
encrypting.value = false;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<SecretInput
|
|
3
|
+
v-if="field.component === 'input' && field.secret"
|
|
4
|
+
v-model="fieldValue"
|
|
5
|
+
v-bind="secretInputProps"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<el-input v-else-if="field.component === 'input'" v-model.trim="fieldValue" v-bind="fieldProps">
|
|
3
9
|
<template v-if="field.suffixSlotName" #append>
|
|
4
10
|
<slot :name="field.suffixSlotName" :field="field" :model="model" />
|
|
5
11
|
</template>
|
|
@@ -10,11 +16,14 @@
|
|
|
10
16
|
v-model.trim="fieldValue"
|
|
11
17
|
type="password"
|
|
12
18
|
show-password
|
|
19
|
+
autocomplete="new-password"
|
|
13
20
|
v-bind="fieldProps"
|
|
14
21
|
/>
|
|
15
22
|
|
|
16
23
|
<el-input v-else-if="field.component === 'textarea'" v-model.trim="fieldValue" type="textarea" v-bind="fieldProps" />
|
|
17
24
|
|
|
25
|
+
<YamlEditor v-else-if="field.component === 'yaml-editor'" v-model="fieldValue" v-bind="fieldProps" />
|
|
26
|
+
|
|
18
27
|
<el-input-number v-else-if="field.component === 'input-number'" v-model="fieldValue" v-bind="fieldProps" />
|
|
19
28
|
|
|
20
29
|
<el-color-picker v-else-if="field.component === 'color-picker'" v-model="fieldValue" v-bind="fieldProps" />
|
|
@@ -42,6 +51,8 @@
|
|
|
42
51
|
</el-option>
|
|
43
52
|
</el-select>
|
|
44
53
|
|
|
54
|
+
<el-cascader v-else-if="field.component === 'cascader'" v-model="fieldValue" :options="fieldOptions" v-bind="fieldProps" />
|
|
55
|
+
|
|
45
56
|
<TenantSelect v-else-if="field.component === 'tenant-select'" v-model="fieldValue" v-bind="fieldProps" />
|
|
46
57
|
|
|
47
58
|
<el-radio-group v-else-if="field.component === 'radio-group'" v-model="fieldValue" v-bind="fieldProps">
|
|
@@ -51,7 +62,7 @@
|
|
|
51
62
|
</el-radio-group>
|
|
52
63
|
|
|
53
64
|
<el-checkbox-group v-else-if="field.component === 'checkbox-group'" v-model="fieldValue" v-bind="fieldProps">
|
|
54
|
-
<el-checkbox v-for="option in fieldOptions" :key="String(option.value)" :
|
|
65
|
+
<el-checkbox v-for="option in fieldOptions" :key="String(option.value)" :value="option.value" :disabled="option.disabled">
|
|
55
66
|
{{ option.label }}
|
|
56
67
|
</el-checkbox>
|
|
57
68
|
</el-checkbox-group>
|
|
@@ -99,10 +110,12 @@ import type { UploadUserFile } from "element-plus";
|
|
|
99
110
|
import type { ProFormField, ProFormOption } from "../../../components/ProForm/interface";
|
|
100
111
|
import Dict from "../../../components/Dict/index.vue";
|
|
101
112
|
import TenantSelect from "../../../components/TenantSelect/index.vue";
|
|
113
|
+
import SecretInput from "../../../components/ProForm/components/SecretInput.vue";
|
|
102
114
|
|
|
103
115
|
// 非基础表单控件按需加载,避免 ProForm 基础包携带上传、富文本、Cron 等重组件。
|
|
104
116
|
const CronExpression = defineAsyncComponent(() => import("../../../components/CronExpression/index.vue"));
|
|
105
117
|
const WangEditor = defineAsyncComponent(() => import("../../../components/WangEditor/index.vue"));
|
|
118
|
+
const YamlEditor = defineAsyncComponent(() => import("../../../components/YamlEditor/index.vue"));
|
|
106
119
|
const DynamicList = defineAsyncComponent(() => import("../../../components/ProForm/components/DynamicList.vue"));
|
|
107
120
|
const KvList = defineAsyncComponent(() => import("../../../components/ProForm/components/KvList.vue"));
|
|
108
121
|
const UploadFile = defineAsyncComponent(() => import("../../../components/Upload/File.vue"));
|
|
@@ -124,6 +137,20 @@ const fieldProps = computed(() => {
|
|
|
124
137
|
return typeof props.field.props === "function" ? props.field.props(props.model) : props.field.props;
|
|
125
138
|
});
|
|
126
139
|
|
|
140
|
+
/** 解析 secret 输入参数:透传输入参数并注入查看所需的资源信息。 */
|
|
141
|
+
const secretInputProps = computed(() => {
|
|
142
|
+
const { showPassword: _ignored, ...rest } = (fieldProps.value ?? {}) as Record<string, any>;
|
|
143
|
+
const config = typeof props.field.secret === "object" ? props.field.secret : null;
|
|
144
|
+
return {
|
|
145
|
+
...rest,
|
|
146
|
+
secret: props.field.secret,
|
|
147
|
+
resource: config?.resource ?? "",
|
|
148
|
+
fieldName: config?.field ?? "",
|
|
149
|
+
recordId: config ? Number(props.model[config.idProp ?? "id"] ?? 0) : 0,
|
|
150
|
+
configured: config ? Boolean(props.model[config.configuredProp ?? `${config.field}_configured`]) : false
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
127
154
|
/** 解析字段选项参数,支持静态数组和函数。 */
|
|
128
155
|
const fieldOptions = computed<ProFormOption[]>(() => {
|
|
129
156
|
if (!props.field.options) return [];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-input
|
|
3
|
+
:model-value="modelValue"
|
|
4
|
+
:type="visible ? 'text' : 'password'"
|
|
5
|
+
autocomplete="new-password"
|
|
6
|
+
v-bind="inputAttrs"
|
|
7
|
+
@update:model-value="onInput"
|
|
8
|
+
>
|
|
9
|
+
<template #suffix>
|
|
10
|
+
<el-icon class="secret-input__toggle" @click="toggle">
|
|
11
|
+
<Loading v-if="loading" />
|
|
12
|
+
<View v-else-if="visible" />
|
|
13
|
+
<Hide v-else />
|
|
14
|
+
</el-icon>
|
|
15
|
+
</template>
|
|
16
|
+
</el-input>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, ref, useAttrs } from "vue";
|
|
21
|
+
import { ElMessage } from "element-plus";
|
|
22
|
+
import { Hide, Loading, View } from "@element-plus/icons-vue";
|
|
23
|
+
import type { ProFormSecretConfig } from "../../../components/ProForm/interface";
|
|
24
|
+
import { revealSecret } from "../../../utils/secretCrypto";
|
|
25
|
+
import { t } from "../../../locales";
|
|
26
|
+
|
|
27
|
+
/** SecretInput 组件属性。 */
|
|
28
|
+
interface SecretInputProps {
|
|
29
|
+
/** 字段值。 */
|
|
30
|
+
modelValue?: string;
|
|
31
|
+
/** 查看配置:声明后眼睛点击时按需拉取明文。 */
|
|
32
|
+
secret?: boolean | ProFormSecretConfig;
|
|
33
|
+
/** 查看的资源标识。 */
|
|
34
|
+
resource?: string;
|
|
35
|
+
/** 查看的字段名。 */
|
|
36
|
+
fieldName?: string;
|
|
37
|
+
/** 记录ID。 */
|
|
38
|
+
recordId?: number;
|
|
39
|
+
/** 是否已配置密钥。 */
|
|
40
|
+
configured?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const props = defineProps<SecretInputProps>();
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
(event: "update:modelValue", value: string): void;
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
/** 明文是否可见。 */
|
|
50
|
+
const visible = ref(false);
|
|
51
|
+
/** 明文是否来自远端查看。 */
|
|
52
|
+
const revealed = ref(false);
|
|
53
|
+
/** 查看请求进行中。 */
|
|
54
|
+
const loading = ref(false);
|
|
55
|
+
|
|
56
|
+
/** 解析查看配置。 */
|
|
57
|
+
const secretConfig = computed<ProFormSecretConfig | null>(() =>
|
|
58
|
+
typeof props.secret === "object" ? props.secret : null
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// 透传调用方的 el-input 参数(maxlength、placeholder 等)。
|
|
62
|
+
const attrs = useAttrs();
|
|
63
|
+
|
|
64
|
+
/** 过滤掉原生密码框专属参数后透传其余输入参数。 */
|
|
65
|
+
const inputAttrs = computed(() => {
|
|
66
|
+
const { showPassword: _ignored, ...rest } = { ...(attrs as Record<string, unknown>) };
|
|
67
|
+
return rest;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** 判断当前是否可以查看远端明文。 */
|
|
71
|
+
function canReveal() {
|
|
72
|
+
return Boolean(
|
|
73
|
+
secretConfig.value && props.resource && props.fieldName && (props.recordId ?? 0) > 0 && props.configured
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 切换明文可见状态:查看态隐藏时清空回填的明文,保持"留空表示保留"语义。 */
|
|
78
|
+
async function toggle() {
|
|
79
|
+
if (loading.value) return;
|
|
80
|
+
if (visible.value) {
|
|
81
|
+
visible.value = false;
|
|
82
|
+
if (revealed.value) {
|
|
83
|
+
revealed.value = false;
|
|
84
|
+
emit("update:modelValue", "");
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (props.modelValue) {
|
|
89
|
+
visible.value = true;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!canReveal()) return;
|
|
93
|
+
loading.value = true;
|
|
94
|
+
try {
|
|
95
|
+
const text = await revealSecret(props.resource!, (props.recordId ?? 0), props.fieldName!);
|
|
96
|
+
emit("update:modelValue", text);
|
|
97
|
+
revealed.value = true;
|
|
98
|
+
visible.value = true;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
ElMessage.error(error instanceof Error ? error.message : t("core.password.reveal_failed"));
|
|
101
|
+
} finally {
|
|
102
|
+
loading.value = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 输入回调:本地输入的值不再视为远端明文。 */
|
|
107
|
+
function onInput(value: string) {
|
|
108
|
+
revealed.value = false;
|
|
109
|
+
emit("update:modelValue", value);
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<style scoped lang="scss">
|
|
114
|
+
.secret-input__toggle {
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -6,12 +6,14 @@ export type ProFormComponentType =
|
|
|
6
6
|
| "input"
|
|
7
7
|
| "password"
|
|
8
8
|
| "textarea"
|
|
9
|
+
| "yaml-editor"
|
|
9
10
|
| "input-number"
|
|
10
11
|
| "color-picker"
|
|
11
12
|
| "segmented"
|
|
12
13
|
| "switch"
|
|
13
14
|
| "checkbox"
|
|
14
15
|
| "select"
|
|
16
|
+
| "cascader"
|
|
15
17
|
| "tenant-select"
|
|
16
18
|
| "dict"
|
|
17
19
|
| "radio-group"
|
|
@@ -44,6 +46,18 @@ export interface ProFormOption {
|
|
|
44
46
|
isLeaf?: boolean;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/** secret 字段的查看配置:声明后输入框眼睛点击时按需拉取明文。 */
|
|
50
|
+
export interface ProFormSecretConfig {
|
|
51
|
+
/** 查看明文的资源标识。 */
|
|
52
|
+
resource: string;
|
|
53
|
+
/** 查看明文的字段名。 */
|
|
54
|
+
field: string;
|
|
55
|
+
/** 记录ID字段名,默认 "id"。 */
|
|
56
|
+
idProp?: string;
|
|
57
|
+
/** 已配置标记字段名,默认 `${field}_configured`。 */
|
|
58
|
+
configuredProp?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
/** ProForm 字段配置。 */
|
|
48
62
|
export interface ProFormField {
|
|
49
63
|
/** 字段绑定路径,支持点路径访问嵌套对象。 */
|
|
@@ -68,6 +82,8 @@ export interface ProFormField {
|
|
|
68
82
|
suffixSlotName?: string;
|
|
69
83
|
/** 标题提示文案。 */
|
|
70
84
|
labelTooltip?: string;
|
|
85
|
+
/** 提交时是否加密为 SecretCrypto 结构;配置对象时输入框眼睛支持按需查看明文。 */
|
|
86
|
+
secret?: boolean | ProFormSecretConfig;
|
|
71
87
|
/** 字段校验规则。 */
|
|
72
88
|
rules?: FormItemRule[];
|
|
73
89
|
/** 字段是否显示。 */
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="yaml-editor">
|
|
3
|
-
<div
|
|
2
|
+
<div class="yaml-editor" :class="{ 'is-invalid': invalid, 'is-bounded': !!maxHeight }">
|
|
3
|
+
<div class="yaml-editor__toolbar">
|
|
4
|
+
<span class="yaml-editor__lang">
|
|
5
|
+
<el-icon><Document /></el-icon>
|
|
6
|
+
<span>YAML</span>
|
|
7
|
+
</span>
|
|
8
|
+
<div class="yaml-editor__actions">
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
class="yaml-editor__action"
|
|
12
|
+
:class="{ 'is-active': wrapEnabled }"
|
|
13
|
+
:title="t('core.yaml_editor.action.wrap')"
|
|
14
|
+
:aria-label="t('core.yaml_editor.action.wrap')"
|
|
15
|
+
@click="toggleWrap"
|
|
16
|
+
>
|
|
17
|
+
<el-icon :size="15"><WrapText /></el-icon>
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
class="yaml-editor__action"
|
|
22
|
+
:title="t('core.markdown.action.copy_code')"
|
|
23
|
+
:aria-label="t('core.markdown.action.copy_code')"
|
|
24
|
+
@click="copyCode"
|
|
25
|
+
>
|
|
26
|
+
<el-icon :size="15">
|
|
27
|
+
<Select v-if="copied" />
|
|
28
|
+
<CopyDocument v-else />
|
|
29
|
+
</el-icon>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div ref="editorHost" class="yaml-editor__host" />
|
|
4
34
|
<div class="yaml-editor__footer">
|
|
5
35
|
<span>{{ statusText }}</span>
|
|
6
36
|
<span>{{ lineCount }} {{ lineLabel }}</span>
|
|
@@ -11,14 +41,25 @@
|
|
|
11
41
|
<script setup lang="ts">
|
|
12
42
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
13
43
|
import { basicSetup, EditorView } from "codemirror";
|
|
44
|
+
import { Compartment } from "@codemirror/state";
|
|
45
|
+
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
|
46
|
+
import { tags } from "@lezer/highlight";
|
|
14
47
|
import { yaml } from "@codemirror/lang-yaml";
|
|
15
48
|
import { placeholder } from "@codemirror/view";
|
|
49
|
+
import { CopyDocument, Document, Select } from "@element-plus/icons-vue";
|
|
50
|
+
import { WrapText } from "@lucide/vue";
|
|
51
|
+
import { t } from "../../locales";
|
|
52
|
+
import { copyText } from "../../utils/clipboard";
|
|
16
53
|
|
|
17
54
|
/** YAML 编辑器组件属性。 */
|
|
18
55
|
interface YamlEditorProps {
|
|
19
56
|
modelValue: string;
|
|
20
57
|
placeholderText?: string;
|
|
21
58
|
minHeight?: string;
|
|
59
|
+
/** 编辑器最大高度;内容超出后编辑器不再撑高页面,改为内部滚动。 */
|
|
60
|
+
maxHeight?: string;
|
|
61
|
+
/** 是否默认开启自动换行,可通过工具栏切换。 */
|
|
62
|
+
wrap?: boolean;
|
|
22
63
|
invalid?: boolean;
|
|
23
64
|
lineLabel?: string;
|
|
24
65
|
/** 根据光标行列生成状态栏文案。 */
|
|
@@ -28,6 +69,8 @@ interface YamlEditorProps {
|
|
|
28
69
|
const props = withDefaults(defineProps<YamlEditorProps>(), {
|
|
29
70
|
placeholderText: "",
|
|
30
71
|
minHeight: "280px",
|
|
72
|
+
maxHeight: undefined,
|
|
73
|
+
wrap: true,
|
|
31
74
|
invalid: false,
|
|
32
75
|
lineLabel: "lines",
|
|
33
76
|
formatPosition: (line: number, column: number) => `Ln ${line}, Col ${column}`
|
|
@@ -37,8 +80,23 @@ const emit = defineEmits<{ "update:modelValue": [value: string] }>();
|
|
|
37
80
|
const editorHost = ref<HTMLElement>();
|
|
38
81
|
const lineCount = ref(1);
|
|
39
82
|
const statusText = ref("");
|
|
83
|
+
const wrapEnabled = ref(props.wrap);
|
|
84
|
+
const copied = ref(false);
|
|
85
|
+
let copiedTimer: ReturnType<typeof setTimeout> | undefined;
|
|
40
86
|
let editor: EditorView | undefined;
|
|
41
87
|
|
|
88
|
+
const wrapCompartment = new Compartment();
|
|
89
|
+
|
|
90
|
+
/** 与管理端配色一致的 YAML 高亮样式(键绿、串蓝、值橙、注释灰斜体)。 */
|
|
91
|
+
const yamlHighlightStyle = HighlightStyle.define([
|
|
92
|
+
{ tag: [tags.propertyName, tags.attributeName], color: "var(--el-color-success)" },
|
|
93
|
+
{ tag: [tags.string, tags.attributeValue], color: "var(--el-color-primary)" },
|
|
94
|
+
{ tag: [tags.number, tags.bool, tags.keyword], color: "var(--el-color-warning)" },
|
|
95
|
+
{ tag: [tags.lineComment, tags.blockComment], color: "var(--el-text-color-secondary)", fontStyle: "italic" },
|
|
96
|
+
{ tag: [tags.typeName, tags.labelName, tags.definition(tags.variableName)], color: "var(--el-color-danger)" },
|
|
97
|
+
{ tag: [tags.meta, tags.separator, tags.punctuation], color: "var(--el-text-color-placeholder)" }
|
|
98
|
+
]);
|
|
99
|
+
|
|
42
100
|
const editorTheme = EditorView.theme({
|
|
43
101
|
"&": { color: "var(--el-text-color-primary)", backgroundColor: "var(--el-fill-color-blank)", fontSize: "13px" },
|
|
44
102
|
".cm-content": { minHeight: props.minHeight, padding: "12px 0", caretColor: "var(--el-color-primary)" },
|
|
@@ -59,7 +117,8 @@ onMounted(() => {
|
|
|
59
117
|
basicSetup,
|
|
60
118
|
yaml(),
|
|
61
119
|
editorTheme,
|
|
62
|
-
|
|
120
|
+
syntaxHighlighting(yamlHighlightStyle),
|
|
121
|
+
wrapCompartment.of(wrapEnabled.value ? EditorView.lineWrapping : []),
|
|
63
122
|
EditorView.contentAttributes.of({ "aria-label": "YAML editor", spellcheck: "false" }),
|
|
64
123
|
EditorView.updateListener.of(update => {
|
|
65
124
|
if (!update.docChanged) return;
|
|
@@ -88,20 +147,57 @@ onMounted(() => {
|
|
|
88
147
|
lineCount.value = editor.state.doc.lines;
|
|
89
148
|
});
|
|
90
149
|
|
|
150
|
+
/** 切换自动换行,通过 Compartment 热更新编辑器扩展。 */
|
|
151
|
+
function toggleWrap() {
|
|
152
|
+
wrapEnabled.value = !wrapEnabled.value;
|
|
153
|
+
editor?.dispatch({
|
|
154
|
+
effects: wrapCompartment.reconfigure(wrapEnabled.value ? EditorView.lineWrapping : [])
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 复制编辑器全文并短暂反馈复制结果。 */
|
|
159
|
+
async function copyCode() {
|
|
160
|
+
const code = editor?.state.doc.toString() ?? "";
|
|
161
|
+
try {
|
|
162
|
+
await copyText(code);
|
|
163
|
+
} catch {
|
|
164
|
+
ElMessage.error(t("core.markdown.message.copy_failed"));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
ElMessage.success(t("core.clipboard.success"));
|
|
168
|
+
copied.value = true;
|
|
169
|
+
clearTimeout(copiedTimer);
|
|
170
|
+
copiedTimer = setTimeout(() => {
|
|
171
|
+
copied.value = false;
|
|
172
|
+
}, 2000);
|
|
173
|
+
}
|
|
174
|
+
|
|
91
175
|
watch(() => props.modelValue, value => {
|
|
92
176
|
if (!editor || value === editor.state.doc.toString()) return;
|
|
93
177
|
editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: value } });
|
|
94
178
|
lineCount.value = editor.state.doc.lines;
|
|
95
179
|
});
|
|
96
180
|
|
|
97
|
-
onBeforeUnmount(() =>
|
|
181
|
+
onBeforeUnmount(() => {
|
|
182
|
+
clearTimeout(copiedTimer);
|
|
183
|
+
editor?.destroy();
|
|
184
|
+
});
|
|
98
185
|
</script>
|
|
99
186
|
|
|
100
187
|
<style scoped>
|
|
101
|
-
.yaml-editor { width: 100%; overflow: hidden; border: 1px solid var(--el-border-color); border-radius: 4px; }
|
|
188
|
+
.yaml-editor { display: flex; flex-direction: column; width: 100%; overflow: hidden; background: var(--el-fill-color-blank); border: 1px solid var(--el-border-color); border-radius: 4px; }
|
|
189
|
+
.yaml-editor.is-bounded { max-height: v-bind(maxHeight); }
|
|
102
190
|
.yaml-editor:focus-within { border-color: var(--el-color-primary); }
|
|
103
|
-
.yaml-
|
|
191
|
+
.yaml-editor.is-invalid { border-color: var(--el-color-danger); }
|
|
192
|
+
.yaml-editor__toolbar { display: flex; align-items: center; justify-content: space-between; min-height: 32px; padding: 2px 8px; background: var(--el-fill-color-light); border-bottom: 1px solid var(--el-border-color-lighter); }
|
|
193
|
+
.yaml-editor__lang { display: inline-flex; gap: 5px; align-items: center; color: var(--el-text-color-secondary); font-size: 12px; font-weight: 600; letter-spacing: 0.5px; }
|
|
194
|
+
.yaml-editor__actions { display: inline-flex; gap: 2px; }
|
|
195
|
+
.yaml-editor__action { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; color: var(--el-text-color-secondary); cursor: pointer; background: transparent; border: none; border-radius: 4px; transition: color 0.15s ease, background-color 0.15s ease; }
|
|
196
|
+
.yaml-editor__action:hover, .yaml-editor__action:focus-visible { color: var(--el-color-primary); background: var(--el-fill-color); outline: none; }
|
|
197
|
+
.yaml-editor__action.is-active { color: var(--el-color-primary); }
|
|
198
|
+
.yaml-editor__host { flex: 1 1 auto; min-height: 0; }
|
|
199
|
+
.yaml-editor__host :deep(.cm-editor) { display: flex; flex-direction: column; min-height: v-bind(minHeight); height: 100%; }
|
|
104
200
|
.yaml-editor__host :deep(.cm-editor.cm-focused) { outline: none; }
|
|
105
|
-
.yaml-editor__host.
|
|
201
|
+
.yaml-editor__host :deep(.cm-scroller) { flex: 1 1 auto; min-height: 0; }
|
|
106
202
|
.yaml-editor__footer { display: flex; justify-content: space-between; min-height: 24px; padding: 3px 10px; color: var(--el-text-color-secondary); background: var(--el-fill-color-lighter); border-top: 1px solid var(--el-border-color-lighter); font-size: 12px; }
|
|
107
203
|
</style>
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
| `en-US.json` | 英文公共语言包。 | 管理端切换为英文时由 Vue I18n 使用。 |
|
|
10
10
|
| `ja-JP.json` | 日文公共语言包。 | 管理端切换为日文时由 Vue I18n 使用。 |
|
|
11
11
|
|
|
12
|
-
语言包是标准 JSON,不能在文件内直接写注释;新增语言或修改键后执行仓库根目录的 `make i18n`,脚本会校验所有语言并生成注册产物。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[
|
|
12
|
+
语言包是标准 JSON,不能在文件内直接写注释;新增语言或修改键后执行仓库根目录的 `make i18n`,脚本会校验所有语言并生成注册产物。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化设计与语言扩展](../../../../../../docs/国际化最终方案.md)。
|