@liujitcn/kratos-uni-app-core 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.mjs +796 -3
- package/package.json +1 -1
- package/src/api/base/language.ts +25 -0
- package/src/bootstrap.ts +21 -0
- package/src/components/BootstrapStatus.vue +4 -1
- package/src/index.ts +1 -0
- package/src/locales/README.md +15 -0
- package/src/locales/en-US.json +110 -0
- package/src/locales/es-ES.json +110 -0
- package/src/locales/fr-FR.json +110 -0
- package/src/locales/index.ts +235 -0
- package/src/locales/ja-JP.json +110 -0
- package/src/locales/ko-KR.json +110 -0
- package/src/locales/zh-CN.json +110 -0
- package/src/locales/zh-TW.json +110 -0
- package/src/module.ts +21 -0
- package/src/navigation.ts +38 -26
- package/src/pages.ts +3 -3
- package/src/rpc/base/v1/config.ts +2 -0
- package/src/rpc/base/v1/language.ts +37 -0
- package/src/stores/index.ts +1 -0
- package/src/stores/modules/locale.ts +28 -0
- package/src/stores/modules/setting.ts +9 -4
- package/src/utils/file.ts +19 -26
- package/src/utils/http.ts +12 -6
- package/src/utils/passwordCrypto.ts +16 -15
- package/src/views/pages/index/index.vue +16 -14
- package/src/views/pages/login/login.vue +105 -51
- package/src/views/pages/login/protocal.vue +5 -3
- package/src/views/pages/status/index.vue +7 -10
- package/src/views/pages/webview/webview.vue +6 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defLoginService } from '../api/base/login'
|
|
2
|
+
import { t } from '../locales'
|
|
2
3
|
import { PasswordCryptoScene } from '../rpc/base/v1/enum'
|
|
3
4
|
import type { PasswordCrypto } from '../rpc/common/v1/types'
|
|
4
5
|
|
|
@@ -19,7 +20,7 @@ function pemToArrayBuffer(pem: string) {
|
|
|
19
20
|
.replace(/\s/g, '')
|
|
20
21
|
const decode = globalThis.atob
|
|
21
22
|
if (!decode) {
|
|
22
|
-
throw new Error('
|
|
23
|
+
throw new Error(t('core.crypto.base64DecodeUnsupported'))
|
|
23
24
|
}
|
|
24
25
|
const binary = decode(base64)
|
|
25
26
|
const buffer = new Uint8Array(binary.length)
|
|
@@ -38,7 +39,7 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
|
38
39
|
}
|
|
39
40
|
const encode = globalThis.btoa
|
|
40
41
|
if (!encode) {
|
|
41
|
-
throw new Error('
|
|
42
|
+
throw new Error(t('core.crypto.base64EncodeUnsupported'))
|
|
42
43
|
}
|
|
43
44
|
return encode(binary)
|
|
44
45
|
}
|
|
@@ -47,7 +48,7 @@ function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
|
47
48
|
function getSubtleCrypto() {
|
|
48
49
|
const cryptoApi = globalThis.crypto
|
|
49
50
|
if (!cryptoApi?.subtle) {
|
|
50
|
-
throw new Error('
|
|
51
|
+
throw new Error(t('core.crypto.unsupported'))
|
|
51
52
|
}
|
|
52
53
|
return cryptoApi
|
|
53
54
|
}
|
|
@@ -56,7 +57,7 @@ function getSubtleCrypto() {
|
|
|
56
57
|
function getMiniRandomBytes(length: number) {
|
|
57
58
|
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
58
59
|
if (typeof wx === 'undefined' || typeof wx.getRandomValues !== 'function') {
|
|
59
|
-
reject(new Error('
|
|
60
|
+
reject(new Error(t('core.crypto.unsupported')))
|
|
60
61
|
return
|
|
61
62
|
}
|
|
62
63
|
wx.getRandomValues({
|
|
@@ -65,7 +66,7 @@ function getMiniRandomBytes(length: number) {
|
|
|
65
66
|
resolve(randomValues)
|
|
66
67
|
},
|
|
67
68
|
fail: ({ errMsg }) => {
|
|
68
|
-
reject(new Error(errMsg || '
|
|
69
|
+
reject(new Error(errMsg || t('core.crypto.randomFailed')))
|
|
69
70
|
},
|
|
70
71
|
})
|
|
71
72
|
})
|
|
@@ -80,7 +81,7 @@ type DerNode = {
|
|
|
80
81
|
/** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
|
|
81
82
|
function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
82
83
|
if (offset + 2 > data.length) {
|
|
83
|
-
throw new Error('
|
|
84
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
84
85
|
}
|
|
85
86
|
const tag = data[offset]
|
|
86
87
|
const firstLengthByte = data[offset + 1]
|
|
@@ -89,7 +90,7 @@ function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
|
89
90
|
if (firstLengthByte & 0x80) {
|
|
90
91
|
const lengthByteCount = firstLengthByte & 0x7f
|
|
91
92
|
if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
|
|
92
|
-
throw new Error('
|
|
93
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
93
94
|
}
|
|
94
95
|
length = 0
|
|
95
96
|
for (let index = 0; index < lengthByteCount; index += 1) {
|
|
@@ -99,7 +100,7 @@ function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
|
99
100
|
}
|
|
100
101
|
const end = valueOffset + length
|
|
101
102
|
if (end > data.length) {
|
|
102
|
-
throw new Error('
|
|
103
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
103
104
|
}
|
|
104
105
|
return {
|
|
105
106
|
tag,
|
|
@@ -128,13 +129,13 @@ function parseMiniRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
|
|
|
128
129
|
const algorithm = readDerNode(subjectPublicKeyInfo.value, 0)
|
|
129
130
|
const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next)
|
|
130
131
|
if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
|
|
131
|
-
throw new Error('
|
|
132
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
132
133
|
}
|
|
133
134
|
const rsaPublicKey = readDerNode(bitString.value, 1)
|
|
134
135
|
const modulus = readDerNode(rsaPublicKey.value, 0)
|
|
135
136
|
const exponent = readDerNode(rsaPublicKey.value, modulus.next)
|
|
136
137
|
if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
|
|
137
|
-
throw new Error('
|
|
138
|
+
throw new Error(t('core.crypto.publicKeyInvalid'))
|
|
138
139
|
}
|
|
139
140
|
return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)]
|
|
140
141
|
}
|
|
@@ -152,7 +153,7 @@ function generateMgf1Mask(crypto: MiniCrypto, seed: Uint8Array, length: number)
|
|
|
152
153
|
counter[3] = index
|
|
153
154
|
let block = hash.reset().process(seed).process(counter).finish().result
|
|
154
155
|
if (!block) {
|
|
155
|
-
throw new Error('
|
|
156
|
+
throw new Error(t('core.crypto.encryptFailed'))
|
|
156
157
|
}
|
|
157
158
|
const target = mask.subarray(index * hash.HASH_SIZE)
|
|
158
159
|
if (block.length > target.length) {
|
|
@@ -189,14 +190,14 @@ async function encryptMiniProgramPassword(
|
|
|
189
190
|
const dataBlockLength = keySize - hash.HASH_SIZE - 1
|
|
190
191
|
const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1
|
|
191
192
|
if (paddingLength < 0) {
|
|
192
|
-
throw new Error('
|
|
193
|
+
throw new Error(t('core.crypto.passwordTooLong'))
|
|
193
194
|
}
|
|
194
195
|
const encodedMessage = new Uint8Array(keySize)
|
|
195
196
|
const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1)
|
|
196
197
|
const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1)
|
|
197
198
|
const labelHash = hash.reset().process(new Uint8Array()).finish().result
|
|
198
199
|
if (!labelHash) {
|
|
199
|
-
throw new Error('
|
|
200
|
+
throw new Error(t('core.crypto.encryptFailed'))
|
|
200
201
|
}
|
|
201
202
|
dataBlock.set(labelHash, 0)
|
|
202
203
|
dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1)
|
|
@@ -214,7 +215,7 @@ async function encryptMiniProgramPassword(
|
|
|
214
215
|
new crypto.BigNumber(encodedMessage),
|
|
215
216
|
).result
|
|
216
217
|
if (!encryptedKey) {
|
|
217
|
-
throw new Error('
|
|
218
|
+
throw new Error(t('core.crypto.encryptFailed'))
|
|
218
219
|
}
|
|
219
220
|
const ciphertext = crypto.AES_GCM.encrypt(plaintext, aesKey, iv)
|
|
220
221
|
return {
|
|
@@ -234,7 +235,7 @@ export async function encryptPassword(
|
|
|
234
235
|
): Promise<PasswordCrypto> {
|
|
235
236
|
const plainPassword = password.trim()
|
|
236
237
|
if (!plainPassword) {
|
|
237
|
-
throw new Error('
|
|
238
|
+
throw new Error(t('core.crypto.passwordRequired'))
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })
|
|
@@ -3,10 +3,12 @@ import { onLoad } from '@dcloudio/uni-app'
|
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import { useSettingStore } from '../../../stores'
|
|
5
5
|
import defaultLogo from '../../../static/images/logo_icon.png'
|
|
6
|
+
import { useI18n } from '../../../locales'
|
|
6
7
|
|
|
7
8
|
const settingStore = useSettingStore()
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const { t } = useI18n()
|
|
10
|
+
const mainTitle = computed(() => settingStore.getData('mainTitle') || t('core.home.mainTitle'))
|
|
11
|
+
const subTitle = computed(() => settingStore.getData('subTitle') || t('core.home.subTitle'))
|
|
10
12
|
const appLogo = computed(() => settingStore.getData('appLogo') || defaultLogo)
|
|
11
13
|
|
|
12
14
|
onLoad(() => {
|
|
@@ -25,49 +27,49 @@ onLoad(() => {
|
|
|
25
27
|
</view>
|
|
26
28
|
|
|
27
29
|
<view class="section">
|
|
28
|
-
<text class="section-title"
|
|
30
|
+
<text class="section-title">{{ t('core.home.techStack') }}</text>
|
|
29
31
|
<view class="info-list">
|
|
30
32
|
<view class="info-row">
|
|
31
|
-
<text class="info-label"
|
|
33
|
+
<text class="info-label">{{ t('core.home.framework') }}</text>
|
|
32
34
|
<text class="info-value">uni-app + Vue 3</text>
|
|
33
35
|
</view>
|
|
34
36
|
<view class="info-row">
|
|
35
|
-
<text class="info-label"
|
|
37
|
+
<text class="info-label">{{ t('core.home.language') }}</text>
|
|
36
38
|
<text class="info-value">TypeScript</text>
|
|
37
39
|
</view>
|
|
38
40
|
<view class="info-row">
|
|
39
|
-
<text class="info-label"
|
|
41
|
+
<text class="info-label">{{ t('core.home.stateManagement') }}</text>
|
|
40
42
|
<text class="info-value">Pinia</text>
|
|
41
43
|
</view>
|
|
42
44
|
<view class="info-row">
|
|
43
|
-
<text class="info-label"
|
|
45
|
+
<text class="info-label">{{ t('core.home.styleSolution') }}</text>
|
|
44
46
|
<text class="info-value">Sass + rpx</text>
|
|
45
47
|
</view>
|
|
46
48
|
</view>
|
|
47
49
|
</view>
|
|
48
50
|
|
|
49
51
|
<view class="section">
|
|
50
|
-
<text class="section-title"
|
|
52
|
+
<text class="section-title">{{ t('core.home.demoScope') }}</text>
|
|
51
53
|
<view class="demo-list">
|
|
52
54
|
<view class="demo-item">
|
|
53
55
|
<view class="demo-dot">1</view>
|
|
54
56
|
<view class="demo-copy">
|
|
55
|
-
<text class="demo-title"
|
|
56
|
-
<text class="demo-desc"
|
|
57
|
+
<text class="demo-title">{{ t('core.home.crossPlatform') }}</text>
|
|
58
|
+
<text class="demo-desc">{{ t('core.home.crossPlatformDescription') }}</text>
|
|
57
59
|
</view>
|
|
58
60
|
</view>
|
|
59
61
|
<view class="demo-item">
|
|
60
62
|
<view class="demo-dot">2</view>
|
|
61
63
|
<view class="demo-copy">
|
|
62
|
-
<text class="demo-title"
|
|
63
|
-
<text class="demo-desc"
|
|
64
|
+
<text class="demo-title">{{ t('core.home.accountCapability') }}</text>
|
|
65
|
+
<text class="demo-desc">{{ t('core.home.accountCapabilityDescription') }}</text>
|
|
64
66
|
</view>
|
|
65
67
|
</view>
|
|
66
68
|
<view class="demo-item">
|
|
67
69
|
<view class="demo-dot">3</view>
|
|
68
70
|
<view class="demo-copy">
|
|
69
|
-
<text class="demo-title"
|
|
70
|
-
<text class="demo-desc"
|
|
71
|
+
<text class="demo-title">{{ t('core.home.staticHome') }}</text>
|
|
72
|
+
<text class="demo-desc">{{ t('core.home.staticHomeDescription') }}</text>
|
|
71
73
|
</view>
|
|
72
74
|
</view>
|
|
73
75
|
</view>
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { useSettingStore, useUserStore } from '../../../stores'
|
|
3
3
|
import type { LoginRequest } from '../../../rpc/base/v1/login'
|
|
4
4
|
import { onLoad } from '@dcloudio/uni-app'
|
|
5
|
-
import { computed, defineAsyncComponent, reactive, ref } from 'vue'
|
|
5
|
+
import { computed, defineAsyncComponent, reactive, ref, watch } from 'vue'
|
|
6
6
|
import { defLoginService } from '../../../api/base/login'
|
|
7
7
|
import defaultLogo from '../../../static/images/logo_icon.png'
|
|
8
8
|
import { homeTabPage } from '../../../utils/navigation'
|
|
9
9
|
import { PASSWORD_CRYPTO_SCENE, encryptPassword } from '../../../utils/passwordCrypto'
|
|
10
10
|
import { navigateAppRoute } from '../../../navigation'
|
|
11
|
+
import { getLanguageOptions, useI18n, type SupportedLocale } from '../../../locales'
|
|
11
12
|
|
|
12
13
|
// go-captcha-uni 仅在 H5 端使用,小程序端不支持 defineAsyncComponent,需按平台裁剪。
|
|
13
14
|
// #ifdef H5
|
|
@@ -16,12 +17,25 @@ const GoCaptchaUni = defineAsyncComponent(() => import('go-captcha-uni'))
|
|
|
16
17
|
|
|
17
18
|
const userStore = useUserStore()
|
|
18
19
|
const settingStore = useSettingStore()
|
|
20
|
+
const { locale, setLocale, t } = useI18n()
|
|
21
|
+
const localeOptions = computed(() => getLanguageOptions())
|
|
22
|
+
const localeLabels = computed(() => localeOptions.value.map((item) => item.language_name))
|
|
23
|
+
const localeIndex = computed(() =>
|
|
24
|
+
Math.max(
|
|
25
|
+
0,
|
|
26
|
+
localeOptions.value.findIndex((item) => item.language_code === locale.value),
|
|
27
|
+
),
|
|
28
|
+
)
|
|
19
29
|
const wechatMiniProvider = 'wechatmini'
|
|
20
30
|
const loginSettingsReady = ref(false)
|
|
21
31
|
let loginSettingsPromise: Promise<boolean> | undefined
|
|
22
32
|
|
|
23
|
-
const configuredMainTitle = computed(
|
|
24
|
-
|
|
33
|
+
const configuredMainTitle = computed(
|
|
34
|
+
() => settingStore.getData('mainTitle') || t('core.home.mainTitle'),
|
|
35
|
+
)
|
|
36
|
+
const configuredSubTitle = computed(
|
|
37
|
+
() => settingStore.getData('subTitle') || t('core.login.defaultSubTitle'),
|
|
38
|
+
)
|
|
25
39
|
const configuredAppLogo = computed(() => settingStore.getData('appLogo') || defaultLogo)
|
|
26
40
|
const showTenantCode = computed(() => settingStore.getData('showTenantCode') !== 'false')
|
|
27
41
|
|
|
@@ -30,6 +44,18 @@ const isAgreePrivacy = ref(false)
|
|
|
30
44
|
const isAgreePrivacyShakeY = ref(false)
|
|
31
45
|
const loading = ref(false)
|
|
32
46
|
|
|
47
|
+
watch(locale, () => void uni.setNavigationBarTitle({ title: t('common.action.login') }), {
|
|
48
|
+
immediate: true,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** 切换登录页语言。 */
|
|
52
|
+
const onLocaleChange = (event: { detail: { value: string | number } }) => {
|
|
53
|
+
const nextLocale = localeOptions.value[Number(event.detail.value)]?.language_code as
|
|
54
|
+
| SupportedLocale
|
|
55
|
+
| undefined
|
|
56
|
+
if (nextLocale) void setLocale(nextLocale)
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
const toggleAgreePrivacy = () => {
|
|
34
60
|
isAgreePrivacy.value = !isAgreePrivacy.value
|
|
35
61
|
}
|
|
@@ -95,7 +121,7 @@ const resolveMiniCaptchaImage = (payload: string, captchaId: string) => {
|
|
|
95
121
|
data: payload.slice(commaIndex + 1),
|
|
96
122
|
encoding: 'base64',
|
|
97
123
|
success: () => resolve(filePath),
|
|
98
|
-
fail: () => reject(new Error('
|
|
124
|
+
fail: () => reject(new Error(t('core.login.captchaWriteFailed'))),
|
|
99
125
|
})
|
|
100
126
|
})
|
|
101
127
|
}
|
|
@@ -111,7 +137,7 @@ const refreshMiniCaptcha = async () => {
|
|
|
111
137
|
miniForm.captcha_id = captcha.captcha_id
|
|
112
138
|
miniForm.captcha_code = ''
|
|
113
139
|
} catch {
|
|
114
|
-
await uni.showToast({ icon: 'none', title: '
|
|
140
|
+
await uni.showToast({ icon: 'none', title: t('core.login.captchaLoadFailed') })
|
|
115
141
|
}
|
|
116
142
|
}
|
|
117
143
|
|
|
@@ -128,12 +154,7 @@ const isWechatUnboundError = (error: unknown) => {
|
|
|
128
154
|
}
|
|
129
155
|
}
|
|
130
156
|
const reason = response.data?.reason ?? response.data?.error?.reason
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
response.data?.binding_required === true ||
|
|
134
|
-
String(reason || '') === 'UNAUTHENTICATED' ||
|
|
135
|
-
message === '微信账号未绑定,请先绑定已有账号'
|
|
136
|
-
)
|
|
157
|
+
return response.data?.binding_required === true || String(reason || '') === 'UNAUTHENTICATED'
|
|
137
158
|
}
|
|
138
159
|
|
|
139
160
|
// 微信授权失败时保留请求层提示,避免异步登录异常冒泡到小程序调试器。
|
|
@@ -150,7 +171,7 @@ const wxLogin = async () => {
|
|
|
150
171
|
try {
|
|
151
172
|
loginCode = (await wx.login()).code
|
|
152
173
|
} catch {
|
|
153
|
-
await uni.showToast({ icon: 'none', title: '
|
|
174
|
+
await uni.showToast({ icon: 'none', title: t('core.login.wechatFailed') })
|
|
154
175
|
loading.value = false
|
|
155
176
|
return
|
|
156
177
|
}
|
|
@@ -167,7 +188,7 @@ const wxLogin = async () => {
|
|
|
167
188
|
await loginSuccess()
|
|
168
189
|
} catch (error) {
|
|
169
190
|
if (!isWechatUnboundError(error)) {
|
|
170
|
-
await uni.showToast({ icon: 'none', title: '
|
|
191
|
+
await uni.showToast({ icon: 'none', title: t('core.login.wechatFailed') })
|
|
171
192
|
return
|
|
172
193
|
}
|
|
173
194
|
miniBinding.value = true
|
|
@@ -182,15 +203,15 @@ const bindMiniAccount = async () => {
|
|
|
182
203
|
if (loading.value) return
|
|
183
204
|
if (!(await checkedAgreePrivacy())) return
|
|
184
205
|
if (showTenantCode.value && !miniForm.tenant_code) {
|
|
185
|
-
await uni.showToast({ icon: 'none', title: '
|
|
206
|
+
await uni.showToast({ icon: 'none', title: t('core.login.tenant') })
|
|
186
207
|
return
|
|
187
208
|
}
|
|
188
209
|
if (!miniForm.user_name || !miniPasswordValue.value) {
|
|
189
|
-
await uni.showToast({ icon: 'none', title: '
|
|
210
|
+
await uni.showToast({ icon: 'none', title: t('core.login.userNamePassword') })
|
|
190
211
|
return
|
|
191
212
|
}
|
|
192
213
|
if (!miniForm.captcha_id || !miniForm.captcha_code) {
|
|
193
|
-
await uni.showToast({ icon: 'none', title: '
|
|
214
|
+
await uni.showToast({ icon: 'none', title: t('core.login.captcha') })
|
|
194
215
|
return
|
|
195
216
|
}
|
|
196
217
|
loading.value = true
|
|
@@ -317,7 +338,7 @@ const behaviorCaptchaConfig = computed(() => {
|
|
|
317
338
|
verticalPadding: 0,
|
|
318
339
|
horizontalPadding: 0,
|
|
319
340
|
iconSize: 20,
|
|
320
|
-
title: '
|
|
341
|
+
title: t('core.login.behaviorRotate'),
|
|
321
342
|
}
|
|
322
343
|
}
|
|
323
344
|
return {
|
|
@@ -328,10 +349,13 @@ const behaviorCaptchaConfig = computed(() => {
|
|
|
328
349
|
showTheme: false,
|
|
329
350
|
verticalPadding: 0,
|
|
330
351
|
horizontalPadding: 0,
|
|
331
|
-
buttonText: '
|
|
352
|
+
buttonText: t('common.action.confirm'),
|
|
332
353
|
iconSize: 20,
|
|
333
354
|
dotSize: 20,
|
|
334
|
-
title:
|
|
355
|
+
title:
|
|
356
|
+
currentCaptchaType.value === 'click'
|
|
357
|
+
? t('core.login.behaviorClick')
|
|
358
|
+
: t('core.login.behaviorPuzzle'),
|
|
335
359
|
}
|
|
336
360
|
})
|
|
337
361
|
const behaviorCaptchaTheme = {
|
|
@@ -353,11 +377,11 @@ const behaviorCaptchaTheme = {
|
|
|
353
377
|
const getCaptcha = async () => {
|
|
354
378
|
const requestedCaptchaType = configuredCaptchaType.value
|
|
355
379
|
if (!requestedCaptchaType) {
|
|
356
|
-
throw new Error('
|
|
380
|
+
throw new Error(t('core.login.captchaTypeMissing'))
|
|
357
381
|
}
|
|
358
382
|
const data = await defLoginService.Captcha({ type: requestedCaptchaType })
|
|
359
383
|
if (!data.type) {
|
|
360
|
-
throw new Error('
|
|
384
|
+
throw new Error(t('core.login.captchaTypeResponseMissing'))
|
|
361
385
|
}
|
|
362
386
|
currentCaptchaType.value = data.type
|
|
363
387
|
if (!isBehaviorCaptcha.value) {
|
|
@@ -381,7 +405,7 @@ const refreshCaptcha = async () => {
|
|
|
381
405
|
behaviorDialogVisible.value = false
|
|
382
406
|
await uni.showToast({
|
|
383
407
|
icon: 'none',
|
|
384
|
-
title: error instanceof Error ? error.message : '
|
|
408
|
+
title: error instanceof Error ? error.message : t('core.login.captchaLoadFailed'),
|
|
385
409
|
})
|
|
386
410
|
return false
|
|
387
411
|
}
|
|
@@ -442,28 +466,28 @@ const validateLoginForm = async () => {
|
|
|
442
466
|
if (showTenantCode.value && !form.value.tenant_code) {
|
|
443
467
|
await uni.showToast({
|
|
444
468
|
icon: 'none',
|
|
445
|
-
title: '
|
|
469
|
+
title: t('core.login.tenant'),
|
|
446
470
|
})
|
|
447
471
|
return false
|
|
448
472
|
}
|
|
449
473
|
if (!form.value.user_name) {
|
|
450
474
|
await uni.showToast({
|
|
451
475
|
icon: 'none',
|
|
452
|
-
title: '
|
|
476
|
+
title: t('core.login.userName'),
|
|
453
477
|
})
|
|
454
478
|
return false
|
|
455
479
|
}
|
|
456
480
|
if (!passwordValue.value) {
|
|
457
481
|
await uni.showToast({
|
|
458
482
|
icon: 'none',
|
|
459
|
-
title: '
|
|
483
|
+
title: t('core.login.password'),
|
|
460
484
|
})
|
|
461
485
|
return false
|
|
462
486
|
}
|
|
463
487
|
if (!isBehaviorCaptcha.value && !form.value.captcha_code) {
|
|
464
488
|
await uni.showToast({
|
|
465
489
|
icon: 'none',
|
|
466
|
-
title: '
|
|
490
|
+
title: t('core.login.captcha'),
|
|
467
491
|
})
|
|
468
492
|
return false
|
|
469
493
|
}
|
|
@@ -567,7 +591,7 @@ const onSubmit = async () => {
|
|
|
567
591
|
const loginSuccess = async () => {
|
|
568
592
|
await userStore.getUserProfile()
|
|
569
593
|
// 成功提示
|
|
570
|
-
await uni.showToast({ icon: 'success', title: '
|
|
594
|
+
await uni.showToast({ icon: 'success', title: t('core.login.loginSuccess') })
|
|
571
595
|
setTimeout(() => {
|
|
572
596
|
const lastRoute = uni.getStorageSync('lastRoute') || homeTabPage
|
|
573
597
|
if (lastRoute.startsWith(homeTabPage)) {
|
|
@@ -586,7 +610,7 @@ const checkedAgreePrivacy = async () => {
|
|
|
586
610
|
}
|
|
587
611
|
|
|
588
612
|
if (!settingStore.getData('serviceProtocol') || !settingStore.getData('privacyProtocol')) {
|
|
589
|
-
await uni.showToast({ icon: 'none', title: '
|
|
613
|
+
await uni.showToast({ icon: 'none', title: t('core.login.protocolMissing') })
|
|
590
614
|
return false
|
|
591
615
|
}
|
|
592
616
|
|
|
@@ -598,10 +622,10 @@ const checkedAgreePrivacy = async () => {
|
|
|
598
622
|
|
|
599
623
|
return new Promise<boolean>((resolve) => {
|
|
600
624
|
uni.showModal({
|
|
601
|
-
title: '
|
|
602
|
-
content: '
|
|
603
|
-
confirmText: '
|
|
604
|
-
cancelText: '
|
|
625
|
+
title: t('common.title.notice'),
|
|
626
|
+
content: t('core.login.protocolPrompt'),
|
|
627
|
+
confirmText: t('common.action.confirm'),
|
|
628
|
+
cancelText: t('common.action.cancel'),
|
|
605
629
|
success: ({ confirm }) => {
|
|
606
630
|
if (confirm) {
|
|
607
631
|
isAgreePrivacy.value = true
|
|
@@ -630,7 +654,7 @@ const loadLoginSettings = () => {
|
|
|
630
654
|
// #ifdef H5
|
|
631
655
|
const captchaType = configuredCaptchaType.value
|
|
632
656
|
if (!captchaType) {
|
|
633
|
-
throw new Error('
|
|
657
|
+
throw new Error(t('core.login.captchaTypeMissing'))
|
|
634
658
|
}
|
|
635
659
|
currentCaptchaType.value = captchaType
|
|
636
660
|
await loadPageCaptcha()
|
|
@@ -643,7 +667,7 @@ const loadLoginSettings = () => {
|
|
|
643
667
|
} catch (error) {
|
|
644
668
|
await uni.showToast({
|
|
645
669
|
icon: 'none',
|
|
646
|
-
title: error instanceof Error ? error.message : '
|
|
670
|
+
title: error instanceof Error ? error.message : t('core.protocol.loadFailed'),
|
|
647
671
|
})
|
|
648
672
|
loginSettingsPromise = undefined
|
|
649
673
|
return false
|
|
@@ -664,6 +688,14 @@ onLoad(() => {
|
|
|
664
688
|
|
|
665
689
|
<template>
|
|
666
690
|
<view class="login-page">
|
|
691
|
+
<picker
|
|
692
|
+
class="login-locale"
|
|
693
|
+
:range="localeLabels"
|
|
694
|
+
:value="localeIndex"
|
|
695
|
+
@change="onLocaleChange"
|
|
696
|
+
>
|
|
697
|
+
<view class="login-locale__value">{{ localeLabels[localeIndex] }}</view>
|
|
698
|
+
</picker>
|
|
667
699
|
<view class="login-hero">
|
|
668
700
|
<view class="login-logo-shell">
|
|
669
701
|
<image :src="configuredAppLogo" />
|
|
@@ -683,14 +715,14 @@ onLoad(() => {
|
|
|
683
715
|
class="login-input"
|
|
684
716
|
type="text"
|
|
685
717
|
confirm-type="next"
|
|
686
|
-
placeholder="
|
|
718
|
+
:placeholder="t('core.login.tenant')"
|
|
687
719
|
/>
|
|
688
720
|
<input
|
|
689
721
|
v-model="form.user_name"
|
|
690
722
|
class="login-input"
|
|
691
723
|
type="text"
|
|
692
724
|
confirm-type="next"
|
|
693
|
-
placeholder="
|
|
725
|
+
:placeholder="t('core.login.userNameMobile')"
|
|
694
726
|
@confirm="onSubmit"
|
|
695
727
|
/>
|
|
696
728
|
<input
|
|
@@ -699,7 +731,7 @@ onLoad(() => {
|
|
|
699
731
|
type="text"
|
|
700
732
|
password
|
|
701
733
|
confirm-type="done"
|
|
702
|
-
placeholder="
|
|
734
|
+
:placeholder="t('core.login.password')"
|
|
703
735
|
@confirm="onSubmit"
|
|
704
736
|
/>
|
|
705
737
|
<view v-if="!isBehaviorCaptcha" class="captcha-row">
|
|
@@ -708,7 +740,7 @@ onLoad(() => {
|
|
|
708
740
|
class="login-input captcha-input"
|
|
709
741
|
type="text"
|
|
710
742
|
confirm-type="done"
|
|
711
|
-
placeholder="
|
|
743
|
+
:placeholder="t('core.login.captcha')"
|
|
712
744
|
@confirm="onSubmit"
|
|
713
745
|
/>
|
|
714
746
|
<view class="captcha-divider"></view>
|
|
@@ -722,10 +754,14 @@ onLoad(() => {
|
|
|
722
754
|
/>
|
|
723
755
|
</view>
|
|
724
756
|
</view>
|
|
725
|
-
<button @tap="onSubmit" class="login-button login-button-primary"
|
|
757
|
+
<button @tap="onSubmit" class="login-button login-button-primary">
|
|
758
|
+
{{ t('common.action.login') }}
|
|
759
|
+
</button>
|
|
726
760
|
<view v-if="behaviorDialogVisible" class="login-behavior-mask">
|
|
727
761
|
<view class="login-behavior-panel">
|
|
728
|
-
<view v-if="behaviorLoading" class="login-behavior-loading"
|
|
762
|
+
<view v-if="behaviorLoading" class="login-behavior-loading">{{
|
|
763
|
+
t('core.login.behaviorLoading')
|
|
764
|
+
}}</view>
|
|
729
765
|
<GoCaptchaUni
|
|
730
766
|
:type="currentCaptchaType"
|
|
731
767
|
:data="behaviorCaptchaData"
|
|
@@ -748,36 +784,36 @@ onLoad(() => {
|
|
|
748
784
|
@tap="wxLogin"
|
|
749
785
|
>
|
|
750
786
|
<text class="icon icon-phone"></text>
|
|
751
|
-
|
|
787
|
+
{{ t('core.login.wechat') }}
|
|
752
788
|
</button>
|
|
753
789
|
<view v-else class="login-form">
|
|
754
|
-
<view class="login-bind-tip"
|
|
790
|
+
<view class="login-bind-tip">{{ t('core.login.wechatBindTip') }}</view>
|
|
755
791
|
<input
|
|
756
792
|
v-if="showTenantCode"
|
|
757
793
|
v-model="miniForm.tenant_code"
|
|
758
794
|
class="login-input"
|
|
759
795
|
type="text"
|
|
760
|
-
placeholder="
|
|
796
|
+
:placeholder="t('core.login.tenant')"
|
|
761
797
|
/>
|
|
762
798
|
<input
|
|
763
799
|
v-model="miniForm.user_name"
|
|
764
800
|
class="login-input"
|
|
765
801
|
type="text"
|
|
766
|
-
placeholder="
|
|
802
|
+
:placeholder="t('core.login.userNameMobile')"
|
|
767
803
|
/>
|
|
768
804
|
<input
|
|
769
805
|
v-model="miniPasswordValue"
|
|
770
806
|
class="login-input"
|
|
771
807
|
type="text"
|
|
772
808
|
password
|
|
773
|
-
placeholder="
|
|
809
|
+
:placeholder="t('core.login.password')"
|
|
774
810
|
/>
|
|
775
811
|
<view class="captcha-row">
|
|
776
812
|
<input
|
|
777
813
|
v-model="miniForm.captcha_code"
|
|
778
814
|
class="login-input captcha-input"
|
|
779
815
|
type="text"
|
|
780
|
-
placeholder="
|
|
816
|
+
:placeholder="t('core.login.captcha')"
|
|
781
817
|
/>
|
|
782
818
|
<view class="captcha-divider"></view>
|
|
783
819
|
<view class="captcha-trigger" @tap="refreshMiniCaptcha">
|
|
@@ -789,7 +825,7 @@ onLoad(() => {
|
|
|
789
825
|
:loading="loading"
|
|
790
826
|
@tap="bindMiniAccount"
|
|
791
827
|
>
|
|
792
|
-
|
|
828
|
+
{{ t('core.login.bindWechat') }}
|
|
793
829
|
</button>
|
|
794
830
|
</view>
|
|
795
831
|
<!-- #endif -->
|
|
@@ -797,10 +833,14 @@ onLoad(() => {
|
|
|
797
833
|
<view class="login-tips" :class="{ animate__shakeY: isAgreePrivacyShakeY }">
|
|
798
834
|
<view class="login-agreement" @tap="toggleAgreePrivacy">
|
|
799
835
|
<view class="login-agree-icon" :class="{ checked: isAgreePrivacy }"></view>
|
|
800
|
-
<text class="login-agree-desc"
|
|
801
|
-
<text class="login-agree-link" @tap.stop="onOpenServiceProtocol"
|
|
802
|
-
|
|
803
|
-
|
|
836
|
+
<text class="login-agree-desc">{{ t('core.login.agreePrefix') }}</text>
|
|
837
|
+
<text class="login-agree-link" @tap.stop="onOpenServiceProtocol">{{
|
|
838
|
+
t('core.login.service')
|
|
839
|
+
}}</text>
|
|
840
|
+
<text class="login-agree-separator">{{ t('core.login.agreeSeparator') }}</text>
|
|
841
|
+
<text class="login-agree-link" @tap.stop="onOpenPrivacyContract">{{
|
|
842
|
+
t('core.login.privacy')
|
|
843
|
+
}}</text>
|
|
804
844
|
</view>
|
|
805
845
|
</view>
|
|
806
846
|
</view>
|
|
@@ -809,6 +849,7 @@ onLoad(() => {
|
|
|
809
849
|
|
|
810
850
|
<style lang="scss">
|
|
811
851
|
.login-page {
|
|
852
|
+
position: relative;
|
|
812
853
|
display: flex;
|
|
813
854
|
flex-direction: column;
|
|
814
855
|
justify-content: center;
|
|
@@ -818,6 +859,19 @@ onLoad(() => {
|
|
|
818
859
|
box-sizing: border-box;
|
|
819
860
|
}
|
|
820
861
|
|
|
862
|
+
.login-locale {
|
|
863
|
+
position: absolute;
|
|
864
|
+
top: calc(24rpx + env(safe-area-inset-top));
|
|
865
|
+
right: 28rpx;
|
|
866
|
+
z-index: 2;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.login-locale__value {
|
|
870
|
+
padding: 12rpx 16rpx;
|
|
871
|
+
color: #16806d;
|
|
872
|
+
font-size: 24rpx;
|
|
873
|
+
}
|
|
874
|
+
|
|
821
875
|
.login-hero {
|
|
822
876
|
margin-bottom: 40rpx;
|
|
823
877
|
text-align: center;
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { onLoad } from '@dcloudio/uni-app'
|
|
3
3
|
import { useSettingStore } from '../../../stores'
|
|
4
4
|
import { ref } from 'vue'
|
|
5
|
+
import { useI18n } from '../../../locales'
|
|
5
6
|
|
|
6
7
|
const settingStore = useSettingStore()
|
|
8
|
+
const { t } = useI18n()
|
|
7
9
|
const content = ref('')
|
|
8
10
|
|
|
9
11
|
const protocolClassMap: Record<string, string> = {
|
|
@@ -46,14 +48,14 @@ const decorateProtocolContent = (html: string) => {
|
|
|
46
48
|
// 加载协议内容
|
|
47
49
|
const loadProtocol = async (type?: string) => {
|
|
48
50
|
const isPrivacy = type === 'privacy'
|
|
49
|
-
const title = isPrivacy ? '
|
|
51
|
+
const title = isPrivacy ? t('core.protocol.privacy') : t('core.protocol.service')
|
|
50
52
|
const key = isPrivacy ? 'privacyProtocol' : 'serviceProtocol'
|
|
51
53
|
|
|
52
54
|
try {
|
|
53
55
|
await settingStore.loadData()
|
|
54
56
|
const protocol = settingStore.getData(key)
|
|
55
57
|
if (!protocol) {
|
|
56
|
-
throw new Error(
|
|
58
|
+
throw new Error(t('core.protocol.notConfigured', { title }))
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
await uni.setNavigationBarTitle({ title })
|
|
@@ -61,7 +63,7 @@ const loadProtocol = async (type?: string) => {
|
|
|
61
63
|
} catch (error) {
|
|
62
64
|
await uni.showToast({
|
|
63
65
|
icon: 'none',
|
|
64
|
-
title: error instanceof Error ? error.message : '
|
|
66
|
+
title: error instanceof Error ? error.message : t('core.protocol.loadFailed'),
|
|
65
67
|
})
|
|
66
68
|
setTimeout(() => {
|
|
67
69
|
uni.navigateBack()
|