@liujitcn/kratos-uni-app-core 0.0.20 → 0.0.23

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.
@@ -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.native_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.main_title'),
35
+ )
36
+ const configuredSubTitle = computed(
37
+ () => settingStore.getData('subTitle') || t('core.login.default_sub_title'),
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.captcha_write_failed'))),
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.captcha_load_failed') })
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.wechat_failed') })
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.wechat_failed') })
171
192
  return
172
193
  }
173
194
  miniBinding.value = true
@@ -182,20 +203,23 @@ 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.user_name_password') })
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
197
218
  try {
198
- const password = await encryptPassword(miniPasswordValue.value, PASSWORD_CRYPTO_SCENE.LOGIN)
219
+ const password = await encryptPassword(
220
+ miniPasswordValue.value,
221
+ PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_LOGIN,
222
+ )
199
223
  const code = (await wx.login()).code
200
224
  await userStore.bindOauthSession({
201
225
  provider: wechatMiniProvider,
@@ -317,7 +341,7 @@ const behaviorCaptchaConfig = computed(() => {
317
341
  verticalPadding: 0,
318
342
  horizontalPadding: 0,
319
343
  iconSize: 20,
320
- title: '拖动滑块,将内圈图片转正',
344
+ title: t('core.login.behavior_rotate'),
321
345
  }
322
346
  }
323
347
  return {
@@ -328,10 +352,13 @@ const behaviorCaptchaConfig = computed(() => {
328
352
  showTheme: false,
329
353
  verticalPadding: 0,
330
354
  horizontalPadding: 0,
331
- buttonText: '确认',
355
+ buttonText: t('common.action.confirm'),
332
356
  iconSize: 20,
333
357
  dotSize: 20,
334
- title: currentCaptchaType.value === 'click' ? '请按顺序点击文字' : '请拖动滑块完成拼图',
358
+ title:
359
+ currentCaptchaType.value === 'click'
360
+ ? t('core.login.behavior_click')
361
+ : t('core.login.behavior_puzzle'),
335
362
  }
336
363
  })
337
364
  const behaviorCaptchaTheme = {
@@ -353,11 +380,11 @@ const behaviorCaptchaTheme = {
353
380
  const getCaptcha = async () => {
354
381
  const requestedCaptchaType = configuredCaptchaType.value
355
382
  if (!requestedCaptchaType) {
356
- throw new Error('登录验证码类型未配置')
383
+ throw new Error(t('core.login.captcha_type_missing'))
357
384
  }
358
385
  const data = await defLoginService.Captcha({ type: requestedCaptchaType })
359
386
  if (!data.type) {
360
- throw new Error('验证码接口未返回类型')
387
+ throw new Error(t('core.login.captcha_type_response_missing'))
361
388
  }
362
389
  currentCaptchaType.value = data.type
363
390
  if (!isBehaviorCaptcha.value) {
@@ -381,7 +408,7 @@ const refreshCaptcha = async () => {
381
408
  behaviorDialogVisible.value = false
382
409
  await uni.showToast({
383
410
  icon: 'none',
384
- title: error instanceof Error ? error.message : '验证码加载失败',
411
+ title: error instanceof Error ? error.message : t('core.login.captcha_load_failed'),
385
412
  })
386
413
  return false
387
414
  }
@@ -442,28 +469,28 @@ const validateLoginForm = async () => {
442
469
  if (showTenantCode.value && !form.value.tenant_code) {
443
470
  await uni.showToast({
444
471
  icon: 'none',
445
- title: '请输入租户编号',
472
+ title: t('core.login.tenant'),
446
473
  })
447
474
  return false
448
475
  }
449
476
  if (!form.value.user_name) {
450
477
  await uni.showToast({
451
478
  icon: 'none',
452
- title: '请输入用户名或手机号',
479
+ title: t('core.login.user_name'),
453
480
  })
454
481
  return false
455
482
  }
456
483
  if (!passwordValue.value) {
457
484
  await uni.showToast({
458
485
  icon: 'none',
459
- title: '请输入密码',
486
+ title: t('core.login.password'),
460
487
  })
461
488
  return false
462
489
  }
463
490
  if (!isBehaviorCaptcha.value && !form.value.captcha_code) {
464
491
  await uni.showToast({
465
492
  icon: 'none',
466
- title: '请输入验证码',
493
+ title: t('core.login.captcha'),
467
494
  })
468
495
  return false
469
496
  }
@@ -479,7 +506,10 @@ const verifyCaptchaToken = async (captchaCode: string) => {
479
506
  }
480
507
  // 执行真正的账号登录流程。
481
508
  const submitLogin = async (captchaCode: string) => {
482
- const password = await encryptPassword(passwordValue.value, PASSWORD_CRYPTO_SCENE.LOGIN)
509
+ const password = await encryptPassword(
510
+ passwordValue.value,
511
+ PASSWORD_CRYPTO_SCENE.PASSWORD_CRYPTO_SCENE_LOGIN,
512
+ )
483
513
  return userStore.login({
484
514
  ...form.value,
485
515
  tenant_code: showTenantCode.value ? form.value.tenant_code : '0000',
@@ -567,7 +597,7 @@ const onSubmit = async () => {
567
597
  const loginSuccess = async () => {
568
598
  await userStore.getUserProfile()
569
599
  // 成功提示
570
- await uni.showToast({ icon: 'success', title: '登录成功' })
600
+ await uni.showToast({ icon: 'success', title: t('core.login.login_success') })
571
601
  setTimeout(() => {
572
602
  const lastRoute = uni.getStorageSync('lastRoute') || homeTabPage
573
603
  if (lastRoute.startsWith(homeTabPage)) {
@@ -586,7 +616,7 @@ const checkedAgreePrivacy = async () => {
586
616
  }
587
617
 
588
618
  if (!settingStore.getData('serviceProtocol') || !settingStore.getData('privacyProtocol')) {
589
- await uni.showToast({ icon: 'none', title: '服务条款和隐私协议未配置' })
619
+ await uni.showToast({ icon: 'none', title: t('core.login.protocol_missing') })
590
620
  return false
591
621
  }
592
622
 
@@ -598,10 +628,10 @@ const checkedAgreePrivacy = async () => {
598
628
 
599
629
  return new Promise<boolean>((resolve) => {
600
630
  uni.showModal({
601
- title: '提示',
602
- content: '请先阅读并勾选协议内容,点击确定后将自动勾选并继续登录',
603
- confirmText: '确定',
604
- cancelText: '取消',
631
+ title: t('common.title.notice'),
632
+ content: t('core.login.protocol_prompt'),
633
+ confirmText: t('common.action.confirm'),
634
+ cancelText: t('common.action.cancel'),
605
635
  success: ({ confirm }) => {
606
636
  if (confirm) {
607
637
  isAgreePrivacy.value = true
@@ -630,7 +660,7 @@ const loadLoginSettings = () => {
630
660
  // #ifdef H5
631
661
  const captchaType = configuredCaptchaType.value
632
662
  if (!captchaType) {
633
- throw new Error('登录验证码类型未配置')
663
+ throw new Error(t('core.login.captcha_type_missing'))
634
664
  }
635
665
  currentCaptchaType.value = captchaType
636
666
  await loadPageCaptcha()
@@ -643,7 +673,7 @@ const loadLoginSettings = () => {
643
673
  } catch (error) {
644
674
  await uni.showToast({
645
675
  icon: 'none',
646
- title: error instanceof Error ? error.message : '移动端配置加载失败',
676
+ title: error instanceof Error ? error.message : t('core.protocol.load_failed'),
647
677
  })
648
678
  loginSettingsPromise = undefined
649
679
  return false
@@ -664,6 +694,14 @@ onLoad(() => {
664
694
 
665
695
  <template>
666
696
  <view class="login-page">
697
+ <picker
698
+ class="login-locale"
699
+ :range="localeLabels"
700
+ :value="localeIndex"
701
+ @change="onLocaleChange"
702
+ >
703
+ <view class="login-locale__value">{{ localeLabels[localeIndex] }}</view>
704
+ </picker>
667
705
  <view class="login-hero">
668
706
  <view class="login-logo-shell">
669
707
  <image :src="configuredAppLogo" />
@@ -683,14 +721,14 @@ onLoad(() => {
683
721
  class="login-input"
684
722
  type="text"
685
723
  confirm-type="next"
686
- placeholder="请输入租户编号"
724
+ :placeholder="t('core.login.tenant')"
687
725
  />
688
726
  <input
689
727
  v-model="form.user_name"
690
728
  class="login-input"
691
729
  type="text"
692
730
  confirm-type="next"
693
- placeholder="请输入用户名/手机号码"
731
+ :placeholder="t('core.login.user_name_mobile')"
694
732
  @confirm="onSubmit"
695
733
  />
696
734
  <input
@@ -699,7 +737,7 @@ onLoad(() => {
699
737
  type="text"
700
738
  password
701
739
  confirm-type="done"
702
- placeholder="请输入密码"
740
+ :placeholder="t('core.login.password')"
703
741
  @confirm="onSubmit"
704
742
  />
705
743
  <view v-if="!isBehaviorCaptcha" class="captcha-row">
@@ -708,7 +746,7 @@ onLoad(() => {
708
746
  class="login-input captcha-input"
709
747
  type="text"
710
748
  confirm-type="done"
711
- placeholder="请输入验证码"
749
+ :placeholder="t('core.login.captcha')"
712
750
  @confirm="onSubmit"
713
751
  />
714
752
  <view class="captcha-divider"></view>
@@ -722,10 +760,14 @@ onLoad(() => {
722
760
  />
723
761
  </view>
724
762
  </view>
725
- <button @tap="onSubmit" class="login-button login-button-primary">登录</button>
763
+ <button @tap="onSubmit" class="login-button login-button-primary">
764
+ {{ t('common.action.login') }}
765
+ </button>
726
766
  <view v-if="behaviorDialogVisible" class="login-behavior-mask">
727
767
  <view class="login-behavior-panel">
728
- <view v-if="behaviorLoading" class="login-behavior-loading">加载中...</view>
768
+ <view v-if="behaviorLoading" class="login-behavior-loading">{{
769
+ t('core.login.behavior_loading')
770
+ }}</view>
729
771
  <GoCaptchaUni
730
772
  :type="currentCaptchaType"
731
773
  :data="behaviorCaptchaData"
@@ -748,36 +790,36 @@ onLoad(() => {
748
790
  @tap="wxLogin"
749
791
  >
750
792
  <text class="icon icon-phone"></text>
751
- 微信一键登录
793
+ {{ t('core.login.wechat') }}
752
794
  </button>
753
795
  <view v-else class="login-form">
754
- <view class="login-bind-tip">该微信尚未绑定,请登录已有账号完成绑定</view>
796
+ <view class="login-bind-tip">{{ t('core.login.wechat_bind_tip') }}</view>
755
797
  <input
756
798
  v-if="showTenantCode"
757
799
  v-model="miniForm.tenant_code"
758
800
  class="login-input"
759
801
  type="text"
760
- placeholder="请输入租户编号"
802
+ :placeholder="t('core.login.tenant')"
761
803
  />
762
804
  <input
763
805
  v-model="miniForm.user_name"
764
806
  class="login-input"
765
807
  type="text"
766
- placeholder="请输入用户名/手机号码"
808
+ :placeholder="t('core.login.user_name_mobile')"
767
809
  />
768
810
  <input
769
811
  v-model="miniPasswordValue"
770
812
  class="login-input"
771
813
  type="text"
772
814
  password
773
- placeholder="请输入密码"
815
+ :placeholder="t('core.login.password')"
774
816
  />
775
817
  <view class="captcha-row">
776
818
  <input
777
819
  v-model="miniForm.captcha_code"
778
820
  class="login-input captcha-input"
779
821
  type="text"
780
- placeholder="请输入验证码"
822
+ :placeholder="t('core.login.captcha')"
781
823
  />
782
824
  <view class="captcha-divider"></view>
783
825
  <view class="captcha-trigger" @tap="refreshMiniCaptcha">
@@ -789,7 +831,7 @@ onLoad(() => {
789
831
  :loading="loading"
790
832
  @tap="bindMiniAccount"
791
833
  >
792
- 登录并绑定微信
834
+ {{ t('core.login.bind_wechat') }}
793
835
  </button>
794
836
  </view>
795
837
  <!-- #endif -->
@@ -797,10 +839,14 @@ onLoad(() => {
797
839
  <view class="login-tips" :class="{ animate__shakeY: isAgreePrivacyShakeY }">
798
840
  <view class="login-agreement" @tap="toggleAgreePrivacy">
799
841
  <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>
842
+ <text class="login-agree-desc">{{ t('core.login.agree_prefix') }}</text>
843
+ <text class="login-agree-link" @tap.stop="onOpenServiceProtocol">{{
844
+ t('core.login.service')
845
+ }}</text>
846
+ <text class="login-agree-separator">{{ t('core.login.agree_separator') }}</text>
847
+ <text class="login-agree-link" @tap.stop="onOpenPrivacyContract">{{
848
+ t('core.login.privacy')
849
+ }}</text>
804
850
  </view>
805
851
  </view>
806
852
  </view>
@@ -809,6 +855,7 @@ onLoad(() => {
809
855
 
810
856
  <style lang="scss">
811
857
  .login-page {
858
+ position: relative;
812
859
  display: flex;
813
860
  flex-direction: column;
814
861
  justify-content: center;
@@ -818,6 +865,19 @@ onLoad(() => {
818
865
  box-sizing: border-box;
819
866
  }
820
867
 
868
+ .login-locale {
869
+ position: absolute;
870
+ top: calc(24rpx + env(safe-area-inset-top));
871
+ right: 28rpx;
872
+ z-index: 2;
873
+ }
874
+
875
+ .login-locale__value {
876
+ padding: 12rpx 16rpx;
877
+ color: #16806d;
878
+ font-size: 24rpx;
879
+ }
880
+
821
881
  .login-hero {
822
882
  margin-bottom: 40rpx;
823
883
  text-align: center;
@@ -1095,16 +1155,22 @@ onLoad(() => {
1095
1155
  .login-agreement {
1096
1156
  display: flex;
1097
1157
  align-items: center;
1158
+ flex-wrap: wrap;
1098
1159
  justify-content: center;
1160
+ width: 100%;
1161
+ max-width: 100%;
1162
+ min-width: 0;
1099
1163
  gap: 6rpx;
1100
- white-space: nowrap;
1164
+ box-sizing: border-box;
1101
1165
  }
1102
1166
 
1103
1167
  .login-agree-separator {
1104
- white-space: nowrap;
1168
+ min-width: 0;
1169
+ overflow-wrap: anywhere;
1105
1170
  }
1106
1171
 
1107
1172
  .login-agree-icon {
1173
+ flex: 0 0 auto;
1108
1174
  width: 26rpx;
1109
1175
  height: 26rpx;
1110
1176
  margin-right: 2rpx;
@@ -1121,13 +1187,15 @@ onLoad(() => {
1121
1187
  }
1122
1188
 
1123
1189
  .login-agree-link {
1124
- white-space: nowrap;
1190
+ min-width: 0;
1191
+ overflow-wrap: anywhere;
1125
1192
  color: #28bb9c;
1126
1193
  }
1127
1194
 
1128
1195
  .login-agree-desc {
1196
+ min-width: 0;
1197
+ overflow-wrap: anywhere;
1129
1198
  color: #9ca3af;
1130
- white-space: nowrap;
1131
1199
  }
1132
1200
  }
1133
1201
  </style>
@@ -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.not_configured', { 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.load_failed'),
65
67
  })
66
68
  setTimeout(() => {
67
69
  uni.navigateBack()
@@ -1,36 +1,34 @@
1
1
  <script setup lang="ts">
2
2
  import { onLoad } from '@dcloudio/uni-app'
3
- import { ref } from 'vue'
3
+ import { computed, ref } from 'vue'
4
4
  import BootstrapStatus from '../../../components/BootstrapStatus.vue'
5
5
  import { initializeAppNavigation, launchAppStatus, navigateAppRoute } from '../../../navigation'
6
6
  import type { BootstrapViewKey } from '../../../module'
7
+ import { useI18n } from '../../../locales'
7
8
 
8
9
  const state = ref<BootstrapViewKey>('BOOTSTRAP_LOADING')
9
10
  const detail = ref('')
10
- const stateTitles: Record<BootstrapViewKey, string> = {
11
- BOOTSTRAP_LOADING: '正在加载',
12
- FORBIDDEN: '暂无访问权限',
13
- NOT_FOUND: '页面不存在',
14
- OFFLINE: '网络不可用',
15
- CONFIG_ERROR: '导航配置无效',
16
- PAGE_UNAVAILABLE: '页面暂不可用',
17
- }
11
+ const { t } = useI18n()
12
+ const stateTitle = computed(() =>
13
+ state.value === 'BOOTSTRAP_LOADING' ? t('core.status.loading') : t(`core.status.${state.value}`),
14
+ )
18
15
 
19
- onLoad(async (options) => {
16
+ onLoad((options) => {
20
17
  state.value = (options?.state as BootstrapViewKey | undefined) ?? 'BOOTSTRAP_LOADING'
21
18
  detail.value = options?.detail ? decodeURIComponent(options.detail) : ''
22
19
  if (options?.bootstrap !== '1') return
23
- try {
24
- await initializeAppNavigation()
25
- navigateAppRoute(options?.route ? decodeURIComponent(options.route) : 'app/home', {
26
- replace: true,
20
+ void initializeAppNavigation()
21
+ .then(() => {
22
+ navigateAppRoute(options?.route ? decodeURIComponent(options.route) : 'app/home', {
23
+ replace: true,
24
+ })
25
+ })
26
+ .catch((error) => {
27
+ launchAppStatus('CONFIG_ERROR', error instanceof Error ? error.message : String(error))
27
28
  })
28
- } catch (error) {
29
- launchAppStatus('CONFIG_ERROR', error instanceof Error ? error.message : String(error))
30
- }
31
29
  })
32
30
  </script>
33
31
 
34
32
  <template>
35
- <BootstrapStatus :title="stateTitles[state]" :detail="detail" />
33
+ <BootstrapStatus :title="stateTitle" :detail="detail" />
36
34
  </template>
@@ -17,14 +17,14 @@
17
17
  <!-- #endif -->
18
18
 
19
19
  <view v-if="showFallback" class="webview-empty">
20
- <view class="webview-empty__title">链接无法打开</view>
20
+ <view class="webview-empty__title">{{ t('core.webview.open_failed') }}</view>
21
21
  <view class="webview-empty__desc">{{ emptyDesc }}</view>
22
22
  </view>
23
23
 
24
24
  <!-- #ifdef H5 -->
25
25
  <!-- X-Frame-Options 拒绝嵌入时浏览器仍可能触发 iframe load,始终保留外部打开入口。 -->
26
26
  <button v-if="isH5 && url && isIframeTimedOut" class="webview-open-button" @tap="openInBrowser">
27
- 新窗口打开
27
+ {{ t('common.action.open_in_new_window') }}
28
28
  </button>
29
29
  <!-- #endif -->
30
30
  </view>
@@ -33,20 +33,22 @@
33
33
  <script setup lang="ts">
34
34
  import { computed, ref } from 'vue'
35
35
  import { onLoad } from '@dcloudio/uni-app'
36
+ import { useI18n } from '../../../locales'
36
37
 
37
38
  const url = ref('')
38
39
  const isH5 = ref(false)
39
40
  const isIframeLoaded = ref(false)
40
41
  const isIframeTimedOut = ref(false)
42
+ const { t } = useI18n()
41
43
 
42
44
  const showFallback = computed(
43
45
  () => !url.value || (isH5.value && isIframeTimedOut.value && !isIframeLoaded.value),
44
46
  )
45
47
  const emptyDesc = computed(() => {
46
48
  if (!url.value) {
47
- return '缺少有效链接地址'
49
+ return t('core.webview.invalid_url')
48
50
  }
49
- return '当前 H5 页面可能被目标站点限制嵌入'
51
+ return t('core.webview.embed_blocked')
50
52
  })
51
53
 
52
54
  onLoad((query) => {
package/src/vite.ts CHANGED
@@ -582,6 +582,7 @@ function scanViews(root: string): Array<{ route: string; source: string }> {
582
582
  function appendPage(manifest: PagesManifest, page: ScannedPage): void {
583
583
  const [first, ...rest] = page.route.split('/')
584
584
  if (!first.startsWith('pages') || first === 'pages') {
585
+ if (manifest.pages.some((item) => normalizeRoute(item.path) === page.route)) return
585
586
  manifest.pages.push({ path: page.route, style: page.style })
586
587
  return
587
588
  }
@@ -591,22 +592,22 @@ function appendPage(manifest: PagesManifest, page: ScannedPage): void {
591
592
  subPackage = { root: first, pages: [] }
592
593
  manifest.subPackages.push(subPackage)
593
594
  }
595
+ if (subPackage.pages.some((item) => normalizeRoute(`${first}/${item.path}`) === page.route))
596
+ return
594
597
  subPackage.pages.push({ path: rest.join('/'), style: page.style })
595
598
  }
596
599
 
597
600
  function createPageWrapper(page: ScannedPage): string {
598
601
  const source = page.source.replace(/\\/g, '/')
599
602
  return `<script setup lang="ts">
600
- import type { Component } from 'vue'
601
603
  import KratosPage from ${JSON.stringify(source)}
602
604
  import KratosTabBar from '@liujitcn/kratos-uni-app-core/components/KratosTabBar.vue'
603
605
 
604
606
  defineOptions({ inheritAttrs: false })
605
- const Page = KratosPage as Component
606
607
  </script>
607
608
 
608
609
  <template>
609
- <Page v-bind="$attrs" />
610
+ <KratosPage v-bind="$attrs" />
610
611
  <KratosTabBar route=${JSON.stringify(page.route)} />
611
612
  </template>
612
613
  `