@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.
- package/README.md +30 -7
- package/dist/module.mjs +164 -16
- package/dist/vite.mjs +4 -5
- package/package.json +3 -1
- package/src/api/base/{config.ts → v1/config.ts} +6 -2
- package/src/api/base/{file.ts → v1/file.ts} +6 -2
- package/src/api/base/{language.ts → v1/language.ts} +2 -2
- package/src/api/base/{login.ts → v1/login.ts} +3 -3
- package/src/api/base/v1/mfa.ts +127 -0
- package/src/api/base/{oauth.ts → v1/oauth.ts} +3 -3
- package/src/api/system/{auth.ts → app/v1/auth.ts} +8 -15
- package/src/api/system/{base_area.ts → app/v1/base_area.ts} +2 -2
- package/src/api/system/app/v1/base_dict.ts +21 -0
- package/src/api/{base/menu.ts → system/app/v1/base_menu.ts} +2 -2
- package/src/bootstrap.ts +1 -1
- package/src/components/KratosSettingsPage.vue +292 -0
- package/src/components/KratosTabBar.vue +71 -1
- package/src/components/MfaManageDialog.vue +469 -0
- package/src/components/MfaRecoveryCodesDialog.vue +192 -0
- package/src/components/MfaSetupPanel.vue +160 -0
- package/src/components/PasswordVerifyDialog.vue +277 -0
- package/src/index.ts +2 -0
- package/src/locales/README.md +2 -1
- package/src/locales/en-US.json +41 -4
- package/src/locales/index.ts +10 -1
- package/src/locales/ja-JP.json +41 -4
- package/src/locales/zh-CN.json +41 -4
- package/src/locales/zh-TW.json +41 -4
- package/src/navigation.ts +115 -53
- package/src/rpc/base/v1/login.ts +37 -4
- package/src/rpc/base/v1/mfa.ts +180 -0
- package/src/rpc/base/v1/oauth.ts +25 -0
- package/src/rpc/system/app/v1/auth.ts +7 -0
- package/src/rpc/system/common/v1/common.ts +35 -0
- package/src/stores/modules/setting.ts +1 -1
- package/src/stores/modules/user.ts +29 -10
- package/src/utils/clipboard.ts +33 -0
- package/src/utils/http.ts +10 -13
- package/src/utils/passwordCrypto.ts +1 -1
- package/src/utils/webauthn.ts +101 -0
- package/src/views/pages/login/login.vue +299 -13
- package/src/vite.ts +4 -7
- package/src/api/system/base_dict.ts +0 -24
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
|
+
import type { LoginResponse } from '../../../rpc/base/v1/login'
|
|
3
|
+
import type {
|
|
4
|
+
BeginMfaDisableRequest,
|
|
5
|
+
BeginMfaDisableResponse,
|
|
6
|
+
BeginMfaEnrollmentRequest,
|
|
7
|
+
BeginMfaEnrollmentResponse,
|
|
8
|
+
BeginMfaSetupRequest,
|
|
9
|
+
BeginMfaSetupResponse,
|
|
10
|
+
ConfirmMfaEnrollmentRequest,
|
|
11
|
+
ConfirmMfaEnrollmentResponse,
|
|
12
|
+
ConfirmMfaSetupRequest,
|
|
13
|
+
ConfirmMfaSetupResponse,
|
|
14
|
+
DisableMfaRequest,
|
|
15
|
+
GetMfaStatusRequest,
|
|
16
|
+
MfaService,
|
|
17
|
+
MfaStatusResponse,
|
|
18
|
+
RecoveryCodesResponse,
|
|
19
|
+
RegenerateMfaRecoveryCodesRequest,
|
|
20
|
+
VerifyMfaRequest,
|
|
21
|
+
} from '../../../rpc/base/v1/mfa'
|
|
22
|
+
import type { Empty } from '../../../rpc/google/protobuf/empty'
|
|
23
|
+
|
|
24
|
+
const MFA_URL = '/v1/base/mfa'
|
|
25
|
+
|
|
26
|
+
/** 多因素认证公共服务。 */
|
|
27
|
+
export class MfaServiceImpl implements MfaService {
|
|
28
|
+
/** 校验登录阶段多因素认证。 */
|
|
29
|
+
VerifyMfa(request: VerifyMfaRequest): Promise<LoginResponse> {
|
|
30
|
+
return http<LoginResponse>({
|
|
31
|
+
url: `${MFA_URL}/verify`,
|
|
32
|
+
method: 'POST',
|
|
33
|
+
authMode: 'none',
|
|
34
|
+
data: request,
|
|
35
|
+
header: { Authorization: 'no-auth' },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 开始登录阶段的强制多因素认证绑定。 */
|
|
40
|
+
BeginMfaEnrollment(request: BeginMfaEnrollmentRequest): Promise<BeginMfaEnrollmentResponse> {
|
|
41
|
+
return http<BeginMfaEnrollmentResponse>({
|
|
42
|
+
url: `${MFA_URL}/enrollment`,
|
|
43
|
+
method: 'POST',
|
|
44
|
+
authMode: 'none',
|
|
45
|
+
data: request,
|
|
46
|
+
header: { Authorization: 'no-auth' },
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 确认登录阶段的强制多因素认证绑定。 */
|
|
51
|
+
ConfirmMfaEnrollment(
|
|
52
|
+
request: ConfirmMfaEnrollmentRequest,
|
|
53
|
+
): Promise<ConfirmMfaEnrollmentResponse> {
|
|
54
|
+
return http<ConfirmMfaEnrollmentResponse>({
|
|
55
|
+
url: `${MFA_URL}/enrollment/confirm`,
|
|
56
|
+
method: 'POST',
|
|
57
|
+
authMode: 'none',
|
|
58
|
+
data: request,
|
|
59
|
+
header: { Authorization: 'no-auth' },
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 查询当前用户多因素认证状态。 */
|
|
64
|
+
GetMfaStatus(request: GetMfaStatusRequest): Promise<MfaStatusResponse> {
|
|
65
|
+
return http<MfaStatusResponse>({
|
|
66
|
+
url: MFA_URL,
|
|
67
|
+
method: 'GET',
|
|
68
|
+
authMode: 'required',
|
|
69
|
+
data: request,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 开始绑定当前用户的多因素认证方式。 */
|
|
74
|
+
BeginMfaSetup(request: BeginMfaSetupRequest): Promise<BeginMfaSetupResponse> {
|
|
75
|
+
return http<BeginMfaSetupResponse>({
|
|
76
|
+
url: `${MFA_URL}/setup`,
|
|
77
|
+
method: 'POST',
|
|
78
|
+
authMode: 'required',
|
|
79
|
+
data: request,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 确认绑定当前用户的多因素认证方式。 */
|
|
84
|
+
ConfirmMfaSetup(request: ConfirmMfaSetupRequest): Promise<ConfirmMfaSetupResponse> {
|
|
85
|
+
return http<ConfirmMfaSetupResponse>({
|
|
86
|
+
url: `${MFA_URL}/setup/confirm`,
|
|
87
|
+
method: 'POST',
|
|
88
|
+
authMode: 'required',
|
|
89
|
+
data: request,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 开始禁用 WebAuthn 多因素认证的 Passkey 验证。 */
|
|
94
|
+
BeginMfaDisable(request: BeginMfaDisableRequest): Promise<BeginMfaDisableResponse> {
|
|
95
|
+
return http<BeginMfaDisableResponse>({
|
|
96
|
+
url: `${MFA_URL}/disable/challenge`,
|
|
97
|
+
method: 'POST',
|
|
98
|
+
authMode: 'required',
|
|
99
|
+
data: request,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 禁用当前用户的多因素认证。 */
|
|
104
|
+
DisableMfa(request: DisableMfaRequest): Promise<Empty> {
|
|
105
|
+
return http<Empty>({
|
|
106
|
+
url: `${MFA_URL}/disable`,
|
|
107
|
+
method: 'POST',
|
|
108
|
+
authMode: 'required',
|
|
109
|
+
data: request,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 重新生成当前用户的多因素认证恢复码。 */
|
|
114
|
+
RegenerateMfaRecoveryCodes(
|
|
115
|
+
request: RegenerateMfaRecoveryCodesRequest,
|
|
116
|
+
): Promise<RecoveryCodesResponse> {
|
|
117
|
+
return http<RecoveryCodesResponse>({
|
|
118
|
+
url: `${MFA_URL}/recovery-codes`,
|
|
119
|
+
method: 'POST',
|
|
120
|
+
authMode: 'required',
|
|
121
|
+
data: request,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** defMfaService 多因素认证公共服务实例。 */
|
|
127
|
+
export const defMfaService = new MfaServiceImpl()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../utils/http'
|
|
2
2
|
import type {
|
|
3
3
|
CreateOauthAuthorizationRequest,
|
|
4
4
|
CreateOauthAuthorizationResponse,
|
|
@@ -19,8 +19,8 @@ import type {
|
|
|
19
19
|
ListOauthProviderResponse,
|
|
20
20
|
OauthService,
|
|
21
21
|
UnbindOauthAccountRequest,
|
|
22
|
-
} from '
|
|
23
|
-
import type { Empty } from '
|
|
22
|
+
} from '../../../rpc/base/v1/oauth'
|
|
23
|
+
import type { Empty } from '../../../rpc/google/protobuf/empty'
|
|
24
24
|
|
|
25
25
|
const OAUTH_PROVIDER_URL = '/v1/base/oauth/provider'
|
|
26
26
|
const OAUTH_AUTHORIZATION_URL = '/v1/base/oauth/authorization'
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
2
|
import {
|
|
3
3
|
type AuthService,
|
|
4
4
|
type BindUserPhoneRequest,
|
|
5
5
|
type BindUserPhoneResponse,
|
|
6
|
+
type GetUserProfileRequest,
|
|
7
|
+
type UpdateUserProfileRequest,
|
|
6
8
|
type UserProfileForm,
|
|
7
|
-
} from '
|
|
8
|
-
import type { Empty } from '
|
|
9
|
+
} from '../../../../rpc/system/app/v1/auth'
|
|
10
|
+
import type { Empty } from '../../../../rpc/google/protobuf/empty'
|
|
9
11
|
|
|
10
12
|
const AUTH_URL = '/v1/app/auth'
|
|
11
13
|
|
|
12
|
-
/** 获取用户资料请求兼容空请求结构。 */
|
|
13
|
-
type GetUserProfileRequestCompat = Empty
|
|
14
|
-
|
|
15
|
-
/** 更新用户资料请求兼容新旧两种表单包裹方式。 */
|
|
16
|
-
type UpdateUserProfileRequestCompat = Partial<UserProfileForm> & {
|
|
17
|
-
user_profile?: UserProfileForm
|
|
18
|
-
}
|
|
19
|
-
|
|
20
14
|
/** 用户认证资料服务 */
|
|
21
15
|
export class AuthServiceImpl implements AuthService {
|
|
22
16
|
/** 获取已登录用户资料 */
|
|
23
|
-
GetUserProfile(request:
|
|
17
|
+
GetUserProfile(request: GetUserProfileRequest): Promise<UserProfileForm> {
|
|
24
18
|
return http<UserProfileForm>({
|
|
25
19
|
url: `${AUTH_URL}/profile`,
|
|
26
20
|
method: 'GET',
|
|
@@ -30,13 +24,12 @@ export class AuthServiceImpl implements AuthService {
|
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
/** 修改个人中心用户信息 */
|
|
33
|
-
UpdateUserProfile(request:
|
|
34
|
-
const userProfile = request.user_profile ?? (request as UserProfileForm)
|
|
27
|
+
UpdateUserProfile(request: UpdateUserProfileRequest): Promise<Empty> {
|
|
35
28
|
return http<Empty>({
|
|
36
29
|
url: `${AUTH_URL}/profile`,
|
|
37
30
|
method: 'PUT',
|
|
38
31
|
authMode: 'required',
|
|
39
|
-
data:
|
|
32
|
+
data: request.user_profile,
|
|
40
33
|
})
|
|
41
34
|
}
|
|
42
35
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { http } from '
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
2
|
import type {
|
|
3
3
|
BaseAreaService,
|
|
4
4
|
TreeBaseAreaRequest,
|
|
5
5
|
TreeBaseAreaResponse,
|
|
6
|
-
} from '
|
|
6
|
+
} from '../../../../rpc/system/app/v1/base_area'
|
|
7
7
|
|
|
8
8
|
const BASE_AREA_URL = '/v1/app/base/area'
|
|
9
9
|
|
|
@@ -0,0 +1,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 '
|
|
2
|
-
import type { ListBaseMenuResponse } from '
|
|
1
|
+
import { http } from '../../../../utils/http'
|
|
2
|
+
import type { ListBaseMenuResponse } from '../../../../rpc/system/app/v1/base_menu'
|
|
3
3
|
|
|
4
4
|
/** 远端移动菜单服务。 */
|
|
5
5
|
export class BaseMenuServiceImpl {
|
package/src/bootstrap.ts
CHANGED
|
@@ -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 {
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { useUserStore } from '../stores'
|
|
4
|
+
import { navigateToLogin } from '../utils/navigation'
|
|
5
|
+
import { getLanguageOptions, useI18n, type SupportedLocale } from '../locales'
|
|
6
|
+
import { defMfaService } from '../api/base/v1/mfa'
|
|
7
|
+
import MfaManageDialog from './MfaManageDialog.vue'
|
|
8
|
+
|
|
9
|
+
defineOptions({ name: 'KratosSettingsPage' })
|
|
10
|
+
|
|
11
|
+
defineSlots<{
|
|
12
|
+
/** 在所有公共设置卡片之前渲染的业务扩展内容。 */
|
|
13
|
+
extensions?: () => unknown
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const userStore = useUserStore()
|
|
17
|
+
const { locale, setLocale, t } = useI18n()
|
|
18
|
+
const logoutLoading = ref(false)
|
|
19
|
+
const mfaDialogVisible = ref(false)
|
|
20
|
+
const mfaDialogMode = ref<'setup' | 'disable'>('setup')
|
|
21
|
+
const mfaEnabled = ref(false)
|
|
22
|
+
const mfaMethod = ref('totp')
|
|
23
|
+
const mfaStatusLoading = ref(false)
|
|
24
|
+
const localeOptions = computed(() => getLanguageOptions())
|
|
25
|
+
const localeLabels = computed(() => localeOptions.value.map((item) => item.native_name))
|
|
26
|
+
const localeIndex = computed(() =>
|
|
27
|
+
Math.max(
|
|
28
|
+
0,
|
|
29
|
+
localeOptions.value.findIndex((item) => item.language_code === locale.value),
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
watch(locale, () => void uni.setNavigationBarTitle({ title: t('core.settings.title') }), {
|
|
34
|
+
immediate: true,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** 处理语言选择并刷新动态本地化数据。 */
|
|
38
|
+
const onLocaleChange = (event: { detail: { value: string | number } }) => {
|
|
39
|
+
const nextLocale = localeOptions.value[Number(event.detail.value)]?.language_code as
|
|
40
|
+
| SupportedLocale
|
|
41
|
+
| undefined
|
|
42
|
+
if (nextLocale) void setLocale(nextLocale)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 加载当前用户的多因素认证状态。 */
|
|
46
|
+
const loadMfaStatus = async (force = false) => {
|
|
47
|
+
if (!userStore.isAuthenticated() || (mfaStatusLoading.value && !force)) return
|
|
48
|
+
mfaStatusLoading.value = true
|
|
49
|
+
try {
|
|
50
|
+
const result = await defMfaService.GetMfaStatus({})
|
|
51
|
+
mfaEnabled.value = result.enabled
|
|
52
|
+
mfaMethod.value = result.method || 'totp'
|
|
53
|
+
} finally {
|
|
54
|
+
mfaStatusLoading.value = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 当前多因素认证状态展示文案。 */
|
|
59
|
+
const mfaStatusText = computed(() => {
|
|
60
|
+
if (!mfaEnabled.value) return t('core.settings.mfa_disabled')
|
|
61
|
+
const method =
|
|
62
|
+
mfaMethod.value === 'webauthn'
|
|
63
|
+
? t('core.settings.mfa_method_webauthn')
|
|
64
|
+
: t('core.settings.mfa_method_totp')
|
|
65
|
+
return t('core.settings.mfa_enabled_method', { method })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/** 根据当前状态打开多因素认证操作组件。 */
|
|
69
|
+
const onMfaTap = async () => {
|
|
70
|
+
if (!userStore.ensureAuthenticated()) {
|
|
71
|
+
navigateToLogin()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const currentMfaEnabled = mfaEnabled.value
|
|
75
|
+
try {
|
|
76
|
+
await loadMfaStatus(true)
|
|
77
|
+
} catch {
|
|
78
|
+
// 状态刷新失败时保留页面已有状态,避免已启用用户无法进入禁用流程。
|
|
79
|
+
mfaEnabled.value = currentMfaEnabled
|
|
80
|
+
}
|
|
81
|
+
if (mfaEnabled.value) {
|
|
82
|
+
mfaDialogMode.value = 'disable'
|
|
83
|
+
mfaDialogVisible.value = true
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
mfaDialogMode.value = 'setup'
|
|
87
|
+
mfaDialogVisible.value = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 绑定成功后清理旧会话并重新登录。 */
|
|
91
|
+
const onMfaSuccess = async (method: string) => {
|
|
92
|
+
mfaEnabled.value = true
|
|
93
|
+
mfaMethod.value = method || 'totp'
|
|
94
|
+
await uni.showToast({ icon: 'none', title: t('core.settings.mfa_bind_success') })
|
|
95
|
+
// 绑定 MFA 后后端会撤销当前会话,不能再调用登出接口或继续使用旧 token。
|
|
96
|
+
await userStore.clearUserData()
|
|
97
|
+
uni.reLaunch({ url: '/pages/login/login' })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 禁用成功后清理旧会话并重新登录。 */
|
|
101
|
+
const onMfaDisabled = async () => {
|
|
102
|
+
mfaEnabled.value = false
|
|
103
|
+
await uni.showToast({ icon: 'none', title: t('core.settings.mfa_disable_success') })
|
|
104
|
+
await userStore.clearUserData()
|
|
105
|
+
uni.reLaunch({ url: '/pages/login/login' })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 公共设置模块挂载时加载 MFA 状态,兼容页面包装和业务插槽场景。
|
|
109
|
+
onMounted(() => {
|
|
110
|
+
if (!userStore.ensureAuthenticated()) {
|
|
111
|
+
// #ifndef MP-WEIXIN
|
|
112
|
+
navigateToLogin()
|
|
113
|
+
// #endif
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
void loadMfaStatus()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
/** 确认后退出登录并返回上一页。 */
|
|
120
|
+
const onLogout = () => {
|
|
121
|
+
if (logoutLoading.value) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
// 模态弹窗
|
|
125
|
+
uni.showModal({
|
|
126
|
+
content: t('core.settings.logout_confirm'),
|
|
127
|
+
confirmColor: '#27BA9B',
|
|
128
|
+
success: async (res) => {
|
|
129
|
+
if (!res.confirm) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
logoutLoading.value = true
|
|
134
|
+
try {
|
|
135
|
+
// 先完成退出和本地登录态清理,再返回个人中心,避免 onShow 读取到旧登录态。
|
|
136
|
+
await userStore.logout()
|
|
137
|
+
uni.navigateBack()
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await uni.showToast({
|
|
140
|
+
icon: 'none',
|
|
141
|
+
title: t('core.settings.logout_failed'),
|
|
142
|
+
})
|
|
143
|
+
} finally {
|
|
144
|
+
logoutLoading.value = false
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<view class="viewport">
|
|
153
|
+
<slot name="extensions" />
|
|
154
|
+
<view class="list">
|
|
155
|
+
<view v-if="userStore.isAuthenticated()" class="item arrow mfa-item" @tap="onMfaTap">
|
|
156
|
+
<text>{{ t('core.settings.mfa_title') }}</text>
|
|
157
|
+
<text class="mfa-value">{{ mfaStatusText }}</text>
|
|
158
|
+
</view>
|
|
159
|
+
<picker :range="localeLabels" :value="localeIndex" @change="onLocaleChange">
|
|
160
|
+
<view class="item arrow locale-item">
|
|
161
|
+
<text>{{ t('common.field.language') }}</text>
|
|
162
|
+
<text class="locale-value">{{ localeLabels[localeIndex] }}</text>
|
|
163
|
+
</view>
|
|
164
|
+
</picker>
|
|
165
|
+
</view>
|
|
166
|
+
<!-- #ifdef MP-WEIXIN -->
|
|
167
|
+
<!-- 列表2 -->
|
|
168
|
+
<view class="list">
|
|
169
|
+
<button hover-class="none" class="item arrow" open-type="openSetting">
|
|
170
|
+
{{ t('core.settings.authorization') }}
|
|
171
|
+
</button>
|
|
172
|
+
<button hover-class="none" class="item arrow" open-type="feedback">
|
|
173
|
+
{{ t('core.settings.feedback') }}
|
|
174
|
+
</button>
|
|
175
|
+
<button hover-class="none" class="item arrow" open-type="contact">
|
|
176
|
+
{{ t('core.settings.contact') }}
|
|
177
|
+
</button>
|
|
178
|
+
</view>
|
|
179
|
+
<!-- #endif -->
|
|
180
|
+
<!-- 操作按钮 -->
|
|
181
|
+
<view class="action" v-if="userStore.isAuthenticated()">
|
|
182
|
+
<view @tap="onLogout" class="button">{{ t('common.action.logout') }}</view>
|
|
183
|
+
</view>
|
|
184
|
+
</view>
|
|
185
|
+
<MfaManageDialog
|
|
186
|
+
v-model="mfaDialogVisible"
|
|
187
|
+
:mode="mfaDialogMode"
|
|
188
|
+
:method="mfaMethod"
|
|
189
|
+
@success="onMfaSuccess"
|
|
190
|
+
@disabled="onMfaDisabled"
|
|
191
|
+
/>
|
|
192
|
+
</template>
|
|
193
|
+
|
|
194
|
+
<style scoped lang="scss">
|
|
195
|
+
.viewport {
|
|
196
|
+
min-height: 100vh;
|
|
197
|
+
padding: 20rpx;
|
|
198
|
+
box-sizing: border-box;
|
|
199
|
+
background-color: #f4f4f4;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* 列表 */
|
|
203
|
+
.list {
|
|
204
|
+
padding: 0 20rpx;
|
|
205
|
+
background-color: #fff;
|
|
206
|
+
margin-bottom: 20rpx;
|
|
207
|
+
border-radius: 10rpx;
|
|
208
|
+
.item {
|
|
209
|
+
line-height: 90rpx;
|
|
210
|
+
padding-left: 10rpx;
|
|
211
|
+
font-size: 30rpx;
|
|
212
|
+
color: #333;
|
|
213
|
+
border-top: 1rpx solid #ddd;
|
|
214
|
+
position: relative;
|
|
215
|
+
text-align: left;
|
|
216
|
+
border-radius: 0;
|
|
217
|
+
background-color: #fff;
|
|
218
|
+
&::after {
|
|
219
|
+
width: auto;
|
|
220
|
+
height: auto;
|
|
221
|
+
left: auto;
|
|
222
|
+
border: none;
|
|
223
|
+
}
|
|
224
|
+
&::after {
|
|
225
|
+
right: 5rpx;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
> .item:first-child {
|
|
229
|
+
border: none;
|
|
230
|
+
}
|
|
231
|
+
.arrow::after {
|
|
232
|
+
content: '›';
|
|
233
|
+
position: absolute;
|
|
234
|
+
top: 50%;
|
|
235
|
+
color: #ccc;
|
|
236
|
+
font-size: 36rpx;
|
|
237
|
+
transform: translateY(-50%);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.locale-item {
|
|
242
|
+
display: flex;
|
|
243
|
+
min-width: 0;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
padding-right: 42rpx;
|
|
246
|
+
|
|
247
|
+
> text:first-child {
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.locale-value {
|
|
253
|
+
flex: 1;
|
|
254
|
+
min-width: 0;
|
|
255
|
+
overflow: hidden;
|
|
256
|
+
margin-left: 24rpx;
|
|
257
|
+
color: #888;
|
|
258
|
+
text-align: right;
|
|
259
|
+
text-overflow: ellipsis;
|
|
260
|
+
white-space: nowrap;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.mfa-item {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
justify-content: space-between;
|
|
267
|
+
padding-right: 42rpx;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.mfa-value {
|
|
271
|
+
max-width: 55%;
|
|
272
|
+
overflow: hidden;
|
|
273
|
+
color: #888;
|
|
274
|
+
font-size: 26rpx;
|
|
275
|
+
text-overflow: ellipsis;
|
|
276
|
+
white-space: nowrap;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* 操作按钮 */
|
|
280
|
+
.action {
|
|
281
|
+
text-align: center;
|
|
282
|
+
line-height: 90rpx;
|
|
283
|
+
margin-top: 40rpx;
|
|
284
|
+
font-size: 32rpx;
|
|
285
|
+
color: #333;
|
|
286
|
+
.button {
|
|
287
|
+
background-color: #fff;
|
|
288
|
+
margin-bottom: 20rpx;
|
|
289
|
+
border-radius: 10rpx;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { resolveModuleIcon, resolveStaticView } from '../module'
|
|
4
|
-
import { APP_MENU_ROOT_ID, useAppNavigation } from '../navigation'
|
|
4
|
+
import { APP_MENU_ROOT_ID, useAppMenuBadge, useAppNavigation } from '../navigation'
|
|
5
5
|
import { resolveRootMenuId } from '../navigation-tree.mjs'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{ route: string }>()
|
|
@@ -13,6 +13,7 @@ const activeMenu = computed(() => {
|
|
|
13
13
|
return tabBar.value.find((menu) => menu.id === tabMenuId)
|
|
14
14
|
})
|
|
15
15
|
const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
16
|
+
const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
|
|
16
17
|
</script>
|
|
17
18
|
|
|
18
19
|
<template>
|
|
@@ -23,6 +24,11 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
23
24
|
class="kratos-tab-bar__item"
|
|
24
25
|
@tap="navigate(item.path)"
|
|
25
26
|
>
|
|
27
|
+
<view
|
|
28
|
+
v-if="item.viewKey === 'MESSAGE_INBOX' && messageBadge"
|
|
29
|
+
class="kratos-tab-bar__badge"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
/>
|
|
26
32
|
<image
|
|
27
33
|
v-if="resolveModuleIcon(item.icon)"
|
|
28
34
|
class="kratos-tab-bar__icon"
|
|
@@ -33,6 +39,12 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
33
39
|
"
|
|
34
40
|
mode="aspectFit"
|
|
35
41
|
/>
|
|
42
|
+
<view
|
|
43
|
+
v-else-if="item.viewKey === 'MESSAGE_INBOX'"
|
|
44
|
+
class="kratos-tab-bar__message-icon"
|
|
45
|
+
:class="{ 'kratos-tab-bar__message-icon--active': activeMenu?.id === item.id }"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
/>
|
|
36
48
|
<text :class="{ 'kratos-tab-bar__text--active': activeMenu?.id === item.id }">
|
|
37
49
|
{{ item.title }}
|
|
38
50
|
</text>
|
|
@@ -55,6 +67,7 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
55
67
|
background: #fff;
|
|
56
68
|
}
|
|
57
69
|
.kratos-tab-bar__item {
|
|
70
|
+
position: relative;
|
|
58
71
|
display: flex;
|
|
59
72
|
flex: 1;
|
|
60
73
|
align-items: center;
|
|
@@ -70,7 +83,64 @@ const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
|
|
|
70
83
|
height: 28px;
|
|
71
84
|
margin-bottom: 4px;
|
|
72
85
|
}
|
|
86
|
+
.kratos-tab-bar__message-icon {
|
|
87
|
+
position: relative;
|
|
88
|
+
width: 22px;
|
|
89
|
+
height: 18px;
|
|
90
|
+
margin-bottom: 6px;
|
|
91
|
+
border: 2px solid #9aa4b2;
|
|
92
|
+
border-radius: 6px;
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
}
|
|
95
|
+
.kratos-tab-bar__message-icon::before {
|
|
96
|
+
position: absolute;
|
|
97
|
+
top: 5px;
|
|
98
|
+
left: 4px;
|
|
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;
|
|
114
|
+
border-right: 2px solid #9aa4b2;
|
|
115
|
+
border-bottom: 2px solid #9aa4b2;
|
|
116
|
+
background: #fff;
|
|
117
|
+
content: '';
|
|
118
|
+
transform: rotate(45deg);
|
|
119
|
+
}
|
|
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
|
+
}
|
|
129
|
+
.kratos-tab-bar__message-icon--active::after {
|
|
130
|
+
border-color: #27ba9b;
|
|
131
|
+
}
|
|
73
132
|
.kratos-tab-bar__text--active {
|
|
74
133
|
color: #27ba9b;
|
|
75
134
|
}
|
|
135
|
+
.kratos-tab-bar__badge {
|
|
136
|
+
position: absolute;
|
|
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%;
|
|
144
|
+
background: #e5484d;
|
|
145
|
+
}
|
|
76
146
|
</style>
|