@liujitcn/kratos-taro-app-core 0.0.78 → 0.0.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/build.mjs +4 -2
  2. package/package.json +1 -1
  3. package/src/components/BootstrapStatus.scss +3 -3
  4. package/src/components/BootstrapStatus.tsx +3 -1
  5. package/src/components/KratosTabBar.scss +32 -13
  6. package/src/components/KratosTabBar.tsx +34 -3
  7. package/src/components/MfaRecoveryCodesDialog.scss +11 -10
  8. package/src/components/MfaSetupPanel.scss +4 -4
  9. package/src/components/PasswordVerifyDialog.scss +21 -21
  10. package/src/navigation.ts +6 -3
  11. package/src/pages.ts +4 -2
  12. package/src/rpc/base/v1/config.ts +1 -1
  13. package/src/rpc/base/v1/file.ts +1 -1
  14. package/src/rpc/base/v1/language.ts +1 -1
  15. package/src/rpc/base/v1/login.ts +1 -1
  16. package/src/rpc/base/v1/mcp.ts +1 -1
  17. package/src/rpc/base/v1/mfa.ts +1 -1
  18. package/src/rpc/base/v1/oauth.ts +1 -1
  19. package/src/rpc/base/v1/secret_crypto.ts +1 -1
  20. package/src/rpc/base/v1/sse.ts +1 -1
  21. package/src/rpc/common/v1/common.ts +1 -1
  22. package/src/rpc/common/v1/types.ts +1 -1
  23. package/src/rpc/google/protobuf/empty.ts +1 -1
  24. package/src/rpc/google/protobuf/struct.ts +2 -2
  25. package/src/rpc/google/protobuf/wrappers.ts +1 -1
  26. package/src/rpc/system/app/v1/auth.ts +1 -1
  27. package/src/rpc/system/app/v1/base_area.ts +1 -1
  28. package/src/rpc/system/app/v1/base_dict.ts +1 -1
  29. package/src/rpc/system/app/v1/base_menu.ts +1 -1
  30. package/src/rpc/system/common/v1/common.ts +1 -1
  31. package/src/stores/index.ts +1 -0
  32. package/src/stores/theme.ts +108 -0
  33. package/src/styles/base.scss +90 -5
  34. package/src/styles/components.scss +765 -0
  35. package/src/utils/http.ts +3 -0
  36. package/src/utils/index.ts +19 -0
  37. package/src/utils/navigation.ts +12 -0
  38. package/src/views/pages/index/index.scss +69 -73
  39. package/src/views/pages/index/index.tsx +50 -34
  40. package/src/views/pages/login/login.scss +109 -142
  41. package/src/views/pages/login/login.tsx +142 -82
  42. package/src/views/pages/login/protocal.scss +14 -14
  43. package/src/views/pages/login/protocal.tsx +16 -3
  44. package/src/views/pages/webview/webview.scss +6 -5
  45. package/src/views/pages/webview/webview.tsx +3 -1
@@ -6,9 +6,9 @@ import { defLoginService } from '../../../api/base/v1/login'
6
6
  import { defMfaService } from '../../../api/base/v1/mfa'
7
7
  import { navigateAppRoute } from '../../../navigation'
8
8
  import { LoginStatus, type LoginRequest, type LoginResponse } from '../../../rpc/base/v1/login'
9
- import { useSettingStore, useUserStore } from '../../../stores'
9
+ import { useSettingStore, useThemeStore, useUserStore } from '../../../stores'
10
10
  import { formatSrc } from '../../../utils'
11
- import { restoreLoginRedirect } from '../../../utils/navigation'
11
+ import { homeTabPage, restoreLoginRedirect } from '../../../utils/navigation'
12
12
  import { encryptPassword } from '../../../utils/secretCrypto'
13
13
  import { createWebAuthnCredential, getWebAuthnAssertion } from '../../../utils/webauthn'
14
14
  import MfaRecoveryCodesDialog from '../../../components/MfaRecoveryCodesDialog'
@@ -21,6 +21,12 @@ import { t as translate, useLocaleStore, useI18n, type SupportedLocale } from '.
21
21
  const behaviorCaptchaTypes = new Set(['slide', 'click', 'rotate'])
22
22
  const wechatMiniProvider = 'wechatmini'
23
23
 
24
+ /** 参考管理端 View、Hide 图标视觉的本端 SVG 路径。 */
25
+ const passwordIconPaths = {
26
+ view: 'M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288m0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.19 160.19 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160',
27
+ hide: 'M876.8 156.8c0-9.6-3.2-16-9.6-22.4s-12.8-9.6-22.4-9.6-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176S0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4m-646.4 528Q115.2 579.2 76.8 512q43.2-72 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4m140.8-96Q352 555.2 352 512c0-44.8 16-83.2 48-112s67.2-48 112-48c28.8 0 54.4 6.4 73.6 19.2zM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6q-43.2 72-153.6 172.8c-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176S1024 528 1024 512s-48.001-73.6-134.401-176M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112s-67.2 48-112 48',
28
+ } as const
29
+
24
30
  type BehaviorPayload = BehaviorCaptchaData & {
25
31
  type?: string
26
32
  width?: number
@@ -76,6 +82,14 @@ export default function LoginPage() {
76
82
  const { locale, setLocale, t } = useI18n()
77
83
  const languageOptions = useLocaleStore((state) => state.languageOptions)
78
84
  const settings = useSettingStore((state) => state.data)
85
+ const themeStore = useThemeStore()
86
+
87
+ /** 返回上一页;登录页可能是冷启动首页,无上级页面时回主页。 */
88
+ const goBack = () => {
89
+ Taro.navigateBack({
90
+ fail: () => void Taro.reLaunch({ url: homeTabPage }),
91
+ })
92
+ }
79
93
  const loadSettings = useSettingStore((state) => state.loadData)
80
94
  const userStore = useUserStore.getState()
81
95
  const [agreed, setAgreed] = useState(false)
@@ -85,6 +99,7 @@ export default function LoginPage() {
85
99
  const settingsPromise = useRef<Promise<boolean>>()
86
100
  const [form, setForm] = useState<LoginRequest>(emptyLoginForm)
87
101
  const [password, setPassword] = useState('')
102
+ const [passwordVisible, setPasswordVisible] = useState(false)
88
103
  const [captchaImage, setCaptchaImage] = useState('')
89
104
  const [captchaType, setCaptchaType] = useState('')
90
105
  const [behaviorVisible, setBehaviorVisible] = useState(false)
@@ -95,6 +110,7 @@ export default function LoginPage() {
95
110
  const [miniCaptchaImage, setMiniCaptchaImage] = useState('')
96
111
  const [miniForm, setMiniForm] = useState(emptyLoginForm)
97
112
  const [miniPassword, setMiniPassword] = useState('')
113
+ const [miniPasswordVisible, setMiniPasswordVisible] = useState(false)
98
114
  const [mfaVisible, setMfaVisible] = useState(false)
99
115
  const [mfaLoading, setMfaLoading] = useState(false)
100
116
  const [mfaChallengeId, setMfaChallengeId] = useState('')
@@ -122,6 +138,13 @@ export default function LoginPage() {
122
138
  const configuredLogoUrl = configuredAppLogo ? formatSrc(configuredAppLogo) : defaultLogo
123
139
  const [appLogo, setAppLogo] = useState(configuredLogoUrl)
124
140
  const showTenantCode = settings?.get('showTenantCode') !== 'false'
141
+ /** 按管理端 View、Hide 视觉生成本端密码可见性图标。 */
142
+ const passwordIconSrc = (visible: boolean) => {
143
+ const color = themeStore.theme === 'light' ? '#5a7083' : '#8aa0b4'
144
+ const path = visible ? passwordIconPaths.hide : passwordIconPaths.view
145
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="${color}"><path d="${path}"/></svg>`
146
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
147
+ }
125
148
  const configuredCaptchaType = settings?.get('captchaType') || ''
126
149
  const isBehaviorCaptcha = behaviorCaptchaTypes.has(captchaType)
127
150
  const behaviorWidth = Math.round(
@@ -169,10 +192,6 @@ export default function LoginPage() {
169
192
  setAppLogo(configuredLogoUrl)
170
193
  }, [configuredLogoUrl])
171
194
 
172
- useEffect(() => {
173
- void Taro.setNavigationBarTitle({ title: t('common.action.login') })
174
- }, [locale, t])
175
-
176
195
  useEffect(() => {
177
196
  return () => {
178
197
  if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current)
@@ -288,6 +307,8 @@ export default function LoginPage() {
288
307
  content: t('core.login.protocol_prompt'),
289
308
  confirmText: t('common.action.confirm'),
290
309
  cancelText: t('common.action.cancel'),
310
+ confirmColor: themeStore.theme === 'light' ? '#039e8c' : '#28f0d4',
311
+ cancelColor: themeStore.theme === 'light' ? '#5a7083' : '#8aa0b4',
291
312
  })
292
313
  if (result.confirm) setAgreed(true)
293
314
  return result.confirm
@@ -558,11 +579,11 @@ export default function LoginPage() {
558
579
 
559
580
  const agreement = (
560
581
  <View className={`login-tips${shake ? ' login-tips--shake' : ''}`}>
561
- <View className='login-agreement' onClick={() => setAgreed((value) => !value)}>
562
- <View className={`login-agree-icon${agreed ? ' checked' : ''}`} />
582
+ <View className='k-lg-agree login-agreement' onClick={() => setAgreed((value) => !value)}>
583
+ <View className={`k-lg-chk${agreed ? '' : ' k-lg-chk--off'}`}>✓</View>
563
584
  <Text className='login-agree-desc'>{t('core.login.agree_prefix')}</Text>
564
585
  <Text
565
- className='login-agree-link'
586
+ className='k-lg-link login-agree-link'
566
587
  onClick={(event) => {
567
588
  event.stopPropagation()
568
589
  navigateAppRoute('app/protocol/service')
@@ -572,7 +593,7 @@ export default function LoginPage() {
572
593
  </Text>
573
594
  <Text className='login-agree-separator'>{t('core.login.agree_separator')}</Text>
574
595
  <Text
575
- className='login-agree-link'
596
+ className='k-lg-link login-agree-link'
576
597
  onClick={(event) => {
577
598
  event.stopPropagation()
578
599
  navigateAppRoute('app/protocol/privacy')
@@ -585,78 +606,100 @@ export default function LoginPage() {
585
606
  )
586
607
 
587
608
  return (
588
- <View className='login-page'>
589
- <Picker
590
- className='login-locale'
591
- mode='selector'
592
- range={languageOptions.map((item) => item.native_name)}
593
- value={Math.max(
594
- 0,
595
- languageOptions.findIndex((item) => item.language_code === locale),
596
- )}
597
- onChange={(event) => {
598
- const nextLocale = languageOptions[Number(event.detail.value)]?.language_code as
599
- | SupportedLocale
600
- | undefined
601
- if (nextLocale) void setLocale(nextLocale)
602
- }}
603
- >
604
- <View className='login-locale__value'>{currentLanguageName}</View>
605
- </Picker>
609
+ <View className={`login-page ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}>
610
+ <View className='login-header'>
611
+ <View className='k-ic login-back' aria-hidden onClick={goBack}>
612
+ ‹
613
+ </View>
614
+ <Picker
615
+ className='login-locale'
616
+ mode='selector'
617
+ range={languageOptions.map((item) => item.native_name)}
618
+ value={Math.max(
619
+ 0,
620
+ languageOptions.findIndex((item) => item.language_code === locale),
621
+ )}
622
+ onChange={(event) => {
623
+ const nextLocale = languageOptions[Number(event.detail.value)]?.language_code as
624
+ | SupportedLocale
625
+ | undefined
626
+ if (nextLocale) void setLocale(nextLocale)
627
+ }}
628
+ >
629
+ <View className='k-lg-lang login-locale__value'>{currentLanguageName}</View>
630
+ </Picker>
631
+ </View>
606
632
  <View className='login-hero'>
607
- <View className='login-logo-shell'>
633
+ <View className='k-lg-logo login-logo-shell'>
608
634
  <Image className='login-logo' src={appLogo} onError={() => setAppLogo(defaultLogo)} />
609
635
  </View>
610
636
  <View className='login-hero-copy'>
611
- <Text className='login-title'>{mainTitle}</Text>
612
- <Text className='login-subtitle'>{subTitle}</Text>
637
+ <Text className='k-lg-title login-title'>{mainTitle}</Text>
638
+ <Text className='k-lg-sub login-subtitle'>{subTitle}</Text>
613
639
  </View>
614
640
  </View>
615
- <View className='login-panel'>
641
+ <View className='k-glass k-glass--lit login-panel'>
642
+ <View className='k-cnr k-cnr--tl' aria-hidden />
643
+ <View className='k-cnr k-cnr--tr' aria-hidden />
644
+ <View className='k-cnr k-cnr--bl' aria-hidden />
645
+ <View className='k-cnr k-cnr--br' aria-hidden />
616
646
  {process.env.TARO_ENV === 'h5' ? (
617
647
  <View className='login-form'>
618
648
  {showTenantCode ? (
649
+ <View className='k-lg-field'>
650
+ <Input
651
+ className='k-lg-field__input'
652
+ placeholder={t('core.login.tenant')}
653
+ value={form.tenant_code}
654
+ onInput={(event) => updateForm('tenant_code', event.detail.value)}
655
+ />
656
+ </View>
657
+ ) : null}
658
+ <View className='k-lg-field'>
619
659
  <Input
620
- className='login-input'
621
- placeholder={t('core.login.tenant')}
622
- value={form.tenant_code}
623
- onInput={(event) => updateForm('tenant_code', event.detail.value)}
660
+ className='k-lg-field__input'
661
+ placeholder={t('core.login.user_name_mobile')}
662
+ value={form.user_name}
663
+ onInput={(event) => updateForm('user_name', event.detail.value)}
624
664
  />
625
- ) : null}
626
- <Input
627
- className='login-input'
628
- placeholder={t('core.login.user_name_mobile')}
629
- value={form.user_name}
630
- onInput={(event) => updateForm('user_name', event.detail.value)}
631
- />
632
- <Input
633
- className='login-input'
634
- password
635
- placeholder={t('core.login.password')}
636
- value={password}
637
- onInput={(event) => setPassword(event.detail.value)}
638
- onConfirm={() => void onSubmit()}
639
- />
665
+ </View>
666
+ <View className='k-lg-field'>
667
+ <Input
668
+ className='k-lg-field__input'
669
+ password={!passwordVisible}
670
+ placeholder={t('core.login.password')}
671
+ value={password}
672
+ onInput={(event) => setPassword(event.detail.value)}
673
+ onConfirm={() => void onSubmit()}
674
+ />
675
+ <View
676
+ className='password-toggle'
677
+ aria-label={passwordVisible ? '隐藏密码' : '显示密码'}
678
+ onClick={() => setPasswordVisible((visible) => !visible)}
679
+ >
680
+ <Image
681
+ className='password-toggle-icon'
682
+ src={passwordIconSrc(passwordVisible)}
683
+ mode='aspectFit'
684
+ />
685
+ </View>
686
+ </View>
640
687
  {!isBehaviorCaptcha ? (
641
- <View className='captcha-row'>
688
+ <View className='k-lg-field'>
642
689
  <Input
643
- className='login-input captcha-input'
690
+ className='k-lg-field__input'
644
691
  placeholder={t('core.login.captcha')}
645
692
  value={form.captcha_code}
646
693
  onInput={(event) => updateForm('captcha_code', event.detail.value)}
647
694
  onConfirm={() => void onSubmit()}
648
695
  />
649
696
  <View className='captcha-divider' />
650
- <View className='captcha-trigger' onClick={() => void refreshCaptcha()}>
697
+ <View className='k-lg-cap captcha-trigger' onClick={() => void refreshCaptcha()}>
651
698
  <Image className='captcha-image' src={captchaImage} mode='aspectFit' />
652
699
  </View>
653
700
  </View>
654
701
  ) : null}
655
- <Button
656
- className='login-button login-button-primary'
657
- loading={loading}
658
- onClick={() => void onSubmit()}
659
- >
702
+ <Button className='k-lg-btn k-lg-btn--pri' loading={loading} onClick={() => void onSubmit()}>
660
703
  {t('common.action.login')}
661
704
  </Button>
662
705
  {behaviorVisible ? (
@@ -681,7 +724,7 @@ export default function LoginPage() {
681
724
  </View>
682
725
  ) : !miniBinding ? (
683
726
  <Button
684
- className='login-button login-button-primary'
727
+ className='k-lg-btn k-lg-btn--wx'
685
728
  loading={loading}
686
729
  onClick={() => void wxLogin()}
687
730
  >
@@ -692,40 +735,57 @@ export default function LoginPage() {
692
735
  <View className='login-form'>
693
736
  <View className='login-bind-tip'>{t('core.login.wechat_bind_tip')}</View>
694
737
  {showTenantCode ? (
738
+ <View className='k-lg-field'>
739
+ <Input
740
+ className='k-lg-field__input'
741
+ placeholder={t('core.login.tenant')}
742
+ value={miniForm.tenant_code}
743
+ onInput={(event) => updateMiniForm('tenant_code', event.detail.value)}
744
+ />
745
+ </View>
746
+ ) : null}
747
+ <View className='k-lg-field'>
695
748
  <Input
696
- className='login-input'
697
- placeholder={t('core.login.tenant')}
698
- value={miniForm.tenant_code}
699
- onInput={(event) => updateMiniForm('tenant_code', event.detail.value)}
749
+ className='k-lg-field__input'
750
+ placeholder={t('core.login.user_name_mobile')}
751
+ value={miniForm.user_name}
752
+ onInput={(event) => updateMiniForm('user_name', event.detail.value)}
700
753
  />
701
- ) : null}
702
- <Input
703
- className='login-input'
704
- placeholder={t('core.login.user_name_mobile')}
705
- value={miniForm.user_name}
706
- onInput={(event) => updateMiniForm('user_name', event.detail.value)}
707
- />
708
- <Input
709
- className='login-input'
710
- password
711
- placeholder={t('core.login.password')}
712
- value={miniPassword}
713
- onInput={(event) => setMiniPassword(event.detail.value)}
714
- />
715
- <View className='captcha-row'>
754
+ </View>
755
+ <View className='k-lg-field'>
756
+ <Input
757
+ className='k-lg-field__input'
758
+ password={!miniPasswordVisible}
759
+ placeholder={t('core.login.password')}
760
+ value={miniPassword}
761
+ onInput={(event) => setMiniPassword(event.detail.value)}
762
+ />
763
+ <View
764
+ className='password-toggle'
765
+ aria-label={miniPasswordVisible ? '隐藏密码' : '显示密码'}
766
+ onClick={() => setMiniPasswordVisible((visible) => !visible)}
767
+ >
768
+ <Image
769
+ className='password-toggle-icon'
770
+ src={passwordIconSrc(miniPasswordVisible)}
771
+ mode='aspectFit'
772
+ />
773
+ </View>
774
+ </View>
775
+ <View className='k-lg-field'>
716
776
  <Input
717
- className='login-input captcha-input'
777
+ className='k-lg-field__input'
718
778
  placeholder={t('core.login.captcha')}
719
779
  value={miniForm.captcha_code}
720
780
  onInput={(event) => updateMiniForm('captcha_code', event.detail.value)}
721
781
  />
722
782
  <View className='captcha-divider' />
723
- <View className='captcha-trigger' onClick={() => void refreshMiniCaptcha()}>
783
+ <View className='k-lg-cap captcha-trigger' onClick={() => void refreshMiniCaptcha()}>
724
784
  <Image className='captcha-image' src={miniCaptchaImage} mode='aspectFit' />
725
785
  </View>
726
786
  </View>
727
787
  <Button
728
- className='login-button login-button-primary'
788
+ className='k-lg-btn k-lg-btn--pri'
729
789
  loading={loading}
730
790
  onClick={() => void bindMiniAccount()}
731
791
  >
@@ -2,28 +2,28 @@
2
2
  min-height: 100vh;
3
3
  padding: 24px;
4
4
  box-sizing: border-box;
5
- background-color: #f5f7f6;
5
+ background: var(--kratos-bg-gradient);
6
6
  }
7
7
 
8
8
  .protocol-card {
9
9
  overflow: hidden;
10
10
  padding: 40px 32px 56px;
11
- border: 1px solid #edf1ef;
11
+ border: 1PX solid var(--kratos-color-border);
12
12
  border-radius: 24px;
13
- background-color: #fff;
14
- box-shadow: 0 8px 24px rgba(33, 54, 48, 0.05);
13
+ background-color: var(--kratos-color-surface);
14
+ box-shadow: var(--kratos-glow-soft);
15
15
  }
16
16
 
17
17
  .protocol-content {
18
18
  display: block;
19
- color: #53635d;
19
+ color: var(--kratos-color-text-muted);
20
20
  font-size: 28px;
21
21
  line-height: 1.85;
22
22
  word-break: break-word;
23
23
 
24
24
  .protocol-title {
25
25
  margin: 0 0 28px;
26
- color: #1f2d29;
26
+ color: var(--kratos-color-text);
27
27
  font-size: 42px;
28
28
  font-weight: 700;
29
29
  line-height: 1.35;
@@ -32,8 +32,8 @@
32
32
  .protocol-section-title {
33
33
  margin: 38px 0 14px;
34
34
  padding-left: 16px;
35
- border-left: 6px solid #27ba9b;
36
- color: #263a34;
35
+ border-left: 6px solid var(--kratos-color-primary);
36
+ color: var(--kratos-color-text);
37
37
  font-size: 31px;
38
38
  font-weight: 600;
39
39
  line-height: 1.45;
@@ -41,7 +41,7 @@
41
41
 
42
42
  .protocol-paragraph {
43
43
  margin: 0 0 22px;
44
- color: #53635d;
44
+ color: var(--kratos-color-text-muted);
45
45
  font-size: 28px;
46
46
  line-height: 1.85;
47
47
  }
@@ -58,12 +58,12 @@
58
58
  }
59
59
 
60
60
  .protocol-emphasis {
61
- color: #263a34;
61
+ color: var(--kratos-color-text);
62
62
  font-weight: 600;
63
63
  }
64
64
 
65
65
  .protocol-link {
66
- color: #16806d;
66
+ color: var(--kratos-color-primary);
67
67
  text-decoration: underline;
68
68
  }
69
69
 
@@ -77,8 +77,8 @@
77
77
  .protocol-quote {
78
78
  margin: 24px 0;
79
79
  padding: 18px 22px;
80
- border-left: 6px solid #b8e8dc;
81
- color: #6c7b75;
82
- background-color: #f3fbf8;
80
+ border-left: 6px solid var(--kratos-color-primary-edge);
81
+ color: var(--kratos-color-text-muted);
82
+ background-color: var(--kratos-color-primary-soft);
83
83
  }
84
84
  }
@@ -1,8 +1,9 @@
1
1
  import { RichText, View } from '@tarojs/components'
2
2
  import Taro, { useLoad } from '@tarojs/taro'
3
3
  import { useState } from 'react'
4
- import { useSettingStore } from '../../../stores'
4
+ import { useSettingStore, useThemeStore } from '../../../stores'
5
5
  import { useI18n } from '../../../locales'
6
+ import { homeTabPage } from '../../../utils/navigation'
6
7
  import './protocal.scss'
7
8
 
8
9
  const protocolClassMap: Record<string, string> = {
@@ -41,11 +42,14 @@ function decorateProtocolContent(html: string): string {
41
42
  /** 服务条款与隐私协议页。 */
42
43
  export default function ProtocolPage() {
43
44
  const { t } = useI18n()
45
+ const themeStore = useThemeStore()
44
46
  const [content, setContent] = useState('')
47
+ const [pageTitle, setPageTitle] = useState(t('core.navigation.protocol'))
45
48
 
46
49
  useLoad((query) => {
47
50
  const isPrivacy = query?.type === 'privacy'
48
51
  const title = isPrivacy ? t('core.protocol.privacy') : t('core.protocol.service')
52
+ setPageTitle(title)
49
53
  const key = isPrivacy ? 'privacyProtocol' : 'serviceProtocol'
50
54
  const settingStore = useSettingStore.getState()
51
55
  void settingStore
@@ -53,7 +57,6 @@ export default function ProtocolPage() {
53
57
  .then(() => {
54
58
  const protocol = settingStore.getData(key)
55
59
  if (!protocol) throw new Error(t('core.protocol.not_configured', { title }))
56
- void Taro.setNavigationBarTitle({ title })
57
60
  setContent(decorateProtocolContent(protocol))
58
61
  })
59
62
  .catch(async (error: unknown) => {
@@ -65,8 +68,18 @@ export default function ProtocolPage() {
65
68
  })
66
69
  })
67
70
 
71
+ /** 返回来源页,页面栈为空时回到首页。 */
72
+ const onBack = () => {
73
+ void Taro.navigateBack({ fail: () => void Taro.reLaunch({ url: homeTabPage }) })
74
+ }
75
+
68
76
  return (
69
- <View className='protocol-page'>
77
+ <View className={`protocol-page ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}>
78
+ <View className={`k-page-nav ${process.env.TARO_ENV === 'weapp' ? 'k-page-nav--weapp' : ''}`}>
79
+ <View className='k-page-nav__back' aria-label={t('common.action.back')} onClick={onBack} />
80
+ <View className='k-page-nav__title'>{pageTitle}</View>
81
+ <View className='k-page-nav__side' aria-hidden />
82
+ </View>
70
83
  <View className='protocol-card'>
71
84
  <RichText className='protocol-content' nodes={content} />
72
85
  </View>
@@ -2,6 +2,7 @@
2
2
  position: relative;
3
3
  flex: 1;
4
4
  height: 100vh;
5
+ background: var(--kratos-bg-gradient);
5
6
  }
6
7
 
7
8
  .webview-empty {
@@ -13,19 +14,19 @@
13
14
  justify-content: center;
14
15
  padding: 48px;
15
16
  text-align: center;
16
- background-color: #fff;
17
+ background: var(--kratos-bg-gradient);
17
18
  }
18
19
 
19
20
  .webview-empty__title {
20
21
  font-size: 32px;
21
22
  font-weight: 600;
22
- color: #333;
23
+ color: var(--kratos-color-text);
23
24
  }
24
25
 
25
26
  .webview-empty__desc {
26
27
  margin-top: 16px;
27
28
  font-size: 26px;
28
- color: #888;
29
+ color: var(--kratos-color-text-muted);
29
30
  }
30
31
 
31
32
  .webview-open-button {
@@ -37,7 +38,7 @@
37
38
  height: 72px;
38
39
  line-height: 72px;
39
40
  border-radius: 72px;
40
- background-color: #27ba9b;
41
- color: #fff;
41
+ background: linear-gradient(135deg, var(--kratos-color-primary-deep), var(--kratos-color-primary));
42
+ color: var(--kratos-color-on-primary);
42
43
  font-size: 26px;
43
44
  }
@@ -3,6 +3,7 @@ import Taro, { useLoad } from '@tarojs/taro'
3
3
  import { useEffect, useState } from 'react'
4
4
  import './webview.scss'
5
5
  import { useI18n } from '../../../locales'
6
+ import { useThemeStore } from '../../../stores'
6
7
 
7
8
  /** 只放行 http(s) 外链,其它协议按非法地址处理。 */
8
9
  const isAllowedUrl = (value: string) => /^https?:\/\//i.test(value)
@@ -18,6 +19,7 @@ const safeDecodeURIComponent = (value: string) => {
18
19
  /** 统一外链承载页。 */
19
20
  export default function WebViewPage() {
20
21
  const { t } = useI18n()
22
+ const themeStore = useThemeStore()
21
23
  const [url, setUrl] = useState('')
22
24
  const [iframeLoaded, setIframeLoaded] = useState(false)
23
25
  const [iframeTimedOut, setIframeTimedOut] = useState(false)
@@ -44,7 +46,7 @@ export default function WebViewPage() {
44
46
  : t('core.webview.invalid_url')
45
47
 
46
48
  return (
47
- <View className='webview-container'>
49
+ <View className={`webview-container ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}>
48
50
  {url ? (
49
51
  <WebView src={url} onLoad={() => setIframeLoaded(true)} />
50
52
  ) : null}