@liujitcn/kratos-taro-app-core 0.0.96 → 0.0.99

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
@@ -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
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git@github.com:liujitcn/kratos-admin.git"
9
9
  },
10
- "version": "0.0.96",
10
+ "version": "0.0.99",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -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.theme === 'light' ? '#102232' : '#e9f4f6'
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, theme: string): string => {
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.theme)
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:H5 替换路由,微信端复用页面栈,避免重建整个页面栈。 */
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)
@@ -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 backgroundColor = theme === 'light' ? '#f4f9fd' : '#060a12'
130
+ const colors = getThemeColors(theme)
39
131
  void Taro.setNavigationBarColor({
40
- frontColor: theme === 'light' ? '#000000' : '#ffffff',
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') useThemeStore.setState({ theme })
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)
@@ -19,10 +19,10 @@ html {
19
19
  page,
20
20
  body {
21
21
  min-height: 100%;
22
- color: #333;
23
- font-size: 16PX;
22
+ color: var(--kratos-color-text, #102232);
23
+ font-size: 14PX;
24
24
  line-height: normal;
25
- background: #f7f7f8;
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
@@ -278,7 +278,7 @@ async function promptRelogin(): Promise<void> {
278
278
  if (isPromptingRelogin) return
279
279
  isPromptingRelogin = true
280
280
  try {
281
- const theme = useThemeStore.getState().theme
281
+ const colors = useThemeStore.getState().colors
282
282
  const modal = await Promise.race([
283
283
  // 按钮文案复用通用确认键:平台对 confirmText 限制 4 个汉字/8 字符,长文案会被整体拒绝。
284
284
  Taro.showModal({
@@ -286,7 +286,7 @@ async function promptRelogin(): Promise<void> {
286
286
  content: t('core.auth.session_expired'),
287
287
  showCancel: false,
288
288
  confirmText: t('common.action.confirm'),
289
- confirmColor: theme === 'light' ? '#039e8c' : '#28f0d4',
289
+ confirmColor: colors.primary,
290
290
  }).catch(() => undefined as undefined),
291
291
  new Promise<undefined>((resolve) =>
292
292
  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.theme === 'light' ? '#5a7083' : '#8aa0b4'
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.theme === 'light' ? '#039e8c' : '#28f0d4',
314
- cancelColor: themeStore.theme === 'light' ? '#5a7083' : '#8aa0b4',
313
+ confirmColor: themeStore.colors.primary,
314
+ cancelColor: themeStore.colors.textMuted,
315
315
  })
316
316
  if (result.confirm) setAgreed(true)
317
317
  return result.confirm