@liujitcn/kratos-uni-app-core 0.0.31 → 0.0.33

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 CHANGED
@@ -7,7 +7,7 @@
7
7
  ```text
8
8
  packages/core
9
9
  ├── src
10
- │ ├── api # base、system 基础接口 service
10
+ │ ├── api # 与 Proto 完整路径一致的接口 service
11
11
  │ ├── components # 启动状态、自绘 tabBar、公共设置页和 MFA 组件
12
12
  │ ├── rpc # Buf 生成的 TypeScript 接口类型
13
13
  │ ├── static # 底座图片和 tab 图标
@@ -31,7 +31,7 @@ packages/core
31
31
  ## 功能
32
32
 
33
33
  - 定义 `KratosAppModule`、`viewKey`、静态页面覆盖和模块图标协议。
34
- - 从 `/v1/app/base/menu` 加载 `999` 下的扁平菜单,按 `parent_id` 构建树,以二级页面生成 tabBar,并原子更新动态路由和层级导航。
34
+ - 从 `/v1/app/base/menu` 加载 `99000000` 下的扁平菜单,按 `parent_id` 构建树,以二级页面生成 tabBar,并原子更新动态路由和层级导航。
35
35
  - 按匿名态、登录态缓存导航,并在远端不可用时回退到最近缓存或默认菜单。
36
36
  - 统一认证、token、MFA、HTTP、上传、配置、Pinia 和基础页面能力。
37
37
  - 提供可复用的 `KratosSettingsPage`,统一语言、MFA、微信设置和退出登录,并允许产品在最上方追加业务设置。
package/dist/module.mjs CHANGED
@@ -121,13 +121,10 @@ var zh_CN_default = {
121
121
  "core.navigation.error.child_tab": "\u53EA\u6709\u79FB\u52A8\u7AEF\u4E8C\u7EA7\u83DC\u5355\u53EF\u4EE5\u4F5C\u4E3A tab\uFF1A{name}",
122
122
  "core.navigation.error.cycle": "\u79FB\u52A8\u7AEF\u83DC\u5355\u5B58\u5728\u5FAA\u73AF\u7236\u7EA7\uFF1A{name}",
123
123
  "core.navigation.error.identity": "\u83DC\u5355 id\u3001name \u6216 app/ \u8DEF\u5F84\u65E0\u6548",
124
- "core.navigation.error.item_object": "\u83DC\u5355\u9879\u4E0D\u662F\u5BF9\u8C61",
125
- "core.navigation.error.list_missing": "\u83DC\u5355\u54CD\u5E94\u7F3A\u5C11\u6241\u5E73\u5217\u8868",
126
124
  "core.navigation.error.name_prefix": "\u83DC\u5355\u540D\u79F0\u5FC5\u987B\u4F7F\u7528 App \u524D\u7F00\uFF1A{name}",
127
125
  "core.navigation.error.parent_invalid": "\u79FB\u52A8\u7AEF\u83DC\u5355\u7236\u7EA7\u7F16\u53F7\u65E0\u6548\uFF1A{name}",
128
126
  "core.navigation.error.parent_missing": "\u83DC\u5355\u7F3A\u5C11\u7236\u7EA7\u7F16\u53F7\uFF1A{name}",
129
127
  "core.navigation.error.parent_not_found": "\u79FB\u52A8\u7AEF\u83DC\u5355\u7236\u7EA7\u4E0D\u5B58\u5728\uFF1A{name}",
130
- "core.navigation.error.response_object": "\u83DC\u5355\u54CD\u5E94\u4E0D\u662F\u5BF9\u8C61",
131
128
  "core.navigation.error.root_tab": "\u79FB\u52A8\u7AEF\u4E8C\u7EA7\u83DC\u5355\u5FC5\u987B\u4F5C\u4E3A tab\uFF1A{name}",
132
129
  "core.navigation.error.tab_count": "tabBar \u53EA\u80FD\u914D\u7F6E 0 \u6216 2-5 \u9879",
133
130
  "core.navigation.error.unique": "\u83DC\u5355 id\u3001name\u3001path \u5FC5\u987B\u552F\u4E00",
@@ -267,13 +264,10 @@ var en_US_default = {
267
264
  "core.navigation.error.child_tab": "Only second-level app menus can be tabs: {name}",
268
265
  "core.navigation.error.cycle": "Menu parent cycle detected: {name}",
269
266
  "core.navigation.error.identity": "Menu id, name, or app/ path is invalid",
270
- "core.navigation.error.item_object": "Menu item is not an object",
271
- "core.navigation.error.list_missing": "Menu response does not contain a flat list",
272
267
  "core.navigation.error.name_prefix": "Menu name must start with App: {name}",
273
268
  "core.navigation.error.parent_invalid": "Menu parent id is invalid: {name}",
274
269
  "core.navigation.error.parent_missing": "Menu parent id is missing: {name}",
275
270
  "core.navigation.error.parent_not_found": "Menu parent was not found: {name}",
276
- "core.navigation.error.response_object": "Menu response is not an object",
277
271
  "core.navigation.error.root_tab": "Second-level app menu must be a tab: {name}",
278
272
  "core.navigation.error.tab_count": "tabBar must contain 0 or 2-5 items",
279
273
  "core.navigation.error.unique": "Menu id, name, and path must be unique",
@@ -413,13 +407,10 @@ var ja_JP_default = {
413
407
  "core.navigation.error.child_tab": "\u30E2\u30D0\u30A4\u30EB \u30BB\u30AB\u30F3\u30C0\u30EA \u30E1\u30CB\u30E5\u30FC\u306E\u307F\u304C\u30BF\u30D6\u3068\u3057\u3066\u4F7F\u7528\u3067\u304D\u307E\u3059: {name}",
414
408
  "core.navigation.error.cycle": "\u30E2\u30D0\u30A4\u30EB \u30E1\u30CB\u30E5\u30FC\u306B\u306F\u30EB\u30FC\u30D7\u306E\u89AA\u304C\u3042\u308A\u307E\u3059: {name}",
415
409
  "core.navigation.error.identity": "\u30E1\u30CB\u30E5\u30FC ID\u3001\u540D\u524D\u3001\u307E\u305F\u306F\u30A2\u30D7\u30EA/\u30D1\u30B9\u304C\u7121\u52B9\u3067\u3059",
416
- "core.navigation.error.item_object": "\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
417
- "core.navigation.error.list_missing": "\u30E1\u30CB\u30E5\u30FC\u5FDC\u7B54\u306B\u30D5\u30E9\u30C3\u30C8 \u30EA\u30B9\u30C8\u304C\u3042\u308A\u307E\u305B\u3093",
418
410
  "core.navigation.error.name_prefix": "\u30E1\u30CB\u30E5\u30FC\u540D\u306B\u306F\u30A2\u30D7\u30EA\u306E\u63A5\u982D\u8F9E\u3092\u4F7F\u7528\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059: {name}",
419
411
  "core.navigation.error.parent_invalid": "\u30E2\u30D0\u30A4\u30EB \u30E1\u30CB\u30E5\u30FC\u306E\u89AA\u756A\u53F7\u304C\u7121\u52B9\u3067\u3059: {name}",
420
412
  "core.navigation.error.parent_missing": "\u30E1\u30CB\u30E5\u30FC\u306B\u89AA\u756A\u53F7\u304C\u3042\u308A\u307E\u305B\u3093: {name}",
421
413
  "core.navigation.error.parent_not_found": "\u30E2\u30D0\u30A4\u30EB \u30E1\u30CB\u30E5\u30FC\u306E\u89AA\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: {name}",
422
- "core.navigation.error.response_object": "\u30E1\u30CB\u30E5\u30FC\u5FDC\u7B54\u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
423
414
  "core.navigation.error.root_tab": "\u30E2\u30D0\u30A4\u30EB \u30BB\u30AB\u30F3\u30C0\u30EA \u30E1\u30CB\u30E5\u30FC\u306F\u30BF\u30D6\u3068\u3057\u3066\u4F7F\u7528\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059: {name}",
424
415
  "core.navigation.error.tab_count": "tabBar \u306F 0 \u307E\u305F\u306F 2 \uFF5E 5 \u500B\u306E\u9805\u76EE\u306E\u307F\u3092\u8A2D\u5B9A\u3067\u304D\u307E\u3059",
425
416
  "core.navigation.error.unique": "\u30E1\u30CB\u30E5\u30FC ID\u3001\u540D\u524D\u3001\u30D1\u30B9\u306F\u4E00\u610F\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059",
@@ -559,13 +550,10 @@ var zh_TW_default = {
559
550
  "core.navigation.error.child_tab": "\u53EA\u6709\u79FB\u52D5\u7AEF\u4E8C\u7D1A\u9078\u55AE\u53EF\u4EE5\u4F5C\u70BA tab\uFF1A{name}",
560
551
  "core.navigation.error.cycle": "\u79FB\u52D5\u7AEF\u9078\u55AE\u5B58\u5728\u8FF4\u5708\u7236\u7D1A\uFF1A{name}",
561
552
  "core.navigation.error.identity": "\u9078\u55AE id\u3001name \u6216 app/ \u8DEF\u5F91\u7121\u6548",
562
- "core.navigation.error.item_object": "\u9078\u55AE\u9805\u4E0D\u662F\u7269\u4EF6",
563
- "core.navigation.error.list_missing": "\u9078\u55AE\u97FF\u61C9\u7F3A\u5C11\u6241\u5E73\u5217\u8868",
564
553
  "core.navigation.error.name_prefix": "\u9078\u55AE\u540D\u7A31\u5FC5\u9808\u4F7F\u7528 App \u5B57\u9996\uFF1A{name}",
565
554
  "core.navigation.error.parent_invalid": "\u79FB\u52D5\u7AEF\u9078\u55AE\u7236\u7D1A\u7DE8\u865F\u7121\u6548\uFF1A{name}",
566
555
  "core.navigation.error.parent_missing": "\u9078\u55AE\u7F3A\u5C11\u7236\u7D1A\u7DE8\u865F\uFF1A{name}",
567
556
  "core.navigation.error.parent_not_found": "\u79FB\u52D5\u7AEF\u9078\u55AE\u7236\u7D1A\u4E0D\u5B58\u5728\uFF1A{name}",
568
- "core.navigation.error.response_object": "\u9078\u55AE\u97FF\u61C9\u4E0D\u662F\u7269\u4EF6",
569
557
  "core.navigation.error.root_tab": "\u79FB\u52D5\u7AEF\u4E8C\u7D1A\u9078\u55AE\u5FC5\u9808\u4F5C\u70BA tab\uFF1A{name}",
570
558
  "core.navigation.error.tab_count": "tabBar \u53EA\u80FD\u914D\u7F6E 0 \u6216 2-5 \u9805",
571
559
  "core.navigation.error.unique": "\u9078\u55AE id\u3001name\u3001path \u5FC5\u9808\u552F\u4E00",
package/dist/vite.mjs CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  readFileSync,
7
7
  readdirSync,
8
8
  rmdirSync,
9
- rmSync,
10
9
  statSync,
11
10
  unlinkSync,
12
11
  writeFileSync
@@ -284,12 +283,9 @@ function restoreBuildTransaction(transaction, transactionFile) {
284
283
  removeEmptyParents(dirname(file), transaction.inputDir);
285
284
  }
286
285
  removeGeneratedStaticFiles(
287
- transaction.generatedStaticFiles ?? [],
286
+ transaction.generatedStaticFiles,
288
287
  resolve(transaction.inputDir, "static")
289
288
  );
290
- if (transaction.generatedStatic && transaction.generatedStaticFiles === void 0) {
291
- rmSync(resolve(transaction.inputDir, "static"), { recursive: true, force: true });
292
- }
293
289
  writeFileSync(transaction.pagesFile, transaction.originalPages);
294
290
  if (existsSync(transactionFile))
295
291
  unlinkSync(transactionFile);
@@ -505,10 +501,13 @@ function appendPage(manifest, page) {
505
501
  function createPageWrapper(page) {
506
502
  const source = page.source.replace(/\\/g, "/");
507
503
  return `<script setup lang="ts">
504
+ import { onShow } from '@dcloudio/uni-app'
505
+ import { setAppPageTitle } from '@liujitcn/kratos-uni-app-core'
508
506
  import KratosPage from ${JSON.stringify(source)}
509
507
  import KratosTabBar from '@liujitcn/kratos-uni-app-core/components/KratosTabBar.vue'
510
508
 
511
509
  defineOptions({ inheritAttrs: false })
510
+ onShow(() => setAppPageTitle(${JSON.stringify(page.route)}))
512
511
  </script>
513
512
 
514
513
  <template>
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.31",
10
+ "version": "0.0.33",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -1,5 +1,9 @@
1
- import { http } from '../../utils/http'
2
- import type { ConfigService, GetConfigRequest, GetConfigResponse } from '../../rpc/base/v1/config'
1
+ import { http } from '../../../utils/http'
2
+ import type {
3
+ ConfigService,
4
+ GetConfigRequest,
5
+ GetConfigResponse,
6
+ } from '../../../rpc/base/v1/config'
3
7
 
4
8
  const CONFIG_URL = '/v1/base/config'
5
9
 
@@ -1,5 +1,9 @@
1
- import { http } from '../../utils/http'
2
- import type { DownloadFileRequest, MultiUploadFileResponse, FileInfo } from '../../rpc/base/v1/file'
1
+ import { http } from '../../../utils/http'
2
+ import type {
3
+ DownloadFileRequest,
4
+ MultiUploadFileResponse,
5
+ FileInfo,
6
+ } from '../../../rpc/base/v1/file'
3
7
 
4
8
  const FILE_URL = '/v1/base/file'
5
9
 
@@ -1,9 +1,9 @@
1
- import { http } from '../../utils/http'
1
+ import { http } from '../../../utils/http'
2
2
  import type {
3
3
  OptionLanguageRequest,
4
4
  OptionLanguageResponse,
5
5
  LanguageService,
6
- } from '../../rpc/base/v1/language'
6
+ } from '../../../rpc/base/v1/language'
7
7
 
8
8
  const LANGUAGE_URL = '/v1/base/language'
9
9
 
@@ -1,4 +1,4 @@
1
- import { http } from '../../utils/http'
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 '../../rpc/base/v1/login'
16
- import type { Empty } from '../../rpc/google/protobuf/empty'
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'
@@ -1,5 +1,5 @@
1
- import { http } from '../../utils/http'
2
- import type { LoginResponse } from '../../rpc/base/v1/login'
1
+ import { http } from '../../../utils/http'
2
+ import type { LoginResponse } from '../../../rpc/base/v1/login'
3
3
  import type {
4
4
  BeginMfaDisableRequest,
5
5
  BeginMfaDisableResponse,
@@ -18,8 +18,8 @@ import type {
18
18
  RecoveryCodesResponse,
19
19
  RegenerateMfaRecoveryCodesRequest,
20
20
  VerifyMfaRequest,
21
- } from '../../rpc/base/v1/mfa'
22
- import type { Empty } from '../../rpc/google/protobuf/empty'
21
+ } from '../../../rpc/base/v1/mfa'
22
+ import type { Empty } from '../../../rpc/google/protobuf/empty'
23
23
 
24
24
  const MFA_URL = '/v1/base/mfa'
25
25
 
@@ -1,4 +1,4 @@
1
- import { http } from '../../utils/http'
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 '../../rpc/base/v1/oauth'
23
- import type { Empty } from '../../rpc/google/protobuf/empty'
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 '../../utils/http'
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 '../../rpc/system/app/v1/auth'
8
- import type { Empty } from '../../rpc/google/protobuf/empty'
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: GetUserProfileRequestCompat): Promise<UserProfileForm> {
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: UpdateUserProfileRequestCompat): Promise<Empty> {
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: userProfile,
32
+ data: request.user_profile,
40
33
  })
41
34
  }
42
35
 
@@ -1,9 +1,9 @@
1
- import { http } from '../../utils/http'
1
+ import { http } from '../../../../utils/http'
2
2
  import type {
3
3
  BaseAreaService,
4
4
  TreeBaseAreaRequest,
5
5
  TreeBaseAreaResponse,
6
- } from '../../rpc/system/app/v1/base_area'
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,21 @@
1
+ import { http } from '../../../../utils/http'
2
+ import type {
3
+ BaseDictForm,
4
+ BaseDictService,
5
+ GetBaseDictRequest,
6
+ } from '../../../../rpc/system/app/v1/base_dict'
7
+
8
+ const BASE_DICT_URL = '/v1/app/base/dict'
9
+
10
+ /** 字典服务 */
11
+ export class BaseDictServiceImpl implements BaseDictService {
12
+ /** 查询单个字典 */
13
+ GetBaseDict(request: GetBaseDictRequest): Promise<BaseDictForm> {
14
+ return http<BaseDictForm>({
15
+ url: `${BASE_DICT_URL}/${request.code}`,
16
+ method: 'GET',
17
+ })
18
+ }
19
+ }
20
+
21
+ export const defBaseDictService = new BaseDictServiceImpl()
@@ -1,5 +1,5 @@
1
- import { http } from '../../utils/http'
2
- import type { ListBaseMenuResponse } from '../../rpc/system/app/v1/base_menu'
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
@@ -9,7 +9,7 @@ import {
9
9
  } from './locales'
10
10
  import { initializeAppNavigation } from './navigation'
11
11
  import { useSettingStore } from './stores'
12
- import { defLanguageService } from './api/base/language'
12
+ import { defLanguageService } from './api/base/v1/language'
13
13
 
14
14
  /** uni-app 启动参数。 */
15
15
  export interface KratosAppBootstrapOptions {
@@ -3,7 +3,7 @@ import { computed, onMounted, ref, watch } from 'vue'
3
3
  import { useUserStore } from '../stores'
4
4
  import { navigateToLogin } from '../utils/navigation'
5
5
  import { getLanguageOptions, useI18n, type SupportedLocale } from '../locales'
6
- import { defMfaService } from '../api/base/mfa'
6
+ import { defMfaService } from '../api/base/v1/mfa'
7
7
  import MfaManageDialog from './MfaManageDialog.vue'
8
8
 
9
9
  defineOptions({ name: 'KratosSettingsPage' })
@@ -240,12 +240,24 @@ const onLogout = () => {
240
240
 
241
241
  .locale-item {
242
242
  display: flex;
243
+ min-width: 0;
243
244
  justify-content: space-between;
244
245
  padding-right: 42rpx;
246
+
247
+ > text:first-child {
248
+ flex-shrink: 0;
249
+ }
245
250
  }
246
251
 
247
252
  .locale-value {
253
+ flex: 1;
254
+ min-width: 0;
255
+ overflow: hidden;
256
+ margin-left: 24rpx;
248
257
  color: #888;
258
+ text-align: right;
259
+ text-overflow: ellipsis;
260
+ white-space: nowrap;
249
261
  }
250
262
 
251
263
  .mfa-item {
@@ -24,9 +24,11 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
24
24
  class="kratos-tab-bar__item"
25
25
  @tap="navigate(item.path)"
26
26
  >
27
- <text v-if="item.viewKey === 'MESSAGE_INBOX' && messageBadge" class="kratos-tab-bar__badge">{{
28
- messageBadge
29
- }}</text>
27
+ <view
28
+ v-if="item.viewKey === 'MESSAGE_INBOX' && messageBadge"
29
+ class="kratos-tab-bar__badge"
30
+ aria-hidden="true"
31
+ />
30
32
  <image
31
33
  v-if="resolveModuleIcon(item.icon)"
32
34
  class="kratos-tab-bar__icon"
@@ -84,24 +86,46 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
84
86
  .kratos-tab-bar__message-icon {
85
87
  position: relative;
86
88
  width: 22px;
87
- height: 16px;
88
- margin-bottom: 8px;
89
+ height: 18px;
90
+ margin-bottom: 6px;
89
91
  border: 2px solid #9aa4b2;
90
- border-radius: 3px;
92
+ border-radius: 6px;
91
93
  box-sizing: border-box;
92
94
  }
93
- .kratos-tab-bar__message-icon::after {
95
+ .kratos-tab-bar__message-icon::before {
94
96
  position: absolute;
95
- top: 1px;
97
+ top: 5px;
96
98
  left: 4px;
97
- width: 9px;
98
- height: 9px;
99
+ width: 3px;
100
+ height: 3px;
101
+ border-radius: 50%;
102
+ background: #9aa4b2;
103
+ box-shadow:
104
+ 5px 0 #9aa4b2,
105
+ 10px 0 #9aa4b2;
106
+ content: '';
107
+ }
108
+ .kratos-tab-bar__message-icon::after {
109
+ position: absolute;
110
+ bottom: -5px;
111
+ left: 3px;
112
+ width: 7px;
113
+ height: 7px;
99
114
  border-right: 2px solid #9aa4b2;
100
115
  border-bottom: 2px solid #9aa4b2;
116
+ background: #fff;
101
117
  content: '';
102
118
  transform: rotate(45deg);
103
119
  }
104
- .kratos-tab-bar__message-icon--active,
120
+ .kratos-tab-bar__message-icon--active {
121
+ border-color: #27ba9b;
122
+ }
123
+ .kratos-tab-bar__message-icon--active::before {
124
+ background: #27ba9b;
125
+ box-shadow:
126
+ 5px 0 #27ba9b,
127
+ 10px 0 #27ba9b;
128
+ }
105
129
  .kratos-tab-bar__message-icon--active::after {
106
130
  border-color: #27ba9b;
107
131
  }
@@ -110,16 +134,13 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
110
134
  }
111
135
  .kratos-tab-bar__badge {
112
136
  position: absolute;
113
- top: 2px;
114
- right: calc(50% - 22px);
115
- min-width: 14px;
116
- height: 14px;
117
- padding: 0 3px;
118
- border-radius: 8px;
137
+ top: 0;
138
+ right: calc(50% - 19px);
139
+ width: 10px;
140
+ height: 10px;
141
+ box-sizing: border-box;
142
+ border: 2px solid #fff;
143
+ border-radius: 50%;
119
144
  background: #e5484d;
120
- color: #fff;
121
- font-size: 9px;
122
- line-height: 14px;
123
- text-align: center;
124
145
  }
125
146
  </style>
@@ -10,7 +10,7 @@ import type {
10
10
  import { createWebAuthnCredential, getWebAuthnAssertion } from '../utils/webauthn'
11
11
  import { PASSWORD_CRYPTO_SCENE, encryptPassword } from '../utils/passwordCrypto'
12
12
  import { useI18n } from '../locales'
13
- import { defMfaService } from '../api/base/mfa'
13
+ import { defMfaService } from '../api/base/v1/mfa'
14
14
 
15
15
  const props = withDefaults(
16
16
  defineProps<{
@@ -95,13 +95,10 @@
95
95
  "core.navigation.error.child_tab": "Only second-level app menus can be tabs: {name}",
96
96
  "core.navigation.error.cycle": "Menu parent cycle detected: {name}",
97
97
  "core.navigation.error.identity": "Menu id, name, or app/ path is invalid",
98
- "core.navigation.error.item_object": "Menu item is not an object",
99
- "core.navigation.error.list_missing": "Menu response does not contain a flat list",
100
98
  "core.navigation.error.name_prefix": "Menu name must start with App: {name}",
101
99
  "core.navigation.error.parent_invalid": "Menu parent id is invalid: {name}",
102
100
  "core.navigation.error.parent_missing": "Menu parent id is missing: {name}",
103
101
  "core.navigation.error.parent_not_found": "Menu parent was not found: {name}",
104
- "core.navigation.error.response_object": "Menu response is not an object",
105
102
  "core.navigation.error.root_tab": "Second-level app menu must be a tab: {name}",
106
103
  "core.navigation.error.tab_count": "tabBar must contain 0 or 2-5 items",
107
104
  "core.navigation.error.unique": "Menu id, name, and path must be unique",
@@ -95,13 +95,10 @@
95
95
  "core.navigation.error.child_tab": "モバイル セカンダリ メニューのみがタブとして使用できます: {name}",
96
96
  "core.navigation.error.cycle": "モバイル メニューにはループの親があります: {name}",
97
97
  "core.navigation.error.identity": "メニュー ID、名前、またはアプリ/パスが無効です",
98
- "core.navigation.error.item_object": "メニュー項目はオブジェクトではありません",
99
- "core.navigation.error.list_missing": "メニュー応答にフラット リストがありません",
100
98
  "core.navigation.error.name_prefix": "メニュー名にはアプリの接頭辞を使用する必要があります: {name}",
101
99
  "core.navigation.error.parent_invalid": "モバイル メニューの親番号が無効です: {name}",
102
100
  "core.navigation.error.parent_missing": "メニューに親番号がありません: {name}",
103
101
  "core.navigation.error.parent_not_found": "モバイル メニューの親が存在しません: {name}",
104
- "core.navigation.error.response_object": "メニュー応答はオブジェクトではありません",
105
102
  "core.navigation.error.root_tab": "モバイル セカンダリ メニューはタブとして使用する必要があります: {name}",
106
103
  "core.navigation.error.tab_count": "tabBar は 0 または 2 ~ 5 個の項目のみを設定できます",
107
104
  "core.navigation.error.unique": "メニュー ID、名前、パスは一意である必要があります",
@@ -95,13 +95,10 @@
95
95
  "core.navigation.error.child_tab": "只有移动端二级菜单可以作为 tab:{name}",
96
96
  "core.navigation.error.cycle": "移动端菜单存在循环父级:{name}",
97
97
  "core.navigation.error.identity": "菜单 id、name 或 app/ 路径无效",
98
- "core.navigation.error.item_object": "菜单项不是对象",
99
- "core.navigation.error.list_missing": "菜单响应缺少扁平列表",
100
98
  "core.navigation.error.name_prefix": "菜单名称必须使用 App 前缀:{name}",
101
99
  "core.navigation.error.parent_invalid": "移动端菜单父级编号无效:{name}",
102
100
  "core.navigation.error.parent_missing": "菜单缺少父级编号:{name}",
103
101
  "core.navigation.error.parent_not_found": "移动端菜单父级不存在:{name}",
104
- "core.navigation.error.response_object": "菜单响应不是对象",
105
102
  "core.navigation.error.root_tab": "移动端二级菜单必须作为 tab:{name}",
106
103
  "core.navigation.error.tab_count": "tabBar 只能配置 0 或 2-5 项",
107
104
  "core.navigation.error.unique": "菜单 id、name、path 必须唯一",
@@ -95,13 +95,10 @@
95
95
  "core.navigation.error.child_tab": "只有移動端二級選單可以作為 tab:{name}",
96
96
  "core.navigation.error.cycle": "移動端選單存在迴圈父級:{name}",
97
97
  "core.navigation.error.identity": "選單 id、name 或 app/ 路徑無效",
98
- "core.navigation.error.item_object": "選單項不是物件",
99
- "core.navigation.error.list_missing": "選單響應缺少扁平列表",
100
98
  "core.navigation.error.name_prefix": "選單名稱必須使用 App 字首:{name}",
101
99
  "core.navigation.error.parent_invalid": "移動端選單父級編號無效:{name}",
102
100
  "core.navigation.error.parent_missing": "選單缺少父級編號:{name}",
103
101
  "core.navigation.error.parent_not_found": "移動端選單父級不存在:{name}",
104
- "core.navigation.error.response_object": "選單響應不是物件",
105
102
  "core.navigation.error.root_tab": "移動端二級選單必須作為 tab:{name}",
106
103
  "core.navigation.error.tab_count": "tabBar 只能配置 0 或 2-5 項",
107
104
  "core.navigation.error.unique": "選單 id、name、path 必須唯一",
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,7 @@ const createDefaultMenus = (): AppMenu[] => [
60
61
  selectedIcon: 'HOME_SELECTED',
61
62
  },
62
63
  {
63
- id: 9990904,
64
+ id: 99040000,
64
65
  parentId: APP_MENU_ROOT_ID,
65
66
  name: 'AppMessage',
66
67
  path: 'app/message',
@@ -72,7 +73,7 @@ const createDefaultMenus = (): AppMenu[] => [
72
73
  selectedIcon: 'MESSAGE_SELECTED',
73
74
  },
74
75
  {
75
- id: 99909,
76
+ id: 99090000,
76
77
  parentId: APP_MENU_ROOT_ID,
77
78
  name: 'AppMy',
78
79
  path: 'app/my',
@@ -84,8 +85,8 @@ const createDefaultMenus = (): AppMenu[] => [
84
85
  selectedIcon: 'USER_SELECTED',
85
86
  },
86
87
  {
87
- id: 9990101,
88
- parentId: 99901,
88
+ id: 99010100,
89
+ parentId: 99010000,
89
90
  name: 'AppLogin',
90
91
  path: 'app/login',
91
92
  viewKey: 'LOGIN',
@@ -94,8 +95,8 @@ const createDefaultMenus = (): AppMenu[] => [
94
95
  inTabBar: false,
95
96
  },
96
97
  {
97
- id: 999010101,
98
- parentId: 9990101,
98
+ id: 99010101,
99
+ parentId: 99010100,
99
100
  name: 'AppProtocol',
100
101
  path: 'app/protocol/:type',
101
102
  viewKey: 'PROTOCOL',
@@ -104,8 +105,8 @@ const createDefaultMenus = (): AppMenu[] => [
104
105
  inTabBar: false,
105
106
  },
106
107
  {
107
- id: 9990901,
108
- parentId: 99909,
108
+ id: 99090100,
109
+ parentId: 99090000,
109
110
  name: 'AppProfile',
110
111
  path: 'app/profile',
111
112
  viewKey: 'PROFILE',
@@ -114,8 +115,8 @@ const createDefaultMenus = (): AppMenu[] => [
114
115
  inTabBar: false,
115
116
  },
116
117
  {
117
- id: 9990902,
118
- parentId: 99909,
118
+ id: 99090200,
119
+ parentId: 99090000,
119
120
  name: 'AppSettings',
120
121
  path: 'app/settings',
121
122
  viewKey: 'SETTINGS',
@@ -124,8 +125,8 @@ const createDefaultMenus = (): AppMenu[] => [
124
125
  inTabBar: false,
125
126
  },
126
127
  {
127
- id: 9990903,
128
- parentId: 99909,
128
+ id: 99090300,
129
+ parentId: 99090000,
129
130
  name: 'AppAi',
130
131
  path: 'app/ai',
131
132
  viewKey: 'AI',
@@ -134,8 +135,8 @@ const createDefaultMenus = (): AppMenu[] => [
134
135
  inTabBar: false,
135
136
  },
136
137
  {
137
- id: 9990102,
138
- parentId: 99901,
138
+ id: 99010200,
139
+ parentId: 99010000,
139
140
  name: 'AppWebView',
140
141
  path: 'app/webview',
141
142
  viewKey: 'WEBVIEW',
@@ -148,12 +149,25 @@ const createDefaultMenus = (): AppMenu[] => [
148
149
  const menus = ref<AppMenu[]>([])
149
150
  const ready = ref(false)
150
151
  const appMenuBadges = ref<Record<string, number>>({})
152
+ const isH5Runtime = typeof window !== 'undefined'
151
153
  let tabNavigationTarget: string | undefined
152
154
  let adapter: AppNavigationAdapter = {
153
155
  list: () => defBaseMenuService.ListBaseMenu(),
154
156
  }
155
157
 
156
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']
157
171
 
158
172
  /** 替换导航远端适配器,供宿主接入自定义契约。 */
159
173
  export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void {
@@ -241,20 +255,29 @@ function navigateTabRoute(url: string): void {
241
255
  const pages = getCurrentPages()
242
256
  const currentIndex = pages.length - 1
243
257
  const currentRoute = pages[currentIndex]?.route?.replace(/^\/+/, '')
244
- if (currentRoute === targetRoute || tabNavigationTarget) return
258
+ if (currentRoute === targetRoute || tabNavigationTarget === targetRoute) return
245
259
 
246
260
  tabNavigationTarget = targetRoute
247
261
  const release = () => {
248
262
  if (tabNavigationTarget === targetRoute) tabNavigationTarget = undefined
249
263
  }
264
+ if (isH5Runtime) {
265
+ uni.reLaunch({ url, success: release, fail: release, complete: release })
266
+ return
267
+ }
250
268
  if (nativeTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
251
269
  uni.switchTab({
252
270
  url,
253
271
  success: release,
254
272
  fail: () => navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release),
273
+ complete: release,
255
274
  })
256
275
  return
257
276
  }
277
+ if (resetTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
278
+ uni.reLaunch({ url, success: release, fail: release, complete: release })
279
+ return
280
+ }
258
281
  navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release)
259
282
  }
260
283
 
@@ -318,6 +341,15 @@ export function useAppMenuBadge(viewKey: string) {
318
341
  return computed(() => appMenuBadges.value[viewKey] ?? 0)
319
342
  }
320
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
+
321
353
  /** 获取导航响应式状态。 */
322
354
  export function useAppNavigation() {
323
355
  const menuTree = computed<AppMenuNode[]>(() => buildMenuTree(menus.value, APP_MENU_ROOT_ID))
@@ -347,48 +379,32 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
347
379
  const cached = uni.getStorageSync(cacheKey) as unknown
348
380
  if (!Array.isArray(cached)) return
349
381
  try {
350
- const normalized = cached.map(normalizeMenu)
351
- validateMenus(normalized)
352
- return normalized
382
+ const cachedMenus = cached as AppMenu[]
383
+ validateMenus(cachedMenus)
384
+ return cachedMenus
353
385
  } catch {
354
386
  uni.removeStorageSync(cacheKey)
355
387
  }
356
388
  }
357
389
 
358
- function normalizeMenuResponse(response: unknown): AppMenu[] {
359
- if (Array.isArray(response)) return response.map(normalizeMenu)
360
- if (!response || typeof response !== 'object')
361
- throw new Error(t('core.navigation.error.response_object'))
362
- const record = response as Record<string, unknown>
363
- const list = record.items ?? record.list ?? record.data
364
- if (!Array.isArray(list)) throw new Error(t('core.navigation.error.list_missing'))
365
- return list.map(normalizeMenu)
390
+ function normalizeMenuResponse(response: ListBaseMenuResponse): AppMenu[] {
391
+ return response.items.map(normalizeMenu)
366
392
  }
367
393
 
368
- function normalizeMenu(value: unknown): AppMenu {
369
- if (!value || typeof value !== 'object') throw new Error(t('core.navigation.error.item_object'))
370
- const item = value as Record<string, unknown>
371
- const meta = (item.meta ?? {}) as Record<string, unknown>
372
- 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
373
397
  return {
374
- id: Number(item.id),
375
- parentId:
376
- item.parent_id === undefined && item.parentId === undefined
377
- ? undefined
378
- : Number(item.parent_id ?? item.parentId),
379
- name: String(item.name ?? ''),
380
- path: String(item.path ?? ''),
381
- viewKey: String(app.view_key ?? app.viewKey ?? item.view_key ?? item.viewKey ?? ''),
382
- title: String(meta.title ?? item.title ?? ''),
383
- access: String(app.access ?? 'AUTHENTICATED') as AppMenuAccess,
384
- inTabBar: Boolean(app.in_tab_bar ?? app.inTabBar),
385
- icon: typeof meta.icon === 'string' ? meta.icon : undefined,
386
- selectedIcon:
387
- typeof app.selected_icon === 'string'
388
- ? app.selected_icon
389
- : typeof app.selectedIcon === 'string'
390
- ? app.selectedIcon
391
- : 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,
392
408
  }
393
409
  }
394
410
 
@@ -22,6 +22,8 @@ export enum PasswordCryptoScene {
22
22
  PASSWORD_CRYPTO_SCENE_UPDATE_USER_PASSWORD = 4,
23
23
  /** PASSWORD_CRYPTO_SCENE_MFA - 多因素认证敏感操作场景。 */
24
24
  PASSWORD_CRYPTO_SCENE_MFA = 5,
25
+ /** PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY - 密码策略初始化密码配置场景。 */
26
+ PASSWORD_CRYPTO_SCENE_CONFIGURE_PASSWORD_POLICY = 6,
25
27
  }
26
28
 
27
29
  /**
@@ -107,7 +109,7 @@ export interface RefreshTokenRequest {
107
109
  export interface RefreshTokenResponse {
108
110
  /** 访问令牌,必选项。 */
109
111
  access_token: string;
110
- /** 更新令牌,用来获取下一次的访问令牌,可选项。 */
112
+ /** 更新令牌;管理端 Cookie-only 请求不在响应体返回,应用端请求可返回。 */
111
113
  refresh_token: string;
112
114
  /** 令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。 */
113
115
  token_type: string;
@@ -135,7 +137,7 @@ export interface LoginRequest {
135
137
  export interface LoginResponse {
136
138
  /** 访问令牌,必选项。 */
137
139
  access_token: string;
138
- /** 更新令牌,用来获取下一次的访问令牌,可选项。 */
140
+ /** 更新令牌;管理端 Cookie-only 请求不在响应体返回,应用端请求可返回。 */
139
141
  refresh_token: string;
140
142
  /** 令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。 */
141
143
  token_type: string;
@@ -6,6 +6,7 @@
6
6
 
7
7
  /* eslint-disable */
8
8
  import type { Empty } from "../../../google/protobuf/empty";
9
+ import type { BaseUserIDType } from "../../common/v1/common";
9
10
 
10
11
  /** 获取用户资料请求参数 */
11
12
  export interface GetUserProfileRequest {
@@ -21,6 +22,12 @@ export interface UserProfileForm {
21
22
  gender: number;
22
23
  /** 手机号 */
23
24
  phone: string;
25
+ /** 邮箱 */
26
+ email: string;
27
+ /** 证件类型:枚举【BaseUserIDType】 */
28
+ id_type: BaseUserIDType;
29
+ /** 证件号 */
30
+ id_code: string;
24
31
  /** 头像 */
25
32
  avatar: string;
26
33
  }
@@ -0,0 +1,35 @@
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: system/common/v1/common.proto
6
+
7
+ /* eslint-disable */
8
+
9
+ /** 系统公共用户相关枚举。 */
10
+ export enum BaseUserIDType {
11
+ /** BASE_USER_ID_TYPE_UNSPECIFIED - 未指定证件类型。 */
12
+ BASE_USER_ID_TYPE_UNSPECIFIED = 0,
13
+ /** BASE_USER_ID_TYPE_ID_CARD - 居民身份证。 */
14
+ BASE_USER_ID_TYPE_ID_CARD = 1,
15
+ /** BASE_USER_ID_TYPE_PASSPORT - 护照。 */
16
+ BASE_USER_ID_TYPE_PASSPORT = 2,
17
+ /** BASE_USER_ID_TYPE_HK_MACAO_PERMIT - 港澳居民来往内地通行证。 */
18
+ BASE_USER_ID_TYPE_HK_MACAO_PERMIT = 3,
19
+ /** BASE_USER_ID_TYPE_TAIWAN_PERMIT - 台湾居民来往大陆通行证。 */
20
+ BASE_USER_ID_TYPE_TAIWAN_PERMIT = 4,
21
+ /** BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE - 外国人永久居留身份证。 */
22
+ BASE_USER_ID_TYPE_FOREIGN_PERMANENT_RESIDENCE = 5,
23
+ /** BASE_USER_ID_TYPE_OTHER - 其他证件。 */
24
+ BASE_USER_ID_TYPE_OTHER = 6,
25
+ }
26
+
27
+ /** 用户密码修改状态。 */
28
+ export enum BaseUserPasswordChangeStatus {
29
+ /** BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED - 未指定密码修改状态。 */
30
+ BASE_USER_PASSWORD_CHANGE_STATUS_UNSPECIFIED = 0,
31
+ /** BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED - 当前必须修改密码。 */
32
+ BASE_USER_PASSWORD_CHANGE_STATUS_REQUIRED = 1,
33
+ /** BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED - 当前无需修改密码。 */
34
+ BASE_USER_PASSWORD_CHANGE_STATUS_NOT_REQUIRED = 2,
35
+ }
@@ -1,4 +1,4 @@
1
- import { defConfigService } from '../../api/base/config'
1
+ import { defConfigService } from '../../api/base/v1/config'
2
2
  import { defineStore } from 'pinia'
3
3
  import { ref } from 'vue'
4
4
  import { BaseConfigSite } from '../../rpc/base/v1/config'
@@ -6,10 +6,10 @@ import type {
6
6
  } from '../../rpc/base/v1/oauth'
7
7
  import type { LoginRequest, LoginResponse } from '../../rpc/base/v1/login'
8
8
  import type { VerifyMfaRequest } from '../../rpc/base/v1/mfa'
9
- import { defMfaService } from '../../api/base/mfa'
10
- import { defAuthService } from '../../api/system/auth'
11
- import { defLoginService } from '../../api/base/login'
12
- import { defOauthService } from '../../api/base/oauth'
9
+ import { defMfaService } from '../../api/base/v1/mfa'
10
+ import { defAuthService } from '../../api/system/app/v1/auth'
11
+ import { defLoginService } from '../../api/base/v1/login'
12
+ import { defOauthService } from '../../api/base/v1/oauth'
13
13
  import { defineStore } from 'pinia'
14
14
  import { ref } from 'vue'
15
15
  import {
package/src/utils/http.ts CHANGED
@@ -43,10 +43,7 @@ const CAPTCHA_URL = '/v1/base/captcha'
43
43
  const CONFIG_URL = '/v1/base/config'
44
44
  const LANGUAGE_URL = '/v1/base/language'
45
45
  const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
46
- const LEGACY_AUTH_URL = '/auth'
47
- const LEGACY_REFRESH_TOKEN_URL = `${LEGACY_AUTH_URL}/token`
48
- const LEGACY_CAPTCHA_URL = '/login/captcha'
49
- // 认证公共接口不携带旧 token,同时保留旧路径兼容迁移期间的灰度访问。
46
+ // 认证公共接口不携带访问令牌。
50
47
  const NO_AUTH_URL_SET = new Set([
51
48
  SESSION_URL,
52
49
  REFRESH_TOKEN_URL,
@@ -54,9 +51,6 @@ const NO_AUTH_URL_SET = new Set([
54
51
  CONFIG_URL,
55
52
  LANGUAGE_URL,
56
53
  PASSWORD_PUBLIC_KEY_URL,
57
- LEGACY_AUTH_URL,
58
- LEGACY_CAPTCHA_URL,
59
- LEGACY_REFRESH_TOKEN_URL,
60
54
  ])
61
55
  const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
62
56
  SESSION_URL,
@@ -64,8 +58,6 @@ const AUTH_EXPIRED_EXCLUDED_URL_SET = new Set([
64
58
  CONFIG_URL,
65
59
  LANGUAGE_URL,
66
60
  PASSWORD_PUBLIC_KEY_URL,
67
- LEGACY_AUTH_URL,
68
- LEGACY_CAPTCHA_URL,
69
61
  ])
70
62
 
71
63
  const AUTH_SILENT_LOGOUT_EVENT = 'auth:silent-logout'
@@ -147,7 +139,7 @@ function isAuthErrorResponse(data: unknown) {
147
139
  )
148
140
  }
149
141
 
150
- // 当前请求属于公共认证接口时,不应自动附带旧登录态。
142
+ // 当前请求属于公共认证接口时,不应自动附带登录态。
151
143
  function isNoAuthRequest(url: string) {
152
144
  return NO_AUTH_URL_SET.has(url)
153
145
  }
@@ -1,11 +1,8 @@
1
- import { defLoginService } from '../api/base/login'
1
+ import { defLoginService } from '../api/base/v1/login'
2
2
  import { t } from '../locales'
3
3
  import { PasswordCryptoScene } from '../rpc/base/v1/login'
4
4
  import type { PasswordCrypto } from '../rpc/common/v1/types'
5
-
6
- // #ifdef MP-WEIXIN
7
5
  import * as miniCrypto from 'asmcrypto.js'
8
- // #endif
9
6
 
10
7
  type MiniCrypto = typeof import('asmcrypto.js')
11
8
 
@@ -53,8 +50,12 @@ function getSubtleCrypto() {
53
50
  return cryptoApi
54
51
  }
55
52
 
56
- /** 生成小程序端密码加密所需的随机字节。 */
57
- function getMiniRandomBytes(length: number) {
53
+ /** 生成纯 JavaScript 密码加密所需的随机字节。 */
54
+ function getPortableRandomBytes(length: number) {
55
+ const cryptoApi = globalThis.crypto
56
+ if (cryptoApi?.getRandomValues) {
57
+ return Promise.resolve(cryptoApi.getRandomValues(new Uint8Array(length)).buffer)
58
+ }
58
59
  return new Promise<ArrayBuffer>((resolve, reject) => {
59
60
  if (typeof wx === 'undefined' || typeof wx.getRandomValues !== 'function') {
60
61
  reject(new Error(t('core.crypto.unsupported')))
@@ -119,12 +120,16 @@ function trimDerInteger(value: Uint8Array) {
119
120
  }
120
121
 
121
122
  /** 从 PEM SubjectPublicKeyInfo 中提取 RSA 模数和指数。 */
122
- function parseMiniRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
123
+ function parsePortableRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
123
124
  const base64 = publicKey
124
125
  .replace(/-----BEGIN PUBLIC KEY-----/g, '')
125
126
  .replace(/-----END PUBLIC KEY-----/g, '')
126
127
  .replace(/\s/g, '')
127
- const der = new Uint8Array(wx.base64ToArrayBuffer(base64))
128
+ const der = new Uint8Array(
129
+ typeof wx !== 'undefined' && typeof wx.base64ToArrayBuffer === 'function'
130
+ ? wx.base64ToArrayBuffer(base64)
131
+ : pemToArrayBuffer(publicKey),
132
+ )
128
133
  const subjectPublicKeyInfo = readDerNode(der, 0)
129
134
  const algorithm = readDerNode(subjectPublicKeyInfo.value, 0)
130
135
  const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next)
@@ -164,13 +169,17 @@ function generateMgf1Mask(crypto: MiniCrypto, seed: Uint8Array, length: number)
164
169
  return mask
165
170
  }
166
171
 
167
- /** 将字节数组编码为小程序可提交的 Base64。 */
168
- function miniBytesToBase64(value: Uint8Array) {
169
- return wx.arrayBufferToBase64(value.slice().buffer)
172
+ /** 将字节数组编码为当前运行端可提交的 Base64。 */
173
+ function portableBytesToBase64(value: Uint8Array) {
174
+ const buffer = value.slice().buffer
175
+ if (typeof wx !== 'undefined' && typeof wx.arrayBufferToBase64 === 'function') {
176
+ return wx.arrayBufferToBase64(buffer)
177
+ }
178
+ return arrayBufferToBase64(buffer)
170
179
  }
171
180
 
172
- /** 使用纯 JavaScript 加密实现兼容微信小程序运行时的密码加密协议。 */
173
- async function encryptMiniProgramPassword(
181
+ /** 使用纯 JavaScript 实现兼容缺少 WebCrypto 的密码加密协议。 */
182
+ async function encryptPortablePassword(
174
183
  crypto: MiniCrypto,
175
184
  password: string,
176
185
  publicKeyResponse: {
@@ -180,13 +189,13 @@ async function encryptMiniProgramPassword(
180
189
  nonce: string
181
190
  },
182
191
  ): Promise<PasswordCrypto> {
183
- const aesKey = new Uint8Array(await getMiniRandomBytes(32))
184
- const iv = new Uint8Array(await getMiniRandomBytes(12))
192
+ const aesKey = new Uint8Array(await getPortableRandomBytes(32))
193
+ const iv = new Uint8Array(await getPortableRandomBytes(12))
185
194
  const plaintext = crypto.string_to_bytes(password, true)
186
- const [modulus, exponent] = parseMiniRsaPublicKey(publicKeyResponse.public_key)
195
+ const [modulus, exponent] = parsePortableRsaPublicKey(publicKeyResponse.public_key)
187
196
  const hash = new crypto.Sha256()
188
197
  const keySize = Math.ceil(new crypto.BigNumber(modulus).bitLength / 8)
189
- const seed = new Uint8Array(await getMiniRandomBytes(hash.HASH_SIZE))
198
+ const seed = new Uint8Array(await getPortableRandomBytes(hash.HASH_SIZE))
190
199
  const dataBlockLength = keySize - hash.HASH_SIZE - 1
191
200
  const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1
192
201
  if (paddingLength < 0) {
@@ -222,9 +231,9 @@ async function encryptMiniProgramPassword(
222
231
  key_id: publicKeyResponse.key_id,
223
232
  nonce: publicKeyResponse.nonce,
224
233
  algorithm: publicKeyResponse.algorithm,
225
- encrypted_key: miniBytesToBase64(encryptedKey),
226
- iv: miniBytesToBase64(iv),
227
- ciphertext: miniBytesToBase64(ciphertext),
234
+ encrypted_key: portableBytesToBase64(encryptedKey),
235
+ iv: portableBytesToBase64(iv),
236
+ ciphertext: portableBytesToBase64(ciphertext),
228
237
  }
229
238
  }
230
239
 
@@ -240,30 +249,31 @@ export async function encryptPassword(
240
249
 
241
250
  const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })
242
251
 
243
- // 微信小程序没有 window 和 WebCrypto,使用纯 JavaScript 实现保持与后端协议一致。
244
- // #ifdef MP-WEIXIN
245
- return encryptMiniProgramPassword(miniCrypto, plainPassword, publicKeyResponse)
246
- // #endif
252
+ const cryptoApi = globalThis.crypto
253
+ const isSecureContext = typeof window === 'undefined' || window.isSecureContext
254
+ if (!cryptoApi?.subtle || !isSecureContext) {
255
+ return encryptPortablePassword(miniCrypto, plainPassword, publicKeyResponse)
256
+ }
247
257
 
248
- const cryptoApi = getSubtleCrypto()
249
- const publicKey = await cryptoApi.subtle.importKey(
258
+ const subtleCrypto = getSubtleCrypto()
259
+ const publicKey = await subtleCrypto.subtle.importKey(
250
260
  'spki',
251
261
  pemToArrayBuffer(publicKeyResponse.public_key),
252
262
  { name: 'RSA-OAEP', hash: 'SHA-256' },
253
263
  false,
254
264
  ['encrypt'],
255
265
  )
256
- const aesKey = await cryptoApi.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
266
+ const aesKey = await subtleCrypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
257
267
  'encrypt',
258
268
  ])
259
- const rawAesKey = await cryptoApi.subtle.exportKey('raw', aesKey)
260
- const iv = cryptoApi.getRandomValues(new Uint8Array(12))
261
- const ciphertext = await cryptoApi.subtle.encrypt(
269
+ const rawAesKey = await subtleCrypto.subtle.exportKey('raw', aesKey)
270
+ const iv = subtleCrypto.getRandomValues(new Uint8Array(12))
271
+ const ciphertext = await subtleCrypto.subtle.encrypt(
262
272
  { name: 'AES-GCM', iv },
263
273
  aesKey,
264
274
  new TextEncoder().encode(plainPassword),
265
275
  )
266
- const encryptedKey = await cryptoApi.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, rawAesKey)
276
+ const encryptedKey = await subtleCrypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, rawAesKey)
267
277
 
268
278
  return {
269
279
  key_id: publicKeyResponse.key_id,
@@ -4,8 +4,8 @@ import type { LoginRequest } from '../../../rpc/base/v1/login'
4
4
  import { LoginStatus, type LoginResponse } from '../../../rpc/base/v1/login'
5
5
  import { onLoad } from '@dcloudio/uni-app'
6
6
  import { computed, defineAsyncComponent, nextTick, reactive, ref, watch } from 'vue'
7
- import { defLoginService } from '../../../api/base/login'
8
- import { defMfaService } from '../../../api/base/mfa'
7
+ import { defLoginService } from '../../../api/base/v1/login'
8
+ import { defMfaService } from '../../../api/base/v1/mfa'
9
9
  import defaultLogo from '../../../static/images/logo_icon.png'
10
10
  import { homeTabPage } from '../../../utils/navigation'
11
11
  import { PASSWORD_CRYPTO_SCENE, encryptPassword } from '../../../utils/passwordCrypto'
@@ -1087,12 +1087,17 @@ onLoad(() => {
1087
1087
  top: calc(24rpx + env(safe-area-inset-top));
1088
1088
  right: 28rpx;
1089
1089
  z-index: 2;
1090
+ max-width: calc(100% - 56rpx);
1090
1091
  }
1091
1092
 
1092
1093
  .login-locale__value {
1094
+ max-width: 240rpx;
1095
+ overflow: hidden;
1093
1096
  padding: 12rpx 16rpx;
1094
1097
  color: #16806d;
1095
1098
  font-size: 24rpx;
1099
+ text-overflow: ellipsis;
1100
+ white-space: nowrap;
1096
1101
  }
1097
1102
 
1098
1103
  .login-hero {
package/src/vite.ts CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  readFileSync,
6
6
  readdirSync,
7
7
  rmdirSync,
8
- rmSync,
9
8
  statSync,
10
9
  unlinkSync,
11
10
  writeFileSync,
@@ -39,7 +38,6 @@ type BuildTransaction = {
39
38
  originalPages: string
40
39
  generatedFiles: string[]
41
40
  generatedStaticFiles: string[]
42
- generatedStatic?: boolean
43
41
  ownerPid?: number
44
42
  ready?: boolean
45
43
  }
@@ -368,13 +366,9 @@ function restoreBuildTransaction(transaction: BuildTransaction, transactionFile:
368
366
  removeEmptyParents(dirname(file), transaction.inputDir)
369
367
  }
370
368
  removeGeneratedStaticFiles(
371
- transaction.generatedStaticFiles ?? [],
369
+ transaction.generatedStaticFiles,
372
370
  resolve(transaction.inputDir, 'static'),
373
371
  )
374
- // 兼容旧事务:旧格式仅在 static 原本不存在时记录整个目录由插件生成。
375
- if (transaction.generatedStatic && transaction.generatedStaticFiles === undefined) {
376
- rmSync(resolve(transaction.inputDir, 'static'), { recursive: true, force: true })
377
- }
378
372
  writeFileSync(transaction.pagesFile, transaction.originalPages)
379
373
  if (existsSync(transactionFile)) unlinkSync(transactionFile)
380
374
  }
@@ -600,10 +594,13 @@ function appendPage(manifest: PagesManifest, page: ScannedPage): void {
600
594
  function createPageWrapper(page: ScannedPage): string {
601
595
  const source = page.source.replace(/\\/g, '/')
602
596
  return `<script setup lang="ts">
597
+ import { onShow } from '@dcloudio/uni-app'
598
+ import { setAppPageTitle } from '@liujitcn/kratos-uni-app-core'
603
599
  import KratosPage from ${JSON.stringify(source)}
604
600
  import KratosTabBar from '@liujitcn/kratos-uni-app-core/components/KratosTabBar.vue'
605
601
 
606
602
  defineOptions({ inheritAttrs: false })
603
+ onShow(() => setAppPageTitle(${JSON.stringify(page.route)}))
607
604
  </script>
608
605
 
609
606
  <template>
@@ -1,24 +0,0 @@
1
- import { http } from '../../utils/http'
2
- import type { BaseDictForm, BaseDictService } from '../../rpc/system/app/v1/base_dict'
3
-
4
- const BASE_DICT_URL = '/v1/app/base/dict'
5
-
6
- /** 字典查询请求兼容结构,支持 code 和旧版 value。 */
7
- type GetBaseDictRequestCompat = {
8
- code?: string
9
- value?: string
10
- }
11
-
12
- /** 字典服务 */
13
- export class BaseDictServiceImpl implements BaseDictService {
14
- /** 查询单个字典 */
15
- GetBaseDict(request: GetBaseDictRequestCompat): Promise<BaseDictForm> {
16
- const code = request.code ?? request.value ?? ''
17
- return http<BaseDictForm>({
18
- url: `${BASE_DICT_URL}/${code}`,
19
- method: 'GET',
20
- })
21
- }
22
- }
23
-
24
- export const defBaseDictService = new BaseDictServiceImpl()