@liujitcn/kratos-uni-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 CHANGED
@@ -34,7 +34,7 @@ packages/core
34
34
 
35
35
  - 定义 `KratosAppModule`、`viewKey`、静态页面覆盖和模块图标协议。
36
36
  - 从 `/v1/app/base/menu` 加载 `99000000` 下的扁平菜单,按 `parent_id` 构建树,以二级页面生成 tabBar,并原子更新动态路由和层级导航。
37
- - tabBar 统一自绘,宿主不声明原生 `tabBar`;H5 替换当前路由,微信端复用普通页面栈,栈满时替换栈顶,切换不重建整个页面栈。
37
+ - tabBar 统一自绘,宿主不声明原生 `tabBar`;H5 和微信端优先替换当前路由,微信端替换失败时再复用普通页面栈,避免页面入栈/出栈动画。
38
38
  - 页面装配插件在编译配置中移除宿主的原生 `tabBar`,退出后恢复宿主原文件。导航按模块注册的 `viewKey` 解析物理页面,支持业务模块覆盖首页和扩展 tab;引用项目升级 core 包后即可使用相同策略。
39
39
  - 按匿名态、登录态缓存导航,并在远端不可用时回退到最近缓存或默认菜单。
40
40
  - 统一认证、token、MFA、HTTP、上传、配置、Pinia 和基础页面能力。
@@ -43,6 +43,17 @@ packages/core
43
43
  - 登录阶段支持 TOTP、WebAuthn 和一次性恢复码校验;WebAuthn 编解码封装在 `src/utils/webauthn.ts`。
44
44
  - 构建时扫描模块 `src/views/**/*.vue`,逐文件合并模块 static,生成宿主页并在退出时按事务精确恢复。
45
45
 
46
+ 主题 store 公开 `themeStore.colors` 和 `registerThemeColors`。页面脚本使用语义颜色(例如 `colors.primary`、`colors.textMuted`),业务模块可以在初始化时传入 `light`、`dark` 的局部字段覆盖 Core 默认值;未传入的字段保持默认值,覆盖会立即作用于当前主题。
47
+
48
+ ```ts
49
+ import { registerThemeColors } from '@liujitcn/kratos-uni-app-core/stores'
50
+
51
+ registerThemeColors({
52
+ light: { primary: '#039e8c' },
53
+ dark: { primary: '#28f0d4' },
54
+ })
55
+ ```
56
+
46
57
  ## 公开入口
47
58
 
48
59
  登录页返回会清理本次回跳并替换到首页;直接打开登录页清理旧 `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.97",
10
+ "version": "0.1.0",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -25,7 +25,7 @@ const iconPaths: Record<string, string> = {
25
25
  const actionIcon = computed(() => {
26
26
  const icon = props.action?.icon
27
27
  if (!icon || !iconPaths[icon]) return icon
28
- const color = themeStore.theme === 'light' ? '#102232' : '#e9f4f6'
28
+ const color = themeStore.colors.text
29
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
30
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
31
31
  })
@@ -46,9 +46,9 @@ const localeIndex = computed(() =>
46
46
  )
47
47
 
48
48
  const settingsIcons = computed(() => {
49
- const primary = themeStore.theme === 'light' ? '#039e8c' : '#28f0d4'
50
- const warning = themeStore.theme === 'light' ? '#d9880f' : '#ffb648'
51
- const violet = themeStore.theme === 'light' ? '#5f4fe0' : '#9b8cff'
49
+ const primary = themeStore.colors.primary
50
+ const warning = themeStore.colors.warning
51
+ const violet = themeStore.colors.violet
52
52
  const createIcon = (body: string, color: string) =>
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.8" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`)}`
54
54
 
@@ -170,8 +170,8 @@ const onLogout = () => {
170
170
  // 模态弹窗
171
171
  uni.showModal({
172
172
  content: t('core.settings.logout_confirm'),
173
- confirmColor: themeStore.theme === 'light' ? '#039e8c' : '#28f0d4',
174
- cancelColor: themeStore.theme === 'light' ? '#5a7083' : '#8aa0b4',
173
+ confirmColor: themeStore.colors.primary,
174
+ cancelColor: themeStore.colors.textMuted,
175
175
  success: async (res) => {
176
176
  if (!res.confirm) {
177
177
  return
@@ -22,13 +22,7 @@ const TAB_ICON_PATHS: Record<string, string> = {
22
22
  const tabIconSrc = (viewKey: string, active: boolean): string => {
23
23
  const path = TAB_ICON_PATHS[viewKey]
24
24
  if (!path) return ''
25
- const color = active
26
- ? themeStore.theme === 'light'
27
- ? '#039e8c'
28
- : '#28f0d4'
29
- : themeStore.theme === 'light'
30
- ? '#9aabbb'
31
- : '#4e6378'
25
+ const color = active ? themeStore.colors.primary : themeStore.colors.textDim
32
26
  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>`
33
27
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
34
28
  }
@@ -79,7 +73,10 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
79
73
  :class="{ 'kratos-tab-bar__message-icon--active': activeMenu?.id === item.id }"
80
74
  aria-hidden="true"
81
75
  />
82
- <text :class="{ 'kratos-tab-bar__text--active': activeMenu?.id === item.id }">
76
+ <text
77
+ class="kratos-tab-bar__text"
78
+ :class="{ 'kratos-tab-bar__text--active': activeMenu?.id === item.id }"
79
+ >
83
80
  {{ item.title }}
84
81
  </text>
85
82
  </view>
@@ -97,13 +94,13 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
97
94
  left: 20rpx;
98
95
  z-index: 999;
99
96
  display: flex;
97
+ box-sizing: border-box;
100
98
  height: 104rpx;
101
99
  padding: 0 8rpx;
102
- border: 1px solid var(--kratos-color-border-accent, rgba(120, 230, 215, 0.16));
100
+ border: 1rpx solid var(--kratos-color-border-accent, rgba(120, 230, 215, 0.16));
103
101
  border-radius: 30rpx;
104
102
  background: var(--kratos-dock-bg, rgba(10, 18, 30, 0.72));
105
- box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.45);
106
- backdrop-filter: blur(32rpx);
103
+ box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.2);
107
104
  }
108
105
  .kratos-tab-bar__item {
109
106
  position: relative;
@@ -112,47 +109,50 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
112
109
  align-items: center;
113
110
  justify-content: center;
114
111
  flex-direction: column;
112
+ height: 100%;
113
+ padding: 10rpx 0 8rpx;
114
+ box-sizing: border-box;
115
115
  color: var(--kratos-tab-bar-color);
116
- font-size: 10px;
117
- line-height: 12px;
116
+ font-size: 20rpx;
117
+ line-height: 28rpx;
118
118
  }
119
119
 
120
120
  .kratos-tab-bar__icon {
121
121
  flex-shrink: 0;
122
- width: 28px;
123
- height: 28px;
124
- margin-bottom: 4px;
122
+ width: 44rpx;
123
+ height: 44rpx;
124
+ margin-bottom: 4rpx;
125
125
  }
126
126
  .kratos-tab-bar__message-icon {
127
127
  position: relative;
128
- width: 22px;
129
- height: 18px;
130
- margin-bottom: 6px;
131
- border: 2px solid var(--kratos-tab-bar-color);
132
- border-radius: 6px;
128
+ width: 36rpx;
129
+ height: 30rpx;
130
+ margin-bottom: 8rpx;
131
+ border: 2rpx solid var(--kratos-tab-bar-color);
132
+ border-radius: 8rpx;
133
133
  box-sizing: border-box;
134
134
  }
135
135
  .kratos-tab-bar__message-icon::before {
136
136
  position: absolute;
137
- top: 5px;
138
- left: 4px;
139
- width: 3px;
140
- height: 3px;
137
+ top: 8rpx;
138
+ left: 7rpx;
139
+ width: 4rpx;
140
+ height: 4rpx;
141
141
  border-radius: 50%;
142
142
  background: var(--kratos-tab-bar-color);
143
143
  box-shadow:
144
- 5px 0 var(--kratos-tab-bar-color),
145
- 10px 0 var(--kratos-tab-bar-color);
144
+ 8rpx 0 var(--kratos-tab-bar-color),
145
+ 16rpx 0 var(--kratos-tab-bar-color);
146
146
  content: '';
147
147
  }
148
148
  .kratos-tab-bar__message-icon::after {
149
149
  position: absolute;
150
- bottom: -5px;
151
- left: 3px;
152
- width: 7px;
153
- height: 7px;
154
- border-right: 2px solid var(--kratos-tab-bar-color);
155
- border-bottom: 2px solid var(--kratos-tab-bar-color);
150
+ bottom: -8rpx;
151
+ left: 5rpx;
152
+ width: 10rpx;
153
+ height: 10rpx;
154
+ border-right: 2rpx solid var(--kratos-tab-bar-color);
155
+ border-bottom: 2rpx solid var(--kratos-tab-bar-color);
156
156
  background: var(--kratos-dock-bg, rgba(10, 18, 30, 0.72));
157
157
  content: '';
158
158
  transform: rotate(45deg);
@@ -163,27 +163,32 @@ const messageBadge = useAppMenuBadge('MESSAGE_INBOX')
163
163
  .kratos-tab-bar__message-icon--active::before {
164
164
  background: var(--kratos-tab-bar-active-color);
165
165
  box-shadow:
166
- 5px 0 var(--kratos-tab-bar-active-color),
167
- 10px 0 var(--kratos-tab-bar-active-color);
166
+ 8rpx 0 var(--kratos-tab-bar-active-color),
167
+ 16rpx 0 var(--kratos-tab-bar-active-color);
168
168
  }
169
169
  .kratos-tab-bar__message-icon--active::after {
170
170
  border-color: var(--kratos-tab-bar-active-color);
171
171
  }
172
+ .kratos-tab-bar__text {
173
+ display: block;
174
+ min-height: 28rpx;
175
+ text-align: center;
176
+ white-space: nowrap;
177
+ }
172
178
  .kratos-tab-bar__text--active {
173
179
  color: var(--kratos-tab-bar-active-color);
174
180
  font-weight: 700;
175
- text-shadow: 0 0 12px currentColor;
176
181
  }
177
182
  .kratos-tab-bar__badge {
178
183
  position: absolute;
179
184
  top: 0;
180
- right: calc(50% - 19px);
181
- width: 10px;
182
- height: 10px;
185
+ right: calc(50% - 30rpx);
186
+ width: 16rpx;
187
+ height: 16rpx;
183
188
  box-sizing: border-box;
184
- border: 2px solid var(--kratos-dock-bg, rgba(10, 18, 30, 0.72));
189
+ border: 2rpx solid var(--kratos-dock-bg, rgba(10, 18, 30, 0.72));
185
190
  border-radius: 50%;
186
191
  background: var(--kratos-color-danger, #ff4d6d);
187
- box-shadow: 0 0 12px var(--kratos-color-danger, #ff4d6d);
192
+ box-shadow: 0 0 12rpx var(--kratos-color-danger, #ff4d6d);
188
193
  }
189
194
  </style>
package/src/navigation.ts CHANGED
@@ -178,6 +178,12 @@ const pageTitleKeys: Record<string, string> = {
178
178
  'pagesMember/settings/settings': 'core.settings.title',
179
179
  }
180
180
 
181
+ const fallbackMenuTitleKeys: Record<string, string> = {
182
+ HOME: 'core.navigation.home',
183
+ MESSAGE_INBOX: 'core.navigation.message',
184
+ PROFILE_HOME: 'core.navigation.my',
185
+ }
186
+
181
187
  /** 替换导航远端适配器,供宿主接入自定义契约。 */
182
188
  export function setAppNavigationAdapter(nextAdapter: AppNavigationAdapter): void {
183
189
  adapter = nextAdapter
@@ -266,7 +272,7 @@ export function navigateAppRoute(rawRoute: string, options: { replace?: boolean
266
272
  uni.navigateTo({ url, fail: () => uni.reLaunch({ url }) })
267
273
  }
268
274
 
269
- /** 切换自绘 tab:H5 替换路由,微信端复用页面栈,避免重建整个页面栈。 */
275
+ /** 切换自绘 tab:直接替换当前页面,避免微信端页面入栈/出栈动画造成闪烁。 */
270
276
  function navigateTabRoute(url: string): void {
271
277
  const targetRoute = url.slice(1).split('?', 1)[0].replace(/^\/+/, '')
272
278
  const pages = getCurrentPages()
@@ -286,6 +292,21 @@ function navigateTabRoute(url: string): void {
286
292
  })
287
293
  return
288
294
  }
295
+ uni.redirectTo({
296
+ url,
297
+ success: release,
298
+ fail: () => navigateTabRouteInStack(url, targetRoute, pages, currentIndex, release),
299
+ })
300
+ }
301
+
302
+ /** 微信端替换路由失败时复用已有页面栈。 */
303
+ function navigateTabRouteInStack(
304
+ url: string,
305
+ targetRoute: string,
306
+ pages: ReturnType<typeof getCurrentPages>,
307
+ currentIndex: number,
308
+ release: () => void,
309
+ ): void {
289
310
  const targetIndex = pages.findIndex((page) => page.route?.replace(/^\/+/, '') === targetRoute)
290
311
  if (targetIndex >= 0 && targetIndex < currentIndex) {
291
312
  uni.navigateBack({
@@ -384,7 +405,7 @@ function readCachedMenus(cacheKey: string): AppMenu[] | undefined {
384
405
  ? t('core.navigation.home')
385
406
  : menu.viewKey === 'MESSAGE_INBOX'
386
407
  ? t('core.navigation.message')
387
- : menu.title,
408
+ : resolveMenuTitle(menu.viewKey, menu.title),
388
409
  }))
389
410
  return validateMenus(cachedMenus)
390
411
  } catch {
@@ -399,18 +420,14 @@ function normalizeMenuResponse(response: ListBaseMenuResponse): AppMenu[] {
399
420
  function normalizeMenu(item: BaseMenu): AppMenu {
400
421
  const meta = item.meta
401
422
  const app = meta?.app
423
+ const viewKey = app?.view_key ?? ''
402
424
  return {
403
425
  id: item.id,
404
426
  parentId: item.parent_id,
405
427
  name: item.name,
406
428
  path: item.path,
407
- viewKey: app?.view_key ?? '',
408
- title:
409
- app?.view_key === 'HOME'
410
- ? t('core.navigation.home')
411
- : app?.view_key === 'MESSAGE_INBOX'
412
- ? t('core.navigation.message')
413
- : (meta?.title ?? ''),
429
+ viewKey,
430
+ title: resolveMenuTitle(viewKey, meta?.title ?? ''),
414
431
  access: (app?.access ?? 'AUTHENTICATED') as AppMenuAccess,
415
432
  inTabBar: app?.in_tab_bar ?? false,
416
433
  icon: meta?.icon,
@@ -418,6 +435,13 @@ function normalizeMenu(item: BaseMenu): AppMenu {
418
435
  }
419
436
  }
420
437
 
438
+ /** 为缺少后端标题的菜单提供稳定的本地化文案。 */
439
+ function resolveMenuTitle(viewKey: string, title: string): string {
440
+ if (title) return title
441
+ const titleKey = fallbackMenuTitleKeys[viewKey]
442
+ return titleKey ? t(titleKey) : ''
443
+ }
444
+
421
445
  function validateMenus(nextMenus: AppMenu[]): AppMenu[] {
422
446
  // 多端共用后端时菜单可能携带本应用未注册的视图键,过滤跳过而不是让整份菜单失效。
423
447
  const knownMenus = nextMenus.filter((menu) => {
@@ -1,11 +1,109 @@
1
1
  import { defineStore } from 'pinia'
2
- import { computed, ref, watch } from 'vue'
2
+ import { computed, reactive, ref, watch } from 'vue'
3
3
 
4
4
  /** 主题模式:暗黑、明亮、跟随系统。 */
5
5
  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>> = reactive({
91
+ light: {},
92
+ dark: {},
93
+ })
94
+
95
+ /** 获取指定主题的语义颜色。 */
96
+ export const getThemeColors = (theme: ResolvedTheme): ThemeColors => ({
97
+ ...themeColors[theme],
98
+ ...themeColorOverrides[theme],
99
+ })
100
+
101
+ /** 注册引用模块的主题颜色覆盖,并立即刷新已创建的主题 store。 */
102
+ export const registerThemeColors = (overrides: ThemeColorOverrides): void => {
103
+ if (overrides.light) Object.assign(themeColorOverrides.light, overrides.light)
104
+ if (overrides.dark) Object.assign(themeColorOverrides.dark, overrides.dark)
105
+ }
106
+
9
107
  const THEME_MODE_STORAGE_KEY = 'kratos_theme_mode'
10
108
  const DEFAULT_THEME_MODE: ThemeMode = 'light'
11
109
 
@@ -45,13 +143,13 @@ const readStoredMode = (): ThemeMode => {
45
143
  /** 主题实际生效时给导航栏与页面底色染色,覆盖 pages.json 的静态浅色配置。 */
46
144
  const applyChrome = (theme: ResolvedTheme) => {
47
145
  try {
48
- const navigationBarColor = theme === 'light' ? '#f4f9fd' : '#060a12'
146
+ const colors = getThemeColors(theme)
49
147
  uni.setNavigationBarColor({
50
- frontColor: theme === 'light' ? '#000000' : '#ffffff',
51
- backgroundColor: navigationBarColor,
148
+ frontColor: colors.navigationBarText,
149
+ backgroundColor: colors.surface,
52
150
  fail: () => undefined,
53
151
  })
54
- uni.setBackgroundColor({ backgroundColor: navigationBarColor, fail: () => undefined })
152
+ uni.setBackgroundColor({ backgroundColor: colors.background, fail: () => undefined })
55
153
  } catch {
56
154
  // 部分平台/版本未暴露导航栏染色 API,跳过不影响主题类。
57
155
  }
@@ -71,6 +169,7 @@ export const useThemeStore = defineStore('theme', () => {
71
169
  mode.value === 'auto' ? systemTheme.value : mode.value,
72
170
  )
73
171
  const themeClass = computed(() => `theme-${theme.value}`)
172
+ const colors = computed(() => getThemeColors(theme.value))
74
173
 
75
174
  /** 切换主题模式并持久化。 */
76
175
  const setMode = (nextMode: ThemeMode) => {
@@ -107,5 +206,5 @@ export const useThemeStore = defineStore('theme', () => {
107
206
 
108
207
  watch(theme, (value) => applyChrome(value), { immediate: true })
109
208
 
110
- return { mode, theme, themeClass, setMode }
209
+ return { mode, theme, themeClass, colors, setMode }
111
210
  })
@@ -4,9 +4,8 @@
4
4
  page,
5
5
  body,
6
6
  uni-page-body {
7
- font-family:
8
- -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
9
- 'Microsoft YaHei', sans-serif;
7
+ font-family: var(--kratos-font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif);
8
+ font-size: 28rpx;
10
9
  }
11
10
 
12
11
  view,
@@ -31,7 +31,10 @@
31
31
  --kratos-dock-bg: rgba(10, 18, 30, 0.72);
32
32
  --kratos-glow: 0 0 24px rgba(40, 240, 212, 0.35);
33
33
  --kratos-glow-soft: 0 0 40px rgba(40, 240, 212, 0.06);
34
- --kratos-font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;
34
+ --kratos-font-sans:
35
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
36
+ 'Microsoft YaHei', sans-serif;
37
+ --kratos-font-mono: var(--kratos-font-sans);
35
38
  }
36
39
 
37
40
  /* 明亮主题 */
@@ -62,5 +65,8 @@
62
65
  --kratos-dock-bg: rgba(255, 255, 255, 0.82);
63
66
  --kratos-glow: 0 10px 26px rgba(0, 120, 110, 0.14);
64
67
  --kratos-glow-soft: none;
65
- --kratos-font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace;
68
+ --kratos-font-sans:
69
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
70
+ 'Microsoft YaHei', sans-serif;
71
+ --kratos-font-mono: var(--kratos-font-sans);
66
72
  }
package/src/utils/http.ts CHANGED
@@ -405,7 +405,14 @@ const RELOGIN_PROMPT_TIMEOUT = 60_000
405
405
  async function promptRelogin() {
406
406
  // 401 来自旧会话的在途请求而当前已是有效登录态时直接忽略,避免刚登录成功被弹窗打断。
407
407
  if (hasValidToken()) {
408
- return
408
+ // 本地令牌未过期但服务端已判定失效时,先强制续签一次;续签成功即静默恢复,
409
+ // 续签失败清空残留登录态后继续弹窗,避免停留在“本地有效”的假登录态。
410
+ try {
411
+ await handleTokenRefresh()
412
+ return
413
+ } catch {
414
+ silentClearAuthData()
415
+ }
409
416
  }
410
417
  const pages = getCurrentPages()
411
418
  if (pages[pages.length - 1]?.route === 'pages/login/login') {
@@ -435,6 +442,9 @@ async function promptRelogin() {
435
442
  ])
436
443
 
437
444
  if (!modalRes?.confirm) {
445
+ // 用户关闭提示或超时未确认时也要完成登出,避免残留“本地有效”的假登录态
446
+ // 导致后续请求全部 401 被静默吞掉、界面看起来像按钮失灵。
447
+ await clearUserData()
438
448
  return
439
449
  }
440
450
 
@@ -57,7 +57,7 @@ const appLogo = ref(defaultLogo)
57
57
  const showTenantCode = computed(() => settingStore.getData('showTenantCode') !== 'false')
58
58
  /** 按管理端 View、Hide 视觉生成本端密码可见性图标。 */
59
59
  const passwordIconSrc = (visible: boolean) => {
60
- const color = themeStore.theme === 'light' ? '#5a7083' : '#8aa0b4'
60
+ const color = themeStore.colors.textMuted
61
61
  const path = visible ? passwordIconPaths.hide : passwordIconPaths.view
62
62
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="${color}"><path d="${path}"/></svg>`
63
63
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
@@ -819,8 +819,8 @@ const checkedAgreePrivacy = async () => {
819
819
  content: t('core.login.protocol_prompt'),
820
820
  confirmText: t('common.action.confirm'),
821
821
  cancelText: t('common.action.cancel'),
822
- confirmColor: themeStore.theme === 'light' ? '#039e8c' : '#28f0d4',
823
- cancelColor: themeStore.theme === 'light' ? '#5a7083' : '#8aa0b4',
822
+ confirmColor: themeStore.colors.primary,
823
+ cancelColor: themeStore.colors.textMuted,
824
824
  success: ({ confirm }) => {
825
825
  if (confirm) {
826
826
  isAgreePrivacy.value = true