@liujitcn/kratos-uni-app-core 0.0.1

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 (66) hide show
  1. package/README.md +46 -0
  2. package/dist/module.mjs +84 -0
  3. package/dist/vite.mjs +405 -0
  4. package/package.json +58 -0
  5. package/src/api/base/config.ts +20 -0
  6. package/src/api/base/file.ts +79 -0
  7. package/src/api/base/login.ts +87 -0
  8. package/src/api/base/menu.ts +17 -0
  9. package/src/api/base/oauth.ts +149 -0
  10. package/src/api/system/auth.ts +54 -0
  11. package/src/api/system/base_area.ts +34 -0
  12. package/src/api/system/base_dict.ts +24 -0
  13. package/src/bootstrap.ts +23 -0
  14. package/src/components/BootstrapStatus.vue +39 -0
  15. package/src/components/KratosTabBar.vue +73 -0
  16. package/src/index.ts +6 -0
  17. package/src/module.ts +85 -0
  18. package/src/navigation-pattern.d.mts +2 -0
  19. package/src/navigation-pattern.mjs +29 -0
  20. package/src/navigation-tree.mjs +45 -0
  21. package/src/navigation.ts +330 -0
  22. package/src/pages.ts +26 -0
  23. package/src/rpc/base/v1/config.ts +34 -0
  24. package/src/rpc/base/v1/file.ts +66 -0
  25. package/src/rpc/base/v1/login.ts +128 -0
  26. package/src/rpc/base/v1/mcp.ts +20 -0
  27. package/src/rpc/base/v1/oauth.ts +199 -0
  28. package/src/rpc/base/v1/sse.ts +22 -0
  29. package/src/rpc/common/v1/common.ts +71 -0
  30. package/src/rpc/common/v1/enum.ts +71 -0
  31. package/src/rpc/common/v1/types.ts +77 -0
  32. package/src/rpc/google/protobuf/empty.ts +19 -0
  33. package/src/rpc/google/protobuf/wrappers.ts +124 -0
  34. package/src/rpc/system/app/v1/auth.ts +54 -0
  35. package/src/rpc/system/app/v1/base_area.ts +24 -0
  36. package/src/rpc/system/app/v1/base_dict.ts +37 -0
  37. package/src/rpc/system/app/v1/base_menu.ts +61 -0
  38. package/src/rpc/system/common/v1/enum.ts +80 -0
  39. package/src/static/images/avatar.png +0 -0
  40. package/src/static/images/center_bg.png +0 -0
  41. package/src/static/images/logo_icon.png +0 -0
  42. package/src/static/images/navigator_bg.png +0 -0
  43. package/src/static/tabs/home_default.png +0 -0
  44. package/src/static/tabs/home_selected.png +0 -0
  45. package/src/static/tabs/user_default.png +0 -0
  46. package/src/static/tabs/user_selected.png +0 -0
  47. package/src/stores/index.ts +14 -0
  48. package/src/stores/modules/setting.ts +56 -0
  49. package/src/stores/modules/user.ts +266 -0
  50. package/src/styles/base.scss +42 -0
  51. package/src/types/env.d.ts +24 -0
  52. package/src/types/go-captcha-uni.d.ts +7 -0
  53. package/src/types/uni-helper.d.ts +54 -0
  54. package/src/uni.scss +76 -0
  55. package/src/utils/auth.ts +67 -0
  56. package/src/utils/file.ts +95 -0
  57. package/src/utils/http.ts +430 -0
  58. package/src/utils/index.ts +79 -0
  59. package/src/utils/navigation.ts +81 -0
  60. package/src/utils/passwordCrypto.ts +275 -0
  61. package/src/views/pages/index/index.vue +227 -0
  62. package/src/views/pages/login/login.vue +1133 -0
  63. package/src/views/pages/login/protocal.vue +176 -0
  64. package/src/views/pages/status/index.vue +36 -0
  65. package/src/views/pages/webview/webview.vue +136 -0
  66. package/src/vite.ts +474 -0
@@ -0,0 +1,87 @@
1
+ import { http } from '../../utils/http'
2
+ import type {
3
+ CaptchaRequest,
4
+ CaptchaResponse,
5
+ LogoutRequest,
6
+ LoginRequest,
7
+ LoginResponse,
8
+ PasswordPublicKeyRequest,
9
+ PasswordPublicKeyResponse,
10
+ RefreshTokenRequest,
11
+ RefreshTokenResponse,
12
+ VerifyCaptchaRequest,
13
+ VerifyCaptchaResponse,
14
+ LoginService,
15
+ } from '../../rpc/base/v1/login'
16
+ import type { Empty } from '../../rpc/google/protobuf/empty'
17
+
18
+ const CAPTCHA_URL = '/v1/base/captcha'
19
+ const CAPTCHA_VERIFY_URL = '/v1/base/captcha/verify'
20
+ const PASSWORD_PUBLIC_KEY_URL = '/v1/base/password-public-key'
21
+ const SESSION_URL = '/v1/base/session'
22
+ const TOKEN_URL = '/v1/base/token'
23
+
24
+ /** 登录公共服务 */
25
+ export class LoginServiceImpl implements LoginService {
26
+ /** 验证码 */
27
+ Captcha(request: CaptchaRequest): Promise<CaptchaResponse> {
28
+ return http<CaptchaResponse>({
29
+ url: `${CAPTCHA_URL}`,
30
+ method: 'GET',
31
+ authMode: 'none',
32
+ data: request,
33
+ header: { Authorization: 'no-auth' },
34
+ })
35
+ }
36
+ /** 验证码预校验 */
37
+ VerifyCaptcha(request: VerifyCaptchaRequest): Promise<VerifyCaptchaResponse> {
38
+ return http<VerifyCaptchaResponse>({
39
+ url: `${CAPTCHA_VERIFY_URL}`,
40
+ method: 'POST',
41
+ authMode: 'none',
42
+ data: request,
43
+ header: { Authorization: 'no-auth' },
44
+ })
45
+ }
46
+ /** 获取密码临时公钥 */
47
+ PasswordPublicKey(request: PasswordPublicKeyRequest): Promise<PasswordPublicKeyResponse> {
48
+ return http<PasswordPublicKeyResponse>({
49
+ url: `${PASSWORD_PUBLIC_KEY_URL}`,
50
+ method: 'GET',
51
+ authMode: 'none',
52
+ data: request,
53
+ header: { Authorization: 'no-auth' },
54
+ })
55
+ }
56
+ /** 登出 */
57
+ Logout(request: LogoutRequest): Promise<Empty> {
58
+ return http<Empty>({
59
+ url: `${SESSION_URL}`,
60
+ method: 'DELETE',
61
+ authMode: 'required',
62
+ data: request,
63
+ })
64
+ }
65
+ /** 刷新认证令牌 */
66
+ RefreshToken(request: RefreshTokenRequest): Promise<RefreshTokenResponse> {
67
+ return http<RefreshTokenResponse>({
68
+ url: `${TOKEN_URL}`,
69
+ method: 'POST',
70
+ authMode: 'none',
71
+ data: request,
72
+ header: { Authorization: 'no-auth' },
73
+ })
74
+ }
75
+ /** 登录 */
76
+ Login(request: LoginRequest): Promise<LoginResponse> {
77
+ return http<LoginResponse>({
78
+ url: `${SESSION_URL}`,
79
+ method: 'POST',
80
+ authMode: 'none',
81
+ data: request,
82
+ header: { Authorization: 'no-auth' },
83
+ })
84
+ }
85
+ }
86
+
87
+ export const defLoginService = new LoginServiceImpl()
@@ -0,0 +1,17 @@
1
+ import { http } from '../../utils/http'
2
+ import type { ListBaseMenuResponse } from '../../rpc/system/app/v1/base_menu'
3
+
4
+ /** 远端移动菜单服务。 */
5
+ export class BaseMenuServiceImpl {
6
+ /** 获取扁平移动菜单配置。 */
7
+ ListBaseMenu(): Promise<ListBaseMenuResponse> {
8
+ return http<ListBaseMenuResponse>({
9
+ url: '/v1/app/base/menu',
10
+ method: 'GET',
11
+ authMode: 'optional',
12
+ })
13
+ }
14
+ }
15
+
16
+ /** 默认移动菜单服务。 */
17
+ export const defBaseMenuService = new BaseMenuServiceImpl()
@@ -0,0 +1,149 @@
1
+ import { http } from '../../utils/http'
2
+ import type {
3
+ CreateOauthAuthorizationRequest,
4
+ CreateOauthAuthorizationResponse,
5
+ CreateOauthBindingAuthorizationRequest,
6
+ CreateOauthBindingAuthorizationResponse,
7
+ BindOauthSessionRequest,
8
+ CreateOauthSessionRequest,
9
+ CreateOauthSessionResponse,
10
+ ExchangeOauthTicketRequest,
11
+ ExchangeOauthTicketResponse,
12
+ HandleOauthBindingCallbackRequest,
13
+ HandleOauthBindingCallbackResponse,
14
+ HandleOauthCallbackRequest,
15
+ HandleOauthCallbackResponse,
16
+ ListOauthBindingRequest,
17
+ ListOauthBindingResponse,
18
+ ListOauthProviderRequest,
19
+ ListOauthProviderResponse,
20
+ OauthService,
21
+ UnbindOauthAccountRequest,
22
+ } from '../../rpc/base/v1/oauth'
23
+ import type { Empty } from '../../rpc/google/protobuf/empty'
24
+
25
+ const OAUTH_PROVIDER_URL = '/v1/base/oauth/provider'
26
+ const OAUTH_AUTHORIZATION_URL = '/v1/base/oauth/authorization'
27
+ const OAUTH_CALLBACK_URL = '/v1/base/oauth'
28
+ const OAUTH_TICKET_URL = '/v1/base/oauth/ticket'
29
+ const OAUTH_SESSION_URL = '/v1/base/oauth/session'
30
+ const OAUTH_BINDING_URL = '/v1/base/oauth/binding'
31
+ const OAUTH_BINDING_AUTHORIZATION_URL = '/v1/base/oauth/binding/authorization'
32
+
33
+ /** 三方登录公共服务 */
34
+ export class OauthServiceImpl implements OauthService {
35
+ /** 查询三方登录方式 */
36
+ ListOauthProvider(request: ListOauthProviderRequest): Promise<ListOauthProviderResponse> {
37
+ return http<ListOauthProviderResponse>({
38
+ url: `${OAUTH_PROVIDER_URL}`,
39
+ method: 'GET',
40
+ authMode: 'none',
41
+ data: request,
42
+ header: { Authorization: 'no-auth' },
43
+ })
44
+ }
45
+
46
+ /** 创建三方登录授权地址 */
47
+ CreateOauthAuthorization(
48
+ request: CreateOauthAuthorizationRequest,
49
+ ): Promise<CreateOauthAuthorizationResponse> {
50
+ return http<CreateOauthAuthorizationResponse>({
51
+ url: `${OAUTH_AUTHORIZATION_URL}`,
52
+ method: 'POST',
53
+ authMode: 'none',
54
+ data: request,
55
+ header: { Authorization: 'no-auth' },
56
+ })
57
+ }
58
+
59
+ /** 处理三方登录回调 */
60
+ HandleOauthCallback(request: HandleOauthCallbackRequest): Promise<HandleOauthCallbackResponse> {
61
+ return http<HandleOauthCallbackResponse>({
62
+ url: `${OAUTH_CALLBACK_URL}/${request.provider}/callback`,
63
+ method: 'GET',
64
+ authMode: 'none',
65
+ data: request,
66
+ header: { Authorization: 'no-auth' },
67
+ })
68
+ }
69
+
70
+ /** 兑换三方登录票据 */
71
+ ExchangeOauthTicket(request: ExchangeOauthTicketRequest): Promise<ExchangeOauthTicketResponse> {
72
+ return http<ExchangeOauthTicketResponse>({
73
+ url: `${OAUTH_TICKET_URL}`,
74
+ method: 'POST',
75
+ authMode: 'none',
76
+ data: request,
77
+ header: { Authorization: 'no-auth' },
78
+ })
79
+ }
80
+
81
+ /** 创建三方登录会话 */
82
+ CreateOauthSession(request: CreateOauthSessionRequest): Promise<CreateOauthSessionResponse> {
83
+ return http<CreateOauthSessionResponse>({
84
+ url: `${OAUTH_SESSION_URL}`,
85
+ method: 'POST',
86
+ authMode: 'none',
87
+ data: request,
88
+ header: { Authorization: 'no-auth' },
89
+ })
90
+ }
91
+
92
+ /** 绑定已有账号并创建三方登录会话 */
93
+ BindOauthSession(request: BindOauthSessionRequest): Promise<CreateOauthSessionResponse> {
94
+ return http<CreateOauthSessionResponse>({
95
+ url: `${OAUTH_BINDING_URL}/session`,
96
+ method: 'POST',
97
+ authMode: 'none',
98
+ data: request,
99
+ header: { Authorization: 'no-auth' },
100
+ })
101
+ }
102
+
103
+ /** 查询个人中心三方账号绑定列表 */
104
+ ListOauthBinding(request: ListOauthBindingRequest): Promise<ListOauthBindingResponse> {
105
+ return http<ListOauthBindingResponse>({
106
+ url: `${OAUTH_BINDING_URL}`,
107
+ method: 'GET',
108
+ authMode: 'required',
109
+ data: request,
110
+ })
111
+ }
112
+
113
+ /** 创建个人中心三方账号绑定授权地址 */
114
+ CreateOauthBindingAuthorization(
115
+ request: CreateOauthBindingAuthorizationRequest,
116
+ ): Promise<CreateOauthBindingAuthorizationResponse> {
117
+ return http<CreateOauthBindingAuthorizationResponse>({
118
+ url: `${OAUTH_BINDING_AUTHORIZATION_URL}`,
119
+ method: 'POST',
120
+ authMode: 'required',
121
+ data: request,
122
+ })
123
+ }
124
+
125
+ /** 处理个人中心三方账号绑定回调 */
126
+ HandleOauthBindingCallback(
127
+ request: HandleOauthBindingCallbackRequest,
128
+ ): Promise<HandleOauthBindingCallbackResponse> {
129
+ return http<HandleOauthBindingCallbackResponse>({
130
+ url: `${OAUTH_CALLBACK_URL}/${request.provider}/binding/callback`,
131
+ method: 'GET',
132
+ authMode: 'none',
133
+ data: request,
134
+ header: { Authorization: 'no-auth' },
135
+ })
136
+ }
137
+
138
+ /** 解绑个人中心三方账号 */
139
+ UnbindOauthAccount(request: UnbindOauthAccountRequest): Promise<Empty> {
140
+ return http<Empty>({
141
+ url: `${OAUTH_BINDING_URL}/${request.provider}`,
142
+ method: 'DELETE',
143
+ authMode: 'required',
144
+ data: request,
145
+ })
146
+ }
147
+ }
148
+
149
+ export const defOauthService = new OauthServiceImpl()
@@ -0,0 +1,54 @@
1
+ import { http } from '../../utils/http'
2
+ import {
3
+ type AuthService,
4
+ type BindUserPhoneRequest,
5
+ type BindUserPhoneResponse,
6
+ type UserProfileForm,
7
+ } from '../../rpc/system/app/v1/auth'
8
+ import type { Empty } from '../../rpc/google/protobuf/empty'
9
+
10
+ const AUTH_URL = '/v1/app/auth'
11
+
12
+ /** 获取用户资料请求兼容空请求结构。 */
13
+ type GetUserProfileRequestCompat = Empty
14
+
15
+ /** 更新用户资料请求兼容新旧两种表单包裹方式。 */
16
+ type UpdateUserProfileRequestCompat = Partial<UserProfileForm> & {
17
+ user_profile?: UserProfileForm
18
+ }
19
+
20
+ /** 用户认证资料服务 */
21
+ export class AuthServiceImpl implements AuthService {
22
+ /** 获取已登录用户资料 */
23
+ GetUserProfile(request: GetUserProfileRequestCompat): Promise<UserProfileForm> {
24
+ return http<UserProfileForm>({
25
+ url: `${AUTH_URL}/profile`,
26
+ method: 'GET',
27
+ authMode: 'required',
28
+ data: request,
29
+ })
30
+ }
31
+
32
+ /** 修改个人中心用户信息 */
33
+ UpdateUserProfile(request: UpdateUserProfileRequestCompat): Promise<Empty> {
34
+ const userProfile = request.user_profile ?? (request as UserProfileForm)
35
+ return http<Empty>({
36
+ url: `${AUTH_URL}/profile`,
37
+ method: 'PUT',
38
+ authMode: 'required',
39
+ data: userProfile,
40
+ })
41
+ }
42
+
43
+ /** 手机号授权 */
44
+ BindUserPhone(request: BindUserPhoneRequest): Promise<BindUserPhoneResponse> {
45
+ return http<BindUserPhoneResponse>({
46
+ url: `${AUTH_URL}/phone`,
47
+ method: 'PUT',
48
+ authMode: 'required',
49
+ data: request,
50
+ })
51
+ }
52
+ }
53
+
54
+ export const defAuthService = new AuthServiceImpl()
@@ -0,0 +1,34 @@
1
+ import { http } from '../../utils/http'
2
+ import type {
3
+ BaseAreaService,
4
+ TreeBaseAreaRequest,
5
+ TreeBaseAreaResponse,
6
+ } from '../../rpc/system/app/v1/base_area'
7
+
8
+ const BASE_AREA_URL = '/v1/app/base/area'
9
+
10
+ /**
11
+ * 行政区域服务实现。
12
+ */
13
+ export class BaseAreaServiceImpl implements BaseAreaService {
14
+ /**
15
+ * 查询行政区域树形列表。
16
+ */
17
+ async TreeBaseArea(request: TreeBaseAreaRequest): Promise<TreeBaseAreaResponse> {
18
+ const response = await http<Partial<TreeBaseAreaResponse>>({
19
+ url: `${BASE_AREA_URL}/tree`,
20
+ method: 'GET',
21
+ data: request,
22
+ })
23
+
24
+ return {
25
+ ...response,
26
+ areas: response.areas ?? [],
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 默认行政区域服务。
33
+ */
34
+ export const defBaseAreaService = new BaseAreaServiceImpl()
@@ -0,0 +1,24 @@
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()
@@ -0,0 +1,23 @@
1
+ import type { Pinia } from 'pinia'
2
+ import type { App, Component } from 'vue'
3
+ import { registerKratosAppModules, type KratosAppModule } from './module'
4
+
5
+ /** uni-app 启动参数。 */
6
+ export interface KratosAppBootstrapOptions {
7
+ /** 宿主根组件。 */
8
+ app: Component
9
+ /** uni-app 宿主入口导入的 Vue SSR app 工厂。 */
10
+ createSSRApp: (rootComponent: Component) => App
11
+ /** 宿主 Pinia。 */
12
+ pinia: Pinia
13
+ /** 按覆盖优先级排列的模块。 */
14
+ modules: KratosAppModule[]
15
+ }
16
+
17
+ /** 创建 uni-app 实例并注册模块。 */
18
+ export function bootstrapKratosApp(options: KratosAppBootstrapOptions) {
19
+ registerKratosAppModules(options.modules)
20
+ const app = options.createSSRApp(options.app)
21
+ app.use(options.pinia)
22
+ return { app }
23
+ }
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ defineProps<{ title?: string; detail?: string }>()
3
+ </script>
4
+
5
+ <template>
6
+ <view class="bootstrap-status">
7
+ <image class="bootstrap-status__logo" src="../static/images/logo_icon.png" mode="aspectFit" />
8
+ <text class="bootstrap-status__title">{{ title || '正在加载' }}</text>
9
+ <text v-if="detail" class="bootstrap-status__detail">{{ detail }}</text>
10
+ </view>
11
+ </template>
12
+
13
+ <style scoped>
14
+ .bootstrap-status {
15
+ display: flex;
16
+ min-height: 100vh;
17
+ align-items: center;
18
+ justify-content: center;
19
+ flex-direction: column;
20
+ padding: 48rpx;
21
+ background: #f8f8f8;
22
+ box-sizing: border-box;
23
+ }
24
+ .bootstrap-status__logo {
25
+ width: 112rpx;
26
+ height: 112rpx;
27
+ margin-bottom: 32rpx;
28
+ }
29
+ .bootstrap-status__title {
30
+ color: #222;
31
+ font-size: 32rpx;
32
+ }
33
+ .bootstrap-status__detail {
34
+ margin-top: 16rpx;
35
+ color: #777;
36
+ font-size: 24rpx;
37
+ text-align: center;
38
+ }
39
+ </style>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { resolveModuleIcon, resolveStaticView } from '../module'
4
+ import { APP_MENU_ROOT_ID, useAppNavigation } from '../navigation'
5
+ import { resolveRootMenuId } from '../navigation-tree.mjs'
6
+
7
+ const props = defineProps<{ route: string }>()
8
+ const { menus, tabBar, navigate } = useAppNavigation()
9
+ const activeMenu = computed(() => {
10
+ const routeMenu = menus.value.find((menu) => resolveStaticView(menu.viewKey) === props.route)
11
+ if (!routeMenu) return
12
+ const tabMenuId = resolveRootMenuId(menus.value, routeMenu.id, APP_MENU_ROOT_ID)
13
+ return tabBar.value.find((menu) => menu.id === tabMenuId)
14
+ })
15
+ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
16
+ </script>
17
+
18
+ <template>
19
+ <view v-if="visible" class="kratos-tab-bar">
20
+ <view
21
+ v-for="item in tabBar"
22
+ :key="item.id"
23
+ class="kratos-tab-bar__item"
24
+ @tap="navigate(item.path)"
25
+ >
26
+ <image
27
+ v-if="resolveModuleIcon(item.icon)"
28
+ class="kratos-tab-bar__icon"
29
+ :src="
30
+ resolveModuleIcon(
31
+ activeMenu?.id === item.id && item.selectedIcon ? item.selectedIcon : item.icon,
32
+ )
33
+ "
34
+ mode="aspectFit"
35
+ />
36
+ <text :class="{ 'kratos-tab-bar__text--active': activeMenu?.id === item.id }">
37
+ {{ item.title }}
38
+ </text>
39
+ </view>
40
+ </view>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .kratos-tab-bar {
45
+ position: fixed;
46
+ right: 0;
47
+ bottom: 0;
48
+ left: 0;
49
+ z-index: 999;
50
+ display: flex;
51
+ min-height: 100rpx;
52
+ padding-bottom: env(safe-area-inset-bottom);
53
+ border-top: 1px solid #eee;
54
+ background: #fff;
55
+ }
56
+ .kratos-tab-bar__item {
57
+ display: flex;
58
+ flex: 1;
59
+ align-items: center;
60
+ justify-content: center;
61
+ flex-direction: column;
62
+ color: #333;
63
+ font-size: 24rpx;
64
+ }
65
+ .kratos-tab-bar__icon {
66
+ width: 42rpx;
67
+ height: 42rpx;
68
+ margin-bottom: 4rpx;
69
+ }
70
+ .kratos-tab-bar__text--active {
71
+ color: #27ba9b;
72
+ }
73
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './bootstrap'
2
+ export * from './module'
3
+ export * from './navigation'
4
+ export { default as KratosTabBar } from './components/KratosTabBar.vue'
5
+ export { default as pinia } from './stores'
6
+ export * from './stores'
package/src/module.ts ADDED
@@ -0,0 +1,85 @@
1
+ import { corePages } from './pages'
2
+
3
+ /** bootstrap 可见状态键。 */
4
+ export type BootstrapViewKey =
5
+ | 'BOOTSTRAP_LOADING'
6
+ | 'FORBIDDEN'
7
+ | 'NOT_FOUND'
8
+ | 'OFFLINE'
9
+ | 'CONFIG_ERROR'
10
+ | 'PAGE_UNAVAILABLE'
11
+
12
+ /** 页面编译配置。 */
13
+ export interface KratosAppPageConfig {
14
+ route?: string
15
+ style?: Record<string, unknown>
16
+ }
17
+
18
+ /** uni-app 模块定义。 */
19
+ export interface KratosAppModule {
20
+ name: string
21
+ pages: Record<string, KratosAppPageConfig>
22
+ views: Record<string, string>
23
+ icons?: Record<string, string>
24
+ }
25
+
26
+ /** 声明 uni-app 模块。 */
27
+ export function defineKratosAppModule<T extends KratosAppModule>(module: T): T {
28
+ return module
29
+ }
30
+
31
+ let registeredModules: KratosAppModule[] = []
32
+
33
+ /** 原子替换宿主当前注册的模块。 */
34
+ export function registerKratosAppModules(modules: KratosAppModule[]): void {
35
+ registeredModules = [...modules]
36
+ }
37
+
38
+ /** 获取当前模块列表。 */
39
+ export function getRegisteredKratosAppModules(): KratosAppModule[] {
40
+ return [...registeredModules]
41
+ }
42
+
43
+ /** 按后注册优先级解析静态视图。 */
44
+ export function resolveStaticView(viewKey: string): string | undefined {
45
+ for (let index = registeredModules.length - 1; index >= 0; index -= 1) {
46
+ const route = registeredModules[index].views[viewKey]
47
+ if (route) return route
48
+ }
49
+ }
50
+
51
+ /** 按后注册优先级解析模块图标。 */
52
+ export function resolveModuleIcon(icon: string | undefined): string | undefined {
53
+ if (!icon || /^https:\/\//.test(icon)) return icon
54
+ for (let index = registeredModules.length - 1; index >= 0; index -= 1) {
55
+ const source = registeredModules[index].icons?.[icon]
56
+ if (source) {
57
+ const base = import.meta.env.BASE_URL || '/'
58
+ return `${base.replace(/\/?$/, '/')}${source.replace(/^\/+/, '')}`
59
+ }
60
+ }
61
+ }
62
+
63
+ /** core 内置模块。 */
64
+ export const coreModule = defineKratosAppModule({
65
+ name: '@liujitcn/kratos-uni-app-core',
66
+ pages: corePages,
67
+ views: {
68
+ HOME: 'pages/index/index',
69
+ LOGIN: 'pages/login/login',
70
+ PROTOCOL: 'pages/login/protocal',
71
+ WEBVIEW: 'pages/webview/webview',
72
+ BOOTSTRAP_LOADING: 'pages/status/index',
73
+ FORBIDDEN: 'pages/status/index',
74
+ NOT_FOUND: 'pages/status/index',
75
+ OFFLINE: 'pages/status/index',
76
+ CONFIG_ERROR: 'pages/status/index',
77
+ PAGE_UNAVAILABLE: 'pages/status/index',
78
+ },
79
+ icons: {
80
+ HOME_DEFAULT: 'static/tabs/home_default.png',
81
+ HOME_SELECTED: 'static/tabs/home_selected.png',
82
+ USER_DEFAULT: 'static/tabs/user_default.png',
83
+ USER_SELECTED: 'static/tabs/user_selected.png',
84
+ },
85
+ })
@@ -0,0 +1,2 @@
1
+ export function matchLogicalPath(pattern: string, path: string): Record<string, string> | undefined
2
+ export function parseLogicalQuery(query: string): Record<string, string>
@@ -0,0 +1,29 @@
1
+ /** 匹配逻辑路由,并提取冒号参数。 */
2
+ export function matchLogicalPath(pattern, path) {
3
+ const patternParts = pattern.replace(/^\/+|\/+$/g, '').split('/')
4
+ const pathParts = path.replace(/^\/+|\/+$/g, '').split('/')
5
+ if (patternParts.length !== pathParts.length) return
6
+ const params = {}
7
+ for (let index = 0; index < patternParts.length; index += 1) {
8
+ const patternPart = patternParts[index]
9
+ const pathPart = pathParts[index]
10
+ if (patternPart.startsWith(':')) {
11
+ params[patternPart.slice(1)] = decodeURIComponent(pathPart)
12
+ } else if (patternPart !== pathPart) {
13
+ return
14
+ }
15
+ }
16
+ return params
17
+ }
18
+
19
+ /** 解析逻辑路由 query。 */
20
+ export function parseLogicalQuery(query) {
21
+ return query
22
+ .split('&')
23
+ .filter(Boolean)
24
+ .reduce((result, item) => {
25
+ const [key, value = ''] = item.split('=', 2)
26
+ result[decodeURIComponent(key)] = decodeURIComponent(value)
27
+ return result
28
+ }, {})
29
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @template {{id: number, parentId?: number}} T
3
+ * @typedef {T & {children: MenuNode<T>[]}} MenuNode
4
+ */
5
+
6
+ /**
7
+ * 按父级编号将移动端扁平菜单构建为根目录下的树。
8
+ * @template {{id: number, parentId?: number}} T
9
+ * @param {ReadonlyArray<T>} menus
10
+ * @param {number} rootId
11
+ * @returns {MenuNode<T>[]}
12
+ */
13
+ export function buildMenuTree(menus, rootId) {
14
+ /** @type {Map<number, MenuNode<T>>} */
15
+ const nodeMap = new Map(menus.map((menu) => [menu.id, { ...menu, children: [] }]))
16
+ /** @type {MenuNode<T>[]} */
17
+ const roots = []
18
+ for (const node of nodeMap.values()) {
19
+ if (node.parentId === rootId) {
20
+ roots.push(node)
21
+ continue
22
+ }
23
+ nodeMap.get(node.parentId)?.children.push(node)
24
+ }
25
+ return roots
26
+ }
27
+
28
+ /**
29
+ * 沿父级关系查找页面所属的移动端二级菜单编号。
30
+ * @param {ReadonlyArray<{id: number, parentId?: number}>} menus
31
+ * @param {number} menuId
32
+ * @param {number} rootId
33
+ */
34
+ export function resolveRootMenuId(menus, menuId, rootId) {
35
+ const menuMap = new Map(menus.map((menu) => [menu.id, menu]))
36
+ const visited = new Set()
37
+ let currentId = menuId
38
+ while (!visited.has(currentId)) {
39
+ visited.add(currentId)
40
+ const current = menuMap.get(currentId)
41
+ if (!current) return
42
+ if (current.parentId === rootId) return current.id
43
+ currentId = current.parentId
44
+ }
45
+ }