@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.
@@ -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('当前环境不支持 Base64 解码')
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('当前环境不支持 Base64 编码')
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 mainTitle = computed(() => settingStore.getData('mainTitle') || '应用框架示例')
9
- const subTitle = computed(() => settingStore.getData('subTitle') || '保留通用导航与个人中心体验')
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">当前技术栈</text>
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">应用框架</text>
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">开发语言</text>
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">状态管理</text>
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">样式方案</text>
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">演示范围</text>
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">跨端页面</text>
56
- <text class="demo-desc">同一套页面代码适配 H5、微信小程序和 App</text>
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">账户能力</text>
63
- <text class="demo-desc">个人资料、应用设置与智能助手入口已保留</text>
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">静态首页</text>
70
- <text class="demo-desc">当前首页用于展示框架信息,不依赖业务接口</text>
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(() => settingStore.getData('mainTitle') || '应用框架示例')
24
- const configuredSubTitle = computed(() => settingStore.getData('subTitle') || '欢迎使用本应用')
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
- const message = response.data?.message ?? response.data?.error?.message
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: currentCaptchaType.value === 'click' ? '请按顺序点击文字' : '请拖动滑块完成拼图',
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">登录</button>
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">加载中...</view>
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">该微信尚未绑定,请登录已有账号完成绑定</view>
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">我已阅读并同意</text>
801
- <text class="login-agree-link" @tap.stop="onOpenServiceProtocol">《服务条款》</text>
802
- <text class="login-agree-separator">和</text>
803
- <text class="login-agree-link" @tap.stop="onOpenPrivacyContract">《隐私协议》</text>
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(`${title}未配置,暂时无法查看`)
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()