xto-fronted 0.8.4 → 0.9.1

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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xto-fronted",
3
- "version": "0.8.4",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "XTO 前端应用框架",
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
  // 下拉动画
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()
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,36 @@ 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
+ // 重置时需要清掉的用户偏好键(isCollapsed 属个人习惯,不随默认值重置)
28
+ const PREFERENCE_KEYS = ['layout', 'primaryColor', 'theme', 'isDark', 'showTabs', 'showBreadcrumb', 'showFooter'] as const
29
+
30
+ // 注意:服务端界面配置必须在 store setup 内读取(getServerUiConfig())。
31
+ // 本模块经 index.ts 静态导出,若在模块顶层取值会在 createXtoApp 拉取配置前固化成空对象。
32
+
20
33
  export const useAppStore = defineStore('app', () => {
21
- // 状态
34
+ // 服务端按应用下发的默认值(在首次 useAppStore() 时读取,此时 createXtoApp 已完成配置拉取)
35
+ const serverUi = getServerUiConfig()
36
+ const serverFeatures = serverUi.features || {}
37
+
38
+ // 状态(优先级:用户本地偏好 > 服务端按应用默认 > 内置默认)
22
39
  const appName = ref<string>(local.get<string>('appName') || 'XTO App')
23
40
  const indexPath = ref<string>(local.get<string>('indexPath') || '/dashboard')
24
41
  const loginPath = ref<string>(local.get<string>('loginPath') || '/login')
25
- const isDark = ref<boolean>(local.get<boolean>('isDark') || false)
26
- const theme = ref<ThemeMode>(local.get<ThemeMode>('theme') || 'light')
27
- const layout = ref<LayoutMode>(local.get<LayoutMode>('layout') || 'sidebar')
42
+ const theme = ref<ThemeMode>(readUserPreference<ThemeMode>('theme') ?? serverUi.theme ?? 'light')
43
+ const isDark = ref<boolean>(theme.value === 'dark')
44
+ const layout = ref<LayoutMode>(readUserPreference<LayoutMode>('layout') ?? serverUi.layout ?? 'sidebar')
28
45
  const isCollapsed = ref<boolean>(local.get<boolean>('isCollapsed') || false)
29
- const showTabs = ref<boolean>(local.get<boolean>('showTabs') ?? true)
30
- const showFooter = ref<boolean>(local.get<boolean>('showFooter') ?? true)
31
- const showBreadcrumb = ref<boolean>(local.get<boolean>('showBreadcrumb') ?? true)
32
- const primaryColor = ref<string>(local.get<string>('primaryColor') || '#4F46E5')
46
+ const showTabs = ref<boolean>(readUserPreference<boolean>('showTabs') ?? serverFeatures.showTabs ?? true)
47
+ const showFooter = ref<boolean>(readUserPreference<boolean>('showFooter') ?? serverFeatures.showFooter ?? true)
48
+ const showBreadcrumb = ref<boolean>(readUserPreference<boolean>('showBreadcrumb') ?? serverFeatures.showBreadcrumb ?? true)
49
+ const primaryColor = ref<string>(readUserPreference<string>('primaryColor') ?? serverUi.primaryColor ?? '#4F46E5')
50
+ const allowUserOverride = ref<boolean>(serverUi.allowUserOverride !== false)
33
51
  const cachedViews = ref<string[]>([])
34
52
  const mixSubMenus = ref<any[]>([])
35
53
 
@@ -54,28 +72,33 @@ export const useAppStore = defineStore('app', () => {
54
72
  local.set('loginPath', path)
55
73
  }
56
74
 
57
- // 切换主题
75
+ // 应用深浅主题到 DOM(不写 localStorage,供初始化与重置使用)
76
+ const applyThemeClass = () => {
77
+ const html = document.documentElement
78
+ if (isDark.value) {
79
+ html.classList.add('dark')
80
+ } else {
81
+ html.classList.remove('dark')
82
+ }
83
+ }
84
+
85
+ // 切换主题(用户操作,持久化)
58
86
  const toggleTheme = () => {
59
87
  isDark.value = !isDark.value
60
88
  theme.value = isDark.value ? 'dark' : 'light'
61
89
  updateTheme()
62
90
  }
63
91
 
64
- // 设置主题
92
+ // 设置主题(用户操作,持久化)
65
93
  const setTheme = (mode: ThemeMode) => {
66
94
  theme.value = mode
67
95
  isDark.value = mode === 'dark'
68
96
  updateTheme()
69
97
  }
70
98
 
71
- // 更新主题
99
+ // 更新主题(用户操作路径:应用 DOM + 持久化)
72
100
  const updateTheme = () => {
73
- const html = document.documentElement
74
- if (isDark.value) {
75
- html.classList.add('dark')
76
- } else {
77
- html.classList.remove('dark')
78
- }
101
+ applyThemeClass()
79
102
  local.set('isDark', isDark.value)
80
103
  local.set('theme', theme.value)
81
104
  }
@@ -110,9 +133,8 @@ export const useAppStore = defineStore('app', () => {
110
133
  local.set('showBreadcrumb', showBreadcrumb.value)
111
134
  }
112
135
 
113
- // 设置主题色(同时生成 light-1..9 / dark-1..3 色阶,保证浅色变体跟随)
114
- const setPrimaryColor = (color: string) => {
115
- primaryColor.value = color
136
+ // 主题色 CSS 变量应用(含 light-1..9 / dark-1..3 色阶;不写 localStorage,供初始化与重置使用)
137
+ const applyPrimaryColorVars = (color: string) => {
116
138
  const root = document.documentElement
117
139
  root.style.setProperty('--xto-color-primary', color)
118
140
  const rgb = hexToRgb(color)
@@ -130,9 +152,31 @@ export const useAppStore = defineStore('app', () => {
130
152
  root.style.setProperty('--xto-color-primary-dark-3', mix(0.45, false))
131
153
  root.style.setProperty('--xto-color-ring', color)
132
154
  }
155
+ }
156
+
157
+ // 设置主题色(用户操作,持久化)
158
+ const setPrimaryColor = (color: string) => {
159
+ primaryColor.value = color
160
+ applyPrimaryColorVars(color)
133
161
  local.set('primaryColor', color)
134
162
  }
135
163
 
164
+ // 恢复默认:清掉本地偏好,回到服务端按应用配置(无配置则回内置默认)
165
+ const resetToServerDefaults = () => {
166
+ PREFERENCE_KEYS.forEach((key) => local.remove(key))
167
+ const ui = getServerUiConfig()
168
+ const features = ui.features || {}
169
+ layout.value = ui.layout ?? 'sidebar'
170
+ primaryColor.value = ui.primaryColor ?? '#4F46E5'
171
+ theme.value = ui.theme ?? 'light'
172
+ isDark.value = theme.value === 'dark'
173
+ showTabs.value = features.showTabs ?? true
174
+ showFooter.value = features.showFooter ?? true
175
+ showBreadcrumb.value = features.showBreadcrumb ?? true
176
+ applyThemeClass()
177
+ applyPrimaryColorVars(primaryColor.value)
178
+ }
179
+
136
180
  // 添加缓存页面
137
181
  const addCachedView = (name: string) => {
138
182
  if (!cachedViews.value.includes(name)) {
@@ -158,16 +202,14 @@ export const useAppStore = defineStore('app', () => {
158
202
  mixSubMenus.value = menus
159
203
  }
160
204
 
161
- // 初始化主题
205
+ // 初始化主题:只应用运行时效果,不持久化(避免把服务端默认值固化成用户偏好)
162
206
  const initTheme = () => {
163
- updateTheme()
164
- if (primaryColor.value !== '#4F46E5') {
165
- document.documentElement.style.setProperty('--xto-color-primary', primaryColor.value)
166
- }
207
+ applyThemeClass()
208
+ applyPrimaryColorVars(primaryColor.value)
167
209
  }
168
210
 
169
211
  // 监听主题变化
170
- watch(isDark, updateTheme)
212
+ watch(isDark, applyThemeClass)
171
213
 
172
214
  return {
173
215
  appName,
@@ -181,6 +223,7 @@ export const useAppStore = defineStore('app', () => {
181
223
  showFooter,
182
224
  showBreadcrumb,
183
225
  primaryColor,
226
+ allowUserOverride,
184
227
  cachedViews,
185
228
  mixSubMenus,
186
229
  themeClass,
@@ -195,6 +238,7 @@ export const useAppStore = defineStore('app', () => {
195
238
  toggleFooter,
196
239
  toggleBreadcrumb,
197
240
  setPrimaryColor,
241
+ resetToServerDefaults,
198
242
  addCachedView,
199
243
  removeCachedView,
200
244
  clearCachedViews,
@@ -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
  }