@liujitcn/kratos-taro-app-core 0.0.97 → 0.1.0
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 +12 -1
- package/package.json +1 -1
- package/src/components/KratosNavBar.tsx +1 -1
- package/src/components/KratosTabBar.tsx +4 -10
- package/src/navigation.ts +14 -1
- package/src/stores/theme.ts +110 -7
- package/src/styles/base.scss +3 -3
- package/src/utils/http.ts +12 -3
- package/src/views/pages/login/login.tsx +3 -3
package/README.md
CHANGED
|
@@ -12,12 +12,23 @@
|
|
|
12
12
|
- 登录阶段支持 TOTP、WebAuthn 和一次性恢复码校验。
|
|
13
13
|
- 公开 Base MFA 请求及通用密码弹窗、绑定面板和恢复码弹窗,供登录页与业务模块复用同一套绑定界面。
|
|
14
14
|
- 接口驱动的逻辑路由、访问模式、自绘 tabBar 和稳定 `viewKey`。
|
|
15
|
-
- tab 切换统一使用普通页面路由:H5
|
|
15
|
+
- tab 切换统一使用普通页面路由:H5 和微信端优先替换当前路由,微信端替换失败时再复用页面栈,避免页面入栈/出栈动画。
|
|
16
16
|
- runner 在编译配置中移除宿主的原生 `tabBar`,退出后恢复宿主原文件;导航按模块注册的 `viewKey` 解析物理页面,支持业务模块覆盖首页和扩展 tab。
|
|
17
17
|
- 页面静态资源 export、动态资源公共路径解析,以及 H5 默认导航栏兼容框架。
|
|
18
18
|
- 首页、登录、协议、WebView、启动/错误状态页面。
|
|
19
19
|
- H5 与微信小程序共用的模块页面装配 runner。
|
|
20
20
|
|
|
21
|
+
主题 store 公开 `useThemeStore().colors` 和 `registerThemeColors`。页面脚本使用语义颜色(例如 `colors.primary`、`colors.textMuted`),业务模块可以在初始化时传入 `light`、`dark` 的局部字段覆盖 Core 默认值;未传入的字段保持默认值,覆盖会立即作用于当前主题。
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { registerThemeColors } from '@liujitcn/kratos-taro-app-core/stores'
|
|
25
|
+
|
|
26
|
+
registerThemeColors({
|
|
27
|
+
light: { primary: '#039e8c' },
|
|
28
|
+
dark: { primary: '#28f0d4' },
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
21
32
|
## 公开入口
|
|
22
33
|
|
|
23
34
|
登录页返回会清理本次回跳并替换到首页;直接打开登录页清理旧 `lastRoute`,登录成功默认进入首页。主动登录、路由守卫和会话过期重登通过 `redirect=1` 标记保留本次业务目标及查询参数,成功跳转后才清理目标。登录页每次显示都会检查有效登录态,已登录则替换回首页,避免浏览器回退重新停留在登录页。H5 与微信小程序统一使用 `reLaunch` 替换登录入口并重置应用页面栈。
|
package/package.json
CHANGED
|
@@ -48,7 +48,7 @@ export default function KratosNavBar({ title, variant = 'page', back, action, on
|
|
|
48
48
|
const navStyle = { '--kratos-nav-bar-capsule-gap': `${capsuleGap}px` } as CSSProperties
|
|
49
49
|
const showBack = back ?? variant === 'page'
|
|
50
50
|
const iconPath = action?.icon ? iconPaths[action.icon] : undefined
|
|
51
|
-
const color = themeStore.
|
|
51
|
+
const color = themeStore.colors.text
|
|
52
52
|
const actionIcon = iconPath
|
|
53
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
54
|
: action?.icon
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Image, Text, View } from '@tarojs/components'
|
|
2
2
|
import { resolveModuleIcon, resolveStaticView } from '../module'
|
|
3
3
|
import { APP_MENU_ROOT_ID, navigateAppRoute, useAppMenuBadges, useAppNavigation } from '../navigation'
|
|
4
|
-
import { useThemeStore } from '../stores'
|
|
4
|
+
import { useThemeStore, type ThemeColors } from '../stores'
|
|
5
5
|
import { resolveRootMenuId } from '../navigation-tree.mjs'
|
|
6
6
|
import './KratosTabBar.scss'
|
|
7
7
|
|
|
@@ -20,16 +20,10 @@ const TAB_ICON_PATHS: Record<string, string> = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** 按设计稿线稿生成随主题与激活态取色的图标地址,未知 viewKey 返回空串走后端图标兜底。 */
|
|
23
|
-
const tabIconSrc = (viewKey: string, active: boolean,
|
|
23
|
+
const tabIconSrc = (viewKey: string, active: boolean, colors: ThemeColors): string => {
|
|
24
24
|
const path = TAB_ICON_PATHS[viewKey]
|
|
25
25
|
if (!path) return ''
|
|
26
|
-
const color = active
|
|
27
|
-
? theme === 'light'
|
|
28
|
-
? '#039e8c'
|
|
29
|
-
: '#28f0d4'
|
|
30
|
-
: theme === 'light'
|
|
31
|
-
? '#9aabbb'
|
|
32
|
-
: '#4e6378'
|
|
26
|
+
const color = active ? colors.primary : colors.textDim
|
|
33
27
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${path}"/></svg>`
|
|
34
28
|
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
|
35
29
|
}
|
|
@@ -56,7 +50,7 @@ export function KratosTabBar({ route }: KratosTabBarProps) {
|
|
|
56
50
|
>
|
|
57
51
|
{tabBar.map((item) => {
|
|
58
52
|
const active = activeMenu.id === item.id
|
|
59
|
-
const designIcon = tabIconSrc(item.viewKey, active, themeStore.
|
|
53
|
+
const designIcon = tabIconSrc(item.viewKey, active, themeStore.colors)
|
|
60
54
|
const icon = designIcon || resolveModuleIcon(active && item.selectedIcon ? item.selectedIcon : item.icon)
|
|
61
55
|
return (
|
|
62
56
|
<View
|
package/src/navigation.ts
CHANGED
|
@@ -323,7 +323,7 @@ export function navigateAppView(viewKey: string, params: Record<string, string>
|
|
|
323
323
|
navigateAppRoute(`${path}${query ? `?${query}` : ''}`)
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
/** 切换自绘 tab
|
|
326
|
+
/** 切换自绘 tab:直接替换当前页面,避免微信端页面入栈/出栈动画造成闪烁。 */
|
|
327
327
|
function navigateTabRoute(url: string): void {
|
|
328
328
|
const targetRoute = url.slice(1).split('?', 1)[0].replace(/^\/+/, '')
|
|
329
329
|
const pages = getCurrentPages()
|
|
@@ -339,6 +339,19 @@ function navigateTabRoute(url: string): void {
|
|
|
339
339
|
void Taro.redirectTo({ url }).then(release, release)
|
|
340
340
|
return
|
|
341
341
|
}
|
|
342
|
+
void Taro.redirectTo({ url }).then(release, () => {
|
|
343
|
+
navigateTabRouteInStack(url, pages, currentIndex, release)
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** 微信端替换路由失败时复用已有页面栈。 */
|
|
348
|
+
function navigateTabRouteInStack(
|
|
349
|
+
url: string,
|
|
350
|
+
pages: ReturnType<typeof getCurrentPages>,
|
|
351
|
+
currentIndex: number,
|
|
352
|
+
release: () => void,
|
|
353
|
+
): void {
|
|
354
|
+
const targetRoute = url.slice(1).split('?', 1)[0].replace(/^\/+/, '')
|
|
342
355
|
const targetIndex = pages.findIndex((page) => page.route?.replace(/^\/+/, '') === targetRoute)
|
|
343
356
|
if (targetIndex >= 0 && targetIndex < currentIndex) {
|
|
344
357
|
void Taro.navigateBack({ delta: currentIndex - targetIndex }).then(release, release)
|
package/src/stores/theme.ts
CHANGED
|
@@ -6,6 +6,98 @@ export type ThemeMode = 'dark' | 'light' | 'auto'
|
|
|
6
6
|
/** 实际生效的主题。 */
|
|
7
7
|
export type ResolvedTheme = 'dark' | 'light'
|
|
8
8
|
|
|
9
|
+
/** 当前主题可供脚本使用的语义颜色。 */
|
|
10
|
+
export interface ThemeColors {
|
|
11
|
+
background: string
|
|
12
|
+
surface: string
|
|
13
|
+
surface2: string
|
|
14
|
+
surfaceRgb: string
|
|
15
|
+
primary: string
|
|
16
|
+
primaryDeep: string
|
|
17
|
+
onPrimary: string
|
|
18
|
+
info: string
|
|
19
|
+
warning: string
|
|
20
|
+
danger: string
|
|
21
|
+
violet: string
|
|
22
|
+
text: string
|
|
23
|
+
textMuted: string
|
|
24
|
+
textDim: string
|
|
25
|
+
aiPrimary: string
|
|
26
|
+
aiDanger: string
|
|
27
|
+
aiWarning: string
|
|
28
|
+
aiViolet: string
|
|
29
|
+
aiTextMuted: string
|
|
30
|
+
aiTextDim: string
|
|
31
|
+
navigationBarText: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 引用模块可按主题覆盖的颜色字段。未提供的字段继续使用 Core 默认值。 */
|
|
35
|
+
export type ThemeColorOverrides = {
|
|
36
|
+
light?: Partial<ThemeColors>
|
|
37
|
+
dark?: Partial<ThemeColors>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 明暗主题的统一语义颜色表;页面脚本禁止直接维护主题颜色三元表达式。 */
|
|
41
|
+
export const themeColors: Record<ResolvedTheme, ThemeColors> = {
|
|
42
|
+
light: {
|
|
43
|
+
background: '#eef3f8',
|
|
44
|
+
surface: '#f4f9fd',
|
|
45
|
+
surface2: '#eef4fa',
|
|
46
|
+
surfaceRgb: '255, 255, 255',
|
|
47
|
+
primary: '#039e8c',
|
|
48
|
+
primaryDeep: '#00b3a0',
|
|
49
|
+
onPrimary: '#ffffff',
|
|
50
|
+
info: '#1a93d8',
|
|
51
|
+
warning: '#d9880f',
|
|
52
|
+
danger: '#e0324f',
|
|
53
|
+
violet: '#5f4fe0',
|
|
54
|
+
text: '#102232',
|
|
55
|
+
textMuted: '#5a7083',
|
|
56
|
+
textDim: '#9aabbb',
|
|
57
|
+
aiPrimary: '#039e8c',
|
|
58
|
+
aiDanger: '#e0324f',
|
|
59
|
+
aiWarning: '#d9880f',
|
|
60
|
+
aiViolet: '#5f4fe0',
|
|
61
|
+
aiTextMuted: '#5a7083',
|
|
62
|
+
aiTextDim: '#9aabbb',
|
|
63
|
+
navigationBarText: '#000000',
|
|
64
|
+
},
|
|
65
|
+
dark: {
|
|
66
|
+
background: '#060a12',
|
|
67
|
+
surface: '#060a12',
|
|
68
|
+
surface2: '#121b2a',
|
|
69
|
+
surfaceRgb: '255, 255, 255',
|
|
70
|
+
primary: '#28f0d4',
|
|
71
|
+
primaryDeep: '#13c7b4',
|
|
72
|
+
onPrimary: '#031513',
|
|
73
|
+
info: '#45c2ff',
|
|
74
|
+
warning: '#ffb648',
|
|
75
|
+
danger: '#ff4d6d',
|
|
76
|
+
violet: '#9b8cff',
|
|
77
|
+
text: '#e9f4f6',
|
|
78
|
+
textMuted: '#8aa0b4',
|
|
79
|
+
textDim: '#4e6378',
|
|
80
|
+
aiPrimary: '#28f0d4',
|
|
81
|
+
aiDanger: '#ff4d6d',
|
|
82
|
+
aiWarning: '#ffb648',
|
|
83
|
+
aiViolet: '#9b8cff',
|
|
84
|
+
aiTextMuted: '#8aa0b4',
|
|
85
|
+
aiTextDim: '#8aa0b4',
|
|
86
|
+
navigationBarText: '#ffffff',
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const themeColorOverrides: Record<ResolvedTheme, Partial<ThemeColors>> = {
|
|
91
|
+
light: {},
|
|
92
|
+
dark: {},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 获取指定主题的语义颜色。 */
|
|
96
|
+
export const getThemeColors = (theme: ResolvedTheme): ThemeColors => ({
|
|
97
|
+
...themeColors[theme],
|
|
98
|
+
...themeColorOverrides[theme],
|
|
99
|
+
})
|
|
100
|
+
|
|
9
101
|
const THEME_MODE_STORAGE_KEY = 'kratos_theme_mode'
|
|
10
102
|
const DEFAULT_THEME_MODE: ThemeMode = 'light'
|
|
11
103
|
|
|
@@ -35,13 +127,13 @@ const readStoredMode = (): ThemeMode => {
|
|
|
35
127
|
/** 主题实际生效时给导航栏与页面底色染色,覆盖全局配置的静态浅色。 */
|
|
36
128
|
const applyChrome = (theme: ResolvedTheme) => {
|
|
37
129
|
try {
|
|
38
|
-
const
|
|
130
|
+
const colors = getThemeColors(theme)
|
|
39
131
|
void Taro.setNavigationBarColor({
|
|
40
|
-
frontColor:
|
|
41
|
-
backgroundColor,
|
|
132
|
+
frontColor: colors.navigationBarText,
|
|
133
|
+
backgroundColor: colors.surface,
|
|
42
134
|
fail: () => undefined,
|
|
43
135
|
})
|
|
44
|
-
void Taro.setBackgroundColor({ backgroundColor, fail: () => undefined })
|
|
136
|
+
void Taro.setBackgroundColor({ backgroundColor: colors.background, fail: () => undefined })
|
|
45
137
|
} catch {
|
|
46
138
|
// 部分平台/版本未暴露导航栏染色 API,跳过不影响主题类。
|
|
47
139
|
}
|
|
@@ -77,6 +169,7 @@ const watchSystemTheme = (onChange: (theme: ResolvedTheme) => void) => {
|
|
|
77
169
|
export interface ThemeStoreState {
|
|
78
170
|
mode: ThemeMode
|
|
79
171
|
theme: ResolvedTheme
|
|
172
|
+
colors: ThemeColors
|
|
80
173
|
setMode: (mode: ThemeMode) => void
|
|
81
174
|
}
|
|
82
175
|
|
|
@@ -84,6 +177,7 @@ export interface ThemeStoreState {
|
|
|
84
177
|
export const useThemeStore = create<ThemeStoreState>((set) => ({
|
|
85
178
|
mode: readStoredMode(),
|
|
86
179
|
theme: 'dark',
|
|
180
|
+
colors: getThemeColors('dark'),
|
|
87
181
|
setMode(mode) {
|
|
88
182
|
const theme = mode === 'auto' ? readSystemTheme() : mode
|
|
89
183
|
try {
|
|
@@ -91,16 +185,25 @@ export const useThemeStore = create<ThemeStoreState>((set) => ({
|
|
|
91
185
|
} catch {
|
|
92
186
|
// 存储写入失败不影响当前会话主题。
|
|
93
187
|
}
|
|
94
|
-
set({ mode, theme })
|
|
188
|
+
set({ mode, theme, colors: getThemeColors(theme) })
|
|
95
189
|
},
|
|
96
190
|
}))
|
|
97
191
|
|
|
192
|
+
/** 注册引用模块的主题颜色覆盖,并立即刷新当前主题颜色。 */
|
|
193
|
+
export const registerThemeColors = (overrides: ThemeColorOverrides): void => {
|
|
194
|
+
if (overrides.light) Object.assign(themeColorOverrides.light, overrides.light)
|
|
195
|
+
if (overrides.dark) Object.assign(themeColorOverrides.dark, overrides.dark)
|
|
196
|
+
useThemeStore.setState({ colors: getThemeColors(useThemeStore.getState().theme) })
|
|
197
|
+
}
|
|
198
|
+
|
|
98
199
|
// 初始化:解析启动主题、染色导航栏并订阅系统主题变化。
|
|
99
200
|
const initialMode = useThemeStore.getState().mode
|
|
100
201
|
const initialTheme = initialMode === 'auto' ? readSystemTheme() : initialMode
|
|
101
|
-
useThemeStore.setState({ theme: initialTheme })
|
|
202
|
+
useThemeStore.setState({ theme: initialTheme, colors: getThemeColors(initialTheme) })
|
|
102
203
|
watchSystemTheme((theme) => {
|
|
103
|
-
if (useThemeStore.getState().mode === 'auto')
|
|
204
|
+
if (useThemeStore.getState().mode === 'auto') {
|
|
205
|
+
useThemeStore.setState({ theme, colors: getThemeColors(theme) })
|
|
206
|
+
}
|
|
104
207
|
})
|
|
105
208
|
useThemeStore.subscribe((state, prev) => {
|
|
106
209
|
if (state.theme !== prev.theme) applyChrome(state.theme)
|
package/src/styles/base.scss
CHANGED
|
@@ -19,10 +19,10 @@ html {
|
|
|
19
19
|
page,
|
|
20
20
|
body {
|
|
21
21
|
min-height: 100%;
|
|
22
|
-
color: #
|
|
23
|
-
font-size:
|
|
22
|
+
color: var(--kratos-color-text, #102232);
|
|
23
|
+
font-size: 14PX;
|
|
24
24
|
line-height: normal;
|
|
25
|
-
background: #
|
|
25
|
+
background: var(--kratos-color-background, #eef3f8);
|
|
26
26
|
font-family:
|
|
27
27
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
|
28
28
|
'Microsoft YaHei', sans-serif;
|
package/src/utils/http.ts
CHANGED
|
@@ -268,7 +268,16 @@ const RELOGIN_PROMPT_TIMEOUT = 60_000
|
|
|
268
268
|
/** 提示会话过期并进入携带本次业务回跳标记的登录页。 */
|
|
269
269
|
async function promptRelogin(): Promise<void> {
|
|
270
270
|
// 401 来自旧会话的在途请求而当前已是有效登录态时直接忽略,避免刚登录成功被弹窗打断。
|
|
271
|
-
if (hasValidToken())
|
|
271
|
+
if (hasValidToken()) {
|
|
272
|
+
// 本地令牌未过期但服务端已判定失效时,先强制续签一次;续签成功即静默恢复,
|
|
273
|
+
// 续签失败清空残留登录态后继续弹窗,避免停留在“本地有效”的假登录态。
|
|
274
|
+
try {
|
|
275
|
+
await handleTokenRefresh()
|
|
276
|
+
return
|
|
277
|
+
} catch {
|
|
278
|
+
silentClearAuthData()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
272
281
|
const pages = getCurrentPages()
|
|
273
282
|
// 登录页自身的请求失效只清理残留登录态,避免确认后重载登录页丢失已填写的表单。
|
|
274
283
|
if (pages[pages.length - 1]?.route === 'pages/login/login') {
|
|
@@ -278,7 +287,7 @@ async function promptRelogin(): Promise<void> {
|
|
|
278
287
|
if (isPromptingRelogin) return
|
|
279
288
|
isPromptingRelogin = true
|
|
280
289
|
try {
|
|
281
|
-
const
|
|
290
|
+
const colors = useThemeStore.getState().colors
|
|
282
291
|
const modal = await Promise.race([
|
|
283
292
|
// 按钮文案复用通用确认键:平台对 confirmText 限制 4 个汉字/8 字符,长文案会被整体拒绝。
|
|
284
293
|
Taro.showModal({
|
|
@@ -286,7 +295,7 @@ async function promptRelogin(): Promise<void> {
|
|
|
286
295
|
content: t('core.auth.session_expired'),
|
|
287
296
|
showCancel: false,
|
|
288
297
|
confirmText: t('common.action.confirm'),
|
|
289
|
-
confirmColor:
|
|
298
|
+
confirmColor: colors.primary,
|
|
290
299
|
}).catch(() => undefined as undefined),
|
|
291
300
|
new Promise<undefined>((resolve) =>
|
|
292
301
|
setTimeout(() => resolve(undefined), RELOGIN_PROMPT_TIMEOUT),
|
|
@@ -138,7 +138,7 @@ export default function LoginPage() {
|
|
|
138
138
|
const showTenantCode = settings?.get('showTenantCode') !== 'false'
|
|
139
139
|
/** 按管理端 View、Hide 视觉生成本端密码可见性图标。 */
|
|
140
140
|
const passwordIconSrc = (visible: boolean) => {
|
|
141
|
-
const color = themeStore.
|
|
141
|
+
const color = themeStore.colors.textMuted
|
|
142
142
|
const path = visible ? passwordIconPaths.hide : passwordIconPaths.view
|
|
143
143
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="${color}"><path d="${path}"/></svg>`
|
|
144
144
|
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
|
@@ -310,8 +310,8 @@ export default function LoginPage() {
|
|
|
310
310
|
content: t('core.login.protocol_prompt'),
|
|
311
311
|
confirmText: t('common.action.confirm'),
|
|
312
312
|
cancelText: t('common.action.cancel'),
|
|
313
|
-
confirmColor: themeStore.
|
|
314
|
-
cancelColor: themeStore.
|
|
313
|
+
confirmColor: themeStore.colors.primary,
|
|
314
|
+
cancelColor: themeStore.colors.textMuted,
|
|
315
315
|
})
|
|
316
316
|
if (result.confirm) setAgreed(true)
|
|
317
317
|
return result.confirm
|