xto-fronted 0.8.3 → 0.9.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/dist/api/index.js +20 -19
- package/dist/api/system.d.ts +1 -0
- package/dist/components/Layout/MixTopMenu.vue.d.ts +1 -3
- package/dist/components/Layout/Sidebar.vue.d.ts +0 -1
- package/dist/components/Layout/TopMenu.vue.d.ts +1 -3
- package/dist/components/Layout/index.vue.d.ts +1 -3
- package/dist/{index-MrCwrOqR.js → index-BL_15V_t.js} +1 -1
- package/dist/{index-CZsvJ9LK.js → index-BTlDtwrl.js} +1 -1
- package/dist/{index-DiRXF9DF.js → index-Biperpfs.js} +1 -1
- package/dist/{index-B2_D0EED.js → index-Bv0RBhQX.js} +1 -1
- package/dist/{index-C3QbtrpD.js → index-C4XoKbUh.js} +1245 -1212
- package/dist/index.js +63 -60
- package/dist/stores/app.d.ts +8 -2
- package/dist/style.css +1 -1
- package/dist/{user-BfEOXV7Q.js → user-B0q24FmO.js} +465 -451
- package/dist/utils/config.d.ts +19 -0
- package/package.json +1 -1
- package/src/api/system.ts +6 -0
- package/src/components/Layout/Header.vue +35 -2
- package/src/components/Layout/MixTopMenu.vue +4 -2
- package/src/components/Layout/Sidebar.vue +4 -2
- package/src/components/Layout/TopMenu.vue +4 -2
- package/src/components/Layout/index.vue +6 -4
- package/src/index.ts +19 -1
- package/src/router/layoutRoute.ts +4 -2
- package/src/stores/app.ts +65 -26
- package/src/utils/config.ts +32 -0
package/dist/utils/config.d.ts
CHANGED
|
@@ -54,3 +54,22 @@ export declare const appConfig: {
|
|
|
54
54
|
apiBaseUrl: import('vue').Ref<string, string>;
|
|
55
55
|
basePath: import('vue').Ref<string, string>;
|
|
56
56
|
};
|
|
57
|
+
export interface AppUiFeatures {
|
|
58
|
+
showTabs?: boolean;
|
|
59
|
+
showBreadcrumb?: boolean;
|
|
60
|
+
showFooter?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface AppUiConfig {
|
|
63
|
+
/** 默认布局模式 */
|
|
64
|
+
layout?: 'sidebar' | 'top' | 'mix';
|
|
65
|
+
/** 默认主题色 */
|
|
66
|
+
primaryColor?: string;
|
|
67
|
+
/** 默认深浅主题 */
|
|
68
|
+
theme?: 'light' | 'dark';
|
|
69
|
+
/** 默认功能开关 */
|
|
70
|
+
features?: AppUiFeatures;
|
|
71
|
+
/** 是否允许用户在界面右上角自行修改(false 时锁定设置入口) */
|
|
72
|
+
allowUserOverride?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare function setServerUiConfig(config: AppUiConfig): void;
|
|
75
|
+
export declare function getServerUiConfig(): AppUiConfig;
|
package/package.json
CHANGED
package/src/api/system.ts
CHANGED
|
@@ -51,6 +51,12 @@ export function getMenuTree(appId?: string) {
|
|
|
51
51
|
return http.get<MenuItem[]>(`/user/v1.0/menu/get-menu?appId=${id}`)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// 获取应用界面配置(免登录:布局模式/主题色/功能开关的按应用默认值)
|
|
55
|
+
export function getAppUiConfig(appId?: string) {
|
|
56
|
+
const id = appId || getAppId()
|
|
57
|
+
return http.get<Record<string, any>>('/user/v1.0/app/ui-config', { params: { appId: id } })
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
// 创建菜单
|
|
55
61
|
export function createMenu(data: Partial<MenuItem>) {
|
|
56
62
|
return http.post<MenuItem>('/menu', data)
|
|
@@ -175,6 +175,12 @@ const handleDarkModeToggle = () => {
|
|
|
175
175
|
drawerVisible.value = false
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// 恢复默认(清个人偏好,回到服务端按应用配置的默认值)
|
|
179
|
+
const handleResetDefaults = () => {
|
|
180
|
+
appStore.resetToServerDefaults()
|
|
181
|
+
drawerVisible.value = false
|
|
182
|
+
}
|
|
183
|
+
|
|
178
184
|
// 切换全屏
|
|
179
185
|
const toggleFullscreen = () => {
|
|
180
186
|
if (!document.fullscreenElement) {
|
|
@@ -334,8 +340,8 @@ onUnmounted(() => {
|
|
|
334
340
|
<Icon :name="isFullscreen ? 'fullscreen-exit' : 'fullscreen'" :size="16" />
|
|
335
341
|
</div>
|
|
336
342
|
|
|
337
|
-
<!--
|
|
338
|
-
<div class="header__action" @click="openSettingsDrawer" title="换肤设置">
|
|
343
|
+
<!-- 换肤设置(allowUserOverride=false 时由应用管理员锁定) -->
|
|
344
|
+
<div v-if="appStore.allowUserOverride" class="header__action" @click="openSettingsDrawer" title="换肤设置">
|
|
339
345
|
<Icon name="skin" :size="16" />
|
|
340
346
|
</div>
|
|
341
347
|
|
|
@@ -467,6 +473,14 @@ onUnmounted(() => {
|
|
|
467
473
|
</div>
|
|
468
474
|
</div>
|
|
469
475
|
</div>
|
|
476
|
+
|
|
477
|
+
<!-- 恢复默认:清掉个人偏好,回到管理员在用户中心配置的界面默认值 -->
|
|
478
|
+
<div class="settings-section">
|
|
479
|
+
<div class="settings-reset" @click="handleResetDefaults">
|
|
480
|
+
<Icon name="refresh" :size="14" />
|
|
481
|
+
<span>恢复默认</span>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
470
484
|
</div>
|
|
471
485
|
</Drawer>
|
|
472
486
|
</div>
|
|
@@ -985,6 +999,25 @@ onUnmounted(() => {
|
|
|
985
999
|
}
|
|
986
1000
|
}
|
|
987
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
.settings-reset {
|
|
1004
|
+
display: flex;
|
|
1005
|
+
align-items: center;
|
|
1006
|
+
justify-content: center;
|
|
1007
|
+
gap: 6px;
|
|
1008
|
+
height: 34px;
|
|
1009
|
+
border: 1px solid var(--xto-border-color, #D9DCE1);
|
|
1010
|
+
border-radius: 6px;
|
|
1011
|
+
font-size: 13px;
|
|
1012
|
+
color: var(--xto-color-text-regular, #4E5566);
|
|
1013
|
+
cursor: pointer;
|
|
1014
|
+
transition: all 0.2s;
|
|
1015
|
+
|
|
1016
|
+
&:hover {
|
|
1017
|
+
color: var(--xto-color-primary, #4F46E5);
|
|
1018
|
+
border-color: var(--xto-color-primary, #4F46E5);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
988
1021
|
}
|
|
989
1022
|
|
|
990
1023
|
// 下拉动画
|
|
@@ -14,9 +14,11 @@ import { Drawer } from '@xto/feedback'
|
|
|
14
14
|
const props = withDefaults(defineProps<{
|
|
15
15
|
logoSrc?: string
|
|
16
16
|
}>(), {
|
|
17
|
-
logoSrc: withBasePath('/vite.svg')
|
|
18
17
|
})
|
|
19
18
|
|
|
19
|
+
// logo 地址在渲染期解析:模块加载早于 createXtoApp 的运行时配置,不能作为默认值立即求值
|
|
20
|
+
const resolvedLogoSrc = computed(() => props.logoSrc || withBasePath('/vite.svg'))
|
|
21
|
+
|
|
20
22
|
const route = useRoute()
|
|
21
23
|
const router = useRouter()
|
|
22
24
|
const menuStore = useMenuStore()
|
|
@@ -360,7 +362,7 @@ onUnmounted(() => {
|
|
|
360
362
|
<div class="mix-top-menu">
|
|
361
363
|
<!-- 左侧 Logo -->
|
|
362
364
|
<div class="mix-top-menu__logo">
|
|
363
|
-
<img :src="
|
|
365
|
+
<img :src="resolvedLogoSrc" alt="Logo" class="mix-top-menu__logo-img" />
|
|
364
366
|
<span class="mix-top-menu__logo-text">{{ appStore.appName }}</span>
|
|
365
367
|
</div>
|
|
366
368
|
|
|
@@ -21,9 +21,11 @@ const props = withDefaults(defineProps<{
|
|
|
21
21
|
menuList: () => [],
|
|
22
22
|
showLogo: true,
|
|
23
23
|
showUser: true,
|
|
24
|
-
logoSrc: withBasePath('/vite.svg')
|
|
25
24
|
})
|
|
26
25
|
|
|
26
|
+
// logo 地址在渲染期解析:模块加载早于 createXtoApp 的运行时配置,不能作为默认值立即求值
|
|
27
|
+
const resolvedLogoSrc = computed(() => props.logoSrc || withBasePath('/vite.svg'))
|
|
28
|
+
|
|
27
29
|
const route = useRoute()
|
|
28
30
|
const router = useRouter()
|
|
29
31
|
const menuStore = useMenuStore()
|
|
@@ -130,7 +132,7 @@ const handleLogout = () => {
|
|
|
130
132
|
<div class="sidebar" :class="{ 'sidebar--collapsed': isCollapsed }">
|
|
131
133
|
<!-- Logo -->
|
|
132
134
|
<div v-if="props.showLogo" class="sidebar__logo">
|
|
133
|
-
<img :src="
|
|
135
|
+
<img :src="resolvedLogoSrc" alt="Logo" class="sidebar__logo-img" />
|
|
134
136
|
<span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
|
|
135
137
|
</div>
|
|
136
138
|
|
|
@@ -14,9 +14,11 @@ import { Drawer } from '@xto/feedback'
|
|
|
14
14
|
const props = withDefaults(defineProps<{
|
|
15
15
|
logoSrc?: string
|
|
16
16
|
}>(), {
|
|
17
|
-
logoSrc: withBasePath('/vite.svg')
|
|
18
17
|
})
|
|
19
18
|
|
|
19
|
+
// logo 地址在渲染期解析:模块加载早于 createXtoApp 的运行时配置,不能作为默认值立即求值
|
|
20
|
+
const resolvedLogoSrc = computed(() => props.logoSrc || withBasePath('/vite.svg'))
|
|
21
|
+
|
|
20
22
|
const route = useRoute()
|
|
21
23
|
const router = useRouter()
|
|
22
24
|
const menuStore = useMenuStore()
|
|
@@ -303,7 +305,7 @@ onUnmounted(() => {
|
|
|
303
305
|
<div class="top-menu">
|
|
304
306
|
<!-- 左侧 Logo -->
|
|
305
307
|
<div class="top-menu__logo">
|
|
306
|
-
<img :src="
|
|
308
|
+
<img :src="resolvedLogoSrc" alt="Logo" class="top-menu__logo-img" />
|
|
307
309
|
<span class="top-menu__logo-text">{{ appStore.appName }}</span>
|
|
308
310
|
</div>
|
|
309
311
|
|
|
@@ -12,9 +12,11 @@ import MixTopMenu from './MixTopMenu.vue'
|
|
|
12
12
|
const props = withDefaults(defineProps<{
|
|
13
13
|
logoSrc?: string
|
|
14
14
|
}>(), {
|
|
15
|
-
logoSrc: withBasePath('/vite.svg')
|
|
16
15
|
})
|
|
17
16
|
|
|
17
|
+
// logo 地址在渲染期解析:模块加载早于 createXtoApp 的运行时配置,不能作为默认值立即求值
|
|
18
|
+
const resolvedLogoSrc = computed(() => props.logoSrc || withBasePath('/vite.svg'))
|
|
19
|
+
|
|
18
20
|
const appStore = useAppStore()
|
|
19
21
|
const menuStore = useMenuStore()
|
|
20
22
|
|
|
@@ -52,7 +54,7 @@ const showMixSidebar = computed(() => {
|
|
|
52
54
|
<!-- sidebar模式:左侧菜单 + 顶部Header + 主内容 -->
|
|
53
55
|
<div v-if="layoutMode === 'sidebar'" class="layout layout--sidebar">
|
|
54
56
|
<aside class="layout__aside" :style="{ width: sidebarWidth }">
|
|
55
|
-
<Sidebar :menu-list="sidebarMenuList" :show-logo="true" :show-user="true" :logo-src="
|
|
57
|
+
<Sidebar :menu-list="sidebarMenuList" :show-logo="true" :show-user="true" :logo-src="resolvedLogoSrc" />
|
|
56
58
|
</aside>
|
|
57
59
|
<div class="layout__main">
|
|
58
60
|
<header class="layout__header">
|
|
@@ -67,7 +69,7 @@ const showMixSidebar = computed(() => {
|
|
|
67
69
|
<!-- top模式:顶部菜单 + 主内容 -->
|
|
68
70
|
<div v-if="layoutMode === 'top'" class="layout layout--top">
|
|
69
71
|
<div class="layout__top-menu">
|
|
70
|
-
<TopMenu :logo-src="
|
|
72
|
+
<TopMenu :logo-src="resolvedLogoSrc" />
|
|
71
73
|
</div>
|
|
72
74
|
<div class="layout__main">
|
|
73
75
|
<main class="layout__content">
|
|
@@ -80,7 +82,7 @@ const showMixSidebar = computed(() => {
|
|
|
80
82
|
<div v-if="layoutMode === 'mix'" class="layout layout--mix">
|
|
81
83
|
<!-- 顶部一级菜单 -->
|
|
82
84
|
<div class="layout__mix-top-menu">
|
|
83
|
-
<MixTopMenu :logo-src="
|
|
85
|
+
<MixTopMenu :logo-src="resolvedLogoSrc" />
|
|
84
86
|
</div>
|
|
85
87
|
<!-- 下方:左侧子菜单 + 主内容 -->
|
|
86
88
|
<div class="layout__mix-body">
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import './style.scss'
|
|
3
3
|
|
|
4
4
|
// 配置
|
|
5
|
-
import { initAppConfig, getAppId, getClientId, getApiBaseUrl, loadRuntimeConfig } from './utils/config'
|
|
5
|
+
import { initAppConfig, getAppId, getClientId, getApiBaseUrl, loadRuntimeConfig, setServerUiConfig, type AppUiConfig } from './utils/config'
|
|
6
6
|
import { setBasePath, setApiBaseUrl } from './utils/request'
|
|
7
|
+
import { getAppUiConfig } from './api/system'
|
|
7
8
|
import type { LocaleCode } from '@xto/core/locale'
|
|
8
9
|
|
|
9
10
|
// XtoConfig 类型定义
|
|
@@ -48,6 +49,23 @@ export async function createXtoApp(config: Partial<XtoConfig>) {
|
|
|
48
49
|
setBasePath(config.basePath)
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// 拉取服务端按应用配置的界面默认值(免登录接口,xto-admin「界面设置」维护)
|
|
53
|
+
// 必须在 store 首次实例化前完成,失败/超时静默降级为内置默认
|
|
54
|
+
const resolvedAppId = config.appId || getAppId()
|
|
55
|
+
if (resolvedAppId) {
|
|
56
|
+
try {
|
|
57
|
+
const uiConfig = await Promise.race([
|
|
58
|
+
getAppUiConfig(resolvedAppId),
|
|
59
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), 2000))
|
|
60
|
+
])
|
|
61
|
+
if (uiConfig && typeof uiConfig === 'object') {
|
|
62
|
+
setServerUiConfig(uiConfig as AppUiConfig)
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn('加载应用界面配置失败,使用内置默认', error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
// 设置应用名称和路径到 store
|
|
52
70
|
import('./stores/app').then(({ useAppStore }) => {
|
|
53
71
|
const appStore = useAppStore()
|
|
@@ -28,7 +28,9 @@ export function createLayoutRoute(
|
|
|
28
28
|
options: LayoutRouteOptions = {}
|
|
29
29
|
): RouteRecordRaw {
|
|
30
30
|
const indexPath = options.indexPath || '/dashboard'
|
|
31
|
-
|
|
31
|
+
// 注意:createLayoutRoute 常在模块顶层被调用(早于 createXtoApp 初始化运行时配置),
|
|
32
|
+
// logo 地址必须延迟到渲染时解析,不能在此处求值
|
|
33
|
+
const logoSrc = options.logoSrc
|
|
32
34
|
|
|
33
35
|
// 添加 catch-all 路由,让404在Layout内部渲染,保持左侧菜单显示
|
|
34
36
|
const catchAllRoute: RouteRecordRaw = {
|
|
@@ -44,7 +46,7 @@ export function createLayoutRoute(
|
|
|
44
46
|
const LayoutWrapper = defineComponent({
|
|
45
47
|
name: 'LayoutWrapper',
|
|
46
48
|
render() {
|
|
47
|
-
return h(Layout, { logoSrc })
|
|
49
|
+
return h(Layout, { logoSrc: logoSrc || withBasePath('/vite.svg') })
|
|
48
50
|
}
|
|
49
51
|
})
|
|
50
52
|
|
package/src/stores/app.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { defineStore } from 'pinia'
|
|
6
6
|
import { ref, computed, watch } from 'vue'
|
|
7
7
|
import { local } from '@/utils/storage'
|
|
8
|
+
import { getServerUiConfig } from '@/utils/config'
|
|
8
9
|
|
|
9
10
|
export type ThemeMode = 'light' | 'dark'
|
|
10
11
|
export type LayoutMode = 'sidebar' | 'top' | 'mix'
|
|
@@ -17,19 +18,33 @@ function hexToRgb(hex: string): [number, number, number] | null {
|
|
|
17
18
|
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** 读取用户显式保存过的偏好;未设置过返回 undefined(与「服务端默认」区分开) */
|
|
22
|
+
function readUserPreference<T>(key: string): T | undefined {
|
|
23
|
+
const value = local.get<T>(key)
|
|
24
|
+
return value === null || value === ('' as unknown as T) ? undefined : value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 服务端按应用下发的默认值(xto-admin 界面配置),仅在用户未显式设置时生效
|
|
28
|
+
const serverUi = getServerUiConfig()
|
|
29
|
+
const serverFeatures = serverUi.features || {}
|
|
30
|
+
|
|
31
|
+
// 重置时需要清掉的用户偏好键(isCollapsed 属个人习惯,不随默认值重置)
|
|
32
|
+
const PREFERENCE_KEYS = ['layout', 'primaryColor', 'theme', 'isDark', 'showTabs', 'showBreadcrumb', 'showFooter'] as const
|
|
33
|
+
|
|
20
34
|
export const useAppStore = defineStore('app', () => {
|
|
21
|
-
//
|
|
35
|
+
// 状态(优先级:用户本地偏好 > 服务端按应用默认 > 内置默认)
|
|
22
36
|
const appName = ref<string>(local.get<string>('appName') || 'XTO App')
|
|
23
37
|
const indexPath = ref<string>(local.get<string>('indexPath') || '/dashboard')
|
|
24
38
|
const loginPath = ref<string>(local.get<string>('loginPath') || '/login')
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const layout = ref<LayoutMode>(
|
|
39
|
+
const theme = ref<ThemeMode>(readUserPreference<ThemeMode>('theme') ?? serverUi.theme ?? 'light')
|
|
40
|
+
const isDark = ref<boolean>(theme.value === 'dark')
|
|
41
|
+
const layout = ref<LayoutMode>(readUserPreference<LayoutMode>('layout') ?? serverUi.layout ?? 'sidebar')
|
|
28
42
|
const isCollapsed = ref<boolean>(local.get<boolean>('isCollapsed') || false)
|
|
29
|
-
const showTabs = ref<boolean>(
|
|
30
|
-
const showFooter = ref<boolean>(
|
|
31
|
-
const showBreadcrumb = ref<boolean>(
|
|
32
|
-
const primaryColor = ref<string>(
|
|
43
|
+
const showTabs = ref<boolean>(readUserPreference<boolean>('showTabs') ?? serverFeatures.showTabs ?? true)
|
|
44
|
+
const showFooter = ref<boolean>(readUserPreference<boolean>('showFooter') ?? serverFeatures.showFooter ?? true)
|
|
45
|
+
const showBreadcrumb = ref<boolean>(readUserPreference<boolean>('showBreadcrumb') ?? serverFeatures.showBreadcrumb ?? true)
|
|
46
|
+
const primaryColor = ref<string>(readUserPreference<string>('primaryColor') ?? serverUi.primaryColor ?? '#4F46E5')
|
|
47
|
+
const allowUserOverride = ref<boolean>(serverUi.allowUserOverride !== false)
|
|
33
48
|
const cachedViews = ref<string[]>([])
|
|
34
49
|
const mixSubMenus = ref<any[]>([])
|
|
35
50
|
|
|
@@ -54,28 +69,33 @@ export const useAppStore = defineStore('app', () => {
|
|
|
54
69
|
local.set('loginPath', path)
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
//
|
|
72
|
+
// 应用深浅主题到 DOM(不写 localStorage,供初始化与重置使用)
|
|
73
|
+
const applyThemeClass = () => {
|
|
74
|
+
const html = document.documentElement
|
|
75
|
+
if (isDark.value) {
|
|
76
|
+
html.classList.add('dark')
|
|
77
|
+
} else {
|
|
78
|
+
html.classList.remove('dark')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 切换主题(用户操作,持久化)
|
|
58
83
|
const toggleTheme = () => {
|
|
59
84
|
isDark.value = !isDark.value
|
|
60
85
|
theme.value = isDark.value ? 'dark' : 'light'
|
|
61
86
|
updateTheme()
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
//
|
|
89
|
+
// 设置主题(用户操作,持久化)
|
|
65
90
|
const setTheme = (mode: ThemeMode) => {
|
|
66
91
|
theme.value = mode
|
|
67
92
|
isDark.value = mode === 'dark'
|
|
68
93
|
updateTheme()
|
|
69
94
|
}
|
|
70
95
|
|
|
71
|
-
//
|
|
96
|
+
// 更新主题(用户操作路径:应用 DOM + 持久化)
|
|
72
97
|
const updateTheme = () => {
|
|
73
|
-
|
|
74
|
-
if (isDark.value) {
|
|
75
|
-
html.classList.add('dark')
|
|
76
|
-
} else {
|
|
77
|
-
html.classList.remove('dark')
|
|
78
|
-
}
|
|
98
|
+
applyThemeClass()
|
|
79
99
|
local.set('isDark', isDark.value)
|
|
80
100
|
local.set('theme', theme.value)
|
|
81
101
|
}
|
|
@@ -110,9 +130,8 @@ export const useAppStore = defineStore('app', () => {
|
|
|
110
130
|
local.set('showBreadcrumb', showBreadcrumb.value)
|
|
111
131
|
}
|
|
112
132
|
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
primaryColor.value = color
|
|
133
|
+
// 主题色 CSS 变量应用(含 light-1..9 / dark-1..3 色阶;不写 localStorage,供初始化与重置使用)
|
|
134
|
+
const applyPrimaryColorVars = (color: string) => {
|
|
116
135
|
const root = document.documentElement
|
|
117
136
|
root.style.setProperty('--xto-color-primary', color)
|
|
118
137
|
const rgb = hexToRgb(color)
|
|
@@ -130,9 +149,29 @@ export const useAppStore = defineStore('app', () => {
|
|
|
130
149
|
root.style.setProperty('--xto-color-primary-dark-3', mix(0.45, false))
|
|
131
150
|
root.style.setProperty('--xto-color-ring', color)
|
|
132
151
|
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 设置主题色(用户操作,持久化)
|
|
155
|
+
const setPrimaryColor = (color: string) => {
|
|
156
|
+
primaryColor.value = color
|
|
157
|
+
applyPrimaryColorVars(color)
|
|
133
158
|
local.set('primaryColor', color)
|
|
134
159
|
}
|
|
135
160
|
|
|
161
|
+
// 恢复默认:清掉本地偏好,回到服务端按应用配置(无配置则回内置默认)
|
|
162
|
+
const resetToServerDefaults = () => {
|
|
163
|
+
PREFERENCE_KEYS.forEach((key) => local.remove(key))
|
|
164
|
+
layout.value = serverUi.layout ?? 'sidebar'
|
|
165
|
+
primaryColor.value = serverUi.primaryColor ?? '#4F46E5'
|
|
166
|
+
theme.value = serverUi.theme ?? 'light'
|
|
167
|
+
isDark.value = theme.value === 'dark'
|
|
168
|
+
showTabs.value = serverFeatures.showTabs ?? true
|
|
169
|
+
showFooter.value = serverFeatures.showFooter ?? true
|
|
170
|
+
showBreadcrumb.value = serverFeatures.showBreadcrumb ?? true
|
|
171
|
+
applyThemeClass()
|
|
172
|
+
applyPrimaryColorVars(primaryColor.value)
|
|
173
|
+
}
|
|
174
|
+
|
|
136
175
|
// 添加缓存页面
|
|
137
176
|
const addCachedView = (name: string) => {
|
|
138
177
|
if (!cachedViews.value.includes(name)) {
|
|
@@ -158,16 +197,14 @@ export const useAppStore = defineStore('app', () => {
|
|
|
158
197
|
mixSubMenus.value = menus
|
|
159
198
|
}
|
|
160
199
|
|
|
161
|
-
//
|
|
200
|
+
// 初始化主题:只应用运行时效果,不持久化(避免把服务端默认值固化成用户偏好)
|
|
162
201
|
const initTheme = () => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
document.documentElement.style.setProperty('--xto-color-primary', primaryColor.value)
|
|
166
|
-
}
|
|
202
|
+
applyThemeClass()
|
|
203
|
+
applyPrimaryColorVars(primaryColor.value)
|
|
167
204
|
}
|
|
168
205
|
|
|
169
206
|
// 监听主题变化
|
|
170
|
-
watch(isDark,
|
|
207
|
+
watch(isDark, applyThemeClass)
|
|
171
208
|
|
|
172
209
|
return {
|
|
173
210
|
appName,
|
|
@@ -181,6 +218,7 @@ export const useAppStore = defineStore('app', () => {
|
|
|
181
218
|
showFooter,
|
|
182
219
|
showBreadcrumb,
|
|
183
220
|
primaryColor,
|
|
221
|
+
allowUserOverride,
|
|
184
222
|
cachedViews,
|
|
185
223
|
mixSubMenus,
|
|
186
224
|
themeClass,
|
|
@@ -195,6 +233,7 @@ export const useAppStore = defineStore('app', () => {
|
|
|
195
233
|
toggleFooter,
|
|
196
234
|
toggleBreadcrumb,
|
|
197
235
|
setPrimaryColor,
|
|
236
|
+
resetToServerDefaults,
|
|
198
237
|
addCachedView,
|
|
199
238
|
removeCachedView,
|
|
200
239
|
clearCachedViews,
|
package/src/utils/config.ts
CHANGED
|
@@ -168,4 +168,36 @@ export const appConfig = {
|
|
|
168
168
|
clientId,
|
|
169
169
|
apiBaseUrl,
|
|
170
170
|
basePath
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ==================== 应用界面配置(服务端按应用下发的默认值) ====================
|
|
174
|
+
|
|
175
|
+
export interface AppUiFeatures {
|
|
176
|
+
showTabs?: boolean
|
|
177
|
+
showBreadcrumb?: boolean
|
|
178
|
+
showFooter?: boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface AppUiConfig {
|
|
182
|
+
/** 默认布局模式 */
|
|
183
|
+
layout?: 'sidebar' | 'top' | 'mix'
|
|
184
|
+
/** 默认主题色 */
|
|
185
|
+
primaryColor?: string
|
|
186
|
+
/** 默认深浅主题 */
|
|
187
|
+
theme?: 'light' | 'dark'
|
|
188
|
+
/** 默认功能开关 */
|
|
189
|
+
features?: AppUiFeatures
|
|
190
|
+
/** 是否允许用户在界面右上角自行修改(false 时锁定设置入口) */
|
|
191
|
+
allowUserOverride?: boolean
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 模块级保存,app store 首次实例化时读取(早于任何组件渲染,无闪烁)
|
|
195
|
+
let serverUiConfig: AppUiConfig = {}
|
|
196
|
+
|
|
197
|
+
export function setServerUiConfig(config: AppUiConfig): void {
|
|
198
|
+
serverUiConfig = config || {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getServerUiConfig(): AppUiConfig {
|
|
202
|
+
return serverUiConfig
|
|
171
203
|
}
|