@liujitcn/kratos-taro-app-core 0.0.29 → 0.0.32
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 +14 -7
- package/package.json +2 -1
- package/src/api/base/{config.ts → v1/config.ts} +2 -2
- package/src/api/base/{file.ts → v1/file.ts} +4 -4
- package/src/api/base/{language.ts → v1/language.ts} +2 -2
- package/src/api/base/{login.ts → v1/login.ts} +3 -3
- package/src/api/base/v1/mfa.ts +127 -0
- package/src/api/base/{oauth.ts → v1/oauth.ts} +3 -3
- package/src/api/system/{auth.ts → app/v1/auth.ts} +8 -15
- package/src/api/system/{base_area.ts → app/v1/base_area.ts} +2 -2
- package/src/api/system/app/v1/base_dict.ts +17 -0
- package/src/api/{base/menu.ts → system/app/v1/base_menu.ts} +2 -2
- package/src/bootstrap.ts +27 -3
- package/src/components/KratosTabBar.scss +64 -0
- package/src/components/KratosTabBar.tsx +8 -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 -4
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +25 -4
- package/src/locales/zh-CN.json +25 -4
- package/src/locales/zh-TW.json +25 -4
- package/src/navigation.ts +103 -63
- package/src/rpc/base/v1/login.ts +37 -4
- package/src/rpc/base/v1/mfa.ts +180 -0
- package/src/rpc/base/v1/oauth.ts +25 -0
- package/src/rpc/system/app/v1/auth.ts +7 -0
- package/src/rpc/system/common/v1/common.ts +35 -0
- package/src/runner.ts +23 -7
- package/src/stores/setting.ts +1 -1
- package/src/stores/user.ts +20 -8
- package/src/utils/http.ts +3 -7
- package/src/utils/passwordCrypto.ts +1 -1
- package/src/utils/webauthn.ts +94 -0
- package/src/views/pages/login/login.scss +29 -0
- package/src/views/pages/login/login.tsx +336 -53
- package/src/api/system/base_dict.ts +0 -24
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## 主要能力
|
|
6
6
|
|
|
7
|
-
- 账号密码、行为验证码、微信登录、OAuth、Token 刷新和退出。
|
|
8
|
-
- 统一 HTTP
|
|
9
|
-
- Zustand
|
|
7
|
+
- 账号密码、行为验证码、微信登录、OAuth、MFA(TOTP/WebAuthn/恢复码)、Token 刷新和退出。
|
|
8
|
+
- 统一 HTTP、上传、下载、文件预览、RSA/AES 密码加密和 WebAuthn 工具。
|
|
9
|
+
- Zustand 用户、配置和导航状态,H5 浏览器页签标题使用宿主 HTML 声明的应用名称。
|
|
10
|
+
- 登录阶段支持 TOTP、WebAuthn 和一次性恢复码校验。
|
|
11
|
+
- 公开 Base MFA 请求及通用密码弹窗、绑定面板和恢复码弹窗,供登录页与业务模块复用同一套绑定界面。
|
|
10
12
|
- 接口驱动的逻辑路由、访问模式、自绘 tabBar 和稳定 `viewKey`。
|
|
11
13
|
- 页面静态资源 export、动态资源公共路径解析,以及 H5 默认导航栏兼容框架。
|
|
12
14
|
- 首页、登录、协议、WebView、启动/错误状态页面。
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
- `@liujitcn/kratos-taro-app-core/runner`:Taro 构建装配入口。
|
|
21
23
|
- `api/*`、`components/*`、`rpc/*`、`styles/*`、`utils/*`、`views/*`:白名单子路径。
|
|
22
24
|
|
|
23
|
-
业务模块只能通过这些 exports 使用底座,不得相对引用 core 源码。RPC 由 `make -C
|
|
25
|
+
业务模块只能通过这些 exports 使用底座,不得相对引用 core 源码。RPC 由 `make -C frontend ts-taro-app` 生成,不能手工编辑。
|
|
24
26
|
|
|
25
27
|
## Runner 协议
|
|
26
28
|
|
package/dist/runner.mjs
CHANGED
|
@@ -281,7 +281,7 @@ function restoreBuildTransaction(transaction, transactionFile) {
|
|
|
281
281
|
removeEmptyParents(dirname(file), transaction.inputDir);
|
|
282
282
|
}
|
|
283
283
|
removeGeneratedStaticFiles(
|
|
284
|
-
transaction.generatedStaticFiles
|
|
284
|
+
transaction.generatedStaticFiles,
|
|
285
285
|
resolve(transaction.inputDir, "static")
|
|
286
286
|
);
|
|
287
287
|
writeFileSync(transaction.appConfigFile, transaction.originalAppConfig);
|
|
@@ -447,8 +447,15 @@ function appendPage(manifest, route) {
|
|
|
447
447
|
const subRoute = rest.join("/");
|
|
448
448
|
if (!subpackage.pages.includes(subRoute)) subpackage.pages.push(subRoute);
|
|
449
449
|
}
|
|
450
|
+
function typescriptStringLiteral(value) {
|
|
451
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t")}'`;
|
|
452
|
+
}
|
|
453
|
+
function pageStyleString(style, key, fallback) {
|
|
454
|
+
const value = style?.[key];
|
|
455
|
+
return typeof value === "string" ? value : fallback;
|
|
456
|
+
}
|
|
450
457
|
function createPageWrapper(page) {
|
|
451
|
-
return `import KratosPage from ${
|
|
458
|
+
return `import KratosPage from ${typescriptStringLiteral(`${page.source}.tsx`)}
|
|
452
459
|
import { KratosPageFrame } from '@liujitcn/kratos-taro-app-core/components/KratosPageFrame'
|
|
453
460
|
import { KratosTabBar } from '@liujitcn/kratos-taro-app-core/components/KratosTabBar'
|
|
454
461
|
|
|
@@ -457,14 +464,14 @@ export default function KratosPageWrapper() {
|
|
|
457
464
|
return (
|
|
458
465
|
<>
|
|
459
466
|
<KratosPageFrame
|
|
460
|
-
navigationStyle=${
|
|
461
|
-
navigationBarTitleText=${
|
|
462
|
-
navigationBarBackgroundColor=${
|
|
463
|
-
navigationBarTextStyle=${
|
|
467
|
+
navigationStyle=${typescriptStringLiteral(pageStyleString(page.style, "navigationStyle", "default"))}
|
|
468
|
+
navigationBarTitleText=${typescriptStringLiteral(pageStyleString(page.style, "navigationBarTitleText", ""))}
|
|
469
|
+
navigationBarBackgroundColor=${typescriptStringLiteral(pageStyleString(page.style, "navigationBarBackgroundColor", "#f8f8f8"))}
|
|
470
|
+
navigationBarTextStyle=${typescriptStringLiteral(pageStyleString(page.style, "navigationBarTextStyle", "black"))}
|
|
464
471
|
>
|
|
465
472
|
<KratosPage />
|
|
466
473
|
</KratosPageFrame>
|
|
467
|
-
<KratosTabBar route=${
|
|
474
|
+
<KratosTabBar route=${typescriptStringLiteral(page.route)} />
|
|
468
475
|
</>
|
|
469
476
|
)
|
|
470
477
|
}
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git@github.com:liujitcn/kratos-admin.git"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.0.
|
|
10
|
+
"version": "0.0.32",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@tarojs/taro": "4.2.1",
|
|
43
43
|
"asmcrypto.js": "2.3.2",
|
|
44
44
|
"go-captcha-react": "1.0.5",
|
|
45
|
+
"qrcode-generator": "^2.0.4",
|
|
45
46
|
"react": "18.3.1",
|
|
46
47
|
"typescript": "5.8.3",
|
|
47
48
|
"zustand": "5.0.6"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { http } from '
|
|
2
|
-
import type { ConfigService, GetConfigRequest, GetConfigResponse } from '
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
|
+
import type { ConfigService, GetConfigRequest, GetConfigResponse } from '../../../rpc/base/v1/config'
|
|
3
3
|
|
|
4
4
|
const CONFIG_URL = '/v1/base/config'
|
|
5
5
|
|
|
@@ -3,10 +3,10 @@ import type {
|
|
|
3
3
|
DownloadFileRequest,
|
|
4
4
|
FileInfo,
|
|
5
5
|
MultiUploadFileResponse,
|
|
6
|
-
} from '
|
|
7
|
-
import { uploadFile, uploadFileList } from '
|
|
8
|
-
import { getLocaleRequestHeaders, t } from '
|
|
9
|
-
import { getRequestAccessToken, requestBaseURL } from '
|
|
6
|
+
} from '../../../rpc/base/v1/file'
|
|
7
|
+
import { uploadFile, uploadFileList } from '../../../utils/file'
|
|
8
|
+
import { getLocaleRequestHeaders, t } from '../../../locales'
|
|
9
|
+
import { getRequestAccessToken, requestBaseURL } from '../../../utils/http'
|
|
10
10
|
|
|
11
11
|
const FILE_URL = '/v1/base/file'
|
|
12
12
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { http } from '
|
|
2
|
-
import type { LanguageService, OptionLanguageRequest, OptionLanguageResponse } from '
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
|
+
import type { LanguageService, OptionLanguageRequest, OptionLanguageResponse } from '../../../rpc/base/v1/language'
|
|
3
3
|
|
|
4
4
|
const LANGUAGE_URL = '/v1/base/language'
|
|
5
5
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
2
|
import type {
|
|
3
3
|
CaptchaRequest,
|
|
4
4
|
CaptchaResponse,
|
|
@@ -12,8 +12,8 @@ import type {
|
|
|
12
12
|
VerifyCaptchaRequest,
|
|
13
13
|
VerifyCaptchaResponse,
|
|
14
14
|
LoginService,
|
|
15
|
-
} from '
|
|
16
|
-
import type { Empty } from '
|
|
15
|
+
} from '../../../rpc/base/v1/login'
|
|
16
|
+
import type { Empty } from '../../../rpc/google/protobuf/empty'
|
|
17
17
|
|
|
18
18
|
const CAPTCHA_URL = '/v1/base/captcha'
|
|
19
19
|
const CAPTCHA_VERIFY_URL = '/v1/base/captcha/verify'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
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 http<LoginResponse>({
|
|
31
|
+
url: `${MFA_URL}/verify`,
|
|
32
|
+
method: 'POST',
|
|
33
|
+
authMode: 'none',
|
|
34
|
+
data: request,
|
|
35
|
+
header: { Authorization: 'no-auth' },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 开始登录阶段的强制多因素认证绑定。 */
|
|
40
|
+
BeginMfaEnrollment(request: BeginMfaEnrollmentRequest): Promise<BeginMfaEnrollmentResponse> {
|
|
41
|
+
return http<BeginMfaEnrollmentResponse>({
|
|
42
|
+
url: `${MFA_URL}/enrollment`,
|
|
43
|
+
method: 'POST',
|
|
44
|
+
authMode: 'none',
|
|
45
|
+
data: request,
|
|
46
|
+
header: { Authorization: 'no-auth' },
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 确认登录阶段的强制多因素认证绑定。 */
|
|
51
|
+
ConfirmMfaEnrollment(
|
|
52
|
+
request: ConfirmMfaEnrollmentRequest,
|
|
53
|
+
): Promise<ConfirmMfaEnrollmentResponse> {
|
|
54
|
+
return http<ConfirmMfaEnrollmentResponse>({
|
|
55
|
+
url: `${MFA_URL}/enrollment/confirm`,
|
|
56
|
+
method: 'POST',
|
|
57
|
+
authMode: 'none',
|
|
58
|
+
data: request,
|
|
59
|
+
header: { Authorization: 'no-auth' },
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 查询当前用户多因素认证状态。 */
|
|
64
|
+
GetMfaStatus(request: GetMfaStatusRequest): Promise<MfaStatusResponse> {
|
|
65
|
+
return http<MfaStatusResponse>({
|
|
66
|
+
url: MFA_URL,
|
|
67
|
+
method: 'GET',
|
|
68
|
+
authMode: 'required',
|
|
69
|
+
data: request,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 开始绑定当前用户的多因素认证方式。 */
|
|
74
|
+
BeginMfaSetup(request: BeginMfaSetupRequest): Promise<BeginMfaSetupResponse> {
|
|
75
|
+
return http<BeginMfaSetupResponse>({
|
|
76
|
+
url: `${MFA_URL}/setup`,
|
|
77
|
+
method: 'POST',
|
|
78
|
+
authMode: 'required',
|
|
79
|
+
data: request,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 确认绑定当前用户的多因素认证方式。 */
|
|
84
|
+
ConfirmMfaSetup(request: ConfirmMfaSetupRequest): Promise<ConfirmMfaSetupResponse> {
|
|
85
|
+
return http<ConfirmMfaSetupResponse>({
|
|
86
|
+
url: `${MFA_URL}/setup/confirm`,
|
|
87
|
+
method: 'POST',
|
|
88
|
+
authMode: 'required',
|
|
89
|
+
data: request,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 开始禁用 WebAuthn 多因素认证的 Passkey 验证。 */
|
|
94
|
+
BeginMfaDisable(request: BeginMfaDisableRequest): Promise<BeginMfaDisableResponse> {
|
|
95
|
+
return http<BeginMfaDisableResponse>({
|
|
96
|
+
url: `${MFA_URL}/disable/challenge`,
|
|
97
|
+
method: 'POST',
|
|
98
|
+
authMode: 'required',
|
|
99
|
+
data: request,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 禁用当前用户的多因素认证。 */
|
|
104
|
+
DisableMfa(request: DisableMfaRequest): Promise<Empty> {
|
|
105
|
+
return http<Empty>({
|
|
106
|
+
url: `${MFA_URL}/disable`,
|
|
107
|
+
method: 'POST',
|
|
108
|
+
authMode: 'required',
|
|
109
|
+
data: request,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 重新生成当前用户的多因素认证恢复码。 */
|
|
114
|
+
RegenerateMfaRecoveryCodes(
|
|
115
|
+
request: RegenerateMfaRecoveryCodesRequest,
|
|
116
|
+
): Promise<RecoveryCodesResponse> {
|
|
117
|
+
return http<RecoveryCodesResponse>({
|
|
118
|
+
url: `${MFA_URL}/recovery-codes`,
|
|
119
|
+
method: 'POST',
|
|
120
|
+
authMode: 'required',
|
|
121
|
+
data: request,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** defMfaService 多因素认证公共服务实例。 */
|
|
127
|
+
export const defMfaService = new MfaServiceImpl()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
2
|
import type {
|
|
3
3
|
CreateOauthAuthorizationRequest,
|
|
4
4
|
CreateOauthAuthorizationResponse,
|
|
@@ -19,8 +19,8 @@ import type {
|
|
|
19
19
|
ListOauthProviderResponse,
|
|
20
20
|
OauthService,
|
|
21
21
|
UnbindOauthAccountRequest,
|
|
22
|
-
} from '
|
|
23
|
-
import type { Empty } from '
|
|
22
|
+
} from '../../../rpc/base/v1/oauth'
|
|
23
|
+
import type { Empty } from '../../../rpc/google/protobuf/empty'
|
|
24
24
|
|
|
25
25
|
const OAUTH_PROVIDER_URL = '/v1/base/oauth/provider'
|
|
26
26
|
const OAUTH_AUTHORIZATION_URL = '/v1/base/oauth/authorization'
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
2
|
import {
|
|
3
3
|
type AuthService,
|
|
4
4
|
type BindUserPhoneRequest,
|
|
5
5
|
type BindUserPhoneResponse,
|
|
6
|
+
type GetUserProfileRequest,
|
|
7
|
+
type UpdateUserProfileRequest,
|
|
6
8
|
type UserProfileForm,
|
|
7
|
-
} from '
|
|
8
|
-
import type { Empty } from '
|
|
9
|
+
} from '../../../../rpc/system/app/v1/auth'
|
|
10
|
+
import type { Empty } from '../../../../rpc/google/protobuf/empty'
|
|
9
11
|
|
|
10
12
|
const AUTH_URL = '/v1/app/auth'
|
|
11
13
|
|
|
12
|
-
/** 获取用户资料请求兼容空请求结构。 */
|
|
13
|
-
type GetUserProfileRequestCompat = Empty
|
|
14
|
-
|
|
15
|
-
/** 更新用户资料请求兼容新旧两种表单包裹方式。 */
|
|
16
|
-
type UpdateUserProfileRequestCompat = Partial<UserProfileForm> & {
|
|
17
|
-
user_profile?: UserProfileForm
|
|
18
|
-
}
|
|
19
|
-
|
|
20
14
|
/** 用户认证资料服务 */
|
|
21
15
|
export class AuthServiceImpl implements AuthService {
|
|
22
16
|
/** 获取已登录用户资料 */
|
|
23
|
-
GetUserProfile(request:
|
|
17
|
+
GetUserProfile(request: GetUserProfileRequest): Promise<UserProfileForm> {
|
|
24
18
|
return http<UserProfileForm>({
|
|
25
19
|
url: `${AUTH_URL}/profile`,
|
|
26
20
|
method: 'GET',
|
|
@@ -30,13 +24,12 @@ export class AuthServiceImpl implements AuthService {
|
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
/** 修改个人中心用户信息 */
|
|
33
|
-
UpdateUserProfile(request:
|
|
34
|
-
const userProfile = request.user_profile ?? (request as UserProfileForm)
|
|
27
|
+
UpdateUserProfile(request: UpdateUserProfileRequest): Promise<Empty> {
|
|
35
28
|
return http<Empty>({
|
|
36
29
|
url: `${AUTH_URL}/profile`,
|
|
37
30
|
method: 'PUT',
|
|
38
31
|
authMode: 'required',
|
|
39
|
-
data:
|
|
32
|
+
data: request.user_profile,
|
|
40
33
|
})
|
|
41
34
|
}
|
|
42
35
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
2
|
import type {
|
|
3
3
|
BaseAreaService,
|
|
4
4
|
TreeBaseAreaRequest,
|
|
5
5
|
TreeBaseAreaResponse,
|
|
6
|
-
} from '
|
|
6
|
+
} from '../../../../rpc/system/app/v1/base_area'
|
|
7
7
|
|
|
8
8
|
const BASE_AREA_URL = '/v1/app/base/area'
|
|
9
9
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
|
+
import type { BaseDictForm, BaseDictService, GetBaseDictRequest } from '../../../../rpc/system/app/v1/base_dict'
|
|
3
|
+
|
|
4
|
+
const BASE_DICT_URL = '/v1/app/base/dict'
|
|
5
|
+
|
|
6
|
+
/** 字典服务 */
|
|
7
|
+
export class BaseDictServiceImpl implements BaseDictService {
|
|
8
|
+
/** 查询单个字典 */
|
|
9
|
+
GetBaseDict(request: GetBaseDictRequest): Promise<BaseDictForm> {
|
|
10
|
+
return http<BaseDictForm>({
|
|
11
|
+
url: `${BASE_DICT_URL}/${request.code}`,
|
|
12
|
+
method: 'GET',
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const defBaseDictService = new BaseDictServiceImpl()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { http } from '
|
|
2
|
-
import type { ListBaseMenuResponse } from '
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
|
+
import type { ListBaseMenuResponse } from '../../../../rpc/system/app/v1/base_menu'
|
|
3
3
|
|
|
4
4
|
/** 远端移动菜单服务。 */
|
|
5
5
|
export class BaseMenuServiceImpl {
|
package/src/bootstrap.ts
CHANGED
|
@@ -6,10 +6,17 @@ import { initializeAppNavigation } from './navigation'
|
|
|
6
6
|
import {
|
|
7
7
|
registerUserStoreExtension,
|
|
8
8
|
startUserStoreEventBridge,
|
|
9
|
+
useSettingStore,
|
|
9
10
|
useUserStore,
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
} from './stores'
|
|
12
|
+
import {
|
|
13
|
+
applyLanguageConfig,
|
|
14
|
+
initializeLocale,
|
|
15
|
+
registerLocaleChangeHandler,
|
|
16
|
+
registerLocaleMessages,
|
|
17
|
+
t,
|
|
18
|
+
} from './locales'
|
|
19
|
+
import { defLanguageService } from './api/base/v1/language'
|
|
13
20
|
|
|
14
21
|
/** Taro 应用启动参数。 */
|
|
15
22
|
export interface KratosTaroBootstrapOptions {
|
|
@@ -18,6 +25,21 @@ export interface KratosTaroBootstrapOptions {
|
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
let bootstrapped = false
|
|
28
|
+
let titleObserver: MutationObserver | undefined
|
|
29
|
+
|
|
30
|
+
/** 使用宿主声明的应用名称同步 H5 浏览器页签标题。 */
|
|
31
|
+
function syncH5DocumentTitle(): void {
|
|
32
|
+
if (process.env.TARO_ENV !== 'h5' || typeof document === 'undefined') return
|
|
33
|
+
const titleElement = document.querySelector('title')
|
|
34
|
+
const appTitle = titleElement?.dataset.appTitle || t('core.home.main_title')
|
|
35
|
+
const applyTitle = () => {
|
|
36
|
+
if (document.title !== appTitle) document.title = appTitle
|
|
37
|
+
}
|
|
38
|
+
applyTitle()
|
|
39
|
+
if (!titleElement || titleObserver) return
|
|
40
|
+
titleObserver = new MutationObserver(applyTitle)
|
|
41
|
+
titleObserver.observe(titleElement, { childList: true })
|
|
42
|
+
}
|
|
21
43
|
|
|
22
44
|
/** 注册模块、认证生命周期和导航运行时。 */
|
|
23
45
|
export function bootstrapKratosTaroApp(options: KratosTaroBootstrapOptions): void {
|
|
@@ -34,8 +56,10 @@ export function bootstrapKratosTaroApp(options: KratosTaroBootstrapOptions): voi
|
|
|
34
56
|
})
|
|
35
57
|
if (bootstrapped) return
|
|
36
58
|
bootstrapped = true
|
|
59
|
+
syncH5DocumentTitle()
|
|
37
60
|
registerLocaleChangeHandler(initializeAppNavigation)
|
|
38
61
|
registerLocaleChangeHandler(() => useSettingStore.getState().loadData())
|
|
62
|
+
registerLocaleChangeHandler(syncH5DocumentTitle)
|
|
39
63
|
startUserStoreEventBridge()
|
|
40
64
|
registerUserStoreExtension({
|
|
41
65
|
onLogin: initializeAppNavigation,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.kratos-tab-bar__item {
|
|
16
|
+
position: relative;
|
|
16
17
|
display: flex;
|
|
17
18
|
flex: 1;
|
|
18
19
|
align-items: center;
|
|
@@ -30,6 +31,69 @@
|
|
|
30
31
|
margin-bottom: 4PX;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
.kratos-tab-bar__message-icon {
|
|
35
|
+
position: relative;
|
|
36
|
+
width: 22PX;
|
|
37
|
+
height: 18PX;
|
|
38
|
+
margin-bottom: 6PX;
|
|
39
|
+
border: 2PX solid #9aa4b2;
|
|
40
|
+
border-radius: 6PX;
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
}
|
|
43
|
+
.kratos-tab-bar__message-icon::before {
|
|
44
|
+
position: absolute;
|
|
45
|
+
top: 5PX;
|
|
46
|
+
left: 4PX;
|
|
47
|
+
width: 3PX;
|
|
48
|
+
height: 3PX;
|
|
49
|
+
border-radius: 50%;
|
|
50
|
+
background: #9aa4b2;
|
|
51
|
+
box-shadow:
|
|
52
|
+
5PX 0 #9aa4b2,
|
|
53
|
+
10PX 0 #9aa4b2;
|
|
54
|
+
content: '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kratos-tab-bar__message-icon::after {
|
|
58
|
+
position: absolute;
|
|
59
|
+
bottom: -5PX;
|
|
60
|
+
left: 3PX;
|
|
61
|
+
width: 7PX;
|
|
62
|
+
height: 7PX;
|
|
63
|
+
border-right: 2PX solid #9aa4b2;
|
|
64
|
+
border-bottom: 2PX solid #9aa4b2;
|
|
65
|
+
background: #fff;
|
|
66
|
+
content: '';
|
|
67
|
+
transform: rotate(45deg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.kratos-tab-bar__message-icon--active {
|
|
71
|
+
border-color: #27ba9b;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.kratos-tab-bar__message-icon--active::before {
|
|
75
|
+
background: #27ba9b;
|
|
76
|
+
box-shadow:
|
|
77
|
+
5PX 0 #27ba9b,
|
|
78
|
+
10PX 0 #27ba9b;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.kratos-tab-bar__message-icon--active::after {
|
|
82
|
+
border-color: #27ba9b;
|
|
83
|
+
}
|
|
84
|
+
|
|
33
85
|
.kratos-tab-bar__text--active {
|
|
34
86
|
color: #27ba9b;
|
|
35
87
|
}
|
|
88
|
+
|
|
89
|
+
.kratos-tab-bar__badge {
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: 0;
|
|
92
|
+
right: calc(50% - 19PX);
|
|
93
|
+
width: 10PX;
|
|
94
|
+
height: 10PX;
|
|
95
|
+
box-sizing: border-box;
|
|
96
|
+
border: 2PX solid #fff;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
background: #e5484d;
|
|
99
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Image, Text, View } from '@tarojs/components'
|
|
2
2
|
import { resolveModuleIcon, resolveStaticView } from '../module'
|
|
3
|
-
import { APP_MENU_ROOT_ID, navigateAppRoute, useAppNavigation } from '../navigation'
|
|
3
|
+
import { APP_MENU_ROOT_ID, navigateAppRoute, useAppMenuBadges, useAppNavigation } from '../navigation'
|
|
4
4
|
import { resolveRootMenuId } from '../navigation-tree.mjs'
|
|
5
5
|
import './KratosTabBar.scss'
|
|
6
6
|
|
|
@@ -13,6 +13,7 @@ export interface KratosTabBarProps {
|
|
|
13
13
|
export function KratosTabBar({ route }: KratosTabBarProps) {
|
|
14
14
|
const menus = useAppNavigation((state) => state.menus)
|
|
15
15
|
const tabBar = useAppNavigation((state) => state.tabBar)
|
|
16
|
+
const messageBadge = useAppMenuBadges((state) => state.badges.MESSAGE_INBOX ?? 0)
|
|
16
17
|
const routeMenu = menus.find((menu) => resolveStaticView(menu.viewKey) === route)
|
|
17
18
|
const tabMenuId = routeMenu
|
|
18
19
|
? resolveRootMenuId(menus, routeMenu.id, APP_MENU_ROOT_ID)
|
|
@@ -34,6 +35,12 @@ export function KratosTabBar({ route }: KratosTabBarProps) {
|
|
|
34
35
|
{icon ? (
|
|
35
36
|
<Image className='kratos-tab-bar__icon' src={icon} mode='aspectFit' />
|
|
36
37
|
) : null}
|
|
38
|
+
{!icon && item.viewKey === 'MESSAGE_INBOX' ? (
|
|
39
|
+
<View className={`kratos-tab-bar__message-icon${active ? ' kratos-tab-bar__message-icon--active' : ''}`} />
|
|
40
|
+
) : null}
|
|
41
|
+
{item.viewKey === 'MESSAGE_INBOX' && messageBadge ? (
|
|
42
|
+
<View className='kratos-tab-bar__badge' aria-hidden='true' />
|
|
43
|
+
) : null}
|
|
37
44
|
<Text className={active ? 'kratos-tab-bar__text--active' : ''}>{item.title}</Text>
|
|
38
45
|
</View>
|
|
39
46
|
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.mfa-recovery-dialog {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1001;
|
|
4
|
+
inset: 0;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
padding: 32px;
|
|
9
|
+
background-color: rgb(0 0 0 / 45%);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.mfa-recovery-dialog__panel {
|
|
13
|
+
width: min(100%, 640px);
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
background-color: #fff;
|
|
16
|
+
border-radius: 16px;
|
|
17
|
+
box-shadow: 0 20px 64px rgb(0 0 0 / 20%);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.mfa-recovery-dialog__header {
|
|
21
|
+
padding: 28px 32px;
|
|
22
|
+
border-bottom: 1px solid #eee;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.mfa-recovery-dialog__title {
|
|
26
|
+
color: #222;
|
|
27
|
+
font-size: 34px;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.mfa-recovery-dialog__body {
|
|
32
|
+
padding: 32px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.mfa-recovery-dialog__codes {
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
width: 100%;
|
|
38
|
+
min-height: 220px;
|
|
39
|
+
margin-bottom: 16px;
|
|
40
|
+
padding: 20px 24px;
|
|
41
|
+
color: #222;
|
|
42
|
+
font-size: 30px;
|
|
43
|
+
line-height: 1.6;
|
|
44
|
+
border: 1px solid #ddd;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
overflow-wrap: anywhere;
|
|
47
|
+
white-space: pre-wrap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.mfa-recovery-dialog__copy {
|
|
51
|
+
width: auto;
|
|
52
|
+
min-width: 140px;
|
|
53
|
+
height: 58px;
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0 18px;
|
|
56
|
+
color: #27ba9b;
|
|
57
|
+
font-size: 24px;
|
|
58
|
+
line-height: 58px;
|
|
59
|
+
background-color: #f0fbf8;
|
|
60
|
+
border: 0;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.mfa-recovery-dialog__copy::after,
|
|
65
|
+
.mfa-recovery-dialog__confirm::after {
|
|
66
|
+
border: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.mfa-recovery-dialog__warning {
|
|
70
|
+
display: block;
|
|
71
|
+
margin-top: 12px;
|
|
72
|
+
color: #c58b00;
|
|
73
|
+
font-size: 24px;
|
|
74
|
+
line-height: 1.5;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.mfa-recovery-dialog__footer {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: flex-end;
|
|
80
|
+
padding: 0 32px 32px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.mfa-recovery-dialog__confirm {
|
|
84
|
+
min-width: 160px;
|
|
85
|
+
height: 72px;
|
|
86
|
+
margin: 0;
|
|
87
|
+
padding: 0 24px;
|
|
88
|
+
color: #fff;
|
|
89
|
+
font-size: 28px;
|
|
90
|
+
line-height: 72px;
|
|
91
|
+
background-color: #27ba9b;
|
|
92
|
+
border: 0;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
}
|