@liujitcn/kratos-uni-app-core 0.0.97 → 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 +12 -1
- package/package.json +1 -1
- package/src/components/KratosNavBar.vue +1 -1
- package/src/components/KratosSettingsPage.vue +5 -5
- package/src/components/KratosTabBar.vue +46 -41
- package/src/navigation.ts +33 -9
- package/src/stores/modules/theme.ts +105 -6
- package/src/styles/base.scss +2 -3
- package/src/styles/theme.scss +8 -2
- package/src/views/pages/login/login.vue +3 -3
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
|
@@ -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.
|
|
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.
|
|
50
|
-
const warning = themeStore.
|
|
51
|
-
const violet = themeStore.
|
|
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.
|
|
174
|
-
cancelColor: themeStore.
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
117
|
-
line-height:
|
|
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:
|
|
123
|
-
height:
|
|
124
|
-
margin-bottom:
|
|
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:
|
|
129
|
-
height:
|
|
130
|
-
margin-bottom:
|
|
131
|
-
border:
|
|
132
|
-
border-radius:
|
|
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:
|
|
138
|
-
left:
|
|
139
|
-
width:
|
|
140
|
-
height:
|
|
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
|
-
|
|
145
|
-
|
|
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: -
|
|
151
|
-
left:
|
|
152
|
-
width:
|
|
153
|
-
height:
|
|
154
|
-
border-right:
|
|
155
|
-
border-bottom:
|
|
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
|
-
|
|
167
|
-
|
|
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% -
|
|
181
|
-
width:
|
|
182
|
-
height:
|
|
185
|
+
right: calc(50% - 30rpx);
|
|
186
|
+
width: 16rpx;
|
|
187
|
+
height: 16rpx;
|
|
183
188
|
box-sizing: border-box;
|
|
184
|
-
border:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
146
|
+
const colors = getThemeColors(theme)
|
|
49
147
|
uni.setNavigationBarColor({
|
|
50
|
-
frontColor:
|
|
51
|
-
backgroundColor:
|
|
148
|
+
frontColor: colors.navigationBarText,
|
|
149
|
+
backgroundColor: colors.surface,
|
|
52
150
|
fail: () => undefined,
|
|
53
151
|
})
|
|
54
|
-
uni.setBackgroundColor({ backgroundColor:
|
|
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
|
})
|
package/src/styles/base.scss
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
page,
|
|
5
5
|
body,
|
|
6
6
|
uni-page-body {
|
|
7
|
-
font-family:
|
|
8
|
-
|
|
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,
|
package/src/styles/theme.scss
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
}
|
|
@@ -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.
|
|
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.
|
|
823
|
-
cancelColor: themeStore.
|
|
822
|
+
confirmColor: themeStore.colors.primary,
|
|
823
|
+
cancelColor: themeStore.colors.textMuted,
|
|
824
824
|
success: ({ confirm }) => {
|
|
825
825
|
if (confirm) {
|
|
826
826
|
isAgreePrivacy.value = true
|