@liujitcn/kratos-uni-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.
Files changed (43) hide show
  1. package/README.md +30 -7
  2. package/dist/module.mjs +164 -16
  3. package/dist/vite.mjs +4 -5
  4. package/package.json +3 -1
  5. package/src/api/base/{config.ts → v1/config.ts} +6 -2
  6. package/src/api/base/{file.ts → v1/file.ts} +6 -2
  7. package/src/api/base/{language.ts → v1/language.ts} +2 -2
  8. package/src/api/base/{login.ts → v1/login.ts} +3 -3
  9. package/src/api/base/v1/mfa.ts +127 -0
  10. package/src/api/base/{oauth.ts → v1/oauth.ts} +3 -3
  11. package/src/api/system/{auth.ts → app/v1/auth.ts} +8 -15
  12. package/src/api/system/{base_area.ts → app/v1/base_area.ts} +2 -2
  13. package/src/api/system/app/v1/base_dict.ts +21 -0
  14. package/src/api/{base/menu.ts → system/app/v1/base_menu.ts} +2 -2
  15. package/src/bootstrap.ts +1 -1
  16. package/src/components/KratosSettingsPage.vue +292 -0
  17. package/src/components/KratosTabBar.vue +71 -1
  18. package/src/components/MfaManageDialog.vue +469 -0
  19. package/src/components/MfaRecoveryCodesDialog.vue +192 -0
  20. package/src/components/MfaSetupPanel.vue +160 -0
  21. package/src/components/PasswordVerifyDialog.vue +277 -0
  22. package/src/index.ts +2 -0
  23. package/src/locales/README.md +2 -1
  24. package/src/locales/en-US.json +41 -4
  25. package/src/locales/index.ts +10 -1
  26. package/src/locales/ja-JP.json +41 -4
  27. package/src/locales/zh-CN.json +41 -4
  28. package/src/locales/zh-TW.json +41 -4
  29. package/src/navigation.ts +115 -53
  30. package/src/rpc/base/v1/login.ts +37 -4
  31. package/src/rpc/base/v1/mfa.ts +180 -0
  32. package/src/rpc/base/v1/oauth.ts +25 -0
  33. package/src/rpc/system/app/v1/auth.ts +7 -0
  34. package/src/rpc/system/common/v1/common.ts +35 -0
  35. package/src/stores/modules/setting.ts +1 -1
  36. package/src/stores/modules/user.ts +29 -10
  37. package/src/utils/clipboard.ts +33 -0
  38. package/src/utils/http.ts +10 -13
  39. package/src/utils/passwordCrypto.ts +1 -1
  40. package/src/utils/webauthn.ts +101 -0
  41. package/src/views/pages/login/login.vue +299 -13
  42. package/src/vite.ts +4 -7
  43. package/src/api/system/base_dict.ts +0 -24
@@ -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": "使用 Passkey",
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,17 +90,15 @@
71
90
  "core.login.wechat_bind_tip": "该微信尚未绑定,请登录已有账号完成绑定",
72
91
  "core.login.wechat_failed": "微信登录失败,请稍后重试",
73
92
  "core.navigation.home": "首页",
93
+ "core.navigation.message": "消息",
74
94
  "core.navigation.error.access": "菜单访问模式无效:{access}",
75
95
  "core.navigation.error.child_tab": "只有移动端二级菜单可以作为 tab:{name}",
76
96
  "core.navigation.error.cycle": "移动端菜单存在循环父级:{name}",
77
97
  "core.navigation.error.identity": "菜单 id、name 或 app/ 路径无效",
78
- "core.navigation.error.item_object": "菜单项不是对象",
79
- "core.navigation.error.list_missing": "菜单响应缺少扁平列表",
80
98
  "core.navigation.error.name_prefix": "菜单名称必须使用 App 前缀:{name}",
81
99
  "core.navigation.error.parent_invalid": "移动端菜单父级编号无效:{name}",
82
100
  "core.navigation.error.parent_missing": "菜单缺少父级编号:{name}",
83
101
  "core.navigation.error.parent_not_found": "移动端菜单父级不存在:{name}",
84
- "core.navigation.error.response_object": "菜单响应不是对象",
85
102
  "core.navigation.error.root_tab": "移动端二级菜单必须作为 tab:{name}",
86
103
  "core.navigation.error.tab_count": "tabBar 只能配置 0 或 2-5 项",
87
104
  "core.navigation.error.unique": "菜单 id、name、path 必须唯一",
@@ -92,6 +109,22 @@
92
109
  "core.protocol.not_configured": "{title}未配置,暂时无法查看",
93
110
  "core.protocol.privacy": "隐私协议",
94
111
  "core.protocol.service": "服务条款",
112
+ "core.settings.authorization": "授权管理",
113
+ "core.settings.contact": "联系我们",
114
+ "core.settings.feedback": "问题反馈",
115
+ "core.settings.logout_confirm": "是否退出登录?",
116
+ "core.settings.logout_failed": "退出登录失败",
117
+ "core.settings.mfa_bind_success": "多因素认证绑定成功",
118
+ "core.settings.mfa_disable_action": "禁用",
119
+ "core.settings.mfa_disable_factor_required": "请输入动态口令或恢复码。",
120
+ "core.settings.mfa_disable_success": "多因素认证已禁用",
121
+ "core.settings.mfa_disable_title": "禁用多因素认证",
122
+ "core.settings.mfa_disabled": "未启用",
123
+ "core.settings.mfa_enabled_method": "已启用({method})",
124
+ "core.settings.mfa_method_totp": "TOTP",
125
+ "core.settings.mfa_method_webauthn": "WebAuthn",
126
+ "core.settings.mfa_title": "多因素认证",
127
+ "core.settings.title": "设置",
95
128
  "core.status.CONFIG_ERROR": "导航配置无效",
96
129
  "core.status.forbidden": "暂无访问权限",
97
130
  "core.status.NOT_FOUND": "页面不存在",
@@ -100,5 +133,9 @@
100
133
  "core.status.loading": "正在加载",
101
134
  "core.webview.embed_blocked": "当前 H5 页面可能被目标站点限制嵌入",
102
135
  "core.webview.invalid_url": "缺少有效链接地址",
103
- "core.webview.open_failed": "链接无法打开"
136
+ "core.webview.open_failed": "链接无法打开",
137
+ "common.value.top_level": "顶级节点",
138
+ "common.resource.default": "{resource}",
139
+ "common.action.delete_resource": "删除{resource}",
140
+ "common.action.set_field_status": "设置{field}"
104
141
  }
@@ -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": "使用 Passkey",
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,17 +90,15 @@
71
90
  "core.login.wechat_bind_tip": "該微信尚未繫結,請登入已有賬號完成繫結",
72
91
  "core.login.wechat_failed": "微信登入失敗,請稍後重試",
73
92
  "core.navigation.home": "首頁",
93
+ "core.navigation.message": "消息",
74
94
  "core.navigation.error.access": "選單訪問模式無效:{access}",
75
95
  "core.navigation.error.child_tab": "只有移動端二級選單可以作為 tab:{name}",
76
96
  "core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
77
97
  "core.navigation.error.identity": "選單 id、name 或 app/ 路徑無效",
78
- "core.navigation.error.item_object": "選單項不是物件",
79
- "core.navigation.error.list_missing": "選單響應缺少扁平列表",
80
98
  "core.navigation.error.name_prefix": "選單名稱必須使用 App 字首:{name}",
81
99
  "core.navigation.error.parent_invalid": "移動端選單父級編號無效:{name}",
82
100
  "core.navigation.error.parent_missing": "選單缺少父級編號:{name}",
83
101
  "core.navigation.error.parent_not_found": "移動端選單父級不存在:{name}",
84
- "core.navigation.error.response_object": "選單響應不是物件",
85
102
  "core.navigation.error.root_tab": "移動端二級選單必須作為 tab:{name}",
86
103
  "core.navigation.error.tab_count": "tabBar 只能配置 0 或 2-5 項",
87
104
  "core.navigation.error.unique": "選單 id、name、path 必須唯一",
@@ -92,6 +109,22 @@
92
109
  "core.protocol.not_configured": "{title}未配置,暫時無法檢視",
93
110
  "core.protocol.privacy": "隱私協議",
94
111
  "core.protocol.service": "服務條款",
112
+ "core.settings.authorization": "授權管理",
113
+ "core.settings.contact": "聯絡我們",
114
+ "core.settings.feedback": "問題反饋",
115
+ "core.settings.logout_confirm": "是否退出登入?",
116
+ "core.settings.logout_failed": "退出登入失敗",
117
+ "core.settings.mfa_bind_success": "多因素認證綁定成功",
118
+ "core.settings.mfa_disable_action": "停用",
119
+ "core.settings.mfa_disable_factor_required": "請輸入動態密碼或恢復碼。",
120
+ "core.settings.mfa_disable_success": "多因素認證已停用",
121
+ "core.settings.mfa_disable_title": "停用多因素認證",
122
+ "core.settings.mfa_disabled": "未啟用",
123
+ "core.settings.mfa_enabled_method": "已啟用({method})",
124
+ "core.settings.mfa_method_totp": "TOTP",
125
+ "core.settings.mfa_method_webauthn": "WebAuthn",
126
+ "core.settings.mfa_title": "多因素認證",
127
+ "core.settings.title": "設定",
95
128
  "core.status.CONFIG_ERROR": "導航配置無效",
96
129
  "core.status.forbidden": "暫無訪問許可權",
97
130
  "core.status.NOT_FOUND": "頁面不存在",
@@ -100,5 +133,9 @@
100
133
  "core.status.loading": "正在載入",
101
134
  "core.webview.embed_blocked": "當前 H5 頁面可能被目標站點限制嵌入",
102
135
  "core.webview.invalid_url": "缺少有效連結地址",
103
- "core.webview.open_failed": "連結無法開啟"
136
+ "core.webview.open_failed": "連結無法開啟",
137
+ "common.value.top_level": "頂級節點",
138
+ "common.resource.default": "{resource}",
139
+ "common.action.delete_resource": "刪除{resource}",
140
+ "common.action.set_field_status": "設定{field}"
104
141
  }
package/src/navigation.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { computed, readonly, ref } from 'vue'
2
- import { defBaseMenuService } from './api/base/menu'
2
+ import { defBaseMenuService } from './api/system/app/v1/base_menu'
3
+ import type { BaseMenu, ListBaseMenuResponse } from './rpc/system/app/v1/base_menu'
3
4
  import { hasValidToken } from './utils/auth'
4
5
  import { navigateToLogin } from './utils/navigation'
5
6
  import { resolveStaticView } from './module'
@@ -32,7 +33,7 @@ export interface AppMenuNode extends AppMenu {
32
33
  /** 菜单获取适配器。 */
33
34
  export interface AppNavigationAdapter {
34
35
  /** 获取扁平移动菜单配置。 */
35
- list(): Promise<unknown>
36
+ list(): Promise<ListBaseMenuResponse>
36
37
  }
37
38
 
38
39
  /** 逻辑路由解析结果。 */
@@ -45,10 +46,10 @@ export interface ResolvedAppRoute {
45
46
  const ANONYMOUS_CACHE_KEY = 'kratos-uni-app:navigation:anonymous'
46
47
  const AUTHENTICATED_CACHE_KEY = 'kratos-uni-app:navigation:authenticated'
47
48
  /** 固定移动端菜单根目录编号。 */
48
- export const APP_MENU_ROOT_ID = 999
49
+ export const APP_MENU_ROOT_ID = 99000000
49
50
  const createDefaultMenus = (): AppMenu[] => [
50
51
  {
51
- id: 99901,
52
+ id: 99010000,
52
53
  parentId: APP_MENU_ROOT_ID,
53
54
  name: 'AppHome',
54
55
  path: 'app/home',
@@ -60,7 +61,19 @@ const createDefaultMenus = (): AppMenu[] => [
60
61
  selectedIcon: 'HOME_SELECTED',
61
62
  },
62
63
  {
63
- id: 99909,
64
+ id: 99040000,
65
+ parentId: APP_MENU_ROOT_ID,
66
+ name: 'AppMessage',
67
+ path: 'app/message',
68
+ viewKey: 'MESSAGE_INBOX',
69
+ title: t('core.navigation.message'),
70
+ access: 'AUTHENTICATED',
71
+ inTabBar: true,
72
+ icon: 'MESSAGE_DEFAULT',
73
+ selectedIcon: 'MESSAGE_SELECTED',
74
+ },
75
+ {
76
+ id: 99090000,
64
77
  parentId: APP_MENU_ROOT_ID,
65
78
  name: 'AppMy',
66
79
  path: 'app/my',
@@ -72,8 +85,8 @@ const createDefaultMenus = (): AppMenu[] => [
72
85
  selectedIcon: 'USER_SELECTED',
73
86
  },
74
87
  {
75
- id: 9990101,
76
- parentId: 99901,
88
+ id: 99010100,
89
+ parentId: 99010000,
77
90
  name: 'AppLogin',
78
91
  path: 'app/login',
79
92
  viewKey: 'LOGIN',
@@ -82,8 +95,8 @@ const createDefaultMenus = (): AppMenu[] => [
82
95
  inTabBar: false,
83
96
  },
84
97
  {
85
- id: 999010101,
86
- parentId: 9990101,
98
+ id: 99010101,
99
+ parentId: 99010100,
87
100
  name: 'AppProtocol',
88
101
  path: 'app/protocol/:type',
89
102
  viewKey: 'PROTOCOL',
@@ -92,8 +105,8 @@ const createDefaultMenus = (): AppMenu[] => [
92
105
  inTabBar: false,
93
106
  },
94
107
  {
95
- id: 9990901,
96
- parentId: 99909,
108
+ id: 99090100,
109
+ parentId: 99090000,
97
110
  name: 'AppProfile',
98
111
  path: 'app/profile',
99
112
  viewKey: 'PROFILE',
@@ -102,18 +115,18 @@ const createDefaultMenus = (): AppMenu[] => [
102
115
  inTabBar: false,
103
116
  },
104
117
  {
105
- id: 9990902,
106
- parentId: 99909,
118
+ id: 99090200,
119
+ parentId: 99090000,
107
120
  name: 'AppSettings',
108
121
  path: 'app/settings',
109
122
  viewKey: 'SETTINGS',
110
- title: t('system.settings.title'),
123
+ title: t('core.settings.title'),
111
124
  access: 'AUTHENTICATED',
112
125
  inTabBar: false,
113
126
  },
114
127
  {
115
- id: 9990903,
116
- parentId: 99909,
128
+ id: 99090300,
129
+ parentId: 99090000,
117
130
  name: 'AppAi',
118
131
  path: 'app/ai',
119
132
  viewKey: 'AI',
@@ -122,8 +135,8 @@ const createDefaultMenus = (): AppMenu[] => [
122
135
  inTabBar: false,
123
136
  },
124
137
  {
125
- id: 9990102,
126
- parentId: 99901,
138
+ id: 99010200,
139
+ parentId: 99010000,
127
140
  name: 'AppWebView',
128
141
  path: 'app/webview',
129
142
  viewKey: 'WEBVIEW',
@@ -135,12 +148,26 @@ const createDefaultMenus = (): AppMenu[] => [
135
148
 
136
149
  const menus = ref<AppMenu[]>([])
137
150
  const ready = ref(false)
151
+ const appMenuBadges = ref<Record<string, number>>({})
152
+ const isH5Runtime = typeof window !== 'undefined'
138
153
  let tabNavigationTarget: string | undefined
139
154
  let adapter: AppNavigationAdapter = {
140
155
  list: () => defBaseMenuService.ListBaseMenu(),
141
156
  }
142
157
 
143
158
  const nativeTabViewKeys = ['HOME', 'PROFILE_HOME']
159
+ const pageTitleKeys: Record<string, string> = {
160
+ 'pages/index/index': 'core.home.main_title',
161
+ 'pages/login/login': 'common.action.login',
162
+ 'pages/login/protocal': 'core.navigation.protocol',
163
+ 'pages/my/my': 'core.navigation.my',
164
+ 'pagesMember/ai/index': 'system.ai.chat_title',
165
+ 'pagesMember/message/detail': 'system.notification.title',
166
+ 'pagesMember/message/index': 'system.notification.title',
167
+ 'pagesMember/profile/profile': 'system.profile.title',
168
+ 'pagesMember/settings/settings': 'core.settings.title',
169
+ }
170
+ const resetTabViewKeys = ['MESSAGE_INBOX']
144
171
 
145
172
  /** 替换导航远端适配器,供宿主接入自定义契约。 */
146
173
  export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void {
@@ -228,20 +255,29 @@ function navigateTabRoute(url: string): void {
228
255
  const pages = getCurrentPages()
229
256
  const currentIndex = pages.length - 1
230
257
  const currentRoute = pages[currentIndex]?.route?.replace(/^\/+/, '')
231
- if (currentRoute === targetRoute || tabNavigationTarget) return
258
+ if (currentRoute === targetRoute || tabNavigationTarget === targetRoute) return
232
259
 
233
260
  tabNavigationTarget = targetRoute
234
261
  const release = () => {
235
262
  if (tabNavigationTarget === targetRoute) tabNavigationTarget = undefined
236
263
  }
264
+ if (isH5Runtime) {
265
+ uni.reLaunch({ url, success: release, fail: release, complete: release })
266
+ return
267
+ }
237
268
  if (nativeTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
238
269
  uni.switchTab({
239
270
  url,
240
271
  success: release,
241
272
  fail: () => navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release),
273
+ complete: release,
242
274
  })
243
275
  return
244
276
  }
277
+ if (resetTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
278
+ uni.reLaunch({ url, success: release, fail: release, complete: release })
279
+ return
280
+ }
245
281
  navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release)
246
282
  }
247
283
 
@@ -272,6 +308,48 @@ function navigateTabRouteInStack(
272
308
  })
273
309
  }
274
310
 
311
+ /** 按稳定 viewKey 跳转,并将动作参数作为查询参数传递。 */
312
+ export function navigateAppView(viewKey: string, params: Record<string, string> = {}): void {
313
+ const menu = menus.value.find((item) => item.viewKey === viewKey)
314
+ if (!menu) {
315
+ launchAppStatus('NOT_FOUND')
316
+ return
317
+ }
318
+ const remaining = { ...params }
319
+ const path = menu.path.replace(/:([A-Za-z0-9_]+)/g, (placeholder, key: string) => {
320
+ if (!(key in remaining)) return placeholder
321
+ const value = remaining[key]
322
+ delete remaining[key]
323
+ return encodeURIComponent(value)
324
+ })
325
+ const query = Object.entries(remaining)
326
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
327
+ .join('&')
328
+ navigateAppRoute(`${path}${query ? `?${query}` : ''}`)
329
+ }
330
+
331
+ /** 设置移动端菜单 badge 数量,供业务模块注册未读状态。 */
332
+ export function setAppMenuBadge(viewKey: string, count: number): void {
333
+ const next = { ...appMenuBadges.value }
334
+ if (count > 0) next[viewKey] = Math.min(Math.floor(count), 99)
335
+ else delete next[viewKey]
336
+ appMenuBadges.value = next
337
+ }
338
+
339
+ /** 读取移动端菜单 badge 数量。 */
340
+ export function useAppMenuBadge(viewKey: string) {
341
+ return computed(() => appMenuBadges.value[viewKey] ?? 0)
342
+ }
343
+
344
+ /** 根据物理页面路由更新 H5 和原生导航栏标题。 */
345
+ export function setAppPageTitle(route: string): void {
346
+ const titleKey = pageTitleKeys[route]
347
+ const menuTitle = menus.value.find((menu) => resolveStaticView(menu.viewKey) === route)?.title
348
+ const title = titleKey ? t(titleKey) : menuTitle
349
+ if (!title) return
350
+ void uni.setNavigationBarTitle({ title })
351
+ }
352
+
275
353
  /** 获取导航响应式状态。 */
276
354
  export function useAppNavigation() {
277
355
  const menuTree = computed<AppMenuNode[]>(() => buildMenuTree(menus.value, APP_MENU_ROOT_ID))
@@ -301,48 +379,32 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
301
379
  const cached = uni.getStorageSync(cacheKey) as unknown
302
380
  if (!Array.isArray(cached)) return
303
381
  try {
304
- const normalized = cached.map(normalizeMenu)
305
- validateMenus(normalized)
306
- return normalized
382
+ const cachedMenus = cached as AppMenu[]
383
+ validateMenus(cachedMenus)
384
+ return cachedMenus
307
385
  } catch {
308
386
  uni.removeStorageSync(cacheKey)
309
387
  }
310
388
  }
311
389
 
312
- function normalizeMenuResponse(response: unknown): AppMenu[] {
313
- if (Array.isArray(response)) return response.map(normalizeMenu)
314
- if (!response || typeof response !== 'object')
315
- throw new Error(t('core.navigation.error.response_object'))
316
- const record = response as Record<string, unknown>
317
- const list = record.items ?? record.list ?? record.data
318
- if (!Array.isArray(list)) throw new Error(t('core.navigation.error.list_missing'))
319
- return list.map(normalizeMenu)
390
+ function normalizeMenuResponse(response: ListBaseMenuResponse): AppMenu[] {
391
+ return response.items.map(normalizeMenu)
320
392
  }
321
393
 
322
- function normalizeMenu(value: unknown): AppMenu {
323
- if (!value || typeof value !== 'object') throw new Error(t('core.navigation.error.item_object'))
324
- const item = value as Record<string, unknown>
325
- const meta = (item.meta ?? {}) as Record<string, unknown>
326
- const app = (meta.app ?? item.app ?? {}) as Record<string, unknown>
394
+ function normalizeMenu(item: BaseMenu): AppMenu {
395
+ const meta = item.meta
396
+ const app = meta?.app
327
397
  return {
328
- id: Number(item.id),
329
- parentId:
330
- item.parent_id === undefined && item.parentId === undefined
331
- ? undefined
332
- : Number(item.parent_id ?? item.parentId),
333
- name: String(item.name ?? ''),
334
- path: String(item.path ?? ''),
335
- viewKey: String(app.view_key ?? app.viewKey ?? item.view_key ?? item.viewKey ?? ''),
336
- title: String(meta.title ?? item.title ?? ''),
337
- access: String(app.access ?? 'AUTHENTICATED') as AppMenuAccess,
338
- inTabBar: Boolean(app.in_tab_bar ?? app.inTabBar),
339
- icon: typeof meta.icon === 'string' ? meta.icon : undefined,
340
- selectedIcon:
341
- typeof app.selected_icon === 'string'
342
- ? app.selected_icon
343
- : typeof app.selectedIcon === 'string'
344
- ? app.selectedIcon
345
- : undefined,
398
+ id: item.id,
399
+ parentId: item.parent_id,
400
+ name: item.name,
401
+ path: item.path,
402
+ viewKey: app?.view_key ?? '',
403
+ title: meta?.title ?? '',
404
+ access: (app?.access ?? 'AUTHENTICATED') as AppMenuAccess,
405
+ inTabBar: app?.in_tab_bar ?? false,
406
+ icon: meta?.icon,
407
+ selectedIcon: app?.selected_icon,
346
408
  }
347
409
  }
348
410
 
@@ -20,6 +20,27 @@ export enum PasswordCryptoScene {
20
20
  PASSWORD_CRYPTO_SCENE_RESET_BASE_USER_PASSWORD = 3,
21
21
  /** PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD - 个人修改密码加密场景。 */
22
22
  PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD = 4,
23
+ /** PASSWORD_CRYPTO_SCENE_MFA - 多因素认证敏感操作场景。 */
24
+ PASSWORD_CRYPTO_SCENE_MFA = 5,
25
+ /** PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY - 密码策略初始化密码配置场景。 */
26
+ PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY = 6,
27
+ }
28
+
29
+ /**
30
+ * LoginStatus 表示登录请求当前所处的认证阶段。
31
+ * 只有 AUTHENTICATED 或 PASSWORD_CHANGE_REQUIRED 才会携带正式访问令牌,MFA 状态必须继续完成挑战。
32
+ */
33
+ export enum LoginStatus {
34
+ /** LOGIN_STATUS_UNSPECIFIED - 未指定登录状态。 */
35
+ LOGIN_STATUS_UNSPECIFIED = 0,
36
+ /** LOGIN_STATUS_AUTHENTICATED - 已完成认证并签发令牌。 */
37
+ LOGIN_STATUS_AUTHENTICATED = 1,
38
+ /** LOGIN_STATUS_MFA_REQUIRED - 需要继续校验多因素认证。 */
39
+ LOGIN_STATUS_MFA_REQUIRED = 2,
40
+ /** LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED - 必须先完成多因素认证绑定。 */
41
+ LOGIN_STATUS_MFA_ENROLLMENT_REQUIRED = 3,
42
+ /** LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED - 下次操作前必须修改临时密码。 */
43
+ LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED = 4,
23
44
  }
24
45
 
25
46
  /** 验证码获取条件 */
@@ -80,15 +101,15 @@ export interface PasswordPublicKeyResponse {
80
101
 
81
102
  /** 刷新令牌请求参数 */
82
103
  export interface RefreshTokenRequest {
83
- /** 更新令牌,用来获取下一次的访问令牌,必选项。 */
84
- refresh_token: string;
104
+ /** 更新令牌;未填写时由服务端从HttpOnly Cookie读取。 */
105
+ refresh_token?: string | undefined;
85
106
  }
86
107
 
87
108
  /** 刷新令牌响应 */
88
109
  export interface RefreshTokenResponse {
89
110
  /** 访问令牌,必选项。 */
90
111
  access_token: string;
91
- /** 更新令牌,用来获取下一次的访问令牌,可选项。 */
112
+ /** 更新令牌;管理端 Cookie-only 请求不在响应体返回,应用端请求可返回。 */
92
113
  refresh_token: string;
93
114
  /** 令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。 */
94
115
  token_type: string;
@@ -116,12 +137,24 @@ export interface LoginRequest {
116
137
  export interface LoginResponse {
117
138
  /** 访问令牌,必选项。 */
118
139
  access_token: string;
119
- /** 更新令牌,用来获取下一次的访问令牌,可选项。 */
140
+ /** 更新令牌;管理端 Cookie-only 请求不在响应体返回,应用端请求可返回。 */
120
141
  refresh_token: string;
121
142
  /** 令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。 */
122
143
  token_type: string;
123
144
  /** 令牌有效时间,单位为秒。如果访问令牌过期,服务器应回复授予访问令牌的持续时间。如果省略该参数,必须其他方式设置过期时间。 */
124
145
  expires_in: number;
146
+ /** 登录认证状态 */
147
+ status: LoginStatus;
148
+ /** 多因素认证挑战ID */
149
+ mfa_challenge_id: string;
150
+ /** 多因素认证绑定临时票据 */
151
+ mfa_setup_ticket: string;
152
+ /** 多因素认证挑战或绑定票据有效时间,单位秒 */
153
+ mfa_expires_in: number;
154
+ /** 多因素认证方式:totp、webauthn */
155
+ mfa_method: string;
156
+ /** WebAuthn认证选项JSON */
157
+ mfa_webauthn_options_json: string;
125
158
  }
126
159
 
127
160
  /** Base登录公共服务 */