@liujitcn/kratos-taro-app-core 0.0.20 → 0.0.21
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/build.mjs +3 -3
- package/package.json +1 -1
- package/src/api/base/file.ts +9 -4
- package/src/api/base/language.ts +21 -0
- package/src/bootstrap.ts +15 -1
- package/src/components/BootstrapStatus.tsx +3 -1
- package/src/components/KratosPageFrame.tsx +3 -1
- package/src/index.ts +18 -0
- package/src/locales/README.md +15 -0
- package/src/locales/en-US.json +113 -0
- package/src/locales/es-ES.json +113 -0
- package/src/locales/fr-FR.json +113 -0
- package/src/locales/index.ts +211 -0
- package/src/locales/ja-JP.json +113 -0
- package/src/locales/ko-KR.json +113 -0
- package/src/locales/zh-CN.json +113 -0
- package/src/locales/zh-TW.json +113 -0
- package/src/module.ts +4 -0
- package/src/navigation.ts +48 -23
- package/src/pages.ts +3 -3
- package/src/rpc/base/v1/config.ts +2 -0
- package/src/rpc/base/v1/language.ts +37 -0
- package/src/stores/setting.ts +9 -4
- package/src/utils/file.ts +7 -2
- package/src/utils/http.ts +29 -10
- package/src/utils/passwordCrypto.ts +13 -12
- package/src/views/pages/index/index.tsx +13 -11
- package/src/views/pages/login/login.tsx +71 -48
- package/src/views/pages/login/protocal.tsx +5 -3
- package/src/views/pages/status/index.tsx +4 -10
- package/src/views/pages/webview/webview.tsx +6 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common.action.back": "返回",
|
|
3
|
+
"common.action.cancel": "取消",
|
|
4
|
+
"common.action.confirm": "確定",
|
|
5
|
+
"common.action.delete": "刪除",
|
|
6
|
+
"common.action.login": "登入",
|
|
7
|
+
"common.action.logout": "退出登入",
|
|
8
|
+
"common.action.new": "新建",
|
|
9
|
+
"common.action.openInNewWindow": "新視窗開啟",
|
|
10
|
+
"common.action.retry": "重試",
|
|
11
|
+
"common.action.save": "儲存",
|
|
12
|
+
"common.action.search": "搜尋",
|
|
13
|
+
"common.field.language": "語言",
|
|
14
|
+
"common.language.en-US": "English",
|
|
15
|
+
"common.language.ja-JP": "日本語",
|
|
16
|
+
"common.language.zh-CN": "簡體中文",
|
|
17
|
+
"common.language.zh-TW": "繁體中文",
|
|
18
|
+
"common.language.ko-KR": "韓語",
|
|
19
|
+
"common.language.fr-FR": "法語",
|
|
20
|
+
"common.language.es-ES": "西班牙語",
|
|
21
|
+
"common.message.loading": "正在載入",
|
|
22
|
+
"common.message.networkError": "網路錯誤,換個網路試試",
|
|
23
|
+
"common.message.requestError": "請求錯誤",
|
|
24
|
+
"common.message.unknown": "暫時無法處理,請稍後重試",
|
|
25
|
+
"common.title.notice": "提示",
|
|
26
|
+
"core.auth.loginAgain": "重新登入",
|
|
27
|
+
"core.auth.sessionExpired": "當前頁面已失效,請重新登入",
|
|
28
|
+
"core.config.missing": "移動端配置缺失:{names}",
|
|
29
|
+
"core.crypto.base64DecodeUnsupported": "當前環境不支援 Base64 解碼",
|
|
30
|
+
"core.crypto.base64EncodeUnsupported": "當前環境不支援 Base64 編碼",
|
|
31
|
+
"core.crypto.encryptFailed": "密碼加密失敗",
|
|
32
|
+
"core.crypto.passwordRequired": "密碼不能為空",
|
|
33
|
+
"core.crypto.passwordTooLong": "密碼長度超出限制",
|
|
34
|
+
"core.crypto.publicKeyInvalid": "公鑰格式無效",
|
|
35
|
+
"core.crypto.randomFailed": "隨機數生成失敗",
|
|
36
|
+
"core.crypto.unsupported": "當前環境不支援密碼加密",
|
|
37
|
+
"core.file.downloadFailed": "下載失敗",
|
|
38
|
+
"core.file.temporaryPathRequired": "小程式端請使用檔案臨時路徑上傳",
|
|
39
|
+
"core.file.uploadFailed": "上傳失敗",
|
|
40
|
+
"core.home.accountCapability": "賬戶能力",
|
|
41
|
+
"core.home.accountCapabilityDescription": "個人資料、應用設定與智慧助手入口已保留",
|
|
42
|
+
"core.home.crossPlatform": "跨端頁面",
|
|
43
|
+
"core.home.crossPlatformDescription": "同一套頁面程式碼適配 H5 和微信小程式",
|
|
44
|
+
"core.home.demoScope": "演示範圍",
|
|
45
|
+
"core.home.framework": "應用框架",
|
|
46
|
+
"core.home.language": "開發語言",
|
|
47
|
+
"core.home.mainTitle": "應用框架示例",
|
|
48
|
+
"core.home.stateManagement": "狀態管理",
|
|
49
|
+
"core.home.staticHome": "靜態首頁",
|
|
50
|
+
"core.home.staticHomeDescription": "當前首頁用於展示框架資訊,不依賴業務介面",
|
|
51
|
+
"core.home.styleSolution": "樣式方案",
|
|
52
|
+
"core.home.subTitle": "保留通用導航與個人中心體驗",
|
|
53
|
+
"core.home.techStack": "當前技術棧",
|
|
54
|
+
"core.login.agreePrefix": "我已閱讀並同意",
|
|
55
|
+
"core.login.agreeSeparator": "和",
|
|
56
|
+
"core.login.behaviorClick": "請按順序點選文字",
|
|
57
|
+
"core.login.behaviorLoading": "載入中...",
|
|
58
|
+
"core.login.behaviorPuzzle": "請拖動滑塊完成拼圖",
|
|
59
|
+
"core.login.behaviorRotate": "拖動滑塊,將內圈圖片轉正",
|
|
60
|
+
"core.login.bindWechat": "登入並繫結微信",
|
|
61
|
+
"core.login.captcha": "請輸入驗證碼",
|
|
62
|
+
"core.login.captchaLoadFailed": "驗證碼載入失敗",
|
|
63
|
+
"core.login.captchaTypeMissing": "登入驗證碼型別未配置",
|
|
64
|
+
"core.login.captchaTypeName": "登入驗證碼型別",
|
|
65
|
+
"core.login.captchaTypeResponseMissing": "驗證碼介面未返回型別",
|
|
66
|
+
"core.login.captchaWriteFailed": "驗證碼圖片寫入失敗",
|
|
67
|
+
"core.login.defaultSubTitle": "歡迎使用本應用",
|
|
68
|
+
"core.login.loginFailed": "登入失敗,請重試",
|
|
69
|
+
"core.login.loginSuccess": "登入成功",
|
|
70
|
+
"core.login.password": "請輸入密碼",
|
|
71
|
+
"core.login.privacy": "《隱私協議》",
|
|
72
|
+
"core.login.protocolMissing": "服務條款和隱私協議未配置",
|
|
73
|
+
"core.login.protocolPrompt": "請先閱讀並勾選協議內容,點選確定後將自動勾選並繼續登入",
|
|
74
|
+
"core.login.service": "《服務條款》",
|
|
75
|
+
"core.login.tenant": "請輸入租戶編號",
|
|
76
|
+
"core.login.userName": "請輸入使用者名稱或手機號",
|
|
77
|
+
"core.login.userNameMobile": "請輸入使用者名稱/手機號碼",
|
|
78
|
+
"core.login.userNamePassword": "請輸入使用者名稱和密碼",
|
|
79
|
+
"core.login.wechat": "微信一鍵登入",
|
|
80
|
+
"core.login.wechatBindTip": "該微信尚未繫結,請登入已有賬號完成繫結",
|
|
81
|
+
"core.login.wechatFailed": "微信登入失敗,請稍後重試",
|
|
82
|
+
"core.navigation.home": "首頁",
|
|
83
|
+
"core.navigation.error.access": "選單訪問模式無效:{access}",
|
|
84
|
+
"core.navigation.error.childTab": "只有移動端二級選單可以作為 tab:{name}",
|
|
85
|
+
"core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
|
|
86
|
+
"core.navigation.error.identity": "選單 id、name 或 app/ 路徑無效",
|
|
87
|
+
"core.navigation.error.itemObject": "選單項不是物件",
|
|
88
|
+
"core.navigation.error.listMissing": "選單響應缺少扁平列表",
|
|
89
|
+
"core.navigation.error.namePrefix": "選單名稱必須使用 App 字首:{name}",
|
|
90
|
+
"core.navigation.error.parentInvalid": "移動端選單父級編號無效:{name}",
|
|
91
|
+
"core.navigation.error.parentMissing": "選單缺少父級編號:{name}",
|
|
92
|
+
"core.navigation.error.parentNotFound": "移動端選單父級不存在:{name}",
|
|
93
|
+
"core.navigation.error.responseObject": "選單響應不是物件",
|
|
94
|
+
"core.navigation.error.rootTab": "移動端二級選單必須作為 tab:{name}",
|
|
95
|
+
"core.navigation.error.tabCount": "tabBar 只能配置 0 或 2-5 項",
|
|
96
|
+
"core.navigation.error.unique": "選單 id、name、path 必須唯一",
|
|
97
|
+
"core.navigation.error.viewKey": "未註冊 viewKey:{viewKey}",
|
|
98
|
+
"core.navigation.my": "我的",
|
|
99
|
+
"core.navigation.protocol": "協議詳情",
|
|
100
|
+
"core.protocol.loadFailed": "移動端配置載入失敗",
|
|
101
|
+
"core.protocol.notConfigured": "{title}未配置,暫時無法檢視",
|
|
102
|
+
"core.protocol.privacy": "隱私協議",
|
|
103
|
+
"core.protocol.service": "服務條款",
|
|
104
|
+
"core.status.CONFIG_ERROR": "導航配置無效",
|
|
105
|
+
"core.status.FORBIDDEN": "暫無訪問許可權",
|
|
106
|
+
"core.status.NOT_FOUND": "頁面不存在",
|
|
107
|
+
"core.status.OFFLINE": "網路不可用",
|
|
108
|
+
"core.status.PAGE_UNAVAILABLE": "頁面暫不可用",
|
|
109
|
+
"core.status.loading": "正在載入",
|
|
110
|
+
"core.webview.embedBlocked": "當前 H5 頁面可能被目標站點限制嵌入",
|
|
111
|
+
"core.webview.invalidUrl": "缺少有效連結地址",
|
|
112
|
+
"core.webview.openFailed": "連結無法開啟"
|
|
113
|
+
}
|
package/src/module.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { LocaleMessages, SupportedLocale } from './locales'
|
|
2
|
+
|
|
1
3
|
/** bootstrap 可见状态键。 */
|
|
2
4
|
export type BootstrapViewKey =
|
|
3
5
|
| 'BOOTSTRAP_LOADING'
|
|
@@ -19,6 +21,8 @@ export interface KratosTaroModule {
|
|
|
19
21
|
pages: Record<string, KratosTaroPageConfig>
|
|
20
22
|
views: Record<string, string>
|
|
21
23
|
icons?: Record<string, string>
|
|
24
|
+
/** 模块按语言区域贡献的扁平语言包。 */
|
|
25
|
+
messages?: Partial<Record<SupportedLocale, LocaleMessages>>
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
/** 声明 Taro 模块。 */
|
package/src/navigation.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { matchLogicalPath, parseLogicalQuery } from './navigation-pattern.mjs'
|
|
|
6
6
|
import { buildMenuTree } from './navigation-tree.mjs'
|
|
7
7
|
import { hasValidToken } from './utils/auth'
|
|
8
8
|
import { navigateToLogin } from './utils/navigation'
|
|
9
|
+
import { getCurrentLocale, t } from './locales'
|
|
9
10
|
|
|
10
11
|
/** 移动菜单访问模式。 */
|
|
11
12
|
export type AppMenuAccess = 'PUBLIC' | 'GUEST_ONLY' | 'AUTHENTICATED'
|
|
@@ -62,7 +63,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
62
63
|
name: 'AppHome',
|
|
63
64
|
path: 'app/home',
|
|
64
65
|
viewKey: 'HOME',
|
|
65
|
-
title: '
|
|
66
|
+
title: '',
|
|
66
67
|
access: 'PUBLIC',
|
|
67
68
|
inTabBar: true,
|
|
68
69
|
icon: 'HOME_DEFAULT',
|
|
@@ -74,7 +75,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
74
75
|
name: 'AppMy',
|
|
75
76
|
path: 'app/my',
|
|
76
77
|
viewKey: 'PROFILE_HOME',
|
|
77
|
-
title: '
|
|
78
|
+
title: '',
|
|
78
79
|
access: 'PUBLIC',
|
|
79
80
|
inTabBar: true,
|
|
80
81
|
icon: 'USER_DEFAULT',
|
|
@@ -86,7 +87,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
86
87
|
name: 'AppLogin',
|
|
87
88
|
path: 'app/login',
|
|
88
89
|
viewKey: 'LOGIN',
|
|
89
|
-
title: '
|
|
90
|
+
title: '',
|
|
90
91
|
access: 'GUEST_ONLY',
|
|
91
92
|
inTabBar: false,
|
|
92
93
|
},
|
|
@@ -96,7 +97,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
96
97
|
name: 'AppProtocol',
|
|
97
98
|
path: 'app/protocol/:type',
|
|
98
99
|
viewKey: 'PROTOCOL',
|
|
99
|
-
title: '
|
|
100
|
+
title: '',
|
|
100
101
|
access: 'PUBLIC',
|
|
101
102
|
inTabBar: false,
|
|
102
103
|
},
|
|
@@ -106,7 +107,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
106
107
|
name: 'AppProfile',
|
|
107
108
|
path: 'app/profile',
|
|
108
109
|
viewKey: 'PROFILE',
|
|
109
|
-
title: '
|
|
110
|
+
title: '',
|
|
110
111
|
access: 'AUTHENTICATED',
|
|
111
112
|
inTabBar: false,
|
|
112
113
|
},
|
|
@@ -116,7 +117,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
116
117
|
name: 'AppSettings',
|
|
117
118
|
path: 'app/settings',
|
|
118
119
|
viewKey: 'SETTINGS',
|
|
119
|
-
title: '
|
|
120
|
+
title: '',
|
|
120
121
|
access: 'AUTHENTICATED',
|
|
121
122
|
inTabBar: false,
|
|
122
123
|
},
|
|
@@ -126,7 +127,7 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
126
127
|
name: 'AppAi',
|
|
127
128
|
path: 'app/ai',
|
|
128
129
|
viewKey: 'AI',
|
|
129
|
-
title: '
|
|
130
|
+
title: '',
|
|
130
131
|
access: 'AUTHENTICATED',
|
|
131
132
|
inTabBar: false,
|
|
132
133
|
},
|
|
@@ -142,6 +143,23 @@ export const defaultAppMenus: AppMenu[] = [
|
|
|
142
143
|
},
|
|
143
144
|
]
|
|
144
145
|
|
|
146
|
+
const defaultMenuTitleKeys: Record<string, string> = {
|
|
147
|
+
AppHome: 'core.navigation.home',
|
|
148
|
+
AppMy: 'core.navigation.my',
|
|
149
|
+
AppLogin: 'common.action.login',
|
|
150
|
+
AppProtocol: 'core.navigation.protocol',
|
|
151
|
+
AppProfile: 'system.profile.title',
|
|
152
|
+
AppSettings: 'system.settings.title',
|
|
153
|
+
AppAi: 'system.ai.chatTitle',
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function localizedDefaultAppMenus(): AppMenu[] {
|
|
157
|
+
return defaultAppMenus.map((menu) => ({
|
|
158
|
+
...menu,
|
|
159
|
+
title: defaultMenuTitleKeys[menu.name] ? t(defaultMenuTitleKeys[menu.name]) : menu.title,
|
|
160
|
+
}))
|
|
161
|
+
}
|
|
162
|
+
|
|
145
163
|
function createNavigationState(menus: AppMenu[], ready: boolean): AppNavigationState {
|
|
146
164
|
const menuTree = buildMenuTree(menus, APP_MENU_ROOT_ID) as AppMenuNode[]
|
|
147
165
|
return { menus, ready, menuTree, tabBar: menuTree.filter((menu) => menu.inTabBar) }
|
|
@@ -165,7 +183,7 @@ export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void
|
|
|
165
183
|
export async function initializeAppNavigation(): Promise<void> {
|
|
166
184
|
const cacheKey = resolveCacheKey()
|
|
167
185
|
const cached = readCachedMenus(cacheKey)
|
|
168
|
-
useAppNavigation.setState(createNavigationState(cached ??
|
|
186
|
+
useAppNavigation.setState(createNavigationState(cached ?? localizedDefaultAppMenus(), false))
|
|
169
187
|
try {
|
|
170
188
|
const nextMenus = normalizeMenuResponse(await adapter.list())
|
|
171
189
|
validateMenus(nextMenus)
|
|
@@ -242,7 +260,8 @@ export function launchAppStatus(
|
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
function resolveCacheKey(): string {
|
|
245
|
-
|
|
263
|
+
const identity = hasValidToken() ? AUTHENTICATED_CACHE_KEY : ANONYMOUS_CACHE_KEY
|
|
264
|
+
return `${identity}:${getCurrentLocale()}`
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
|
|
@@ -259,15 +278,15 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
|
|
|
259
278
|
|
|
260
279
|
function normalizeMenuResponse(response: unknown): AppMenu[] {
|
|
261
280
|
if (Array.isArray(response)) return response.map(normalizeMenu)
|
|
262
|
-
if (!response || typeof response !== 'object') throw new Error('
|
|
281
|
+
if (!response || typeof response !== 'object') throw new Error(t('core.navigation.error.responseObject'))
|
|
263
282
|
const record = response as Record<string, unknown>
|
|
264
283
|
const list = record.items ?? record.list ?? record.data
|
|
265
|
-
if (!Array.isArray(list)) throw new Error('
|
|
284
|
+
if (!Array.isArray(list)) throw new Error(t('core.navigation.error.listMissing'))
|
|
266
285
|
return list.map(normalizeMenu)
|
|
267
286
|
}
|
|
268
287
|
|
|
269
288
|
function normalizeMenu(value: unknown): AppMenu {
|
|
270
|
-
if (!value || typeof value !== 'object') throw new Error('
|
|
289
|
+
if (!value || typeof value !== 'object') throw new Error(t('core.navigation.error.itemObject'))
|
|
271
290
|
const item = value as Record<string, unknown>
|
|
272
291
|
const meta = (item.meta ?? {}) as Record<string, unknown>
|
|
273
292
|
const app = (meta.app ?? item.app ?? {}) as Record<string, unknown>
|
|
@@ -299,15 +318,19 @@ function validateMenus(nextMenus: AppMenu[]): void {
|
|
|
299
318
|
const paths = new Set<string>()
|
|
300
319
|
for (const menu of nextMenus) {
|
|
301
320
|
if (!Number.isSafeInteger(menu.id) || !menu.name || !menu.path.startsWith('app/')) {
|
|
302
|
-
throw new Error('
|
|
321
|
+
throw new Error(t('core.navigation.error.identity'))
|
|
322
|
+
}
|
|
323
|
+
if (!menu.name.startsWith('App')) {
|
|
324
|
+
throw new Error(t('core.navigation.error.namePrefix', { name: menu.name }))
|
|
303
325
|
}
|
|
304
|
-
if (!menu.name.startsWith('App')) throw new Error(`菜单名称必须使用 App 前缀:${menu.name}`)
|
|
305
326
|
if (!['PUBLIC', 'GUEST_ONLY', 'AUTHENTICATED'].includes(menu.access)) {
|
|
306
|
-
throw new Error(
|
|
327
|
+
throw new Error(t('core.navigation.error.access', { access: menu.access }))
|
|
328
|
+
}
|
|
329
|
+
if (!resolveStaticView(menu.viewKey)) {
|
|
330
|
+
throw new Error(t('core.navigation.error.viewKey', { viewKey: menu.viewKey }))
|
|
307
331
|
}
|
|
308
|
-
if (!resolveStaticView(menu.viewKey)) throw new Error(`未注册 viewKey:${menu.viewKey}`)
|
|
309
332
|
if (ids.has(menu.id) || names.has(menu.name) || paths.has(menu.path)) {
|
|
310
|
-
throw new Error('
|
|
333
|
+
throw new Error(t('core.navigation.error.unique'))
|
|
311
334
|
}
|
|
312
335
|
ids.add(menu.id)
|
|
313
336
|
names.add(menu.name)
|
|
@@ -315,22 +338,24 @@ function validateMenus(nextMenus: AppMenu[]): void {
|
|
|
315
338
|
}
|
|
316
339
|
const menuMap = new Map(nextMenus.map((menu) => [menu.id, menu]))
|
|
317
340
|
for (const menu of nextMenus) {
|
|
318
|
-
if (menu.parentId === undefined)
|
|
341
|
+
if (menu.parentId === undefined) {
|
|
342
|
+
throw new Error(t('core.navigation.error.parentMissing', { name: menu.name }))
|
|
343
|
+
}
|
|
319
344
|
if (menu.parentId === APP_MENU_ROOT_ID) {
|
|
320
|
-
if (!menu.inTabBar) throw new Error(
|
|
345
|
+
if (!menu.inTabBar) throw new Error(t('core.navigation.error.rootTab', { name: menu.name }))
|
|
321
346
|
continue
|
|
322
347
|
}
|
|
323
|
-
if (menu.inTabBar) throw new Error(
|
|
348
|
+
if (menu.inTabBar) throw new Error(t('core.navigation.error.childTab', { name: menu.name }))
|
|
324
349
|
const visited = new Set<number>([menu.id])
|
|
325
350
|
let parentId = menu.parentId
|
|
326
351
|
while (parentId !== APP_MENU_ROOT_ID) {
|
|
327
|
-
if (visited.has(parentId)) throw new Error(
|
|
352
|
+
if (visited.has(parentId)) throw new Error(t('core.navigation.error.cycle', { name: menu.name }))
|
|
328
353
|
visited.add(parentId)
|
|
329
354
|
const parent = menuMap.get(parentId)
|
|
330
|
-
if (!parent?.parentId) throw new Error(
|
|
355
|
+
if (!parent?.parentId) throw new Error(t('core.navigation.error.parentNotFound', { name: menu.name }))
|
|
331
356
|
parentId = parent.parentId
|
|
332
357
|
}
|
|
333
358
|
}
|
|
334
359
|
const tabCount = nextMenus.filter((menu) => menu.parentId === APP_MENU_ROOT_ID).length
|
|
335
|
-
if (tabCount === 1 || tabCount > 5) throw new Error('
|
|
360
|
+
if (tabCount === 1 || tabCount > 5) throw new Error(t('core.navigation.error.tabCount'))
|
|
336
361
|
}
|
package/src/pages.ts
CHANGED
|
@@ -6,11 +6,11 @@ export const corePages: Record<string, KratosTaroPageConfig> = {
|
|
|
6
6
|
style: {
|
|
7
7
|
navigationStyle: 'custom',
|
|
8
8
|
navigationBarTextStyle: 'white',
|
|
9
|
-
navigationBarTitleText: '
|
|
9
|
+
navigationBarTitleText: '',
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
|
-
'pages/login/login': { style: { navigationBarTitleText: '
|
|
13
|
-
'pages/login/protocal': { style: { navigationBarTitleText: '
|
|
12
|
+
'pages/login/login': { style: { navigationBarTitleText: '' } },
|
|
13
|
+
'pages/login/protocal': { style: { navigationBarTitleText: '' } },
|
|
14
14
|
'pages/webview/webview': { style: { navigationBarTitleText: '' } },
|
|
15
15
|
'pages/status/index': {
|
|
16
16
|
style: {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
2
|
+
// versions:
|
|
3
|
+
// protoc-gen-ts_proto v2.11.8
|
|
4
|
+
// protoc unknown
|
|
5
|
+
// source: base/v1/language.proto
|
|
6
|
+
|
|
7
|
+
/* eslint-disable */
|
|
8
|
+
|
|
9
|
+
/** 查询语言请求参数。 */
|
|
10
|
+
export interface GetLanguageRequest {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 支持的语言。 */
|
|
14
|
+
export interface LanguageItem {
|
|
15
|
+
/** 标准语言代码 */
|
|
16
|
+
language_code: string;
|
|
17
|
+
/** 语言名称 */
|
|
18
|
+
language_name: string;
|
|
19
|
+
/** 本地语言名称 */
|
|
20
|
+
native_name: string;
|
|
21
|
+
/** 排序,值越小越靠前 */
|
|
22
|
+
sort: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 当前支持的语言查询结果。 */
|
|
26
|
+
export interface GetLanguageResponse {
|
|
27
|
+
/** 当前支持的语言列表 */
|
|
28
|
+
languages: LanguageItem[];
|
|
29
|
+
/** 主语言代码 */
|
|
30
|
+
primary_language_code: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 语言公共服务。 */
|
|
34
|
+
export interface LanguageService {
|
|
35
|
+
/** 查询当前支持的语言和主语言。 */
|
|
36
|
+
GetLanguage(request: GetLanguageRequest): Promise<GetLanguageResponse>;
|
|
37
|
+
}
|
package/src/stores/setting.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
2
|
import { defConfigService } from '../api/base/config'
|
|
3
|
+
import { t } from '../locales'
|
|
3
4
|
import { BaseConfigSite } from '../rpc/base/v1/enum'
|
|
4
5
|
|
|
5
6
|
const REQUIRED_APP_CONFIGS = [
|
|
6
|
-
{ key: 'serviceProtocol',
|
|
7
|
-
{ key: 'privacyProtocol',
|
|
8
|
-
{ key: 'captchaType',
|
|
7
|
+
{ key: 'serviceProtocol', nameKey: 'core.login.service' },
|
|
8
|
+
{ key: 'privacyProtocol', nameKey: 'core.login.privacy' },
|
|
9
|
+
{ key: 'captchaType', nameKey: 'core.login.captchaTypeName' },
|
|
9
10
|
] as const
|
|
10
11
|
|
|
11
12
|
/** 应用配置状态及操作。 */
|
|
@@ -30,7 +31,11 @@ export const useSettingStore = create<SettingStoreState>((set, get) => ({
|
|
|
30
31
|
const nextData = new Map(response.configs.map((item) => [item.key, item.value]))
|
|
31
32
|
const missing = REQUIRED_APP_CONFIGS.filter(({ key }) => !nextData.get(key))
|
|
32
33
|
if (missing.length) {
|
|
33
|
-
throw new Error(
|
|
34
|
+
throw new Error(
|
|
35
|
+
t('core.config.missing', {
|
|
36
|
+
names: missing.map(({ nameKey }) => t(nameKey)).join(', '),
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
34
39
|
}
|
|
35
40
|
set({ data: nextData })
|
|
36
41
|
})()
|
package/src/utils/file.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Taro from '@tarojs/taro'
|
|
2
2
|
import type { FileInfo, MultiUploadFileResponse } from '../rpc/base/v1/file'
|
|
3
|
+
import { getLocaleRequestHeaders, t } from '../locales'
|
|
3
4
|
import { formatSrc } from './index'
|
|
4
5
|
import { getRequestAccessToken, requestBaseURL } from './http'
|
|
5
6
|
|
|
@@ -11,9 +12,13 @@ export async function uploadFile(fileType: string, filePath: string): Promise<Fi
|
|
|
11
12
|
name: 'file',
|
|
12
13
|
filePath,
|
|
13
14
|
formData: { fileType },
|
|
14
|
-
header:
|
|
15
|
+
header: {
|
|
16
|
+
...getLocaleRequestHeaders(),
|
|
17
|
+
'source-client': 'miniapp',
|
|
18
|
+
...(token ? { Authorization: token } : {}),
|
|
19
|
+
},
|
|
15
20
|
})
|
|
16
|
-
if (response.statusCode !== 200) throw new Error('
|
|
21
|
+
if (response.statusCode !== 200) throw new Error(t('core.file.uploadFailed'))
|
|
17
22
|
return JSON.parse(response.data) as FileInfo
|
|
18
23
|
}
|
|
19
24
|
|
package/src/utils/http.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
shouldRefreshToken,
|
|
11
11
|
} from './auth'
|
|
12
12
|
import { saveCurrentRoute } from './navigation'
|
|
13
|
+
import { getLocaleRequestHeaders, t } from '../locales'
|
|
13
14
|
|
|
14
15
|
const apiBasePath = process.env.KRATOS_TARO_API_BASE || '/api'
|
|
15
16
|
const apiTargetUrl = process.env.KRATOS_TARO_API_URL || ''
|
|
@@ -22,12 +23,14 @@ const SESSION_URL = '/v1/base/session'
|
|
|
22
23
|
const REFRESH_TOKEN_URL = '/v1/base/token'
|
|
23
24
|
const CAPTCHA_URL = '/v1/base/captcha'
|
|
24
25
|
const CONFIG_URL = '/v1/base/config'
|
|
26
|
+
const LANGUAGE_URL = '/v1/base/language'
|
|
25
27
|
const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
|
|
26
28
|
const NO_AUTH_URL_SET = new Set([
|
|
27
29
|
SESSION_URL,
|
|
28
30
|
REFRESH_TOKEN_URL,
|
|
29
31
|
CAPTCHA_URL,
|
|
30
32
|
CONFIG_URL,
|
|
33
|
+
LANGUAGE_URL,
|
|
31
34
|
PASSWORD_PUBLIC_KEY_URL,
|
|
32
35
|
'/auth',
|
|
33
36
|
'/login/captcha',
|
|
@@ -37,6 +40,7 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
|
|
|
37
40
|
SESSION_URL,
|
|
38
41
|
CAPTCHA_URL,
|
|
39
42
|
CONFIG_URL,
|
|
43
|
+
LANGUAGE_URL,
|
|
40
44
|
PASSWORD_PUBLIC_KEY_URL,
|
|
41
45
|
'/auth',
|
|
42
46
|
'/login/captcha',
|
|
@@ -65,7 +69,9 @@ function isAuthErrorResponse(data: unknown): boolean {
|
|
|
65
69
|
const response = data as ErrorData
|
|
66
70
|
const code = response.code === undefined ? '' : String(response.code)
|
|
67
71
|
const reason = response.reason === undefined ? '' : String(response.reason)
|
|
68
|
-
return
|
|
72
|
+
return (
|
|
73
|
+
authErrorCodeSet.has(code) || authErrorCodeSet.has(reason) || authErrorReasonSet.has(reason)
|
|
74
|
+
)
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
function resolveAuthMode(options: HttpRequestOptions, url: string): AuthMode {
|
|
@@ -75,7 +81,9 @@ function resolveAuthMode(options: HttpRequestOptions, url: string): AuthMode {
|
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
function resolveRequestUrl(url: string): string {
|
|
78
|
-
return /^https?:\/\//.test(url)
|
|
84
|
+
return /^https?:\/\//.test(url)
|
|
85
|
+
? url
|
|
86
|
+
: `${requestBaseURL}${url.startsWith('/') ? url : `/${url}`}`
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
/** 发送经过认证、刷新令牌和统一错误处理的 Taro 请求。 */
|
|
@@ -110,11 +118,16 @@ async function sendRequest<T>(
|
|
|
110
118
|
header: {
|
|
111
119
|
...options.header,
|
|
112
120
|
'source-client': 'miniapp',
|
|
121
|
+
...getLocaleRequestHeaders(),
|
|
113
122
|
...(accessToken ? { Authorization: accessToken } : {}),
|
|
114
123
|
},
|
|
115
124
|
})
|
|
116
125
|
const responseData = response.data as ErrorData | null | undefined
|
|
117
|
-
if (
|
|
126
|
+
if (
|
|
127
|
+
response.statusCode >= 200 &&
|
|
128
|
+
response.statusCode < 300 &&
|
|
129
|
+
!isAuthErrorResponse(responseData)
|
|
130
|
+
) {
|
|
118
131
|
return response.data as T
|
|
119
132
|
}
|
|
120
133
|
if (
|
|
@@ -129,7 +142,10 @@ async function sendRequest<T>(
|
|
|
129
142
|
handleAuthExpiredByMode(authMode, requestUrl, responseData)
|
|
130
143
|
throw response
|
|
131
144
|
}
|
|
132
|
-
await Taro.showToast({
|
|
145
|
+
await Taro.showToast({
|
|
146
|
+
icon: 'none',
|
|
147
|
+
title: responseData?.message || t('common.message.requestError'),
|
|
148
|
+
})
|
|
133
149
|
throw response
|
|
134
150
|
} catch (error) {
|
|
135
151
|
if (
|
|
@@ -139,7 +155,7 @@ async function sendRequest<T>(
|
|
|
139
155
|
) {
|
|
140
156
|
throw error
|
|
141
157
|
}
|
|
142
|
-
await Taro.showToast({ icon: 'none', title: '
|
|
158
|
+
await Taro.showToast({ icon: 'none', title: t('common.message.networkError') })
|
|
143
159
|
throw error
|
|
144
160
|
}
|
|
145
161
|
}
|
|
@@ -195,7 +211,7 @@ async function refreshAccessToken(): Promise<void> {
|
|
|
195
211
|
url: resolveRequestUrl(REFRESH_TOKEN_URL),
|
|
196
212
|
method: 'POST',
|
|
197
213
|
data: { refresh_token: refreshToken },
|
|
198
|
-
header: { 'source-client': 'miniapp' },
|
|
214
|
+
header: { 'source-client': 'miniapp', ...getLocaleRequestHeaders() },
|
|
199
215
|
})
|
|
200
216
|
const data = response.data as ErrorData & {
|
|
201
217
|
token_type?: string
|
|
@@ -224,10 +240,10 @@ async function promptRelogin(): Promise<void> {
|
|
|
224
240
|
isPromptingRelogin = true
|
|
225
241
|
try {
|
|
226
242
|
const modal = await Taro.showModal({
|
|
227
|
-
title: '
|
|
228
|
-
content: '
|
|
243
|
+
title: t('common.title.notice'),
|
|
244
|
+
content: t('core.auth.sessionExpired'),
|
|
229
245
|
showCancel: false,
|
|
230
|
-
confirmText: '
|
|
246
|
+
confirmText: t('core.auth.loginAgain'),
|
|
231
247
|
})
|
|
232
248
|
if (!modal.confirm) return
|
|
233
249
|
await new Promise((resolve) => setTimeout(resolve, 80))
|
|
@@ -251,7 +267,10 @@ function handleAuthExpiredByMode(
|
|
|
251
267
|
}
|
|
252
268
|
silentClearAuthData()
|
|
253
269
|
if (authMode !== 'optional') {
|
|
254
|
-
void Taro.showToast({
|
|
270
|
+
void Taro.showToast({
|
|
271
|
+
icon: 'none',
|
|
272
|
+
title: responseData?.message || t('common.message.requestError'),
|
|
273
|
+
})
|
|
255
274
|
}
|
|
256
275
|
}
|
|
257
276
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Taro from '@tarojs/taro'
|
|
2
2
|
import * as miniCrypto from 'asmcrypto.js'
|
|
3
3
|
import { defLoginService } from '../api/base/login'
|
|
4
|
+
import { t } from '../locales'
|
|
4
5
|
import { PasswordCryptoScene } from '../rpc/base/v1/enum'
|
|
5
6
|
import type { PasswordCrypto } from '../rpc/common/v1/types'
|
|
6
7
|
|
|
@@ -17,7 +18,7 @@ function pemToArrayBuffer(pem: string) {
|
|
|
17
18
|
.replace(/\s/g, '')
|
|
18
19
|
const decode = globalThis.atob
|
|
19
20
|
if (!decode) {
|
|
20
|
-
throw new Error('
|
|
21
|
+
throw new Error(t('core.crypto.base64DecodeUnsupported'))
|
|
21
22
|
}
|
|
22
23
|
const binary = decode(base64)
|
|
23
24
|
const buffer = new Uint8Array(binary.length)
|
|
@@ -36,7 +37,7 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
|
36
37
|
}
|
|
37
38
|
const encode = globalThis.btoa
|
|
38
39
|
if (!encode) {
|
|
39
|
-
throw new Error('
|
|
40
|
+
throw new Error(t('core.crypto.base64EncodeUnsupported'))
|
|
40
41
|
}
|
|
41
42
|
return encode(binary)
|
|
42
43
|
}
|
|
@@ -59,7 +60,7 @@ type DerNode = {
|
|
|
59
60
|
/** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
|
|
60
61
|
function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
61
62
|
if (offset + 2 > data.length) {
|
|
62
|
-
throw new Error('
|
|
63
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
63
64
|
}
|
|
64
65
|
const tag = data[offset]
|
|
65
66
|
const firstLengthByte = data[offset + 1]
|
|
@@ -68,7 +69,7 @@ function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
|
68
69
|
if (firstLengthByte & 0x80) {
|
|
69
70
|
const lengthByteCount = firstLengthByte & 0x7f
|
|
70
71
|
if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
|
|
71
|
-
throw new Error('
|
|
72
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
72
73
|
}
|
|
73
74
|
length = 0
|
|
74
75
|
for (let index = 0; index < lengthByteCount; index += 1) {
|
|
@@ -78,7 +79,7 @@ function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
|
78
79
|
}
|
|
79
80
|
const end = valueOffset + length
|
|
80
81
|
if (end > data.length) {
|
|
81
|
-
throw new Error('
|
|
82
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
82
83
|
}
|
|
83
84
|
return {
|
|
84
85
|
tag,
|
|
@@ -107,13 +108,13 @@ function parseMiniRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
|
|
|
107
108
|
const algorithm = readDerNode(subjectPublicKeyInfo.value, 0)
|
|
108
109
|
const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next)
|
|
109
110
|
if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
|
|
110
|
-
throw new Error('
|
|
111
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
111
112
|
}
|
|
112
113
|
const rsaPublicKey = readDerNode(bitString.value, 1)
|
|
113
114
|
const modulus = readDerNode(rsaPublicKey.value, 0)
|
|
114
115
|
const exponent = readDerNode(rsaPublicKey.value, modulus.next)
|
|
115
116
|
if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
|
|
116
|
-
throw new Error('
|
|
117
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
117
118
|
}
|
|
118
119
|
return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)]
|
|
119
120
|
}
|
|
@@ -131,7 +132,7 @@ function generateMgf1Mask(crypto: MiniCrypto, seed: Uint8Array, length: number)
|
|
|
131
132
|
counter[3] = index
|
|
132
133
|
let block = hash.reset().process(seed).process(counter).finish().result
|
|
133
134
|
if (!block) {
|
|
134
|
-
throw new Error('
|
|
135
|
+
throw new Error(t('core.crypto.encryptFailed'))
|
|
135
136
|
}
|
|
136
137
|
const target = mask.subarray(index * hash.HASH_SIZE)
|
|
137
138
|
if (block.length > target.length) {
|
|
@@ -171,14 +172,14 @@ async function encryptPortablePassword(
|
|
|
171
172
|
const dataBlockLength = keySize - hash.HASH_SIZE - 1
|
|
172
173
|
const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1
|
|
173
174
|
if (paddingLength < 0) {
|
|
174
|
-
throw new Error('
|
|
175
|
+
throw new Error(t('core.crypto.passwordTooLong'))
|
|
175
176
|
}
|
|
176
177
|
const encodedMessage = new Uint8Array(keySize)
|
|
177
178
|
const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1)
|
|
178
179
|
const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1)
|
|
179
180
|
const labelHash = hash.reset().process(new Uint8Array()).finish().result
|
|
180
181
|
if (!labelHash) {
|
|
181
|
-
throw new Error('
|
|
182
|
+
throw new Error(t('core.crypto.encryptFailed'))
|
|
182
183
|
}
|
|
183
184
|
dataBlock.set(labelHash, 0)
|
|
184
185
|
dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1)
|
|
@@ -196,7 +197,7 @@ async function encryptPortablePassword(
|
|
|
196
197
|
new crypto.BigNumber(encodedMessage),
|
|
197
198
|
).result
|
|
198
199
|
if (!encryptedKey) {
|
|
199
|
-
throw new Error('
|
|
200
|
+
throw new Error(t('core.crypto.encryptFailed'))
|
|
200
201
|
}
|
|
201
202
|
const ciphertext = crypto.AES_GCM.encrypt(plaintext, aesKey, iv)
|
|
202
203
|
return {
|
|
@@ -216,7 +217,7 @@ export async function encryptPassword(
|
|
|
216
217
|
): Promise<PasswordCrypto> {
|
|
217
218
|
const plainPassword = password.trim()
|
|
218
219
|
if (!plainPassword) {
|
|
219
|
-
throw new Error('
|
|
220
|
+
throw new Error(t('core.crypto.passwordRequired'))
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })
|