@liujitcn/kratos-uni-app-core 0.0.28 → 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 +28 -5
- package/dist/module.mjs +164 -4
- package/package.json +3 -1
- package/src/api/base/mfa.ts +127 -0
- package/src/components/KratosSettingsPage.vue +280 -0
- package/src/components/KratosTabBar.vue +50 -1
- package/src/components/MfaManageDialog.vue +469 -0
- package/src/components/MfaRecoveryCodesDialog.vue +192 -0
- package/src/components/MfaSetupPanel.vue +160 -0
- package/src/components/PasswordVerifyDialog.vue +277 -0
- package/src/index.ts +2 -0
- package/src/locales/README.md +2 -1
- package/src/locales/en-US.json +41 -1
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +41 -1
- package/src/locales/zh-CN.json +41 -1
- package/src/locales/zh-TW.json +41 -1
- package/src/navigation.ts +47 -1
- package/src/rpc/base/v1/login.ts +33 -2
- package/src/rpc/base/v1/mfa.ts +180 -0
- package/src/rpc/base/v1/oauth.ts +25 -0
- package/src/stores/modules/user.ts +26 -7
- package/src/utils/clipboard.ts +33 -0
- package/src/utils/http.ts +8 -3
- package/src/utils/webauthn.ts +101 -0
- package/src/views/pages/login/login.vue +293 -12
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
3
|
+
import qrcode from 'qrcode-generator'
|
|
4
|
+
import { useI18n } from '../locales'
|
|
5
|
+
import { copyText } from '../utils/clipboard'
|
|
6
|
+
|
|
7
|
+
/** MFA TOTP 绑定面板属性。 */
|
|
8
|
+
interface MfaSetupPanelProps {
|
|
9
|
+
/** TOTP 绑定地址。 */
|
|
10
|
+
uri: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = defineProps<MfaSetupPanelProps>()
|
|
14
|
+
const { t } = useI18n()
|
|
15
|
+
const copyState = ref<'idle' | 'copied' | 'failed'>('idle')
|
|
16
|
+
let resetCopyStateTimer: ReturnType<typeof setTimeout> | undefined
|
|
17
|
+
|
|
18
|
+
const qrDataUrl = computed(() => {
|
|
19
|
+
if (!props.uri) return ''
|
|
20
|
+
const code = qrcode(0, 'M')
|
|
21
|
+
code.addData(props.uri)
|
|
22
|
+
code.make()
|
|
23
|
+
return code.createDataURL(4, 8)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/** 复制 TOTP 绑定地址。 */
|
|
27
|
+
async function copySetupUri() {
|
|
28
|
+
if (!props.uri) return
|
|
29
|
+
try {
|
|
30
|
+
await copyText(props.uri)
|
|
31
|
+
copyState.value = 'copied'
|
|
32
|
+
} catch {
|
|
33
|
+
copyState.value = 'failed'
|
|
34
|
+
}
|
|
35
|
+
if (resetCopyStateTimer) clearTimeout(resetCopyStateTimer)
|
|
36
|
+
resetCopyStateTimer = setTimeout(() => {
|
|
37
|
+
copyState.value = 'idle'
|
|
38
|
+
}, 2000)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onBeforeUnmount(() => {
|
|
42
|
+
if (resetCopyStateTimer) clearTimeout(resetCopyStateTimer)
|
|
43
|
+
})
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<view v-if="uri" class="mfa-setup-panel">
|
|
48
|
+
<image v-if="qrDataUrl" class="mfa-setup-panel__qr" :src="qrDataUrl" mode="aspectFit" />
|
|
49
|
+
<view class="mfa-setup-panel__uri">
|
|
50
|
+
<text class="mfa-setup-panel__uri-text">{{ uri }}</text>
|
|
51
|
+
<button
|
|
52
|
+
class="mfa-setup-panel__copy"
|
|
53
|
+
:class="{ 'is-copied': copyState === 'copied', 'is-failed': copyState === 'failed' }"
|
|
54
|
+
:aria-label="t('core.login.mfa_copy_uri')"
|
|
55
|
+
:title="t('core.login.mfa_copy_uri')"
|
|
56
|
+
@tap="copySetupUri"
|
|
57
|
+
>
|
|
58
|
+
<text v-if="copyState === 'copied'" class="mfa-setup-panel__status" aria-hidden="true"
|
|
59
|
+
>✓</text
|
|
60
|
+
>
|
|
61
|
+
<text v-else-if="copyState === 'failed'" class="mfa-setup-panel__status" aria-hidden="true"
|
|
62
|
+
>!</text
|
|
63
|
+
>
|
|
64
|
+
<view v-else class="mfa-setup-panel__copy-icon" aria-hidden="true">
|
|
65
|
+
<view class="mfa-setup-panel__copy-back" />
|
|
66
|
+
<view class="mfa-setup-panel__copy-front" />
|
|
67
|
+
</view>
|
|
68
|
+
</button>
|
|
69
|
+
</view>
|
|
70
|
+
</view>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<style scoped lang="scss">
|
|
74
|
+
.mfa-setup-panel__qr {
|
|
75
|
+
display: block;
|
|
76
|
+
width: min(360rpx, 100%);
|
|
77
|
+
aspect-ratio: 1;
|
|
78
|
+
margin: 0 auto 24rpx;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.mfa-setup-panel__uri {
|
|
82
|
+
display: flex;
|
|
83
|
+
gap: 16rpx;
|
|
84
|
+
align-items: flex-start;
|
|
85
|
+
margin-bottom: 20rpx;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.mfa-setup-panel__uri-text {
|
|
89
|
+
flex: 1;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
color: #666;
|
|
92
|
+
font-size: 24rpx;
|
|
93
|
+
line-height: 1.5;
|
|
94
|
+
overflow-wrap: anywhere;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.mfa-setup-panel__copy {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
width: 58rpx;
|
|
102
|
+
min-width: 58rpx;
|
|
103
|
+
height: 58rpx;
|
|
104
|
+
margin: 0;
|
|
105
|
+
padding: 0;
|
|
106
|
+
color: #27ba9b;
|
|
107
|
+
background: #f0fbf8;
|
|
108
|
+
border: 0;
|
|
109
|
+
border-radius: 8rpx;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.mfa-setup-panel__copy::after {
|
|
113
|
+
border: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.mfa-setup-panel__copy.is-copied {
|
|
117
|
+
color: #fff;
|
|
118
|
+
background: #27ba9b;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.mfa-setup-panel__copy.is-failed {
|
|
122
|
+
color: #e64340;
|
|
123
|
+
background: #fff1f0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.mfa-setup-panel__status {
|
|
127
|
+
font-size: 30rpx;
|
|
128
|
+
font-weight: 700;
|
|
129
|
+
line-height: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.mfa-setup-panel__copy-icon {
|
|
133
|
+
position: relative;
|
|
134
|
+
display: block;
|
|
135
|
+
width: 24rpx;
|
|
136
|
+
height: 24rpx;
|
|
137
|
+
color: currentColor;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.mfa-setup-panel__copy-back,
|
|
141
|
+
.mfa-setup-panel__copy-front {
|
|
142
|
+
position: absolute;
|
|
143
|
+
width: 15rpx;
|
|
144
|
+
height: 17rpx;
|
|
145
|
+
border: 2rpx solid currentColor;
|
|
146
|
+
border-radius: 3rpx;
|
|
147
|
+
box-sizing: border-box;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.mfa-setup-panel__copy-back {
|
|
151
|
+
top: 0;
|
|
152
|
+
left: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.mfa-setup-panel__copy-front {
|
|
156
|
+
right: 0;
|
|
157
|
+
bottom: 0;
|
|
158
|
+
background-color: #f0fbf8;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useI18n } from '../locales'
|
|
4
|
+
import { PASSWORD_CRYPTO_SCENE, encryptPassword } from '../utils/passwordCrypto'
|
|
5
|
+
import type { PasswordCryptoScene } from '../utils/passwordCrypto'
|
|
6
|
+
import type { PasswordCrypto } from '../rpc/common/v1/types'
|
|
7
|
+
|
|
8
|
+
const { t } = useI18n()
|
|
9
|
+
|
|
10
|
+
/** 通用密码验证弹窗属性。 */
|
|
11
|
+
interface PasswordVerifyDialogProps {
|
|
12
|
+
/** 弹窗显示状态。 */
|
|
13
|
+
modelValue: boolean
|
|
14
|
+
/** 弹窗标题。 */
|
|
15
|
+
title?: string
|
|
16
|
+
/** 密码输入前的可选补充说明。 */
|
|
17
|
+
description?: string
|
|
18
|
+
/** 密码字段标题。 */
|
|
19
|
+
passwordLabel?: string
|
|
20
|
+
/** 密码输入提示。 */
|
|
21
|
+
passwordPlaceholder?: string
|
|
22
|
+
/** 确认按钮文案。 */
|
|
23
|
+
confirmText?: string
|
|
24
|
+
/** 取消按钮文案。 */
|
|
25
|
+
cancelText?: string
|
|
26
|
+
/** 外部业务提交状态。 */
|
|
27
|
+
confirmLoading?: boolean
|
|
28
|
+
/** 密码加密场景。 */
|
|
29
|
+
scene?: PasswordCryptoScene
|
|
30
|
+
/** 是否允许点击遮罩关闭。 */
|
|
31
|
+
closeOnClickModal?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<PasswordVerifyDialogProps>(), {
|
|
35
|
+
title: '',
|
|
36
|
+
description: '',
|
|
37
|
+
passwordLabel: '',
|
|
38
|
+
passwordPlaceholder: '',
|
|
39
|
+
confirmText: '',
|
|
40
|
+
cancelText: '',
|
|
41
|
+
confirmLoading: false,
|
|
42
|
+
scene: PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
43
|
+
closeOnClickModal: false,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<{
|
|
47
|
+
'update:modelValue': [value: boolean]
|
|
48
|
+
confirm: [password: PasswordCrypto]
|
|
49
|
+
cancel: []
|
|
50
|
+
closed: []
|
|
51
|
+
}>()
|
|
52
|
+
|
|
53
|
+
const password = ref('')
|
|
54
|
+
const encrypting = ref(false)
|
|
55
|
+
const errorMessage = ref('')
|
|
56
|
+
const submitting = computed(() => props.confirmLoading || encrypting.value)
|
|
57
|
+
const dialogTitle = computed(() => props.title || t('core.password.verify_title'))
|
|
58
|
+
const passwordLabel = computed(() => props.passwordLabel || t('core.password.verify_label'))
|
|
59
|
+
const passwordPlaceholder = computed(
|
|
60
|
+
() => props.passwordPlaceholder || t('core.crypto.password_required'),
|
|
61
|
+
)
|
|
62
|
+
const confirmText = computed(() => props.confirmText || t('common.action.confirm'))
|
|
63
|
+
const cancelText = computed(() => props.cancelText || t('common.action.cancel'))
|
|
64
|
+
|
|
65
|
+
/** 同步弹窗显示状态到外部。 */
|
|
66
|
+
function handleVisibleChange(value: boolean) {
|
|
67
|
+
emit('update:modelValue', value)
|
|
68
|
+
if (!value) {
|
|
69
|
+
password.value = ''
|
|
70
|
+
errorMessage.value = ''
|
|
71
|
+
emit('closed')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 点击遮罩时按配置关闭弹窗。 */
|
|
76
|
+
function handleMaskTap() {
|
|
77
|
+
if (props.closeOnClickModal && !submitting.value) handleVisibleChange(false)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 校验密码并向业务方返回加密结果。 */
|
|
81
|
+
async function handleConfirm() {
|
|
82
|
+
if (submitting.value) return
|
|
83
|
+
if (!password.value.trim()) {
|
|
84
|
+
errorMessage.value = t('core.crypto.password_required')
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
errorMessage.value = ''
|
|
88
|
+
encrypting.value = true
|
|
89
|
+
try {
|
|
90
|
+
const encryptedPassword = await encryptPassword(password.value, props.scene)
|
|
91
|
+
emit('confirm', encryptedPassword)
|
|
92
|
+
} finally {
|
|
93
|
+
encrypting.value = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 关闭弹窗并清理输入内容。 */
|
|
98
|
+
function handleCancel() {
|
|
99
|
+
if (submitting.value) return
|
|
100
|
+
handleVisibleChange(false)
|
|
101
|
+
emit('cancel')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
watch(
|
|
105
|
+
() => props.modelValue,
|
|
106
|
+
(value) => {
|
|
107
|
+
if (value) {
|
|
108
|
+
errorMessage.value = ''
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
password.value = ''
|
|
112
|
+
errorMessage.value = ''
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<view v-if="modelValue" class="password-verify-dialog" @tap="handleMaskTap">
|
|
119
|
+
<view class="password-verify-dialog__panel" @tap.stop>
|
|
120
|
+
<view class="password-verify-dialog__header">
|
|
121
|
+
<text class="password-verify-dialog__title">{{ dialogTitle }}</text>
|
|
122
|
+
<text class="password-verify-dialog__close" @tap="handleCancel">×</text>
|
|
123
|
+
</view>
|
|
124
|
+
<view class="password-verify-dialog__body">
|
|
125
|
+
<text v-if="description" class="password-verify-dialog__description">{{
|
|
126
|
+
description
|
|
127
|
+
}}</text>
|
|
128
|
+
<text class="password-verify-dialog__label">{{ passwordLabel }}</text>
|
|
129
|
+
<input
|
|
130
|
+
v-model="password"
|
|
131
|
+
class="password-verify-dialog__input"
|
|
132
|
+
password
|
|
133
|
+
:placeholder="passwordPlaceholder"
|
|
134
|
+
autocomplete="current-password"
|
|
135
|
+
@confirm="handleConfirm"
|
|
136
|
+
@input="errorMessage = ''"
|
|
137
|
+
/>
|
|
138
|
+
<text v-if="errorMessage" class="password-verify-dialog__error">{{ errorMessage }}</text>
|
|
139
|
+
</view>
|
|
140
|
+
<view class="password-verify-dialog__footer">
|
|
141
|
+
<button
|
|
142
|
+
class="password-verify-dialog__button password-verify-dialog__button--cancel"
|
|
143
|
+
:disabled="submitting"
|
|
144
|
+
@tap="handleCancel"
|
|
145
|
+
>
|
|
146
|
+
{{ cancelText }}
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
class="password-verify-dialog__button password-verify-dialog__button--confirm"
|
|
150
|
+
:loading="submitting"
|
|
151
|
+
@tap="handleConfirm"
|
|
152
|
+
>
|
|
153
|
+
{{ confirmText }}
|
|
154
|
+
</button>
|
|
155
|
+
</view>
|
|
156
|
+
</view>
|
|
157
|
+
</view>
|
|
158
|
+
</template>
|
|
159
|
+
|
|
160
|
+
<style scoped lang="scss">
|
|
161
|
+
.password-verify-dialog {
|
|
162
|
+
position: fixed;
|
|
163
|
+
z-index: 1000;
|
|
164
|
+
inset: 0;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
padding: 32rpx;
|
|
169
|
+
background-color: rgb(0 0 0 / 45%);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.password-verify-dialog__panel {
|
|
173
|
+
width: min(100%, 640rpx);
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
background-color: #fff;
|
|
176
|
+
border-radius: 16rpx;
|
|
177
|
+
box-shadow: 0 20rpx 64rpx rgb(0 0 0 / 20%);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.password-verify-dialog__header {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: space-between;
|
|
184
|
+
padding: 28rpx 32rpx;
|
|
185
|
+
border-bottom: 1rpx solid #eee;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.password-verify-dialog__title {
|
|
189
|
+
color: #222;
|
|
190
|
+
font-size: 34rpx;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.password-verify-dialog__close {
|
|
195
|
+
padding: 0 8rpx;
|
|
196
|
+
color: #999;
|
|
197
|
+
font-size: 42rpx;
|
|
198
|
+
line-height: 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.password-verify-dialog__body {
|
|
202
|
+
padding: 32rpx;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.password-verify-dialog__description {
|
|
206
|
+
display: block;
|
|
207
|
+
margin-bottom: 24rpx;
|
|
208
|
+
color: #666;
|
|
209
|
+
font-size: 26rpx;
|
|
210
|
+
line-height: 1.6;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.password-verify-dialog__label {
|
|
214
|
+
display: block;
|
|
215
|
+
margin-bottom: 12rpx;
|
|
216
|
+
color: #333;
|
|
217
|
+
font-size: 28rpx;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.password-verify-dialog__input {
|
|
221
|
+
box-sizing: border-box;
|
|
222
|
+
width: 100%;
|
|
223
|
+
height: 84rpx;
|
|
224
|
+
padding: 0 24rpx;
|
|
225
|
+
color: #222;
|
|
226
|
+
font-size: 30rpx;
|
|
227
|
+
border: 1rpx solid #ddd;
|
|
228
|
+
border-radius: 8rpx;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.password-verify-dialog__input:focus {
|
|
232
|
+
border-color: #27ba9b;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.password-verify-dialog__error {
|
|
236
|
+
display: block;
|
|
237
|
+
margin-top: 10rpx;
|
|
238
|
+
color: #e64340;
|
|
239
|
+
font-size: 24rpx;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.password-verify-dialog__footer {
|
|
243
|
+
display: flex;
|
|
244
|
+
justify-content: flex-end;
|
|
245
|
+
gap: 20rpx;
|
|
246
|
+
padding: 0 32rpx 32rpx;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.password-verify-dialog__button {
|
|
250
|
+
min-width: 160rpx;
|
|
251
|
+
height: 72rpx;
|
|
252
|
+
margin: 0;
|
|
253
|
+
padding: 0 24rpx;
|
|
254
|
+
font-size: 28rpx;
|
|
255
|
+
line-height: 72rpx;
|
|
256
|
+
border: 0;
|
|
257
|
+
border-radius: 8rpx;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.password-verify-dialog__button::after {
|
|
261
|
+
border: 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.password-verify-dialog__button--cancel {
|
|
265
|
+
color: #555;
|
|
266
|
+
background-color: #f3f3f3;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.password-verify-dialog__button--confirm {
|
|
270
|
+
color: #fff;
|
|
271
|
+
background-color: #27ba9b;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.password-verify-dialog__button[disabled] {
|
|
275
|
+
opacity: 0.6;
|
|
276
|
+
}
|
|
277
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from './bootstrap'
|
|
|
2
2
|
export * from './module'
|
|
3
3
|
export * from './locales'
|
|
4
4
|
export * from './navigation'
|
|
5
|
+
/** KratosSettingsPage 可通过顶部插槽扩展业务内容的公共设置页。 */
|
|
6
|
+
export { default as KratosSettingsPage } from './components/KratosSettingsPage.vue'
|
|
5
7
|
export { default as KratosTabBar } from './components/KratosTabBar.vue'
|
|
6
8
|
export { default as pinia } from './stores'
|
|
7
9
|
export * from './stores'
|
package/src/locales/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# uni-app Core 语言包
|
|
2
2
|
|
|
3
|
-
本目录提供 uni-app
|
|
3
|
+
本目录提供 uni-app 底座和公共页面使用的稳定文案,包括登录、设置、启动状态、导航、认证、MFA/WebAuthn、文件处理和网络错误提示。语言包由 `../module.ts` 的 `coreModule` 导入并注册,应用启动时由 `bootstrapKratosApp` 调用 `registerLocaleMessages` 合并;页面、hooks、请求工具通过 `useI18n()` 或 `t()` 使用。
|
|
4
4
|
|
|
5
5
|
| 文件 | 用途 | 使用位置 |
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| `zh-CN.json` | 简体中文固定 UI 默认语言包,也是所有语言键集合和占位符校验基准。 | uni-app 固定文案默认语言及缺失翻译回退。 |
|
|
8
8
|
| `zh-TW.json` | 繁体中文 Core 语言包。 | uni-app 切换为繁体中文时使用。 |
|
|
9
9
|
| `en-US.json` | 英文 Core 语言包。 | uni-app 切换为英文时使用。 |
|
|
10
|
+
| `ja-JP.json` | 日文 Core 语言包。 | uni-app 切换为日文时使用。 |
|
|
10
11
|
|
|
11
12
|
语言包是标准 JSON,不能在文件内直接写注释;公共键使用 `common.` 或 `core.` 命名空间。新增语言后执行仓库根目录的 `make i18n-sync`。语言切换选项固定显示后端 `base_language.native_name`,接口不可用时使用代码内置的原生名称;`common.language.*` 仅用于普通文案和语言迁移初始数据。新增语言的完整文件清单见[国际化语言扩展指南](../../../../../../docs/国际化语言扩展指南.md)。
|
package/src/locales/en-US.json
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"core.crypto.public_key_invalid": "Invalid public key",
|
|
29
29
|
"core.crypto.random_failed": "Failed to generate random data",
|
|
30
30
|
"core.crypto.unsupported": "Password encryption is not supported in this environment",
|
|
31
|
+
"core.password.verify_title": "Verify password",
|
|
32
|
+
"core.password.verify_label": "Current password",
|
|
31
33
|
"core.file.upload_failed": "Upload failed",
|
|
32
34
|
"core.home.account_capability": "Account features",
|
|
33
35
|
"core.home.account_capability_description": "Profile, app settings, and AI assistant access are included",
|
|
@@ -58,6 +60,23 @@
|
|
|
58
60
|
"core.login.captcha_write_failed": "Failed to save verification image",
|
|
59
61
|
"core.login.default_sub_title": "Welcome",
|
|
60
62
|
"core.login.login_success": "Logged in",
|
|
63
|
+
"core.login.mfa_title": "Multi-factor authentication",
|
|
64
|
+
"core.login.mfa_code": "Enter the authenticator code",
|
|
65
|
+
"core.login.mfa_recovery_code": "Recovery code (optional)",
|
|
66
|
+
"core.login.mfa_setup_title": "Bind multi-factor authentication",
|
|
67
|
+
"core.login.mfa_copy_uri": "Copy binding address",
|
|
68
|
+
"core.login.mfa_copy_success": "Binding address copied",
|
|
69
|
+
"core.login.mfa_recovery_codes_copied": "Recovery codes copied",
|
|
70
|
+
"core.login.mfa_recovery_codes_title": "Save MFA recovery codes",
|
|
71
|
+
"core.login.mfa_recovery_codes_warning": "Each code works once and will not be shown again after this dialog closes.",
|
|
72
|
+
"core.login.mfa_copy_recovery_codes": "Copy recovery codes",
|
|
73
|
+
"core.login.mfa_recovery_codes_confirm": "I saved them",
|
|
74
|
+
"core.login.mfa_setup_success": "Multi-factor authentication enabled. Please sign in again.",
|
|
75
|
+
"core.login.mfa_webauthn_action": "Use passkey",
|
|
76
|
+
"core.login.mfa_webauthn_setup_unsupported": "WebAuthn is not supported on this platform. Use TOTP or a recovery code.",
|
|
77
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn registration did not return a valid credential.",
|
|
78
|
+
"core.login.mfa_webauthn_login_unsupported": "WebAuthn is not supported on this platform. Use a recovery code.",
|
|
79
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn authentication did not return a valid credential.",
|
|
61
80
|
"core.login.password": "Enter password",
|
|
62
81
|
"core.login.privacy": "Privacy Policy",
|
|
63
82
|
"core.login.protocol_missing": "Terms of Service and Privacy Policy are not configured",
|
|
@@ -71,6 +90,7 @@
|
|
|
71
90
|
"core.login.wechat_bind_tip": "This WeChat account is not linked. Log in to an existing account to link it.",
|
|
72
91
|
"core.login.wechat_failed": "WeChat login failed. Try again later.",
|
|
73
92
|
"core.navigation.home": "Home",
|
|
93
|
+
"core.navigation.message": "Messages",
|
|
74
94
|
"core.navigation.error.access": "Invalid menu access mode: {access}",
|
|
75
95
|
"core.navigation.error.child_tab": "Only second-level app menus can be tabs: {name}",
|
|
76
96
|
"core.navigation.error.cycle": "Menu parent cycle detected: {name}",
|
|
@@ -92,6 +112,22 @@
|
|
|
92
112
|
"core.protocol.not_configured": "{title} is not configured and cannot be viewed",
|
|
93
113
|
"core.protocol.privacy": "Privacy Policy",
|
|
94
114
|
"core.protocol.service": "Terms of Service",
|
|
115
|
+
"core.settings.authorization": "Authorization settings",
|
|
116
|
+
"core.settings.contact": "Contact us",
|
|
117
|
+
"core.settings.feedback": "Feedback",
|
|
118
|
+
"core.settings.logout_confirm": "Log out now?",
|
|
119
|
+
"core.settings.logout_failed": "Logout failed",
|
|
120
|
+
"core.settings.mfa_bind_success": "Multi-factor authentication enabled",
|
|
121
|
+
"core.settings.mfa_disable_action": "Disable",
|
|
122
|
+
"core.settings.mfa_disable_factor_required": "Enter a TOTP code or recovery code.",
|
|
123
|
+
"core.settings.mfa_disable_success": "Multi-factor authentication disabled",
|
|
124
|
+
"core.settings.mfa_disable_title": "Disable multi-factor authentication",
|
|
125
|
+
"core.settings.mfa_disabled": "Not enabled",
|
|
126
|
+
"core.settings.mfa_enabled_method": "Enabled ({method})",
|
|
127
|
+
"core.settings.mfa_method_totp": "TOTP",
|
|
128
|
+
"core.settings.mfa_method_webauthn": "WebAuthn",
|
|
129
|
+
"core.settings.mfa_title": "Multi-factor authentication",
|
|
130
|
+
"core.settings.title": "Settings",
|
|
95
131
|
"core.status.CONFIG_ERROR": "Invalid navigation configuration",
|
|
96
132
|
"core.status.forbidden": "Access denied",
|
|
97
133
|
"core.status.NOT_FOUND": "Page not found",
|
|
@@ -100,5 +136,9 @@
|
|
|
100
136
|
"core.status.loading": "Loading",
|
|
101
137
|
"core.webview.embed_blocked": "The destination may block embedding on H5",
|
|
102
138
|
"core.webview.invalid_url": "No valid link was provided",
|
|
103
|
-
"core.webview.open_failed": "Unable to open link"
|
|
139
|
+
"core.webview.open_failed": "Unable to open link",
|
|
140
|
+
"common.value.top_level": "Top level",
|
|
141
|
+
"common.resource.default": "{resource}",
|
|
142
|
+
"common.action.delete_resource": "Delete {resource}",
|
|
143
|
+
"common.action.set_field_status": "Set {field}"
|
|
104
144
|
}
|
package/src/locales/index.ts
CHANGED
|
@@ -113,7 +113,7 @@ export function registerLocaleMessages(modules: KratosAppModule[]): void {
|
|
|
113
113
|
}
|
|
114
114
|
const target = localeMessages.get(locale) as LocaleMessages
|
|
115
115
|
Object.keys(messages).forEach((key) => {
|
|
116
|
-
if (!
|
|
116
|
+
if (!isAllowedLocaleKey(module.name, key)) {
|
|
117
117
|
throw new Error(`${module.name} 的语言键命名空间无效: ${key}`)
|
|
118
118
|
}
|
|
119
119
|
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
@@ -131,6 +131,15 @@ export function registerLocaleMessages(modules: KratosAppModule[]): void {
|
|
|
131
131
|
})
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** 校验公共键和当前业务模块专属的语言键命名空间。 */
|
|
135
|
+
function isAllowedLocaleKey(moduleName: string, key: string): boolean {
|
|
136
|
+
const packageName = moduleName.split('/').at(-1) ?? moduleName
|
|
137
|
+
const moduleNamespace = packageName.split('-').at(-1) ?? packageName
|
|
138
|
+
return ['common', 'core', 'system', moduleNamespace].some((namespace) =>
|
|
139
|
+
key.startsWith(`${namespace}.`),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
134
143
|
/** 获取当前语言区域。 */
|
|
135
144
|
export function getCurrentLocale(): SupportedLocale {
|
|
136
145
|
return mutableLocaleState.value
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"core.crypto.public_key_invalid": "公開鍵の形式が無効です",
|
|
29
29
|
"core.crypto.random_failed": "乱数の生成に失敗しました",
|
|
30
30
|
"core.crypto.unsupported": "現在の環境はパスワード暗号化をサポートしていません",
|
|
31
|
+
"core.password.verify_title": "パスワードの確認",
|
|
32
|
+
"core.password.verify_label": "現在のパスワード",
|
|
31
33
|
"core.file.upload_failed": "アップロードに失敗しました",
|
|
32
34
|
"core.home.account_capability": "アカウントの機能",
|
|
33
35
|
"core.home.account_capability_description": "個人情報、アプリケーション設定、スマートアシスタント入口が予約されています",
|
|
@@ -58,6 +60,23 @@
|
|
|
58
60
|
"core.login.captcha_write_failed": "検証コードイメージの書き込みに失敗しました",
|
|
59
61
|
"core.login.default_sub_title": "このアプリケーションへようこそ",
|
|
60
62
|
"core.login.login_success": "ログインに成功しました",
|
|
63
|
+
"core.login.mfa_title": "多要素認証",
|
|
64
|
+
"core.login.mfa_code": "認証コードを入力",
|
|
65
|
+
"core.login.mfa_recovery_code": "リカバリーコード(任意)",
|
|
66
|
+
"core.login.mfa_setup_title": "多要素認証を設定",
|
|
67
|
+
"core.login.mfa_copy_uri": "登録アドレスをコピー",
|
|
68
|
+
"core.login.mfa_copy_success": "登録アドレスをコピーしました",
|
|
69
|
+
"core.login.mfa_recovery_codes_copied": "リカバリーコードをコピーしました",
|
|
70
|
+
"core.login.mfa_recovery_codes_title": "MFA リカバリーコードを保存",
|
|
71
|
+
"core.login.mfa_recovery_codes_warning": "各コードは一度だけ使用でき、ダイアログを閉じると再表示されません。",
|
|
72
|
+
"core.login.mfa_copy_recovery_codes": "リカバリーコードをコピー",
|
|
73
|
+
"core.login.mfa_recovery_codes_confirm": "保存しました",
|
|
74
|
+
"core.login.mfa_setup_success": "多要素認証を設定しました。再度ログインしてください。",
|
|
75
|
+
"core.login.mfa_webauthn_action": "パスキーを使用",
|
|
76
|
+
"core.login.mfa_webauthn_setup_unsupported": "このプラットフォームは WebAuthn に対応していません。TOTP またはリカバリーコードを使用してください。",
|
|
77
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn 登録で有効なクレデンシャルが返されませんでした。",
|
|
78
|
+
"core.login.mfa_webauthn_login_unsupported": "このプラットフォームは WebAuthn に対応していません。リカバリーコードを使用してください。",
|
|
79
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn 認証で有効なクレデンシャルが返されませんでした。",
|
|
61
80
|
"core.login.password": "パスワードを入力してください",
|
|
62
81
|
"core.login.privacy": "「プライバシー同意書」",
|
|
63
82
|
"core.login.protocol_missing": "利用規約とプライバシー規約が設定されていません",
|
|
@@ -71,6 +90,7 @@
|
|
|
71
90
|
"core.login.wechat_bind_tip": "この WeChat アカウントはまだバインドされていません。バインドを完了するには、既存のアカウントにログインしてください。",
|
|
72
91
|
"core.login.wechat_failed": "WeChat ログインに失敗しました。後でもう一度試してください。",
|
|
73
92
|
"core.navigation.home": "ホーム",
|
|
93
|
+
"core.navigation.message": "メッセージ",
|
|
74
94
|
"core.navigation.error.access": "無効なメニュー アクセス モード: {access}",
|
|
75
95
|
"core.navigation.error.child_tab": "モバイル セカンダリ メニューのみがタブとして使用できます: {name}",
|
|
76
96
|
"core.navigation.error.cycle": "モバイル メニューにはループの親があります: {name}",
|
|
@@ -92,6 +112,22 @@
|
|
|
92
112
|
"core.protocol.not_configured": "{title} は構成されていないため、一時的に表示できません。",
|
|
93
113
|
"core.protocol.privacy": "プライバシー規約",
|
|
94
114
|
"core.protocol.service": "利用規約",
|
|
115
|
+
"core.settings.authorization": "認可管理",
|
|
116
|
+
"core.settings.contact": "お問い合わせ",
|
|
117
|
+
"core.settings.feedback": "問題のフィードバック",
|
|
118
|
+
"core.settings.logout_confirm": "ログアウトしますか?",
|
|
119
|
+
"core.settings.logout_failed": "ログアウトに失敗しました",
|
|
120
|
+
"core.settings.mfa_bind_success": "多要素認証を有効にしました",
|
|
121
|
+
"core.settings.mfa_disable_action": "無効にする",
|
|
122
|
+
"core.settings.mfa_disable_factor_required": "TOTP コードまたはリカバリーコードを入力してください。",
|
|
123
|
+
"core.settings.mfa_disable_success": "多要素認証を無効にしました",
|
|
124
|
+
"core.settings.mfa_disable_title": "多要素認証を無効化",
|
|
125
|
+
"core.settings.mfa_disabled": "未設定",
|
|
126
|
+
"core.settings.mfa_enabled_method": "有効({method})",
|
|
127
|
+
"core.settings.mfa_method_totp": "TOTP",
|
|
128
|
+
"core.settings.mfa_method_webauthn": "WebAuthn",
|
|
129
|
+
"core.settings.mfa_title": "多要素認証",
|
|
130
|
+
"core.settings.title": "設定",
|
|
95
131
|
"core.status.CONFIG_ERROR": "ナビゲーション構成が無効です",
|
|
96
132
|
"core.status.forbidden": "まだアクセスできません",
|
|
97
133
|
"core.status.NOT_FOUND": "ページが存在しません",
|
|
@@ -100,5 +136,9 @@
|
|
|
100
136
|
"core.status.loading": "をロード中です",
|
|
101
137
|
"core.webview.embed_blocked": "現在の H5 ページは、ターゲット サイトによって埋め込みが制限されている可能性があります",
|
|
102
138
|
"core.webview.invalid_url": "有効なリンク アドレスがありません",
|
|
103
|
-
"core.webview.open_failed": "リンクを開けません"
|
|
139
|
+
"core.webview.open_failed": "リンクを開けません",
|
|
140
|
+
"common.value.top_level": "トップレベル",
|
|
141
|
+
"common.resource.default": "{resource}",
|
|
142
|
+
"common.action.delete_resource": "{resource}を削除",
|
|
143
|
+
"common.action.set_field_status": "{field}を設定"
|
|
104
144
|
}
|