@liujitcn/kratos-taro-app-system 0.0.83 → 0.0.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,34 @@
13
13
 
14
14
  页面配置统一登记在 `src/pages.ts`,运行时映射位于 `src/index.ts`,构建期描述位于 `src/build.ts`。页面私有组件放在页面目录的 `components` 中,不会被 runner 扫描为路由。
15
15
 
16
+ ## 个人中心扩展
17
+
18
+ 公共组件 `KratosMyPage` 保留顶部标题与设置、个人信息和 AI 助手入口;AI 卡片继续由应用的 `aiEnabled` 配置控制显隐。React `children` 作为业务扩展区,位于这些固定内容之后,与它们共用滚动区域、主题和卡片间距。未传入内容时不增加容器或留白,业务卡片、数据请求和权限由项目模块维护。
19
+
20
+ 公共组件与样式位于 `src/components/KratosMyPage.tsx` 和 `KratosMyPage.scss`。内置 [我的页面](src/views/pages/my/my.tsx) 已使用该组件,并挂载 [MyExtensions 空实现](src/views/pages/my/components/MyExtensions.tsx)。空实现返回 `null`,不显示占位卡片;项目可以按这个结构组织自己的业务内容。
21
+
22
+ 在项目业务模块的 `src/views/pages/my/my.tsx` 复用公开组件入口:
23
+
24
+ ```tsx
25
+ import KratosMyPage from '@liujitcn/kratos-taro-app-system/components/KratosMyPage'
26
+ import OrderSection from './components/OrderSection'
27
+ import ServiceSection from './components/ServiceSection'
28
+
29
+ /** MyPage 组合公共个人中心与项目业务卡片。 */
30
+ export default function MyPage() {
31
+ return (
32
+ <KratosMyPage>
33
+ <OrderSection />
34
+ <ServiceSection />
35
+ </KratosMyPage>
36
+ )
37
+ }
38
+ ```
39
+
40
+ 项目模块的 `pages` 声明 `'pages/my/my': { style: { navigationStyle: 'custom', navigationBarTitleText: '' } }`,`views` 声明 `PROFILE_HOME: 'pages/my/my'`,并在宿主 `module-manifest.ts` 中排在 system 模块之后。构建期 `buildModule.pages` 复用相同页面配置。使用相同的物理路由可以保留微信原生 tab 切换;页面 wrapper 和底部 tabBar 继续由 runner 生成,不在业务页面重复挂载。
41
+
42
+ 扩展区可以直接放多个业务卡片,不需要复制公共页面或给顶部入口提供替换内容。需要登录态、用户资料或导航时,通过 core 的公开 store 与导航 API 获取。
43
+
16
44
  ## AI 助手
17
45
 
18
46
  AI 页面支持会话创建与切换、历史会话搜索、消息加载、附件上传、复制、删除、再生成、工具提示和快捷入口。Provider 与模型通过 `AiModelService` 加载,知识库通过 `AiKnowledgeService` 加载并可多选,发送消息时携带所选知识库编号。H5 使用 Fetch SSE,微信小程序使用 chunked 请求,两端共用增量 SSE 解析器和相同事件契约。
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git@github.com:liujitcn/kratos-admin.git"
9
9
  },
10
- "version": "0.0.83",
10
+ "version": "0.0.85",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "./api/*": "./src/api/*",
30
30
  "./rpc/*": "./src/rpc/*",
31
+ "./components/*": "./src/components/*.tsx",
31
32
  "./views/*": "./src/views/*",
32
33
  "./package.json": "./package.json"
33
34
  },
@@ -38,8 +39,8 @@
38
39
  "@tarojs/components": "4.2.1",
39
40
  "@tarojs/taro": "4.2.1",
40
41
  "react": "18.3.1",
41
- "@liujitcn/kratos-taro-app-core": "^0.0.83",
42
- "@liujitcn/kratos-taro-app-ui": "^0.0.83"
42
+ "@liujitcn/kratos-taro-app-core": "^0.0.85",
43
+ "@liujitcn/kratos-taro-app-ui": "^0.0.85"
43
44
  },
44
45
  "scripts": {
45
46
  "build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
@@ -1,3 +1,4 @@
1
+ /* 公共个人中心与业务扩展共用完整页面视口。 */
1
2
  page {
2
3
  height: 100%;
3
4
  overflow: hidden;
@@ -10,24 +11,13 @@ page {
10
11
  }
11
12
 
12
13
  .my-content {
13
- padding: calc(env(safe-area-inset-top) + 24px) 32px calc(40px + env(safe-area-inset-bottom));
14
- }
15
-
16
- .my-viewport--weapp .my-content {
17
- /* k-top 在小程序端自带 44PX 状态栏高度的导航带,页面不再额外让出顶部空间 */
18
- padding-top: 0;
14
+ padding: 0 20px calc(40px + env(safe-area-inset-bottom));
19
15
  }
20
16
 
21
17
  .my-stack {
22
18
  display: flex;
23
19
  flex-direction: column;
24
20
  gap: 24px;
25
- margin-top: 16px;
26
- }
27
-
28
- .my-gear {
29
- width: 34px;
30
- height: 34px;
31
21
  }
32
22
 
33
23
  /* k-pfx 仅定义 flex 排布,卡片内边距交由页面控制 */
@@ -0,0 +1,100 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Image, ScrollView, View } from '@tarojs/components'
3
+ import defaultAvatar from '@liujitcn/kratos-taro-app-core/static/images/avatar.png'
4
+ import {
5
+ formatSrc,
6
+ KratosNavBar,
7
+ navigateAppRoute,
8
+ navigateToLogin,
9
+ useI18n,
10
+ useSettingStore,
11
+ useThemeStore,
12
+ useUserStore,
13
+ } from '@liujitcn/kratos-taro-app-core'
14
+ import './KratosMyPage.scss'
15
+
16
+ /** KratosMyPageProps 个人中心的项目业务扩展属性。 */
17
+ export interface KratosMyPageProps {
18
+ /** 在个人信息和 AI 助手之后渲染的项目业务内容。 */
19
+ children?: ReactNode
20
+ }
21
+
22
+ /** KratosMyPage 保留公共个人中心入口,并在下方渲染项目业务内容。 */
23
+ export default function KratosMyPage({ children }: KratosMyPageProps) {
24
+ const { t } = useI18n()
25
+ const themeStore = useThemeStore()
26
+ const profile = useUserStore((state) => state.userInfo)
27
+ const isLoggedIn = useUserStore((state) => state.isAuthenticated())
28
+ const ensureAuthenticated = useUserStore((state) => state.ensureAuthenticated)
29
+ const aiEnabled = useSettingStore((state) => state.aiEnabled)
30
+ const avatarText = (profile?.nick_name || 'U').slice(0, 1)
31
+ /** 个人卡账号行:登录展示账号,未登录提示去登录。 */
32
+ const accountText =
33
+ isLoggedIn && profile ? profile.user_name || profile.nick_name || '' : t('system.profile.login_prompt')
34
+
35
+ /** 打开需要登录的公共页面。 */
36
+ const openAuthenticatedPage = (route: string) => {
37
+ if (!ensureAuthenticated()) {
38
+ navigateToLogin()
39
+ return
40
+ }
41
+ navigateAppRoute(route)
42
+ }
43
+
44
+ return (
45
+ <ScrollView
46
+ enableBackToTop
47
+ scrollY
48
+ className={`my-viewport ${
49
+ themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'
50
+ }`}
51
+ >
52
+ <View className='my-content'>
53
+ <KratosNavBar
54
+ title={t('core.navigation.my')}
55
+ variant='tab'
56
+ action={{ icon: 'settings', label: t('system.settings.title'), onClick: () => openAuthenticatedPage('app/settings') }}
57
+ />
58
+
59
+ <View className='my-stack'>
60
+ <View
61
+ className='k-glass k-glass--lit k-pfx my-profile'
62
+ onClick={() => navigateAppRoute('app/profile')}
63
+ >
64
+ <View className='my-pfx-main'>
65
+ {isLoggedIn && profile ? (
66
+ <Image
67
+ className='k-pfx__av'
68
+ src={profile.avatar ? formatSrc(profile.avatar) : defaultAvatar}
69
+ mode='aspectFill'
70
+ />
71
+ ) : (
72
+ <View className='k-pfx__av'>{avatarText}</View>
73
+ )}
74
+ <View className='my-pfx-meta'>
75
+ <View className='k-pfx__nm'>
76
+ {isLoggedIn && profile ? profile.nick_name : t('system.profile.not_logged_in')}
77
+ </View>
78
+ <View className='k-pfx__ac'>{accountText}</View>
79
+ </View>
80
+ </View>
81
+ <View className='k-pfx__ent'>{t('system.profile.title')} ›</View>
82
+ </View>
83
+
84
+ {aiEnabled ? (
85
+ <View className='k-aix' onClick={() => openAuthenticatedPage('app/ai')}>
86
+ <View className='k-aix__ic'>AI</View>
87
+ <View className='my-aix-copy'>
88
+ <View className='k-aix__t'>{t('system.settings.ai_title')}</View>
89
+ <View className='k-aix__d'>{t('system.settings.ai_description')}</View>
90
+ </View>
91
+ <View className='k-aix__b'>{t('system.settings.go_ask')}</View>
92
+ </View>
93
+ ) : null}
94
+
95
+ {children}
96
+ </View>
97
+ </View>
98
+ </ScrollView>
99
+ )
100
+ }
@@ -0,0 +1,4 @@
1
+ /** MyExtensions 项目业务扩展的空实现,可添加订单、收藏或统计卡片。 */
2
+ export default function MyExtensions() {
3
+ return null
4
+ }
@@ -1,101 +1,11 @@
1
- import type { CSSProperties } from 'react'
2
- import { Image, ScrollView, View } from '@tarojs/components'
3
- import defaultAvatar from '@liujitcn/kratos-taro-app-core/static/images/avatar.png'
4
- import {
5
- formatSrc,
6
- getMpCapsuleRightGap,
7
- navigateAppRoute,
8
- navigateToLogin,
9
- useI18n,
10
- useSettingStore,
11
- useThemeStore,
12
- useUserStore,
13
- } from '@liujitcn/kratos-taro-app-core'
14
- import './my.scss'
1
+ import KratosMyPage from '@liujitcn/kratos-taro-app-system/components/KratosMyPage'
2
+ import MyExtensions from './components/MyExtensions'
15
3
 
16
- /** 个人中心首页。 */
4
+ /** MyPage 组合公共个人中心与项目业务扩展。 */
17
5
  export default function MyPage() {
18
- const { t } = useI18n()
19
- const themeStore = useThemeStore()
20
- const profile = useUserStore((state) => state.userInfo)
21
- const isLoggedIn = useUserStore((state) => state.isAuthenticated())
22
- const ensureAuthenticated = useUserStore((state) => state.ensureAuthenticated)
23
- const aiEnabled = useSettingStore((state) => state.aiEnabled)
24
- const avatarText = (profile?.nick_name || 'U').slice(0, 1)
25
- /** MP-WEIXIN 原生胶囊按钮右侧避让宽度,写入 --mp-capsule-gap 供 .k-top 预留空间。 */
26
- const capsuleGap = getMpCapsuleRightGap()
27
- const topNavStyle = { '--mp-capsule-gap': `${capsuleGap}px` } as CSSProperties
28
- /** 设计稿齿轮线稿,随主题取色的 data URI,替代 CSS 手绘图标。 */
29
- const gearIcon = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
30
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${
31
- themeStore.theme === 'light' ? '#102232' : '#e9f4f6'
32
- }" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .3 1.8 2 2 0 1 1-2.8 2.8 1.6 1.6 0 0 0-2.7 1.1 2 2 0 1 1-4 0 1.6 1.6 0 0 0-2.6-1.1 2 2 0 1 1-2.8-2.8A1.6 1.6 0 0 0 3 13.9a2 2 0 1 1 0-4 1.6 1.6 0 0 0 1.5-2.7 2 2 0 1 1 2.8-2.8A1.6 1.6 0 0 0 10 4.6a2 2 0 1 1 4 0 1.6 1.6 0 0 0 2.7.6 2 2 0 1 1 2.8 2.8 1.6 1.6 0 0 0-.1 2.6a2.2 2.2 0 1 1 0 4.4Z"/></svg>`,
33
- )}`
34
- /** 个人卡账号行:登录展示账号,未登录提示去登录。 */
35
- const accountText =
36
- isLoggedIn && profile ? profile.user_name || profile.nick_name || '' : t('system.profile.login_prompt')
37
-
38
- const openAuthenticatedPage = (route: string) => {
39
- if (!ensureAuthenticated()) {
40
- navigateToLogin()
41
- return
42
- }
43
- navigateAppRoute(route)
44
- }
45
-
46
6
  return (
47
- <ScrollView
48
- enableBackToTop
49
- scrollY
50
- className={`my-viewport${process.env.TARO_ENV === 'weapp' ? ' my-viewport--weapp' : ''} ${
51
- themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'
52
- }`}
53
- >
54
- <View className='my-content'>
55
- <View className='k-top' style={topNavStyle}>
56
- <View className='k-top__title'>{t('core.navigation.my')}</View>
57
- <View className='k-ic' onClick={() => openAuthenticatedPage('app/settings')}>
58
- <Image className='my-gear' src={gearIcon} mode='aspectFit' />
59
- </View>
60
- </View>
61
-
62
- <View className='my-stack'>
63
- <View
64
- className='k-glass k-glass--lit k-pfx my-profile'
65
- onClick={() => navigateAppRoute('app/profile')}
66
- >
67
- <View className='my-pfx-main'>
68
- {isLoggedIn && profile ? (
69
- <Image
70
- className='k-pfx__av'
71
- src={profile.avatar ? formatSrc(profile.avatar) : defaultAvatar}
72
- mode='aspectFill'
73
- />
74
- ) : (
75
- <View className='k-pfx__av'>{avatarText}</View>
76
- )}
77
- <View className='my-pfx-meta'>
78
- <View className='k-pfx__nm'>
79
- {isLoggedIn && profile ? profile.nick_name : t('system.profile.not_logged_in')}
80
- </View>
81
- <View className='k-pfx__ac'>{accountText}</View>
82
- </View>
83
- </View>
84
- <View className='k-pfx__ent'>{t('system.profile.title')} ›</View>
85
- </View>
86
-
87
- {aiEnabled ? (
88
- <View className='k-aix' onClick={() => openAuthenticatedPage('app/ai')}>
89
- <View className='k-aix__ic'>AI</View>
90
- <View className='my-aix-copy'>
91
- <View className='k-aix__t'>{t('system.settings.ai_title')}</View>
92
- <View className='k-aix__d'>{t('system.settings.ai_description')}</View>
93
- </View>
94
- <View className='k-aix__b'>{t('system.settings.go_ask')}</View>
95
- </View>
96
- ) : null}
97
- </View>
98
- </View>
99
- </ScrollView>
7
+ <KratosMyPage>
8
+ <MyExtensions />
9
+ </KratosMyPage>
100
10
  )
101
11
  }
@@ -1,16 +1,15 @@
1
1
  import { ScrollView, Text, View } from '@tarojs/components'
2
- import Taro, { getCurrentPages, useLoad } from '@tarojs/taro'
2
+ import Taro, { useLoad } from '@tarojs/taro'
3
3
  import { useEffect, useMemo, useRef, useState } from 'react'
4
4
  import {
5
5
  formatSrc,
6
6
  getCurrentLocale,
7
- navigateAppRoute,
7
+ KratosNavBar,
8
8
  t,
9
9
  uploadFile,
10
10
  useI18n,
11
11
  useThemeStore,
12
12
  } from '@liujitcn/kratos-taro-app-core'
13
- import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
14
13
  import { isAuthExpiredError } from '@liujitcn/kratos-taro-app-core/utils/http'
15
14
  import { defAiMessageService, StreamAiMessageByChunkedRequest } from '../../../api/base/v1/ai_message'
16
15
  import { defAiSessionService } from '../../../api/base/v1/ai_session'
@@ -67,12 +66,10 @@ const PENDING_MESSAGE_ID = 'pending'
67
66
  const MAX_ATTACHMENT_COUNT = 6
68
67
  const STARTER_PROMPT_PAGE_SIZE = 4
69
68
 
70
- /** AI 助手页面。 */
69
+ /** AiPage 展示 AI 助手对话与会话历史。 */
71
70
  export default function AiPage() {
72
71
  const { locale } = useI18n()
73
72
  const themeStore = useThemeStore()
74
- /** 导航图标随主题取色,UniIcon 的 color 属性不接受 CSS 变量。 */
75
- const navIconColor = themeStore.theme === 'light' ? '#102232' : '#e9f4f6'
76
73
  const windowInfo = Taro.getWindowInfo() as ReturnType<typeof Taro.getWindowInfo> & {
77
74
  safeAreaInsets?: { top?: number; bottom?: number }
78
75
  }
@@ -658,14 +655,6 @@ export default function AiPage() {
658
655
  }
659
656
  }
660
657
 
661
- const navigateBack = () => {
662
- if (getCurrentPages().length > 1) {
663
- void Taro.navigateBack()
664
- return
665
- }
666
- navigateAppRoute('app/home')
667
- }
668
-
669
658
  const handleSend = async () => {
670
659
  if (isSubmitDisabled) return
671
660
  const text = inputText.trim() || t('system.ai.attachment_answer')
@@ -724,16 +713,11 @@ export default function AiPage() {
724
713
  <View
725
714
  className={`ai-page ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}
726
715
  >
727
- <View className={`ai-navbar k-page-nav ${process.env.TARO_ENV === 'weapp' ? 'k-page-nav--weapp' : ''}`}>
728
- <View className='k-page-nav__back' aria-label={t('common.action.back')} onClick={navigateBack} />
729
- <View className='k-page-nav__title'>{t('system.ai.chat_title')}</View>
730
- <View
731
- className='k-page-nav__side k-ic'
732
- aria-label={t('system.ai.history')}
733
- onClick={() => setShowSessionDrawer((open) => !open)}
734
- >
735
- <UniIcon type='bars' size={22} color={navIconColor} />
736
- </View>
716
+ <View className='ai-navbar'>
717
+ <KratosNavBar
718
+ title={t('system.ai.chat_title')}
719
+ action={{ icon: 'history', label: t('system.ai.history'), onClick: () => setShowSessionDrawer((open) => !open) }}
720
+ />
737
721
  </View>
738
722
  <ScrollView
739
723
  className='ai-body'
@@ -1,7 +1,7 @@
1
- import Taro, { useLoad } from '@tarojs/taro'
1
+ import { useLoad } from '@tarojs/taro'
2
2
  import { Button, RichText, Text, View } from '@tarojs/components'
3
3
  import { useState } from 'react'
4
- import { homeTabPage, navigateAppView, t, useThemeStore } from '@liujitcn/kratos-taro-app-core'
4
+ import { KratosNavBar, navigateAppView, t, useThemeStore } from '@liujitcn/kratos-taro-app-core'
5
5
  import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
6
6
  import { defNotificationService } from '../../../api/base/v1/notification'
7
7
  import type { Notification } from '../../../rpc/base/v1/notification'
@@ -9,7 +9,7 @@ import { MessageActionType, MessageContentFormat } from '../../../rpc/base/v1/no
9
9
  import { resolveMessageCategoryIcon } from './icons'
10
10
  import './message.scss'
11
11
 
12
- /** Taro 站内信详情页面。 */
12
+ /** MessageDetailPage 展示站内信详情与通知操作。 */
13
13
  export default function MessageDetailPage() {
14
14
  const themeStore = useThemeStore()
15
15
  const [detail, setDetail] = useState<Notification>()
@@ -55,20 +55,11 @@ export default function MessageDetailPage() {
55
55
  navigateAppView(detail.action_target, params)
56
56
  }
57
57
 
58
- /** 返回消息列表,页面栈为空时回到首页。 */
59
- function onBack() {
60
- void Taro.navigateBack({ fail: () => void Taro.reLaunch({ url: homeTabPage }) })
61
- }
62
-
63
58
  return (
64
59
  <View
65
60
  className={`detail-page ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}
66
61
  >
67
- <View className={`k-page-nav ${process.env.TARO_ENV === 'weapp' ? 'k-page-nav--weapp' : ''}`}>
68
- <View className='k-page-nav__back' aria-label={t('common.action.back')} onClick={onBack} />
69
- <View className='k-page-nav__title'>{t('system.notification.title')}</View>
70
- <View className='k-page-nav__side' aria-hidden />
71
- </View>
62
+ <KratosNavBar title={t('system.notification.title')} />
72
63
  {detail ? (
73
64
  <>
74
65
  <Text className='detail-title'>{detail.title}</Text>
@@ -1,9 +1,8 @@
1
- import type { CSSProperties } from 'react'
2
1
  import { useDidShow } from '@tarojs/taro'
3
2
  import { Button, Text, View } from '@tarojs/components'
4
3
  import { useRef, useState } from 'react'
5
4
  import {
6
- getMpCapsuleRightGap,
5
+ KratosNavBar,
7
6
  KratosEmpty,
8
7
  navigateAppView,
9
8
  t,
@@ -17,12 +16,9 @@ import { refreshNotificationSummary } from '../../../notification'
17
16
  import { resolveMessageCategoryIcon } from './icons'
18
17
  import './message.scss'
19
18
 
20
- /** Taro 站内信收件箱页面。 */
19
+ /** MessageInboxPage 展示站内信收件箱,并提供全部已读操作。 */
21
20
  export default function MessageInboxPage() {
22
21
  const themeStore = useThemeStore()
23
- /** MP-WEIXIN 原生胶囊按钮右侧避让宽度,写入 --mp-capsule-gap 供 .k-top 预留空间。 */
24
- const capsuleGap = getMpCapsuleRightGap()
25
- const topNavStyle = { '--mp-capsule-gap': `${capsuleGap}px` } as CSSProperties
26
22
  const [items, setItems] = useState<Notification[]>([])
27
23
  const [cursorId, setCursorId] = useState(0)
28
24
  const [loading, setLoading] = useState(false)
@@ -176,20 +172,13 @@ export default function MessageInboxPage() {
176
172
  themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'
177
173
  }`}
178
174
  >
179
- <View className='k-top message-header' style={topNavStyle}>
180
- <View className='message-header__title'>
181
- <Text className='message-header__text'>{t('system.notification.title')}</Text>
182
- {unreadTotal > 0 && <View className='message-unread-dot' aria-hidden='true' />}
183
- </View>
184
- {selectedView !== NotificationView.NOTIFICATION_VIEW_ARCHIVED && unreadTotal > 0 && (
185
- <View
186
- className='k-ch k-ch--pri'
187
- onClick={() => void markAllRead().catch(() => undefined)}
188
- >
189
- {t('system.notification.mark_all_read')}
190
- </View>
191
- )}
192
- </View>
175
+ <KratosNavBar
176
+ title={t('system.notification.title')}
177
+ variant='tab'
178
+ action={selectedView !== NotificationView.NOTIFICATION_VIEW_ARCHIVED && unreadTotal > 0
179
+ ? { display: 'text', label: t('system.notification.mark_all_read'), onClick: () => void markAllRead().catch(() => undefined) }
180
+ : undefined}
181
+ />
193
182
  <View className='k-chips'>
194
183
  <View
195
184
  className={`k-ch${categoryId === undefined ? ' k-ch--pri' : ''}`}
@@ -1,49 +1,11 @@
1
1
  .message-page {
2
2
  box-sizing: border-box;
3
3
  min-height: 100vh;
4
- padding: 16px 24px calc(200px + env(safe-area-inset-bottom));
4
+ padding: 0 20px calc(200px + env(safe-area-inset-bottom));
5
5
  background: var(--kratos-bg-gradient);
6
6
  color: var(--kratos-color-text);
7
7
  }
8
8
 
9
- .message-header {
10
- /* 标题绝对居中后头部不再依赖流内内容撑高,保证标题行与筛选行留出间距 */
11
- min-height: 76px;
12
- padding: 20px 4px 24px;
13
- }
14
-
15
- /* 标题排版与 k-page-nav__title/k-top__title 同规格,绝对居中后文字与红点同行排布 */
16
- .message-header__title {
17
- position: absolute;
18
- left: 50%;
19
- display: flex;
20
- align-items: center;
21
- min-width: 0;
22
- max-width: calc(100% - 190px);
23
- overflow: hidden;
24
- font-size: 16PX;
25
- font-weight: 700;
26
- color: var(--kratos-color-text);
27
- transform: translateX(-50%);
28
- white-space: nowrap;
29
- }
30
-
31
- .message-header__text {
32
- min-width: 0;
33
- overflow: hidden;
34
- text-overflow: ellipsis;
35
- }
36
-
37
- .message-unread-dot {
38
- flex-shrink: 0;
39
- width: 12px;
40
- height: 12px;
41
- margin-left: 12px;
42
- border-radius: 50%;
43
- background: var(--kratos-color-danger);
44
- box-shadow: 0 0 16px var(--kratos-color-danger);
45
- }
46
-
47
9
  .message-tabs {
48
10
  display: flex;
49
11
  margin-top: 4px;
@@ -79,7 +41,7 @@
79
41
  .detail-page {
80
42
  box-sizing: border-box;
81
43
  min-height: 100vh;
82
- padding: 24px 30px calc(96px + env(safe-area-inset-bottom));
44
+ padding: 0 30px calc(96px + env(safe-area-inset-bottom));
83
45
  background: var(--kratos-bg-gradient);
84
46
  color: var(--kratos-color-text);
85
47
  }
@@ -4,7 +4,7 @@
4
4
  color: var(--kratos-color-text);
5
5
  }
6
6
 
7
- /* k-page-nav 自带安全区顶部内边距,这里只补水平与底部 */
7
+ /* 导航组件统一处理顶部空间,页面只维护内容的水平与底部留白。 */
8
8
  .profile-content {
9
9
  padding: 0 32px calc(40px + env(safe-area-inset-bottom));
10
10
  }
@@ -18,6 +18,7 @@ import type { BaseDictForm_DictItem } from '@liujitcn/kratos-taro-app-core/rpc/s
18
18
  import type { UserProfileForm } from '@liujitcn/kratos-taro-app-core/rpc/system/app/v1/auth'
19
19
  import { BaseUserIDType } from '@liujitcn/kratos-taro-app-core/rpc/system/common/v1/common'
20
20
  import {
21
+ KratosNavBar,
21
22
  formatSrc,
22
23
  navigateToLogin,
23
24
  uploadFile,
@@ -31,7 +32,7 @@ const IMG_MAX_SIZE = 1024 * 1024
31
32
  const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
32
33
  const ID_CODE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/
33
34
 
34
- /** 用户个人资料编辑页。 */
35
+ /** ProfilePage 展示并编辑用户个人资料。 */
35
36
  export default function ProfilePage() {
36
37
  const { t } = useI18n()
37
38
  const themeStore = useThemeStore()
@@ -153,15 +154,7 @@ export default function ProfilePage() {
153
154
  className={`profile-viewport ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}
154
155
  >
155
156
  <View className='profile-content'>
156
- <View className={`k-page-nav ${process.env.TARO_ENV === 'weapp' ? 'k-page-nav--weapp' : ''}`}>
157
- <View
158
- className='k-page-nav__back'
159
- aria-label={t('common.action.back')}
160
- onClick={() => void Taro.navigateBack()}
161
- />
162
- <View className='k-page-nav__title'>{t('system.profile.title')}</View>
163
- <View className='k-page-nav__side' aria-hidden />
164
- </View>
157
+ <KratosNavBar title={t('system.profile.title')} />
165
158
 
166
159
  <View className='k-glass k-glass--lit k-pfx profile-id' onClick={() => void onAvatarChange()}>
167
160
  <View className='profile-id__main'>
@@ -9,14 +9,14 @@ import {
9
9
  useThemeStore,
10
10
  useUserStore,
11
11
  type ThemeMode,
12
- homeTabPage,
12
+ KratosNavBar,
13
13
  } from '@liujitcn/kratos-taro-app-core'
14
14
  import { defMfaService } from '@liujitcn/kratos-taro-app-core/api/base/v1/mfa'
15
15
  import { isAuthExpiredError } from '@liujitcn/kratos-taro-app-core/utils/http'
16
16
  import PasswordVerifyDialog from './components/PasswordVerifyDialog'
17
17
  import './settings.scss'
18
18
 
19
- /** 应用设置页。 */
19
+ /** SettingsPage 展示应用设置与账号安全操作。 */
20
20
  export default function SettingsPage() {
21
21
  const { locale, setLocale, t } = useI18n()
22
22
  const themeStore = useThemeStore()
@@ -140,13 +140,7 @@ export default function SettingsPage() {
140
140
  void loadMfaStatus()
141
141
  })
142
142
 
143
- /** 返回个人中心,页面栈不可返回时回到首页。 */
144
- const onBack = () => {
145
- void Taro.navigateBack({
146
- fail: () => void Taro.reLaunch({ url: homeTabPage }),
147
- })
148
- }
149
-
143
+ /** onLogout 确认后退出当前账号并返回来源页面。 */
150
144
  const onLogout = async () => {
151
145
  if (logoutLoading) return
152
146
  const result = await Taro.showModal({
@@ -175,12 +169,7 @@ export default function SettingsPage() {
175
169
  themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'
176
170
  }`}
177
171
  >
178
- <View className={`k-page-nav ${process.env.TARO_ENV === 'weapp' ? 'k-page-nav--weapp' : ''}`}>
179
- <View className='k-page-nav__back' aria-label={t('common.action.back')} onClick={onBack}>
180
- </View>
181
- <View className='k-page-nav__title'>{t('system.settings.title')}</View>
182
- <View className='k-page-nav__side' aria-hidden />
183
- </View>
172
+ <KratosNavBar title={t('system.settings.title')} />
184
173
 
185
174
  <View className='settings-content'>
186
175
  <View className='k-set-cat'>{t('system.settings.section_appearance')}</View>