@liujitcn/kratos-taro-app-core 0.0.84 → 0.0.86
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 +33 -0
- package/dist/runner.mjs +1 -0
- package/package.json +1 -1
- package/src/components/KratosNavBar.scss +100 -0
- package/src/components/KratosNavBar.tsx +87 -0
- package/src/index.ts +3 -0
- package/src/locales/en-US.json +2 -1
- package/src/locales/ja-JP.json +2 -1
- package/src/locales/zh-CN.json +2 -1
- package/src/locales/zh-TW.json +2 -1
- package/src/navigation.ts +8 -34
- package/src/runner.ts +2 -0
- package/src/styles/components.scss +0 -82
- package/src/views/pages/index/index.scss +1 -32
- package/src/views/pages/index/index.tsx +4 -5
- package/src/views/pages/login/login.tsx +2 -2
- package/src/views/pages/login/protocal.scss +1 -1
- package/src/views/pages/login/protocal.tsx +3 -12
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
- 登录阶段支持 TOTP、WebAuthn 和一次性恢复码校验。
|
|
13
13
|
- 公开 Base MFA 请求及通用密码弹窗、绑定面板和恢复码弹窗,供登录页与业务模块复用同一套绑定界面。
|
|
14
14
|
- 接口驱动的逻辑路由、访问模式、自绘 tabBar 和稳定 `viewKey`。
|
|
15
|
+
- tab 切换统一使用普通页面路由:H5 替换当前路由,微信端复用页面栈,栈满时替换栈顶,避免整栈重建。
|
|
16
|
+
- runner 在编译配置中移除宿主的原生 `tabBar`,退出后恢复宿主原文件;导航按模块注册的 `viewKey` 解析物理页面,支持业务模块覆盖首页和扩展 tab。
|
|
15
17
|
- 页面静态资源 export、动态资源公共路径解析,以及 H5 默认导航栏兼容框架。
|
|
16
18
|
- 首页、登录、协议、WebView、启动/错误状态页面。
|
|
17
19
|
- H5 与微信小程序共用的模块页面装配 runner。
|
|
@@ -28,6 +30,37 @@
|
|
|
28
30
|
|
|
29
31
|
业务模块只能通过这些 exports 使用底座,不得相对引用 core 源码。RPC 由 `make -C frontend ts-taro-app` 生成,不能手工编辑。
|
|
30
32
|
|
|
33
|
+
## 通用顶部导航
|
|
34
|
+
|
|
35
|
+
所有自定义顶部导航统一使用公开组件 `KratosNavBar`,页面只提供标题和业务操作,不再定义导航高度、安全区、标题居中、按钮尺寸或微信胶囊避让。普通页与 tabBar 页的标题栏使用相同高度,按钮显隐不会改变标题位置。页面外层只维护水平与底部留白,顶部留白由组件处理;tabBar 页的导航栏还统一预留内容间距。
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { KratosNavBar } from '@liujitcn/kratos-taro-app-core'
|
|
39
|
+
|
|
40
|
+
// 普通页默认带返回,无法返回时回到首页。
|
|
41
|
+
<KratosNavBar title='个人信息' />
|
|
42
|
+
// tabBar 页默认不带返回,也可以不传操作按钮。
|
|
43
|
+
<KratosNavBar title='首页' variant='tab' />
|
|
44
|
+
<KratosNavBar
|
|
45
|
+
title='我的'
|
|
46
|
+
variant='tab'
|
|
47
|
+
action={{ icon: 'settings', label: '设置', onClick: openSettings }}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`back` 可显式控制返回按钮,`onBack` 可覆盖默认返回操作。`action` 包含 `label`、可选 `icon`、可选 `display` 和 `onClick`;`display` 支持 `icon`(纯图标)、`text`(纯文字)和 `both`(图标加文字),默认有图标时显示图标,否则显示文字。图标支持内置 `settings`、`read-all`、`history`,以及自定义图片地址。不传 `action` 时不渲染操作按钮。三种按钮形式共用相同高度;纯图标保持方形,文字与组合按钮按内容增宽,空间不足时省略文字。组件及 `KratosNavBarProps`、`KratosNavBarAction` 类型均从包根公开导出;业务模块应传入翻译后的标题和按钮名称。
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import type { KratosNavBarAction } from '@liujitcn/kratos-taro-app-core'
|
|
55
|
+
|
|
56
|
+
// 纯图标
|
|
57
|
+
const iconAction: KratosNavBarAction = { icon: 'settings', label: '设置', display: 'icon', onClick: openSettings }
|
|
58
|
+
// 纯文字
|
|
59
|
+
const textAction: KratosNavBarAction = { label: '全部已读', display: 'text', onClick: markAllRead }
|
|
60
|
+
// 图标加文字
|
|
61
|
+
const combinedAction: KratosNavBarAction = { icon: 'history', label: '历史', display: 'both', onClick: openHistory }
|
|
62
|
+
```
|
|
63
|
+
|
|
31
64
|
## Runner 协议
|
|
32
65
|
|
|
33
66
|
模块必须同时提供运行时定义和 `./build` 导出。构建入口返回模块名称、包根目录与页面清单;runner 根据宿主的静态 `module-manifest.ts` 解析模块顺序,扫描页面并创建事务。
|
package/dist/runner.mjs
CHANGED
|
@@ -67,6 +67,7 @@ async function main() {
|
|
|
67
67
|
if (options.cleanupOnly) return;
|
|
68
68
|
const originalAppConfig = readFileSync(appConfigFile, "utf8");
|
|
69
69
|
const manifest = JSON.parse(readFileSync(baseConfigFile, "utf8"));
|
|
70
|
+
delete manifest.tabBar;
|
|
70
71
|
const modules = await loadBuildModules(hostRoot, resolve(inputDir, "module-manifest.ts"));
|
|
71
72
|
const pageMap = /* @__PURE__ */ new Map();
|
|
72
73
|
modules.forEach((module) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
.kratos-nav-bar {
|
|
2
|
+
flex-shrink: 0;
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
padding-top: calc(env(safe-area-inset-top) + 24px);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.kratos-nav-bar--tab {
|
|
8
|
+
margin-bottom: 16px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.kratos-nav-bar__row {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 16px;
|
|
16
|
+
height: 76px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.kratos-nav-bar__title {
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 50%;
|
|
22
|
+
left: 50%;
|
|
23
|
+
max-width: calc(100% - 190px);
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
color: var(--kratos-color-text);
|
|
26
|
+
font-size: 16PX;
|
|
27
|
+
font-weight: 700;
|
|
28
|
+
line-height: 22PX;
|
|
29
|
+
text-overflow: ellipsis;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
transform: translate(-50%, -50%);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.kratos-nav-bar__button {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex: 0 0 76px;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
width: 76px;
|
|
41
|
+
height: 76px;
|
|
42
|
+
border: 1PX solid var(--kratos-color-border);
|
|
43
|
+
border-radius: 24px;
|
|
44
|
+
background: var(--kratos-color-surface);
|
|
45
|
+
color: var(--kratos-color-text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.kratos-nav-bar__back::before {
|
|
49
|
+
width: 16px;
|
|
50
|
+
height: 16px;
|
|
51
|
+
border-left: 2PX solid var(--kratos-color-primary);
|
|
52
|
+
border-bottom: 2PX solid var(--kratos-color-primary);
|
|
53
|
+
content: '';
|
|
54
|
+
transform: rotate(45deg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kratos-nav-bar__action {
|
|
58
|
+
margin-left: auto;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.kratos-nav-bar__action--label {
|
|
62
|
+
flex: 0 1 auto;
|
|
63
|
+
gap: 10px;
|
|
64
|
+
min-width: 76px;
|
|
65
|
+
max-width: calc(50% - 48px);
|
|
66
|
+
width: auto;
|
|
67
|
+
padding: 0 20px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.kratos-nav-bar__icon {
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
width: 34px;
|
|
73
|
+
height: 34px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.kratos-nav-bar__label {
|
|
77
|
+
min-width: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
font-size: 22px;
|
|
80
|
+
line-height: 28px;
|
|
81
|
+
text-overflow: ellipsis;
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.kratos-nav-bar--weapp {
|
|
86
|
+
padding-top: 44PX;
|
|
87
|
+
|
|
88
|
+
.kratos-nav-bar__row {
|
|
89
|
+
height: 88px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* 原生胶囊占据右侧,操作按钮与返回按钮统一放在左侧。 */
|
|
93
|
+
.kratos-nav-bar__action {
|
|
94
|
+
margin-left: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.kratos-nav-bar__title {
|
|
98
|
+
max-width: calc(100% - 190px - var(--kratos-nav-bar-capsule-gap));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Image, Text, View } from '@tarojs/components'
|
|
2
|
+
import Taro from '@tarojs/taro'
|
|
3
|
+
import type { CSSProperties } from 'react'
|
|
4
|
+
import { useI18n } from '../locales'
|
|
5
|
+
import { useThemeStore } from '../stores'
|
|
6
|
+
import { getMpCapsuleRightGap } from '../utils'
|
|
7
|
+
import { homeTabPage } from '../utils/navigation'
|
|
8
|
+
import './KratosNavBar.scss'
|
|
9
|
+
|
|
10
|
+
/** KratosNavBarAction 导航栏操作按钮,统一渲染尺寸、图标与无障碍文案。 */
|
|
11
|
+
export interface KratosNavBarAction {
|
|
12
|
+
/** 按钮名称,同时用作可见文字与无障碍文案。 */
|
|
13
|
+
label: string
|
|
14
|
+
/** 内置 settings、read-all、history 图标名称,或自定义图片地址。 */
|
|
15
|
+
icon?: string
|
|
16
|
+
/** 显示方式;默认有图标时显示图标,否则显示文字。 */
|
|
17
|
+
display?: 'icon' | 'text' | 'both'
|
|
18
|
+
/** 点击按钮后执行的业务操作。 */
|
|
19
|
+
onClick: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** KratosNavBarProps 普通页面与 tabBar 页面共用的顶部导航属性。 */
|
|
23
|
+
export interface KratosNavBarProps {
|
|
24
|
+
/** 居中显示的页面标题。 */
|
|
25
|
+
title: string
|
|
26
|
+
/** 普通页面默认显示返回按钮,tabBar 页面默认隐藏返回按钮。 */
|
|
27
|
+
variant?: 'page' | 'tab'
|
|
28
|
+
/** 显式覆盖返回按钮的显隐。 */
|
|
29
|
+
back?: boolean
|
|
30
|
+
/** 可选的页面操作按钮。 */
|
|
31
|
+
action?: KratosNavBarAction
|
|
32
|
+
/** 自定义返回操作;不传时返回上一页,失败后回到首页。 */
|
|
33
|
+
onBack?: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const iconPaths: Record<string, string> = {
|
|
37
|
+
settings:
|
|
38
|
+
'<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"/>',
|
|
39
|
+
'read-all': '<path d="m3 12 4 4L17 6m-5 10 9-9"/>',
|
|
40
|
+
history: '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** KratosNavBar 统一页面标题、返回与操作按钮,并处理安全区和微信胶囊避让。 */
|
|
44
|
+
export default function KratosNavBar({ title, variant = 'page', back, action, onBack }: KratosNavBarProps) {
|
|
45
|
+
const { t } = useI18n()
|
|
46
|
+
const themeStore = useThemeStore()
|
|
47
|
+
const capsuleGap = getMpCapsuleRightGap()
|
|
48
|
+
const navStyle = { '--kratos-nav-bar-capsule-gap': `${capsuleGap}px` } as CSSProperties
|
|
49
|
+
const showBack = back ?? variant === 'page'
|
|
50
|
+
const iconPath = action?.icon ? iconPaths[action.icon] : undefined
|
|
51
|
+
const color = themeStore.theme === 'light' ? '#102232' : '#e9f4f6'
|
|
52
|
+
const actionIcon = iconPath
|
|
53
|
+
? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(`<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">${iconPath}</svg>`)}`
|
|
54
|
+
: action?.icon
|
|
55
|
+
const actionDisplay = action?.display ?? (actionIcon ? 'icon' : 'text')
|
|
56
|
+
const showActionIcon = !!actionIcon && actionDisplay !== 'text'
|
|
57
|
+
const showActionLabel = !actionIcon || actionDisplay !== 'icon'
|
|
58
|
+
|
|
59
|
+
/** goBack 优先执行页面返回操作,否则返回上一页并在失败时回到首页。 */
|
|
60
|
+
function goBack() {
|
|
61
|
+
if (onBack) {
|
|
62
|
+
onBack()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
void Taro.navigateBack().catch(() => Taro.reLaunch({ url: homeTabPage }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View
|
|
70
|
+
className={`kratos-nav-bar${variant === 'tab' ? ' kratos-nav-bar--tab' : ''}${process.env.TARO_ENV === 'weapp' ? ' kratos-nav-bar--weapp' : ''}`}
|
|
71
|
+
style={navStyle}
|
|
72
|
+
>
|
|
73
|
+
<View className='kratos-nav-bar__row'>
|
|
74
|
+
{showBack && (
|
|
75
|
+
<View className='kratos-nav-bar__button kratos-nav-bar__back' aria-label={t('common.action.back')} onClick={goBack} />
|
|
76
|
+
)}
|
|
77
|
+
<View className='kratos-nav-bar__title'>{title}</View>
|
|
78
|
+
{action && (
|
|
79
|
+
<View className={`kratos-nav-bar__button kratos-nav-bar__action${showActionLabel ? ' kratos-nav-bar__action--label' : ''}`} aria-label={action.label} onClick={action.onClick}>
|
|
80
|
+
{showActionIcon && <Image className='kratos-nav-bar__icon' src={actionIcon} mode='aspectFit' aria-hidden='true' />}
|
|
81
|
+
{showActionLabel && <Text className='kratos-nav-bar__label'>{action.label}</Text>}
|
|
82
|
+
</View>
|
|
83
|
+
)}
|
|
84
|
+
</View>
|
|
85
|
+
</View>
|
|
86
|
+
)
|
|
87
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -44,3 +44,6 @@ export { default as PasswordVerifyDialog } from './components/PasswordVerifyDial
|
|
|
44
44
|
export type { PasswordVerifyDialogProps } from './components/PasswordVerifyDialog'
|
|
45
45
|
export { default as KratosEmpty } from './components/KratosEmpty'
|
|
46
46
|
export type { KratosEmptyProps } from './components/KratosEmpty'
|
|
47
|
+
/** KratosNavBar 普通页面与 tabBar 页面共用的顶部导航栏。 */
|
|
48
|
+
export { default as KratosNavBar } from './components/KratosNavBar'
|
|
49
|
+
export type { KratosNavBarAction, KratosNavBarProps } from './components/KratosNavBar'
|
package/src/locales/en-US.json
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"common.message.loading": "Loading",
|
|
14
14
|
"common.message.network_error": "Network error. Check your connection and try again.",
|
|
15
15
|
"common.message.request_error": "Request failed",
|
|
16
|
-
"common.message.unknown": "Unable to process this request. Try again later.",
|
|
17
16
|
"common.title.notice": "Notice",
|
|
18
17
|
"core.auth.session_expired": "Your session has expired. Please log in again.",
|
|
19
18
|
"core.config.missing": "Missing app configuration: {names}",
|
|
@@ -79,6 +78,8 @@
|
|
|
79
78
|
"core.login.mfa_webauthn_login_unsupported": "WebAuthn is not supported on this platform. Use a recovery code.",
|
|
80
79
|
"core.login.mfa_webauthn_login_invalid": "WebAuthn authentication did not return a valid credential.",
|
|
81
80
|
"core.login.password": "Enter password",
|
|
81
|
+
"core.login.password_show": "Show password",
|
|
82
|
+
"core.login.password_hide": "Hide password",
|
|
82
83
|
"core.login.privacy": "Privacy Policy",
|
|
83
84
|
"core.login.protocol_missing": "Terms of Service and Privacy Policy are not configured",
|
|
84
85
|
"core.login.protocol_prompt": "Read and accept the agreements first. Confirm to accept and continue.",
|
package/src/locales/ja-JP.json
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"common.message.loading": "をロード中です",
|
|
14
14
|
"common.message.network_error": "ネットワーク エラー。別のネットワークを試してください",
|
|
15
15
|
"common.message.request_error": "リクエストエラー",
|
|
16
|
-
"common.message.unknown": "は一時的に処理できません。後でもう一度試してください。",
|
|
17
16
|
"common.title.notice": "ヒント",
|
|
18
17
|
"core.auth.session_expired": "現在のページは無効です。再度ログインしてください。",
|
|
19
18
|
"core.config.missing": "モバイル端末の設定がありません: {names}",
|
|
@@ -79,6 +78,8 @@
|
|
|
79
78
|
"core.login.mfa_webauthn_login_unsupported": "このプラットフォームは WebAuthn に対応していません。リカバリーコードを使用してください。",
|
|
80
79
|
"core.login.mfa_webauthn_login_invalid": "WebAuthn 認証で有効なクレデンシャルが返されませんでした。",
|
|
81
80
|
"core.login.password": "パスワードを入力してください",
|
|
81
|
+
"core.login.password_show": "パスワードを表示",
|
|
82
|
+
"core.login.password_hide": "パスワードを非表示",
|
|
82
83
|
"core.login.privacy": "「プライバシー規約」",
|
|
83
84
|
"core.login.protocol_missing": "利用規約とプライバシー規約が設定されていません",
|
|
84
85
|
"core.login.protocol_prompt": "まずは契約内容を読んで確認してください。 「OK」をクリックすると、自動的にチェックが行われ、ログインが続行されます。",
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"common.message.loading": "正在加载",
|
|
14
14
|
"common.message.network_error": "网络错误,换个网络试试",
|
|
15
15
|
"common.message.request_error": "请求错误",
|
|
16
|
-
"common.message.unknown": "暂时无法处理,请稍后重试",
|
|
17
16
|
"common.title.notice": "提示",
|
|
18
17
|
"core.auth.session_expired": "当前页面已失效,请重新登录",
|
|
19
18
|
"core.config.missing": "移动端配置缺失:{names}",
|
|
@@ -79,6 +78,8 @@
|
|
|
79
78
|
"core.login.mfa_webauthn_login_unsupported": "当前平台不支持 WebAuthn,请使用恢复码。",
|
|
80
79
|
"core.login.mfa_webauthn_login_invalid": "WebAuthn 登录未返回有效凭据。",
|
|
81
80
|
"core.login.password": "请输入密码",
|
|
81
|
+
"core.login.password_show": "显示密码",
|
|
82
|
+
"core.login.password_hide": "隐藏密码",
|
|
82
83
|
"core.login.privacy": "《隐私协议》",
|
|
83
84
|
"core.login.protocol_missing": "服务条款和隐私协议未配置",
|
|
84
85
|
"core.login.protocol_prompt": "请先阅读并勾选协议内容,点击确定后将自动勾选并继续登录",
|
package/src/locales/zh-TW.json
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"common.message.loading": "正在載入",
|
|
14
14
|
"common.message.network_error": "網路錯誤,換個網路試試",
|
|
15
15
|
"common.message.request_error": "請求錯誤",
|
|
16
|
-
"common.message.unknown": "暫時無法處理,請稍後重試",
|
|
17
16
|
"common.title.notice": "提示",
|
|
18
17
|
"core.auth.session_expired": "當前頁面已失效,請重新登入",
|
|
19
18
|
"core.config.missing": "移動端配置缺失:{names}",
|
|
@@ -79,6 +78,8 @@
|
|
|
79
78
|
"core.login.mfa_webauthn_login_unsupported": "目前平台不支援 WebAuthn,請使用恢復碼。",
|
|
80
79
|
"core.login.mfa_webauthn_login_invalid": "WebAuthn 登入未回傳有效憑證。",
|
|
81
80
|
"core.login.password": "請輸入密碼",
|
|
81
|
+
"core.login.password_show": "顯示密碼",
|
|
82
|
+
"core.login.password_hide": "隱藏密碼",
|
|
82
83
|
"core.login.privacy": "《隱私協議》",
|
|
83
84
|
"core.login.protocol_missing": "服務條款和隱私協議未配置",
|
|
84
85
|
"core.login.protocol_prompt": "請先閱讀並勾選協議內容,點選確定後將自動勾選並繼續登入",
|
package/src/navigation.ts
CHANGED
|
@@ -197,8 +197,6 @@ const defaultMenuTitleKeys: Record<string, string> = {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
let tabNavigationTarget: string | undefined
|
|
200
|
-
const nativeTabViewKeys = ['HOME', 'PROFILE_HOME']
|
|
201
|
-
const resetTabViewKeys = ['MESSAGE_INBOX']
|
|
202
200
|
|
|
203
201
|
function localizedDefaultAppMenus(): AppMenu[] {
|
|
204
202
|
return defaultAppMenus.map((menu) => ({
|
|
@@ -325,7 +323,7 @@ export function navigateAppView(viewKey: string, params: Record<string, string>
|
|
|
325
323
|
navigateAppRoute(`${path}${query ? `?${query}` : ''}`)
|
|
326
324
|
}
|
|
327
325
|
|
|
328
|
-
/**
|
|
326
|
+
/** 切换自绘 tab:H5 替换路由,微信端复用页面栈,避免重建整个页面栈。 */
|
|
329
327
|
function navigateTabRoute(url: string): void {
|
|
330
328
|
const targetRoute = url.slice(1).split('?', 1)[0].replace(/^\/+/, '')
|
|
331
329
|
const pages = getCurrentPages()
|
|
@@ -338,41 +336,17 @@ function navigateTabRoute(url: string): void {
|
|
|
338
336
|
if (tabNavigationTarget === targetRoute) tabNavigationTarget = undefined
|
|
339
337
|
}
|
|
340
338
|
if (process.env.TARO_ENV === 'h5') {
|
|
341
|
-
void Taro.redirectTo({ url }).then(release,
|
|
342
|
-
void Taro.reLaunch({ url }).then(release, release)
|
|
343
|
-
})
|
|
339
|
+
void Taro.redirectTo({ url }).then(release, release)
|
|
344
340
|
return
|
|
345
341
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
.catch(() => {
|
|
350
|
-
navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release)
|
|
351
|
-
})
|
|
352
|
-
return
|
|
353
|
-
}
|
|
354
|
-
if (resetTabViewKeys.some((viewKey) => resolveStaticView(viewKey) === targetRoute)) {
|
|
355
|
-
void Taro.reLaunch({ url }).then(release, release)
|
|
342
|
+
const targetIndex = pages.findIndex((page) => page.route?.replace(/^\/+/, '') === targetRoute)
|
|
343
|
+
if (targetIndex >= 0 && targetIndex < currentIndex) {
|
|
344
|
+
void Taro.navigateBack({ delta: currentIndex - targetIndex }).then(release, release)
|
|
356
345
|
return
|
|
357
346
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
/** 在没有原生 tab 路由的动态菜单上复用页面栈。 */
|
|
362
|
-
function navigateTabRouteInStack(
|
|
363
|
-
url: string,
|
|
364
|
-
targetRoute: string,
|
|
365
|
-
pages: ReturnType<typeof getCurrentPages>,
|
|
366
|
-
currentIndex: number,
|
|
367
|
-
release: () => void,
|
|
368
|
-
): void {
|
|
369
|
-
const targetIndex = pages.findIndex((page) => page.route?.replace(/^\/+/, '') === targetRoute)
|
|
370
|
-
const navigation =
|
|
371
|
-
targetIndex >= 0 && targetIndex < currentIndex
|
|
372
|
-
? Taro.navigateBack({ delta: currentIndex - targetIndex })
|
|
373
|
-
: Taro.navigateTo({ url })
|
|
374
|
-
void navigation.then(release).catch(() => {
|
|
375
|
-
void Taro.reLaunch({ url }).then(release, release)
|
|
347
|
+
void Taro.navigateTo({ url }).then(release, () => {
|
|
348
|
+
// 小程序页面栈满时只替换栈顶,保留其他 tab 的已加载页面。
|
|
349
|
+
void Taro.redirectTo({ url }).then(release, release)
|
|
376
350
|
})
|
|
377
351
|
}
|
|
378
352
|
|
package/src/runner.ts
CHANGED
|
@@ -69,6 +69,8 @@ async function main(): Promise<void> {
|
|
|
69
69
|
|
|
70
70
|
const originalAppConfig = readFileSync(appConfigFile, 'utf8')
|
|
71
71
|
const manifest = JSON.parse(readFileSync(baseConfigFile, 'utf8')) as AppManifest
|
|
72
|
+
// 所有宿主统一使用 KratosTabBar,原生 tab 声明会限制普通页面跳转并要求额外组件。
|
|
73
|
+
delete manifest.tabBar
|
|
72
74
|
const modules = await loadBuildModules(hostRoot, resolve(inputDir, 'module-manifest.ts'))
|
|
73
75
|
const pageMap = new Map<string, PageEntry>()
|
|
74
76
|
modules.forEach((module) => {
|
|
@@ -105,88 +105,6 @@
|
|
|
105
105
|
box-shadow: var(--kratos-glow);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/* 顶栏 */
|
|
109
|
-
.k-page-nav {
|
|
110
|
-
position: relative;
|
|
111
|
-
display: flex;
|
|
112
|
-
align-items: center;
|
|
113
|
-
justify-content: space-between;
|
|
114
|
-
box-sizing: border-box;
|
|
115
|
-
height: calc(88px + env(safe-area-inset-top));
|
|
116
|
-
padding-top: env(safe-area-inset-top);
|
|
117
|
-
}
|
|
118
|
-
.k-page-nav--weapp {
|
|
119
|
-
height: calc(88px + 44PX);
|
|
120
|
-
padding-top: 44PX;
|
|
121
|
-
}
|
|
122
|
-
.k-page-nav__back,
|
|
123
|
-
.k-page-nav__side {
|
|
124
|
-
display: flex;
|
|
125
|
-
align-items: center;
|
|
126
|
-
justify-content: center;
|
|
127
|
-
flex: 0 0 76px;
|
|
128
|
-
width: 76px;
|
|
129
|
-
height: 76px;
|
|
130
|
-
}
|
|
131
|
-
.k-page-nav__back {
|
|
132
|
-
border: 1PX solid var(--kratos-color-border);
|
|
133
|
-
border-radius: 24px;
|
|
134
|
-
background: var(--kratos-color-surface);
|
|
135
|
-
}
|
|
136
|
-
.k-page-nav__back::before {
|
|
137
|
-
width: 16px;
|
|
138
|
-
height: 16px;
|
|
139
|
-
border-left: 2PX solid var(--kratos-color-primary);
|
|
140
|
-
border-bottom: 2PX solid var(--kratos-color-primary);
|
|
141
|
-
content: '';
|
|
142
|
-
transform: rotate(45deg);
|
|
143
|
-
}
|
|
144
|
-
.k-page-nav__title {
|
|
145
|
-
position: absolute;
|
|
146
|
-
left: 50%;
|
|
147
|
-
max-width: calc(100% - 190px);
|
|
148
|
-
overflow: hidden;
|
|
149
|
-
color: var(--kratos-color-text);
|
|
150
|
-
font-size: 16PX;
|
|
151
|
-
font-weight: 700;
|
|
152
|
-
text-overflow: ellipsis;
|
|
153
|
-
transform: translateX(-50%);
|
|
154
|
-
white-space: nowrap;
|
|
155
|
-
}
|
|
156
|
-
.k-top {
|
|
157
|
-
position: relative;
|
|
158
|
-
display: flex;
|
|
159
|
-
align-items: center;
|
|
160
|
-
/* 标题绝对居中后,流内子元素(右侧动作按钮)统一靠右排布 */
|
|
161
|
-
justify-content: flex-end;
|
|
162
|
-
gap: 16px;
|
|
163
|
-
/* weapp 原生胶囊避让:页面通过 getMpCapsuleRightGap() 写入该变量,
|
|
164
|
-
其余平台未设置时回退 0,不影响布局。 */
|
|
165
|
-
padding-right: var(--mp-capsule-gap, 0px);
|
|
166
|
-
}
|
|
167
|
-
/* #ifdef weapp */
|
|
168
|
-
/* 双写类名提高优先级:页面级 padding 简写(如 .message-header)不得压掉导航带 */
|
|
169
|
-
.k-top.k-top {
|
|
170
|
-
/* 小程序右上角是原生胶囊:操作按钮靠左,标题行与胶囊同行(同 k-page-nav--weapp 几何) */
|
|
171
|
-
justify-content: flex-start;
|
|
172
|
-
height: calc(88px + 44PX);
|
|
173
|
-
padding-top: 44PX;
|
|
174
|
-
padding-right: 0;
|
|
175
|
-
}
|
|
176
|
-
/* #endif */
|
|
177
|
-
.k-top__title {
|
|
178
|
-
position: absolute;
|
|
179
|
-
left: 50%;
|
|
180
|
-
max-width: calc(100% - 190px);
|
|
181
|
-
overflow: hidden;
|
|
182
|
-
color: var(--kratos-color-text);
|
|
183
|
-
font-size: 16PX;
|
|
184
|
-
font-weight: 700;
|
|
185
|
-
text-overflow: ellipsis;
|
|
186
|
-
transform: translateX(-50%);
|
|
187
|
-
white-space: nowrap;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
108
|
/* 方形图标按钮 + 角标 */
|
|
191
109
|
.k-ic {
|
|
192
110
|
position: relative;
|
|
@@ -9,41 +9,10 @@ page {
|
|
|
9
9
|
background: var(--kratos-bg-gradient);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
.home-page--weapp .home-content {
|
|
13
|
-
padding-top: 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/* 小程序端首页标题占导航带:与 k-page-nav--weapp 同几何,标题与胶囊同行 */
|
|
17
|
-
.home-page--weapp .home-top {
|
|
18
|
-
box-sizing: border-box;
|
|
19
|
-
height: calc(88px + 44PX);
|
|
20
|
-
padding: 44PX 4px 0;
|
|
21
|
-
display: flex;
|
|
22
|
-
align-items: center;
|
|
23
|
-
justify-content: center;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.home-page--weapp .home-top__title {
|
|
27
|
-
margin-top: 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
12
|
.home-content {
|
|
31
13
|
position: relative;
|
|
32
14
|
z-index: 1;
|
|
33
|
-
padding:
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.home-top {
|
|
37
|
-
padding: 8px 4px 24px;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* 页面大标题:与其他页面导航标题同规格,向原生导航栏看齐 */
|
|
41
|
-
.home-top__title {
|
|
42
|
-
margin-top: 14px;
|
|
43
|
-
color: var(--kratos-color-text);
|
|
44
|
-
font-size: 16PX;
|
|
45
|
-
font-weight: 700;
|
|
46
|
-
text-align: center;
|
|
15
|
+
padding: 0 20px 220px;
|
|
47
16
|
}
|
|
48
17
|
|
|
49
18
|
.home-hero {
|
|
@@ -5,9 +5,10 @@ import defaultLogo from '@liujitcn/kratos-taro-app-core/static/images/logo_icon.
|
|
|
5
5
|
import { useSettingStore, useThemeStore } from '../../../stores'
|
|
6
6
|
import { formatSrc } from '../../../utils'
|
|
7
7
|
import './index.scss'
|
|
8
|
+
import KratosNavBar from '../../../components/KratosNavBar'
|
|
8
9
|
import { useI18n } from '../../../locales'
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
+
/** HomePage 展示应用首页,并复用固定高度的 Tab 标题栏。 */
|
|
11
12
|
export default function HomePage() {
|
|
12
13
|
const { t } = useI18n()
|
|
13
14
|
const themeStore = useThemeStore()
|
|
@@ -41,15 +42,13 @@ export default function HomePage() {
|
|
|
41
42
|
]
|
|
42
43
|
return (
|
|
43
44
|
<View
|
|
44
|
-
className={`home-page
|
|
45
|
+
className={`home-page ${
|
|
45
46
|
themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'
|
|
46
47
|
}`}
|
|
47
48
|
>
|
|
48
49
|
<View className='k-grid-bg' aria-hidden />
|
|
49
50
|
<View className='home-content'>
|
|
50
|
-
<
|
|
51
|
-
<View className='home-top__title'>{mainTitle}</View>
|
|
52
|
-
</View>
|
|
51
|
+
<KratosNavBar title={mainTitle} variant='tab' />
|
|
53
52
|
|
|
54
53
|
<View className='k-glass k-glass--lit home-hero'>
|
|
55
54
|
<View className='home-hero__main'>
|
|
@@ -673,7 +673,7 @@ export default function LoginPage() {
|
|
|
673
673
|
/>
|
|
674
674
|
<View
|
|
675
675
|
className='password-toggle'
|
|
676
|
-
aria-label={passwordVisible ? '
|
|
676
|
+
aria-label={passwordVisible ? t('core.login.password_hide') : t('core.login.password_show')}
|
|
677
677
|
onClick={() => setPasswordVisible((visible) => !visible)}
|
|
678
678
|
>
|
|
679
679
|
<Image
|
|
@@ -761,7 +761,7 @@ export default function LoginPage() {
|
|
|
761
761
|
/>
|
|
762
762
|
<View
|
|
763
763
|
className='password-toggle'
|
|
764
|
-
aria-label={miniPasswordVisible ? '
|
|
764
|
+
aria-label={miniPasswordVisible ? t('core.login.password_hide') : t('core.login.password_show')}
|
|
765
765
|
onClick={() => setMiniPasswordVisible((visible) => !visible)}
|
|
766
766
|
>
|
|
767
767
|
<Image
|
|
@@ -3,7 +3,7 @@ import Taro, { useLoad } from '@tarojs/taro'
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useSettingStore, useThemeStore } from '../../../stores'
|
|
5
5
|
import { useI18n } from '../../../locales'
|
|
6
|
-
import
|
|
6
|
+
import KratosNavBar from '../../../components/KratosNavBar'
|
|
7
7
|
import './protocal.scss'
|
|
8
8
|
|
|
9
9
|
const protocolClassMap: Record<string, string> = {
|
|
@@ -39,7 +39,7 @@ function decorateProtocolContent(html: string): string {
|
|
|
39
39
|
)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/** ProtocolPage 展示服务条款与隐私协议。 */
|
|
43
43
|
export default function ProtocolPage() {
|
|
44
44
|
const { t } = useI18n()
|
|
45
45
|
const themeStore = useThemeStore()
|
|
@@ -68,18 +68,9 @@ export default function ProtocolPage() {
|
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
/** 返回来源页,页面栈为空时回到首页。 */
|
|
72
|
-
const onBack = () => {
|
|
73
|
-
void Taro.navigateBack({ fail: () => void Taro.reLaunch({ url: homeTabPage }) })
|
|
74
|
-
}
|
|
75
|
-
|
|
76
71
|
return (
|
|
77
72
|
<View className={`protocol-page ${themeStore.theme === 'light' ? 'theme-light' : 'theme-dark'}`}>
|
|
78
|
-
<
|
|
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>
|
|
73
|
+
<KratosNavBar title={pageTitle} />
|
|
83
74
|
<View className='protocol-card'>
|
|
84
75
|
<RichText className='protocol-content' nodes={content} />
|
|
85
76
|
</View>
|