@liujitcn/kratos-taro-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 +6 -4
- package/dist/runner.mjs +13 -6
- package/package.json +2 -1
- package/src/api/base/mfa.ts +127 -0
- package/src/bootstrap.ts +26 -2
- package/src/components/KratosTabBar.scss +43 -0
- package/src/components/KratosTabBar.tsx +6 -1
- package/src/components/MfaRecoveryCodesDialog.scss +94 -0
- package/src/components/MfaRecoveryCodesDialog.tsx +61 -0
- package/src/components/MfaSetupPanel.scss +70 -0
- package/src/components/MfaSetupPanel.tsx +51 -0
- package/src/components/PasswordVerifyDialog.scss +146 -0
- package/src/components/PasswordVerifyDialog.tsx +146 -0
- package/src/index.ts +2 -0
- package/src/locales/README.md +2 -1
- package/src/locales/en-US.json +25 -1
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +25 -1
- package/src/locales/zh-CN.json +25 -1
- package/src/locales/zh-TW.json +25 -1
- package/src/navigation.ts +51 -0
- 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/runner.ts +22 -6
- package/src/stores/user.ts +17 -5
- package/src/utils/http.ts +3 -2
- package/src/utils/webauthn.ts +94 -0
- package/src/views/pages/login/login.scss +29 -0
- package/src/views/pages/login/login.tsx +335 -52
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
.password-verify-dialog {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1000;
|
|
4
|
+
top: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
left: 0;
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
padding: 32px;
|
|
12
|
+
background-color: rgba(0, 0, 0, 0.45);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.password-verify-dialog__panel {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
width: 100%;
|
|
18
|
+
max-width: 640px;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
border-radius: 16px;
|
|
21
|
+
background-color: #fff;
|
|
22
|
+
box-shadow: 0 20px 64px rgba(0, 0, 0, 0.2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.password-verify-dialog__header {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
padding: 28px 32px;
|
|
30
|
+
border-bottom: 1px solid #eee;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.password-verify-dialog__title {
|
|
34
|
+
color: #222;
|
|
35
|
+
font-size: 34px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.password-verify-dialog__close {
|
|
40
|
+
padding: 0 8px;
|
|
41
|
+
color: #999;
|
|
42
|
+
font-size: 42px;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.password-verify-dialog__body {
|
|
47
|
+
padding: 32px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.password-verify-dialog__description,
|
|
51
|
+
.password-verify-dialog__label,
|
|
52
|
+
.password-verify-dialog__error {
|
|
53
|
+
display: block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.password-verify-dialog__description {
|
|
57
|
+
margin-bottom: 24px;
|
|
58
|
+
color: #666;
|
|
59
|
+
font-size: 26px;
|
|
60
|
+
line-height: 1.6;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.password-verify-dialog__label {
|
|
64
|
+
margin-bottom: 12px;
|
|
65
|
+
color: #333;
|
|
66
|
+
font-size: 28px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.password-verify-dialog__input {
|
|
70
|
+
display: block;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
width: 100%;
|
|
73
|
+
height: 84px;
|
|
74
|
+
margin: 0;
|
|
75
|
+
padding: 0 24px;
|
|
76
|
+
color: #222 !important;
|
|
77
|
+
font-size: 30px;
|
|
78
|
+
line-height: 84px;
|
|
79
|
+
text-align: left;
|
|
80
|
+
background: #fff;
|
|
81
|
+
border: 1px solid #ddd;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
outline: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.password-verify-dialog__error {
|
|
87
|
+
margin-top: 10px;
|
|
88
|
+
color: #e64340;
|
|
89
|
+
font-size: 24px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.password-verify-dialog__footer {
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: flex-end;
|
|
95
|
+
gap: 20px;
|
|
96
|
+
padding: 0 32px 32px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.password-verify-dialog__button {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex: 1 1 0;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
width: 0;
|
|
106
|
+
min-width: 160px;
|
|
107
|
+
height: 72px;
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 0 24px;
|
|
110
|
+
font-size: 28px;
|
|
111
|
+
line-height: 72px;
|
|
112
|
+
text-align: center;
|
|
113
|
+
border: 0;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.password-verify-dialog__button::after {
|
|
118
|
+
border: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.password-verify-dialog__button--cancel {
|
|
122
|
+
color: #555 !important;
|
|
123
|
+
-webkit-text-fill-color: #555;
|
|
124
|
+
background-color: #f3f3f3;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.password-verify-dialog__button--confirm {
|
|
128
|
+
color: #fff;
|
|
129
|
+
background-color: #27ba9b;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.password-verify-dialog__button[disabled] {
|
|
133
|
+
opacity: 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.password-verify-dialog__button--cancel[disabled] {
|
|
137
|
+
color: #555 !important;
|
|
138
|
+
-webkit-text-fill-color: #555;
|
|
139
|
+
background-color: #f3f3f3;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.password-verify-dialog__button--confirm[disabled] {
|
|
143
|
+
color: #fff !important;
|
|
144
|
+
-webkit-text-fill-color: #fff;
|
|
145
|
+
background-color: #27ba9b;
|
|
146
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Button, Input, Text, View } from '@tarojs/components'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
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
|
+
import './PasswordVerifyDialog.scss'
|
|
8
|
+
|
|
9
|
+
/** Taro 通用密码验证弹窗属性。 */
|
|
10
|
+
export interface PasswordVerifyDialogProps {
|
|
11
|
+
/** 弹窗显示状态。 */
|
|
12
|
+
open: boolean
|
|
13
|
+
/** 弹窗标题。 */
|
|
14
|
+
title?: string
|
|
15
|
+
/** 密码输入前的可选补充说明。 */
|
|
16
|
+
description?: string
|
|
17
|
+
/** 密码字段标题。 */
|
|
18
|
+
passwordLabel?: string
|
|
19
|
+
/** 密码输入提示。 */
|
|
20
|
+
passwordPlaceholder?: string
|
|
21
|
+
/** 确认按钮文案。 */
|
|
22
|
+
confirmText?: string
|
|
23
|
+
/** 取消按钮文案。 */
|
|
24
|
+
cancelText?: string
|
|
25
|
+
/** 外部业务提交状态。 */
|
|
26
|
+
confirmLoading?: boolean
|
|
27
|
+
/** 密码加密场景。 */
|
|
28
|
+
scene?: PasswordCryptoScene
|
|
29
|
+
/** 是否允许点击遮罩关闭弹窗。 */
|
|
30
|
+
closeOnClickModal?: boolean
|
|
31
|
+
/** 确认并返回加密密码。 */
|
|
32
|
+
onConfirm: (password: PasswordCrypto) => void | Promise<void>
|
|
33
|
+
/** 取消验证。 */
|
|
34
|
+
onCancel: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 通用密码验证弹窗,负责收集并加密当前密码。 */
|
|
38
|
+
export default function PasswordVerifyDialog({
|
|
39
|
+
open,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
passwordLabel,
|
|
43
|
+
passwordPlaceholder,
|
|
44
|
+
confirmText,
|
|
45
|
+
cancelText,
|
|
46
|
+
confirmLoading = false,
|
|
47
|
+
scene = PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_MFA,
|
|
48
|
+
closeOnClickModal = false,
|
|
49
|
+
onConfirm,
|
|
50
|
+
onCancel,
|
|
51
|
+
}: PasswordVerifyDialogProps) {
|
|
52
|
+
const { t } = useI18n()
|
|
53
|
+
const [password, setPassword] = useState('')
|
|
54
|
+
const [errorMessage, setErrorMessage] = useState('')
|
|
55
|
+
const [encrypting, setEncrypting] = useState(false)
|
|
56
|
+
const submitting = confirmLoading || encrypting
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!open) {
|
|
60
|
+
setPassword('')
|
|
61
|
+
setErrorMessage('')
|
|
62
|
+
}
|
|
63
|
+
}, [open])
|
|
64
|
+
|
|
65
|
+
if (!open) return null
|
|
66
|
+
|
|
67
|
+
const handleConfirm = async () => {
|
|
68
|
+
if (submitting) return
|
|
69
|
+
if (!password.trim()) {
|
|
70
|
+
setErrorMessage(t('core.crypto.password_required'))
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
setErrorMessage('')
|
|
74
|
+
setEncrypting(true)
|
|
75
|
+
try {
|
|
76
|
+
const encryptedPassword = await encryptPassword(password, scene)
|
|
77
|
+
await onConfirm(encryptedPassword)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
setErrorMessage(error instanceof Error ? error.message : t('common.message.request_error'))
|
|
80
|
+
} finally {
|
|
81
|
+
setEncrypting(false)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleCancel = () => {
|
|
86
|
+
if (submitting) return
|
|
87
|
+
onCancel()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleMaskClick = () => {
|
|
91
|
+
if (closeOnClickModal) handleCancel()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View className='password-verify-dialog' onClick={handleMaskClick}>
|
|
96
|
+
<View className='password-verify-dialog__panel' onClick={(event) => event.stopPropagation()}>
|
|
97
|
+
<View className='password-verify-dialog__header'>
|
|
98
|
+
<Text className='password-verify-dialog__title'>
|
|
99
|
+
{title || t('core.password.verify_title')}
|
|
100
|
+
</Text>
|
|
101
|
+
<Text className='password-verify-dialog__close' onClick={handleCancel}>
|
|
102
|
+
×
|
|
103
|
+
</Text>
|
|
104
|
+
</View>
|
|
105
|
+
<View className='password-verify-dialog__body'>
|
|
106
|
+
{description ? (
|
|
107
|
+
<Text className='password-verify-dialog__description'>{description}</Text>
|
|
108
|
+
) : null}
|
|
109
|
+
<Text className='password-verify-dialog__label'>
|
|
110
|
+
{passwordLabel || t('core.password.verify_label')}
|
|
111
|
+
</Text>
|
|
112
|
+
<Input
|
|
113
|
+
className='password-verify-dialog__input'
|
|
114
|
+
password
|
|
115
|
+
value={password}
|
|
116
|
+
placeholder={passwordPlaceholder || t('core.crypto.password_required')}
|
|
117
|
+
onInput={(event) => {
|
|
118
|
+
setPassword(event.detail.value)
|
|
119
|
+
setErrorMessage('')
|
|
120
|
+
}}
|
|
121
|
+
onConfirm={() => void handleConfirm()}
|
|
122
|
+
/>
|
|
123
|
+
{errorMessage ? (
|
|
124
|
+
<Text className='password-verify-dialog__error'>{errorMessage}</Text>
|
|
125
|
+
) : null}
|
|
126
|
+
</View>
|
|
127
|
+
<View className='password-verify-dialog__footer'>
|
|
128
|
+
<Button
|
|
129
|
+
className='password-verify-dialog__button password-verify-dialog__button--cancel'
|
|
130
|
+
disabled={submitting}
|
|
131
|
+
onClick={handleCancel}
|
|
132
|
+
>
|
|
133
|
+
{cancelText || t('common.action.cancel')}
|
|
134
|
+
</Button>
|
|
135
|
+
<Button
|
|
136
|
+
className='password-verify-dialog__button password-verify-dialog__button--confirm'
|
|
137
|
+
loading={submitting}
|
|
138
|
+
onClick={() => void handleConfirm()}
|
|
139
|
+
>
|
|
140
|
+
{confirmText || t('common.action.confirm')}
|
|
141
|
+
</Button>
|
|
142
|
+
</View>
|
|
143
|
+
</View>
|
|
144
|
+
</View>
|
|
145
|
+
)
|
|
146
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -38,3 +38,5 @@ export * from './utils/file'
|
|
|
38
38
|
export * from './utils/index'
|
|
39
39
|
export * from './utils/navigation'
|
|
40
40
|
export * from './utils/passwordCrypto'
|
|
41
|
+
export { default as PasswordVerifyDialog } from './components/PasswordVerifyDialog'
|
|
42
|
+
export type { PasswordVerifyDialogProps } from './components/PasswordVerifyDialog'
|
package/src/locales/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Taro Core 语言包
|
|
2
2
|
|
|
3
|
-
本目录提供 Taro
|
|
3
|
+
本目录提供 Taro 应用底座和公共页面使用的稳定文案,包括登录、启动状态、导航、认证、MFA/WebAuthn、文件处理和网络错误提示。语言包由 `../index.ts` 的 `coreModule` 导入并注册,应用启动时由 `bootstrapKratosTaroApp` 调用 `registerLocaleMessages` 合并;页面、hooks、请求工具通过 `useI18n()` 或 `t()` 使用。
|
|
4
4
|
|
|
5
5
|
| 文件 | 用途 | 使用位置 |
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| `zh-CN.json` | 简体中文固定 UI 默认语言包,也是所有语言键集合和占位符校验基准。 | Taro 固定文案默认语言及缺失翻译回退。 |
|
|
8
8
|
| `zh-TW.json` | 繁体中文 Core 语言包。 | Taro 切换为繁体中文时使用。 |
|
|
9
9
|
| `en-US.json` | 英文 Core 语言包。 | Taro 切换为英文时使用。 |
|
|
10
|
+
| `ja-JP.json` | 日文 Core 语言包。 | Taro 切换为日文时使用。 |
|
|
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
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"core.crypto.password_required": "Password is required",
|
|
28
28
|
"core.crypto.password_too_long": "Password exceeds the supported length",
|
|
29
29
|
"core.crypto.public_key_invalid": "Invalid public key",
|
|
30
|
+
"core.password.verify_title": "Verify password",
|
|
31
|
+
"core.password.verify_label": "Current password",
|
|
30
32
|
"core.file.download_failed": "Download failed",
|
|
31
33
|
"core.file.temporary_path_required": "Use a temporary file path when uploading from a Mini Program",
|
|
32
34
|
"core.file.upload_failed": "Upload failed",
|
|
@@ -60,6 +62,23 @@
|
|
|
60
62
|
"core.login.default_sub_title": "Welcome",
|
|
61
63
|
"core.login.login_failed": "Login failed. Try again.",
|
|
62
64
|
"core.login.login_success": "Logged in",
|
|
65
|
+
"core.login.mfa_title": "Multi-factor authentication",
|
|
66
|
+
"core.login.mfa_code": "Enter the authenticator code",
|
|
67
|
+
"core.login.mfa_recovery_code": "Recovery code (optional)",
|
|
68
|
+
"core.login.mfa_setup_title": "Bind multi-factor authentication",
|
|
69
|
+
"core.login.mfa_copy_uri": "Copy binding address",
|
|
70
|
+
"core.login.mfa_copy_success": "Binding address copied",
|
|
71
|
+
"core.login.mfa_recovery_codes_copied": "Recovery codes copied",
|
|
72
|
+
"core.login.mfa_recovery_codes_title": "Save MFA recovery codes",
|
|
73
|
+
"core.login.mfa_recovery_codes_warning": "Each code works once and will not be shown again after this dialog closes.",
|
|
74
|
+
"core.login.mfa_copy_recovery_codes": "Copy recovery codes",
|
|
75
|
+
"core.login.mfa_recovery_codes_confirm": "I saved them",
|
|
76
|
+
"core.login.mfa_setup_success": "Multi-factor authentication enabled. Please sign in again.",
|
|
77
|
+
"core.login.mfa_webauthn_action": "Use passkey",
|
|
78
|
+
"core.login.mfa_webauthn_setup_unsupported": "WebAuthn is not supported on this platform. Use TOTP or a recovery code.",
|
|
79
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn registration did not return a valid credential.",
|
|
80
|
+
"core.login.mfa_webauthn_login_unsupported": "WebAuthn is not supported on this platform. Use a recovery code.",
|
|
81
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn authentication did not return a valid credential.",
|
|
63
82
|
"core.login.password": "Enter password",
|
|
64
83
|
"core.login.privacy": "Privacy Policy",
|
|
65
84
|
"core.login.protocol_missing": "Terms of Service and Privacy Policy are not configured",
|
|
@@ -73,6 +92,7 @@
|
|
|
73
92
|
"core.login.wechat_bind_tip": "This WeChat account is not linked. Log in to an existing account to link it.",
|
|
74
93
|
"core.login.wechat_failed": "WeChat login failed. Try again later.",
|
|
75
94
|
"core.navigation.home": "Home",
|
|
95
|
+
"core.navigation.message": "Messages",
|
|
76
96
|
"core.navigation.error.access": "Invalid menu access mode: {access}",
|
|
77
97
|
"core.navigation.error.child_tab": "Only second-level app menus can be tabs: {name}",
|
|
78
98
|
"core.navigation.error.cycle": "Menu parent cycle detected: {name}",
|
|
@@ -101,5 +121,9 @@
|
|
|
101
121
|
"core.status.loading": "Loading",
|
|
102
122
|
"core.webview.embed_blocked": "The destination may block embedding on H5",
|
|
103
123
|
"core.webview.invalid_url": "No valid link was provided",
|
|
104
|
-
"core.webview.open_failed": "Unable to open link"
|
|
124
|
+
"core.webview.open_failed": "Unable to open link",
|
|
125
|
+
"common.value.top_level": "Top level",
|
|
126
|
+
"common.resource.default": "{resource}",
|
|
127
|
+
"common.action.delete_resource": "Delete {resource}",
|
|
128
|
+
"common.action.set_field_status": "Set {field}"
|
|
105
129
|
}
|
package/src/locales/index.ts
CHANGED
|
@@ -119,7 +119,7 @@ export function registerLocaleMessages(modules: KratosTaroModule[]): void {
|
|
|
119
119
|
}
|
|
120
120
|
const target = localeMessages.get(locale) as LocaleMessages
|
|
121
121
|
Object.keys(messages).forEach((key) => {
|
|
122
|
-
if (!
|
|
122
|
+
if (!isAllowedLocaleKey(module.name, key)) {
|
|
123
123
|
throw new Error(`${module.name} 的语言键命名空间无效: ${key}`)
|
|
124
124
|
}
|
|
125
125
|
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
@@ -132,6 +132,15 @@ export function registerLocaleMessages(modules: KratosTaroModule[]): void {
|
|
|
132
132
|
})
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/** 校验公共键和当前业务模块专属的语言键命名空间。 */
|
|
136
|
+
function isAllowedLocaleKey(moduleName: string, key: string): boolean {
|
|
137
|
+
const packageName = moduleName.split('/').at(-1) ?? moduleName
|
|
138
|
+
const moduleNamespace = packageName.split('-').at(-1) ?? packageName
|
|
139
|
+
return ['common', 'core', 'system', moduleNamespace].some((namespace) =>
|
|
140
|
+
key.startsWith(`${namespace}.`),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
135
144
|
/** 获取当前语言区域。 */
|
|
136
145
|
export function getCurrentLocale(): SupportedLocale {
|
|
137
146
|
return useLocaleStore.getState().locale
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"core.crypto.password_required": "パスワードを空にすることはできません",
|
|
28
28
|
"core.crypto.password_too_long": "パスワードの長さが制限を超えています",
|
|
29
29
|
"core.crypto.public_key_invalid": "公開鍵の形式が無効です",
|
|
30
|
+
"core.password.verify_title": "パスワードの確認",
|
|
31
|
+
"core.password.verify_label": "現在のパスワード",
|
|
30
32
|
"core.file.download_failed": "ダウンロードに失敗しました",
|
|
31
33
|
"core.file.temporary_path_required": "ミニ プログラムをアップロードするには、一時ファイル パスを使用してください。",
|
|
32
34
|
"core.file.upload_failed": "アップロードに失敗しました",
|
|
@@ -60,6 +62,23 @@
|
|
|
60
62
|
"core.login.default_sub_title": "このアプリケーションへようこそ",
|
|
61
63
|
"core.login.login_failed": "ログインに失敗しました。もう一度お試しください。",
|
|
62
64
|
"core.login.login_success": "ログインに成功しました",
|
|
65
|
+
"core.login.mfa_title": "多要素認証",
|
|
66
|
+
"core.login.mfa_code": "認証コードを入力",
|
|
67
|
+
"core.login.mfa_recovery_code": "リカバリーコード(任意)",
|
|
68
|
+
"core.login.mfa_setup_title": "多要素認証を設定",
|
|
69
|
+
"core.login.mfa_copy_uri": "登録アドレスをコピー",
|
|
70
|
+
"core.login.mfa_copy_success": "登録アドレスをコピーしました",
|
|
71
|
+
"core.login.mfa_recovery_codes_copied": "リカバリーコードをコピーしました",
|
|
72
|
+
"core.login.mfa_recovery_codes_title": "MFA リカバリーコードを保存",
|
|
73
|
+
"core.login.mfa_recovery_codes_warning": "各コードは一度だけ使用でき、ダイアログを閉じると再表示されません。",
|
|
74
|
+
"core.login.mfa_copy_recovery_codes": "リカバリーコードをコピー",
|
|
75
|
+
"core.login.mfa_recovery_codes_confirm": "保存しました",
|
|
76
|
+
"core.login.mfa_setup_success": "多要素認証を設定しました。再度ログインしてください。",
|
|
77
|
+
"core.login.mfa_webauthn_action": "パスキーを使用",
|
|
78
|
+
"core.login.mfa_webauthn_setup_unsupported": "このプラットフォームは WebAuthn に対応していません。TOTP またはリカバリーコードを使用してください。",
|
|
79
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn 登録で有効なクレデンシャルが返されませんでした。",
|
|
80
|
+
"core.login.mfa_webauthn_login_unsupported": "このプラットフォームは WebAuthn に対応していません。リカバリーコードを使用してください。",
|
|
81
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn 認証で有効なクレデンシャルが返されませんでした。",
|
|
63
82
|
"core.login.password": "パスワードを入力してください",
|
|
64
83
|
"core.login.privacy": "「プライバシー規約」",
|
|
65
84
|
"core.login.protocol_missing": "利用規約とプライバシー規約が設定されていません",
|
|
@@ -73,6 +92,7 @@
|
|
|
73
92
|
"core.login.wechat_bind_tip": "この WeChat アカウントはまだバインドされていません。バインドを完了するには、既存のアカウントにログインしてください。",
|
|
74
93
|
"core.login.wechat_failed": "WeChat ログインに失敗しました。後でもう一度試してください。",
|
|
75
94
|
"core.navigation.home": "ホーム",
|
|
95
|
+
"core.navigation.message": "メッセージ",
|
|
76
96
|
"core.navigation.error.access": "無効なメニュー アクセス モード: {access}",
|
|
77
97
|
"core.navigation.error.child_tab": "モバイル セカンダリ メニューのみがタブとして使用できます: {name}",
|
|
78
98
|
"core.navigation.error.cycle": "モバイル メニューにはループの親があります: {name}",
|
|
@@ -101,5 +121,9 @@
|
|
|
101
121
|
"core.status.loading": "をロードしています",
|
|
102
122
|
"core.webview.embed_blocked": "現在の H5 ページは、ターゲット サイトによって埋め込みが制限されている可能性があります",
|
|
103
123
|
"core.webview.invalid_url": "有効なリンク アドレスがありません",
|
|
104
|
-
"core.webview.open_failed": "リンクを開けません"
|
|
124
|
+
"core.webview.open_failed": "リンクを開けません",
|
|
125
|
+
"common.value.top_level": "トップレベル",
|
|
126
|
+
"common.resource.default": "{resource}",
|
|
127
|
+
"common.action.delete_resource": "{resource}を削除",
|
|
128
|
+
"common.action.set_field_status": "{field}を設定"
|
|
105
129
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"core.crypto.password_required": "密码不能为空",
|
|
28
28
|
"core.crypto.password_too_long": "密码长度超出限制",
|
|
29
29
|
"core.crypto.public_key_invalid": "公钥格式无效",
|
|
30
|
+
"core.password.verify_title": "验证密码",
|
|
31
|
+
"core.password.verify_label": "当前密码",
|
|
30
32
|
"core.file.download_failed": "下载失败",
|
|
31
33
|
"core.file.temporary_path_required": "小程序端请使用文件临时路径上传",
|
|
32
34
|
"core.file.upload_failed": "上传失败",
|
|
@@ -60,6 +62,23 @@
|
|
|
60
62
|
"core.login.default_sub_title": "欢迎使用本应用",
|
|
61
63
|
"core.login.login_failed": "登录失败,请重试",
|
|
62
64
|
"core.login.login_success": "登录成功",
|
|
65
|
+
"core.login.mfa_title": "多因素认证",
|
|
66
|
+
"core.login.mfa_code": "请输入动态口令",
|
|
67
|
+
"core.login.mfa_recovery_code": "恢复码(可选)",
|
|
68
|
+
"core.login.mfa_setup_title": "绑定多因素认证",
|
|
69
|
+
"core.login.mfa_copy_uri": "复制绑定地址",
|
|
70
|
+
"core.login.mfa_copy_success": "绑定地址已复制",
|
|
71
|
+
"core.login.mfa_recovery_codes_copied": "恢复码已复制",
|
|
72
|
+
"core.login.mfa_recovery_codes_title": "保存 MFA 恢复码",
|
|
73
|
+
"core.login.mfa_recovery_codes_warning": "每个恢复码只能使用一次,此弹窗关闭后不会再次显示。",
|
|
74
|
+
"core.login.mfa_copy_recovery_codes": "复制恢复码",
|
|
75
|
+
"core.login.mfa_recovery_codes_confirm": "我已保存",
|
|
76
|
+
"core.login.mfa_setup_success": "多因素认证绑定成功,请重新登录。",
|
|
77
|
+
"core.login.mfa_webauthn_action": "使用 Passkey",
|
|
78
|
+
"core.login.mfa_webauthn_setup_unsupported": "当前平台不支持 WebAuthn,请使用 TOTP 或恢复码。",
|
|
79
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn 注册未返回有效凭据。",
|
|
80
|
+
"core.login.mfa_webauthn_login_unsupported": "当前平台不支持 WebAuthn,请使用恢复码。",
|
|
81
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn 登录未返回有效凭据。",
|
|
63
82
|
"core.login.password": "请输入密码",
|
|
64
83
|
"core.login.privacy": "《隐私协议》",
|
|
65
84
|
"core.login.protocol_missing": "服务条款和隐私协议未配置",
|
|
@@ -73,6 +92,7 @@
|
|
|
73
92
|
"core.login.wechat_bind_tip": "该微信尚未绑定,请登录已有账号完成绑定",
|
|
74
93
|
"core.login.wechat_failed": "微信登录失败,请稍后重试",
|
|
75
94
|
"core.navigation.home": "首页",
|
|
95
|
+
"core.navigation.message": "消息",
|
|
76
96
|
"core.navigation.error.access": "菜单访问模式无效:{access}",
|
|
77
97
|
"core.navigation.error.child_tab": "只有移动端二级菜单可以作为 tab:{name}",
|
|
78
98
|
"core.navigation.error.cycle": "移动端菜单存在循环父级:{name}",
|
|
@@ -101,5 +121,9 @@
|
|
|
101
121
|
"core.status.loading": "正在加载",
|
|
102
122
|
"core.webview.embed_blocked": "当前 H5 页面可能被目标站点限制嵌入",
|
|
103
123
|
"core.webview.invalid_url": "缺少有效链接地址",
|
|
104
|
-
"core.webview.open_failed": "链接无法打开"
|
|
124
|
+
"core.webview.open_failed": "链接无法打开",
|
|
125
|
+
"common.value.top_level": "顶级节点",
|
|
126
|
+
"common.resource.default": "{resource}",
|
|
127
|
+
"common.action.delete_resource": "删除{resource}",
|
|
128
|
+
"common.action.set_field_status": "设置{field}"
|
|
105
129
|
}
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"core.crypto.password_required": "密碼不能為空",
|
|
28
28
|
"core.crypto.password_too_long": "密碼長度超出限制",
|
|
29
29
|
"core.crypto.public_key_invalid": "公鑰格式無效",
|
|
30
|
+
"core.password.verify_title": "驗證密碼",
|
|
31
|
+
"core.password.verify_label": "當前密碼",
|
|
30
32
|
"core.file.download_failed": "下載失敗",
|
|
31
33
|
"core.file.temporary_path_required": "小程式端請使用檔案臨時路徑上傳",
|
|
32
34
|
"core.file.upload_failed": "上傳失敗",
|
|
@@ -60,6 +62,23 @@
|
|
|
60
62
|
"core.login.default_sub_title": "歡迎使用本應用",
|
|
61
63
|
"core.login.login_failed": "登入失敗,請重試",
|
|
62
64
|
"core.login.login_success": "登入成功",
|
|
65
|
+
"core.login.mfa_title": "多因素認證",
|
|
66
|
+
"core.login.mfa_code": "請輸入動態密碼",
|
|
67
|
+
"core.login.mfa_recovery_code": "恢復碼(可選)",
|
|
68
|
+
"core.login.mfa_setup_title": "綁定多因素認證",
|
|
69
|
+
"core.login.mfa_copy_uri": "複製綁定地址",
|
|
70
|
+
"core.login.mfa_copy_success": "綁定地址已複製",
|
|
71
|
+
"core.login.mfa_recovery_codes_copied": "恢復碼已複製",
|
|
72
|
+
"core.login.mfa_recovery_codes_title": "儲存 MFA 恢復碼",
|
|
73
|
+
"core.login.mfa_recovery_codes_warning": "每個恢復碼只能使用一次,關閉此視窗後不會再次顯示。",
|
|
74
|
+
"core.login.mfa_copy_recovery_codes": "複製恢復碼",
|
|
75
|
+
"core.login.mfa_recovery_codes_confirm": "我已儲存",
|
|
76
|
+
"core.login.mfa_setup_success": "多因素認證綁定成功,請重新登入。",
|
|
77
|
+
"core.login.mfa_webauthn_action": "使用 Passkey",
|
|
78
|
+
"core.login.mfa_webauthn_setup_unsupported": "目前平台不支援 WebAuthn,請使用 TOTP 或恢復碼。",
|
|
79
|
+
"core.login.mfa_webauthn_setup_invalid": "WebAuthn 註冊未回傳有效憑證。",
|
|
80
|
+
"core.login.mfa_webauthn_login_unsupported": "目前平台不支援 WebAuthn,請使用恢復碼。",
|
|
81
|
+
"core.login.mfa_webauthn_login_invalid": "WebAuthn 登入未回傳有效憑證。",
|
|
63
82
|
"core.login.password": "請輸入密碼",
|
|
64
83
|
"core.login.privacy": "《隱私協議》",
|
|
65
84
|
"core.login.protocol_missing": "服務條款和隱私協議未配置",
|
|
@@ -73,6 +92,7 @@
|
|
|
73
92
|
"core.login.wechat_bind_tip": "該微信尚未繫結,請登入已有賬號完成繫結",
|
|
74
93
|
"core.login.wechat_failed": "微信登入失敗,請稍後重試",
|
|
75
94
|
"core.navigation.home": "首頁",
|
|
95
|
+
"core.navigation.message": "消息",
|
|
76
96
|
"core.navigation.error.access": "選單訪問模式無效:{access}",
|
|
77
97
|
"core.navigation.error.child_tab": "只有移動端二級選單可以作為 tab:{name}",
|
|
78
98
|
"core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
|
|
@@ -101,5 +121,9 @@
|
|
|
101
121
|
"core.status.loading": "正在載入",
|
|
102
122
|
"core.webview.embed_blocked": "當前 H5 頁面可能被目標站點限制嵌入",
|
|
103
123
|
"core.webview.invalid_url": "缺少有效連結地址",
|
|
104
|
-
"core.webview.open_failed": "連結無法開啟"
|
|
124
|
+
"core.webview.open_failed": "連結無法開啟",
|
|
125
|
+
"common.value.top_level": "頂級節點",
|
|
126
|
+
"common.resource.default": "{resource}",
|
|
127
|
+
"common.action.delete_resource": "刪除{resource}",
|
|
128
|
+
"common.action.set_field_status": "設定{field}"
|
|
105
129
|
}
|
package/src/navigation.ts
CHANGED
|
@@ -51,6 +51,24 @@ export interface AppNavigationState {
|
|
|
51
51
|
tabBar: AppMenuNode[]
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** 移动端菜单未读 badge 状态。 */
|
|
55
|
+
interface AppMenuBadgeState {
|
|
56
|
+
badges: Record<string, number>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 移动端菜单 badge 状态容器。 */
|
|
60
|
+
export const useAppMenuBadges = create<AppMenuBadgeState>(() => ({ badges: {} }))
|
|
61
|
+
|
|
62
|
+
/** 设置移动端菜单 badge 数量,供业务模块注册未读状态。 */
|
|
63
|
+
export function setAppMenuBadge(viewKey: string, count: number): void {
|
|
64
|
+
useAppMenuBadges.setState((state) => {
|
|
65
|
+
const badges = { ...state.badges }
|
|
66
|
+
if (count > 0) badges[viewKey] = Math.min(Math.floor(count), 99)
|
|
67
|
+
else delete badges[viewKey]
|
|
68
|
+
return { badges }
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
const ANONYMOUS_CACHE_KEY = 'kratos-taro-app:navigation:anonymous'
|
|
55
73
|
const AUTHENTICATED_CACHE_KEY = 'kratos-taro-app:navigation:authenticated'
|
|
56
74
|
/** 固定移动端菜单根目录编号。 */
|
|
@@ -69,6 +87,18 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
69
87
|
icon: 'HOME_DEFAULT',
|
|
70
88
|
selectedIcon: 'HOME_SELECTED',
|
|
71
89
|
},
|
|
90
|
+
{
|
|
91
|
+
id: 9990904,
|
|
92
|
+
parentId: APP_MENU_ROOT_ID,
|
|
93
|
+
name: 'AppMessage',
|
|
94
|
+
path: 'app/message',
|
|
95
|
+
viewKey: 'MESSAGE_INBOX',
|
|
96
|
+
title: '',
|
|
97
|
+
access: 'AUTHENTICATED',
|
|
98
|
+
inTabBar: true,
|
|
99
|
+
icon: 'MESSAGE_DEFAULT',
|
|
100
|
+
selectedIcon: 'MESSAGE_SELECTED',
|
|
101
|
+
},
|
|
72
102
|
{
|
|
73
103
|
id: 99909,
|
|
74
104
|
parentId: APP_MENU_ROOT_ID,
|
|
@@ -145,6 +175,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
145
175
|
|
|
146
176
|
const defaultMenuTitleKeys: Record<string, string> = {
|
|
147
177
|
AppHome: 'core.navigation.home',
|
|
178
|
+
AppMessage: 'core.navigation.message',
|
|
148
179
|
AppMy: 'core.navigation.my',
|
|
149
180
|
AppLogin: 'common.action.login',
|
|
150
181
|
AppProtocol: 'core.navigation.protocol',
|
|
@@ -256,6 +287,26 @@ export function navigateAppRoute(
|
|
|
256
287
|
void Taro.navigateTo({ url, fail: () => void Taro.reLaunch({ url }) })
|
|
257
288
|
}
|
|
258
289
|
|
|
290
|
+
/** 按稳定 viewKey 跳转,并将动作参数作为查询参数传递。 */
|
|
291
|
+
export function navigateAppView(viewKey: string, params: Record<string, string> = {}): void {
|
|
292
|
+
const menu = useAppNavigation.getState().menus.find((item) => item.viewKey === viewKey)
|
|
293
|
+
if (!menu) {
|
|
294
|
+
launchAppStatus('NOT_FOUND')
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
const remaining = { ...params }
|
|
298
|
+
const path = menu.path.replace(/:([A-Za-z0-9_]+)/g, (placeholder, key: string) => {
|
|
299
|
+
if (!(key in remaining)) return placeholder
|
|
300
|
+
const value = remaining[key]
|
|
301
|
+
delete remaining[key]
|
|
302
|
+
return encodeURIComponent(value)
|
|
303
|
+
})
|
|
304
|
+
const query = Object.entries(remaining)
|
|
305
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
306
|
+
.join('&')
|
|
307
|
+
navigateAppRoute(`${path}${query ? `?${query}` : ''}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
259
310
|
/** 切换 tab 页面,优先复用已有页面,避免重建页面栈产生空白帧。 */
|
|
260
311
|
function navigateTabRoute(url: string): void {
|
|
261
312
|
const targetRoute = url.slice(1).split('?', 1)[0].replace(/^\/+/, '')
|