@liujitcn/kratos-uni-app-core 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
@@ -10,7 +10,7 @@
10
10
  packages/core
11
11
  ├── src
12
12
  │ ├── api # 与 Proto 完整路径一致的接口 service
13
- │ ├── components # 启动状态、自绘 tabBar、公共设置页和 MFA 组件
13
+ │ ├── components # 通用导航栏、自绘 tabBar、公共设置页和 MFA 组件
14
14
  │ ├── rpc # Buf 生成的 TypeScript 接口类型
15
15
  │ ├── static # 底座图片和 tab 图标
16
16
  │ ├── stores # Pinia 设置与用户状态
@@ -34,6 +34,8 @@ packages/core
34
34
 
35
35
  - 定义 `KratosAppModule`、`viewKey`、静态页面覆盖和模块图标协议。
36
36
  - 从 `/v1/app/base/menu` 加载 `99000000` 下的扁平菜单,按 `parent_id` 构建树,以二级页面生成 tabBar,并原子更新动态路由和层级导航。
37
+ - tabBar 统一自绘,宿主不声明原生 `tabBar`;H5 替换当前路由,微信端复用普通页面栈,栈满时替换栈顶,切换不重建整个页面栈。
38
+ - 页面装配插件在编译配置中移除宿主的原生 `tabBar`,退出后恢复宿主原文件。导航按模块注册的 `viewKey` 解析物理页面,支持业务模块覆盖首页和扩展 tab;引用项目升级 core 包后即可使用相同策略。
37
39
  - 按匿名态、登录态缓存导航,并在远端不可用时回退到最近缓存或默认菜单。
38
40
  - 统一认证、token、MFA、HTTP、上传、配置、Pinia 和基础页面能力。
39
41
  - 提供可复用的 `KratosSettingsPage`,统一语言、MFA、微信设置和退出登录,并允许产品在最上方追加业务设置。
@@ -50,6 +52,41 @@ packages/core
50
52
  - `@liujitcn/kratos-uni-app-core/vite`:Vite 配置、页面装配插件和构建错误文案格式化器。
51
53
  - `@liujitcn/kratos-uni-app-core/api/*`、`components/*.vue`、`utils/*`、`views/*`:白名单子路径。
52
54
 
55
+ ## 通用顶部导航
56
+
57
+ 所有自定义顶部导航统一使用公开组件 `KratosNavBar`,页面只提供标题和业务操作,不再定义导航高度、安全区、标题居中、按钮尺寸或微信胶囊避让。普通页与 tabBar 页的标题栏使用相同高度,按钮显隐不会改变标题位置。页面外层只维护水平与底部留白,顶部留白由组件处理;tabBar 页的导航栏还统一预留内容间距。
58
+
59
+ ```vue
60
+ <script setup lang="ts">
61
+ import KratosNavBar from '@liujitcn/kratos-uni-app-core/components/KratosNavBar.vue'
62
+ </script>
63
+
64
+ <template>
65
+ <!-- 普通页:默认带返回,无法返回时回到首页 -->
66
+ <KratosNavBar title="个人信息" />
67
+ <!-- tabBar 页:默认不带返回,也可以不传操作按钮 -->
68
+ <KratosNavBar title="首页" variant="tab" />
69
+ <KratosNavBar
70
+ title="我的"
71
+ variant="tab"
72
+ :action="{ icon: 'settings', label: '设置', onClick: openSettings }"
73
+ />
74
+ </template>
75
+ ```
76
+
77
+ `back` 可显式控制返回按钮,`onBack` 可覆盖默认返回操作。`action` 包含 `label`、可选 `icon`、可选 `display` 和 `onClick`;`display` 支持 `icon`(纯图标)、`text`(纯文字)和 `both`(图标加文字),默认有图标时显示图标,否则显示文字。图标支持内置 `settings`、`read-all`、`history`,以及自定义图片地址。不传 `action` 时不渲染操作按钮。三种按钮形式共用相同高度;纯图标保持方形,文字与组合按钮按内容增宽,空间不足时省略文字。组件通过公开的 `components/KratosNavBar.vue` 子路径导入;`KratosNavBarProps`、`KratosNavBarAction` 类型从包根公开导出。小程序的 Vue 组件必须直接导入 `.vue` 文件,避免经 TypeScript 入口重导出导致编译产物漏登记 `usingComponents`;业务模块应传入翻译后的标题和按钮名称。
78
+
79
+ ```ts
80
+ import type { KratosNavBarAction } from '@liujitcn/kratos-uni-app-core'
81
+
82
+ // 纯图标
83
+ const iconAction: KratosNavBarAction = { icon: 'settings', label: '设置', display: 'icon', onClick: openSettings }
84
+ // 纯文字
85
+ const textAction: KratosNavBarAction = { label: '全部已读', display: 'text', onClick: markAllRead }
86
+ // 图标加文字
87
+ const combinedAction: KratosNavBarAction = { icon: 'history', label: '历史', display: 'both', onClick: openHistory }
88
+ ```
89
+
53
90
  ## 公共设置页
54
91
 
55
92
  产品设置页通过公开子路径引用 `KratosSettingsPage`。`extensions` 是唯一业务扩展插槽,固定直接渲染在页面视口最上方;没有传入内容时不会生成容器或额外间距。
package/dist/vite.mjs CHANGED
@@ -239,6 +239,7 @@ function preparePageAssembly(options) {
239
239
  restoreBuildTransaction(transaction, options.transactionFile);
240
240
  }
241
241
  const manifest = JSON.parse(originalPages);
242
+ delete manifest.tabBar;
242
243
  manifest.pages ??= [];
243
244
  const generatedFiles = options.pageMap.map((page) => resolve(options.inputDir, `${page.route}.vue`)).filter((target) => !existsSync(target));
244
245
  const staticTarget = resolve(options.inputDir, "static");
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",
@@ -0,0 +1,183 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useI18n } from '../locales'
4
+ import { useThemeStore } from '../stores'
5
+ import { getMpCapsuleRightGap } from '../utils'
6
+ import { homeTabPage } from '../utils/navigation'
7
+ import type { KratosNavBarProps } from '../types/nav-bar'
8
+
9
+ defineOptions({ name: 'KratosNavBar' })
10
+
11
+ const props = withDefaults(defineProps<KratosNavBarProps>(), {
12
+ variant: 'page',
13
+ back: undefined,
14
+ })
15
+ const { t } = useI18n()
16
+ const themeStore = useThemeStore()
17
+ const capsuleGap = getMpCapsuleRightGap()
18
+ const showBack = computed(() => props.back ?? props.variant === 'page')
19
+ const iconPaths: Record<string, string> = {
20
+ settings:
21
+ '<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"/>',
22
+ 'read-all': '<path d="m3 12 4 4L17 6m-5 10 9-9"/>',
23
+ history: '<path d="M4 6h16M4 12h16M4 18h16"/>',
24
+ }
25
+ const actionIcon = computed(() => {
26
+ const icon = props.action?.icon
27
+ if (!icon || !iconPaths[icon]) return icon
28
+ const color = themeStore.theme === 'light' ? '#102232' : '#e9f4f6'
29
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${iconPaths[icon]}</svg>`
30
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
31
+ })
32
+ const actionDisplay = computed(() => props.action?.display ?? (actionIcon.value ? 'icon' : 'text'))
33
+ const showActionIcon = computed(() => !!actionIcon.value && actionDisplay.value !== 'text')
34
+ const showActionLabel = computed(() => !actionIcon.value || actionDisplay.value !== 'icon')
35
+
36
+ /** goBack 优先执行页面返回操作,否则返回上一页并在失败时回到首页。 */
37
+ function goBack() {
38
+ if (props.onBack) {
39
+ props.onBack()
40
+ return
41
+ }
42
+ uni.navigateBack({ fail: () => uni.reLaunch({ url: homeTabPage }) })
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <view
48
+ class="kratos-nav-bar"
49
+ :class="{ 'kratos-nav-bar--tab': variant === 'tab' }"
50
+ :style="{ '--kratos-nav-bar-capsule-gap': `${capsuleGap}px` }"
51
+ >
52
+ <view class="kratos-nav-bar__row">
53
+ <view
54
+ v-if="showBack"
55
+ class="kratos-nav-bar__button kratos-nav-bar__back"
56
+ :aria-label="t('common.action.back')"
57
+ @tap="goBack"
58
+ />
59
+ <view class="kratos-nav-bar__title">{{ title }}</view>
60
+ <view
61
+ v-if="action"
62
+ class="kratos-nav-bar__button kratos-nav-bar__action"
63
+ :class="{ 'kratos-nav-bar__action--label': showActionLabel }"
64
+ :aria-label="action.label"
65
+ @tap="action.onClick"
66
+ >
67
+ <image
68
+ v-if="showActionIcon"
69
+ class="kratos-nav-bar__icon"
70
+ :src="actionIcon"
71
+ mode="aspectFit"
72
+ aria-hidden="true"
73
+ />
74
+ <text v-if="showActionLabel" class="kratos-nav-bar__label">{{ action.label }}</text>
75
+ </view>
76
+ </view>
77
+ </view>
78
+ </template>
79
+
80
+ <style scoped lang="scss">
81
+ .kratos-nav-bar {
82
+ flex-shrink: 0;
83
+ box-sizing: border-box;
84
+ padding-top: calc(env(safe-area-inset-top) + 24rpx);
85
+ }
86
+
87
+ .kratos-nav-bar--tab {
88
+ margin-bottom: 16rpx;
89
+ }
90
+
91
+ .kratos-nav-bar__row {
92
+ position: relative;
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 16rpx;
96
+ height: 76rpx;
97
+ }
98
+
99
+ .kratos-nav-bar__title {
100
+ position: absolute;
101
+ top: 50%;
102
+ left: 50%;
103
+ max-width: calc(100% - 190rpx);
104
+ overflow: hidden;
105
+ color: var(--kratos-color-text);
106
+ font-size: 32rpx;
107
+ font-weight: 700;
108
+ line-height: 44rpx;
109
+ text-overflow: ellipsis;
110
+ white-space: nowrap;
111
+ transform: translate(-50%, -50%);
112
+ }
113
+
114
+ .kratos-nav-bar__button {
115
+ display: flex;
116
+ flex: 0 0 76rpx;
117
+ align-items: center;
118
+ justify-content: center;
119
+ box-sizing: border-box;
120
+ width: 76rpx;
121
+ height: 76rpx;
122
+ border: 1px solid var(--kratos-color-border);
123
+ border-radius: 24rpx;
124
+ background: var(--kratos-color-surface);
125
+ color: var(--kratos-color-text);
126
+ }
127
+
128
+ .kratos-nav-bar__back::before {
129
+ width: 16rpx;
130
+ height: 16rpx;
131
+ border-left: 2px solid var(--kratos-color-primary);
132
+ border-bottom: 2px solid var(--kratos-color-primary);
133
+ content: '';
134
+ transform: rotate(45deg);
135
+ }
136
+
137
+ .kratos-nav-bar__action {
138
+ margin-left: auto;
139
+ }
140
+
141
+ .kratos-nav-bar__action--label {
142
+ flex: 0 1 auto;
143
+ gap: 10rpx;
144
+ min-width: 76rpx;
145
+ max-width: calc(50% - 48rpx);
146
+ width: auto;
147
+ padding: 0 20rpx;
148
+ }
149
+
150
+ .kratos-nav-bar__icon {
151
+ flex-shrink: 0;
152
+ width: 34rpx;
153
+ height: 34rpx;
154
+ }
155
+
156
+ .kratos-nav-bar__label {
157
+ min-width: 0;
158
+ overflow: hidden;
159
+ font-size: 22rpx;
160
+ line-height: 28rpx;
161
+ text-overflow: ellipsis;
162
+ white-space: nowrap;
163
+ }
164
+
165
+ /* #ifdef MP-WEIXIN */
166
+ .kratos-nav-bar {
167
+ padding-top: 44px;
168
+ }
169
+
170
+ .kratos-nav-bar__row {
171
+ height: 88rpx;
172
+ }
173
+
174
+ /* 原生胶囊占据右侧,操作按钮与返回按钮统一放在左侧。 */
175
+ .kratos-nav-bar__action {
176
+ margin-left: 0;
177
+ }
178
+
179
+ .kratos-nav-bar__title {
180
+ max-width: calc(100% - 190rpx - var(--kratos-nav-bar-capsule-gap));
181
+ }
182
+ /* #endif */
183
+ </style>
@@ -6,6 +6,7 @@ import { getLanguageOptions, useI18n, type SupportedLocale } from '../locales'
6
6
  import { isAuthExpiredError } from '../utils/http'
7
7
  import { defMfaService } from '../api/base/v1/mfa'
8
8
  import MfaManageDialog from './MfaManageDialog.vue'
9
+ import KratosNavBar from './KratosNavBar.vue'
9
10
 
10
11
  defineOptions({ name: 'KratosSettingsPage' })
11
12
 
@@ -150,15 +151,6 @@ const onMfaDisabled = async () => {
150
151
  uni.reLaunch({ url: '/pages/login/login' })
151
152
  }
152
153
 
153
- /** 返回个人中心,页面栈不可返回时回到首页。 */
154
- const onBack = () => {
155
- uni.navigateBack({
156
- fail: () => {
157
- uni.reLaunch({ url: '/pages/index/index' })
158
- },
159
- })
160
- }
161
-
162
154
  // 公共设置模块挂载时加载 MFA 状态,兼容页面包装和业务插槽场景。
163
155
  onMounted(() => {
164
156
  if (!userStore.ensureAuthenticated()) {
@@ -208,11 +200,7 @@ const onLogout = () => {
208
200
  <template>
209
201
  <view class="settings-page" :class="themeStore.themeClass">
210
202
  <slot name="extensions" />
211
- <view class="k-page-nav">
212
- <view class="k-page-nav__back" :aria-label="t('common.action.back')" @tap="onBack"></view>
213
- <view class="k-page-nav__title">{{ t('core.settings.title') }}</view>
214
- <view class="k-page-nav__side" aria-hidden="true"></view>
215
- </view>
203
+ <KratosNavBar :title="t('core.settings.title')" />
216
204
  <view class="settings-content">
217
205
  <view class="k-set-cat">{{ t('core.settings.section_appearance') }}</view>
218
206
  <picker :range="themeModeLabels" :value="themeModeIndex" @change="onThemeModeChange">
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeMount, onMounted } from 'vue'
2
+ import { computed } from 'vue'
3
3
  import { resolveModuleIcon, resolveStaticView } from '../module'
4
4
  import { APP_MENU_ROOT_ID, useAppMenuBadge, useAppNavigation } from '../navigation'
5
5
  import { useThemeStore } from '../stores'
@@ -41,15 +41,6 @@ const activeMenu = computed(() => {
41
41
  })
42
42
  const visible = computed(() => Boolean(activeMenu.value && tabBar.value.length))
43
43
  const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
44
- // #ifdef H5
45
- // H5 端 tabBar.custom 不隐藏内置 tabbar,需手动隐藏,否则其边线从自定义栏上方露出形成双线。
46
- const hideNativeTabBar = () => {
47
- uni.hideTabBar({ fail: () => {} })
48
- }
49
- onBeforeMount(hideNativeTabBar)
50
- // H5 runtime 可能在页面挂载时重新同步内置 tabbar 状态,挂载后幂等重试避免首次切换露出一帧。
51
- onMounted(hideNativeTabBar)
52
- // #endif
53
44
  </script>
54
45
 
55
46
  <template>
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from './bootstrap'
2
2
  export * from './module'
3
3
  export * from './locales'
4
4
  export * from './navigation'
5
+ export type { KratosNavBarAction, KratosNavBarProps } from './types/nav-bar'
5
6
  /** KratosSettingsPage 可通过顶部插槽扩展业务内容的公共设置页。 */
6
7
  export { default as KratosSettingsPage } from './components/KratosSettingsPage.vue'
7
8
  export { default as KratosTabBar } from './components/KratosTabBar.vue'
package/src/navigation.ts CHANGED
@@ -166,7 +166,6 @@ let adapter: AppNavigationAdapter = {
166
166
  list: () => defBaseMenuService.ListBaseMenu(),
167
167
  }
168
168
 
169
- const nativeTabViewKeys = ['HOME', 'PROFILE_HOME']
170
169
  const pageTitleKeys: Record<string, string> = {
171
170
  'pages/index/index': 'core.home.main_title',
172
171
  'pages/login/login': 'common.action.login',
@@ -178,7 +177,6 @@ const pageTitleKeys: Record<string, string> = {
178
177
  'pagesMember/profile/profile': 'system.profile.title',
179
178
  'pagesMember/settings/settings': 'core.settings.title',
180
179
  }
181
- const resetTabViewKeys = ['MESSAGE_INBOX']
182
180
 
183
181
  /** 替换导航远端适配器,供宿主接入自定义契约。 */
184
182
  export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void {
@@ -268,7 +266,7 @@ export function navigateAppRoute(rawRoute: string, options: { replace?: boolean
268
266
  uni.navigateTo({ url, fail: () => uni.reLaunch({ url }) })
269
267
  }
270
268
 
271
- /** 切换 tab 页面,优先复用已有页面,避免重建页面栈产生空白帧。 */
269
+ /** 切换自绘 tab:H5 替换路由,微信端复用页面栈,避免重建整个页面栈。 */
272
270
  function navigateTabRoute(url: string): void {
273
271
  const targetRoute = url.slice(1).split('?', 1)[0].replace(/^\/+/, '')
274
272
  const pages = getCurrentPages()
@@ -284,36 +282,10 @@ function navigateTabRoute(url: string): void {
284
282
  uni.redirectTo({
285
283
  url,
286
284
  success: release,
287
- fail: () => {
288
- uni.reLaunch({ url, success: release, fail: release, complete: release })
289
- },
290
- })
291
- return
292
- }
293
- if (nativeTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
294
- uni.switchTab({
295
- url,
296
- success: release,
297
- fail: () => navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release),
298
- complete: release,
285
+ fail: release,
299
286
  })
300
287
  return
301
288
  }
302
- if (resetTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
303
- uni.reLaunch({ url, success: release, fail: release, complete: release })
304
- return
305
- }
306
- navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release)
307
- }
308
-
309
- /** 在没有原生 tab 路由的动态菜单上复用页面栈。 */
310
- function navigateTabRouteInStack(
311
- url: string,
312
- targetRoute: string,
313
- pages: ReturnType<typeof getCurrentPages>,
314
- currentIndex: number,
315
- release: () => void,
316
- ): void {
317
289
  const targetIndex = pages.findIndex((page) => page.route?.replace(/^\/+/, '') === targetRoute)
318
290
  if (targetIndex >= 0 && targetIndex < currentIndex) {
319
291
  uni.navigateBack({
@@ -328,7 +300,8 @@ function navigateTabRouteInStack(
328
300
  url,
329
301
  success: release,
330
302
  fail: () => {
331
- uni.reLaunch({ url, success: release, fail: release, complete: release })
303
+ // 小程序页面栈满时只替换栈顶,保留其他 tab 的已加载页面。
304
+ uni.redirectTo({ url, success: release, fail: release, complete: release })
332
305
  },
333
306
  })
334
307
  }
@@ -108,90 +108,6 @@
108
108
  box-shadow: var(--kratos-glow);
109
109
  }
110
110
 
111
- /* 顶栏 */
112
- .k-page-nav {
113
- position: relative;
114
- display: flex;
115
- align-items: center;
116
- justify-content: space-between;
117
- box-sizing: border-box;
118
- height: calc(88rpx + env(safe-area-inset-top));
119
- padding-top: env(safe-area-inset-top);
120
- }
121
- /* #ifdef MP-WEIXIN */
122
- .k-page-nav {
123
- height: calc(88rpx + 44px);
124
- padding-top: 44px;
125
- }
126
- /* #endif */
127
- .k-page-nav__back,
128
- .k-page-nav__side {
129
- display: flex;
130
- align-items: center;
131
- justify-content: center;
132
- flex: 0 0 76rpx;
133
- width: 76rpx;
134
- height: 76rpx;
135
- }
136
- .k-page-nav__back {
137
- border: 1px solid var(--kratos-color-border);
138
- border-radius: 24rpx;
139
- background: var(--kratos-color-surface);
140
- }
141
- .k-page-nav__back::before {
142
- width: 16rpx;
143
- height: 16rpx;
144
- border-left: 2px solid var(--kratos-color-primary);
145
- border-bottom: 2px solid var(--kratos-color-primary);
146
- content: '';
147
- transform: rotate(45deg);
148
- }
149
- .k-page-nav__title {
150
- position: absolute;
151
- left: 50%;
152
- max-width: calc(100% - 190rpx);
153
- overflow: hidden;
154
- color: var(--kratos-color-text);
155
- font-size: 32rpx;
156
- font-weight: 700;
157
- text-overflow: ellipsis;
158
- transform: translateX(-50%);
159
- white-space: nowrap;
160
- }
161
- .k-top {
162
- position: relative;
163
- display: flex;
164
- align-items: center;
165
- /* 标题绝对居中后,流内子元素(右侧动作按钮)统一靠右排布 */
166
- justify-content: flex-end;
167
- gap: 16rpx;
168
- /* MP-WEIXIN 原生胶囊避让:页面通过 getMpCapsuleRightGap() 写入该变量,
169
- 其余平台未设置时回退 0,不影响布局。 */
170
- padding-right: var(--mp-capsule-gap, 0px);
171
- }
172
- /* #ifdef MP-WEIXIN */
173
- /* 双写类名提高优先级:页面级 padding 简写(如 .message-header)不得压掉导航带 */
174
- .k-top.k-top {
175
- /* 小程序右上角是原生胶囊:操作按钮靠左,标题行与胶囊同行(同 k-page-nav 几何) */
176
- justify-content: flex-start;
177
- height: calc(88rpx + 44px);
178
- padding-top: 44px;
179
- padding-right: 0;
180
- }
181
- /* #endif */
182
- .k-top__title {
183
- position: absolute;
184
- left: 50%;
185
- max-width: calc(100% - 190rpx);
186
- overflow: hidden;
187
- color: var(--kratos-color-text);
188
- font-size: 32rpx;
189
- font-weight: 700;
190
- text-overflow: ellipsis;
191
- transform: translateX(-50%);
192
- white-space: nowrap;
193
- }
194
-
195
111
  /* 方形图标按钮 + 角标 */
196
112
  .k-ic {
197
113
  position: relative;
@@ -0,0 +1,25 @@
1
+ /** KratosNavBarAction 导航栏操作按钮,统一渲染尺寸、图标与无障碍文案。 */
2
+ export interface KratosNavBarAction {
3
+ /** 按钮名称,同时用作可见文字与无障碍文案。 */
4
+ label: string
5
+ /** 内置 settings、read-all、history 图标名称,或自定义图片地址。 */
6
+ icon?: string
7
+ /** 显示方式;默认有图标时显示图标,否则显示文字。 */
8
+ display?: 'icon' | 'text' | 'both'
9
+ /** 点击按钮后执行的业务操作。 */
10
+ onClick: () => void
11
+ }
12
+
13
+ /** KratosNavBarProps 普通页面与 tabBar 页面共用的顶部导航属性。 */
14
+ export interface KratosNavBarProps {
15
+ /** 居中显示的页面标题。 */
16
+ title: string
17
+ /** 普通页面默认显示返回按钮,tabBar 页面默认隐藏返回按钮。 */
18
+ variant?: 'page' | 'tab'
19
+ /** 显式覆盖返回按钮的显隐。 */
20
+ back?: boolean
21
+ /** 可选的页面操作按钮。 */
22
+ action?: KratosNavBarAction
23
+ /** 自定义返回操作;不传时返回上一页,失败后回到首页。 */
24
+ onBack?: () => void
25
+ }
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { onLoad } from '@dcloudio/uni-app'
3
+ import KratosNavBar from '../../../components/KratosNavBar.vue'
3
4
  import { computed, ref, watch } from 'vue'
4
5
  import { useSettingStore, useThemeStore } from '../../../stores'
5
6
  import defaultLogo from '../../../static/images/logo_icon.png'
@@ -41,9 +42,7 @@ onLoad(() => {
41
42
  <view class="home-page" :class="themeStore.themeClass">
42
43
  <view class="k-grid-bg" aria-hidden="true"></view>
43
44
  <view class="home-content">
44
- <view class="home-top">
45
- <view class="home-top__title">{{ mainTitle }}</view>
46
- </view>
45
+ <KratosNavBar :title="mainTitle" variant="tab" />
47
46
 
48
47
  <view class="k-glass k-glass--lit home-hero">
49
48
  <view class="home-hero__main">
@@ -126,41 +125,7 @@ onLoad(() => {
126
125
  .home-content {
127
126
  position: relative;
128
127
  z-index: 1;
129
- padding: calc(env(safe-area-inset-top) + 20rpx) 32rpx 220rpx;
130
-
131
- /* #ifdef MP-WEIXIN */
132
- /* 首页标题在小程序端占导航带,页面不再额外让出顶部空间 */
133
- padding-top: 0;
134
- /* #endif */
135
- }
136
-
137
- /* 小程序端首页标题占导航带:与 k-page-nav 同几何,标题与胶囊同行 */
138
- /* #ifdef MP-WEIXIN */
139
- .home-top {
140
- box-sizing: border-box;
141
- height: calc(88rpx + 44px);
142
- padding: 44px 4rpx 0;
143
- display: flex;
144
- align-items: center;
145
- justify-content: center;
146
- }
147
-
148
- .home-top__title {
149
- margin-top: 0;
150
- }
151
- /* #endif */
152
-
153
- .home-top {
154
- padding: 8rpx 4rpx 24rpx;
155
- }
156
-
157
- /* 页面大标题:与其他页面导航标题同规格,向原生导航栏看齐 */
158
- .home-top__title {
159
- margin-top: 14rpx;
160
- color: var(--kratos-color-text);
161
- font-size: 32rpx;
162
- font-weight: 700;
163
- text-align: center;
128
+ padding: 0 20rpx 220rpx;
164
129
  }
165
130
 
166
131
  .home-hero {
@@ -3,7 +3,7 @@ import { onLoad } from '@dcloudio/uni-app'
3
3
  import { useSettingStore, useThemeStore } from '../../../stores'
4
4
  import { ref } from 'vue'
5
5
  import { useI18n } from '../../../locales'
6
- import { homeTabPage } from '../../../utils/navigation'
6
+ import KratosNavBar from '../../../components/KratosNavBar.vue'
7
7
 
8
8
  const settingStore = useSettingStore()
9
9
  const themeStore = useThemeStore()
@@ -77,19 +77,10 @@ const loadProtocol = async (type?: string) => {
77
77
  onLoad((query) => {
78
78
  void loadProtocol(query?.type)
79
79
  })
80
-
81
- /** 返回来源页,页面栈为空时回到首页。 */
82
- const onBack = () => {
83
- uni.navigateBack({ fail: () => uni.reLaunch({ url: homeTabPage }) })
84
- }
85
80
  </script>
86
81
  <template>
87
82
  <view class="protocol-page" :class="themeStore.themeClass">
88
- <view class="k-page-nav">
89
- <view class="k-page-nav__back" :aria-label="t('common.action.back')" @tap="onBack"></view>
90
- <view class="k-page-nav__title">{{ pageTitle }}</view>
91
- <view class="k-page-nav__side" aria-hidden="true"></view>
92
- </view>
83
+ <KratosNavBar :title="pageTitle" />
93
84
  <view class="protocol-card">
94
85
  <rich-text class="protocol-content" :nodes="content" />
95
86
  </view>
@@ -99,7 +90,7 @@ const onBack = () => {
99
90
  <style lang="scss">
100
91
  .protocol-page {
101
92
  min-height: 100vh;
102
- padding: 24rpx;
93
+ padding: 0 24rpx 24rpx;
103
94
  box-sizing: border-box;
104
95
  background: var(--kratos-bg-gradient);
105
96
  }
package/src/vite.ts CHANGED
@@ -257,6 +257,7 @@ export function kratosApp(options: KratosAppViteOptions): Plugin {
257
257
  }
258
258
  }
259
259
 
260
+ /** preparePageAssembly 装配自绘导航页面,并通过事务保留宿主原配置。 */
260
261
  function preparePageAssembly(options: {
261
262
  inputDir: string
262
263
  pagesFile: string
@@ -288,6 +289,8 @@ function preparePageAssembly(options: {
288
289
  }
289
290
 
290
291
  const manifest = JSON.parse(originalPages) as PagesManifest
292
+ // 所有宿主统一使用 KratosTabBar,原生 tab 声明会限制普通页面跳转并要求额外组件。
293
+ delete manifest.tabBar
291
294
  manifest.pages ??= []
292
295
  const generatedFiles = options.pageMap
293
296
  .map((page) => resolve(options.inputDir, `${page.route}.vue`))