@pubinfo/core 2.0.14 → 2.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.
Files changed (64) hide show
  1. package/dist/{AppSetting-Bfabd02_.js → AppSetting-Dzj6YBgW.js} +17 -17
  2. package/dist/{HCheckList.vue_vue_type_script_setup_true_lang-LHEWLVoX.js → HCheckList.vue_vue_type_script_setup_true_lang-OWv01eXk.js} +1 -1
  3. package/dist/{HToggle-B3C623BZ.js → HToggle-BnESuuPG.js} +1 -1
  4. package/dist/HeaderThinMenu-Co2S6vIB.js +4 -0
  5. package/dist/{PreferencesContent-B42MPjzA.js → PreferencesContent-DBKQJ8Ob.js} +6 -6
  6. package/dist/{SettingBreadcrumb-D3GS-3MN.js → SettingBreadcrumb-DmP1nJLZ.js} +3 -3
  7. package/dist/{SettingCopyright-KDpCggiE.js → SettingCopyright-C8_F7BB2.js} +2 -2
  8. package/dist/{SettingEnableTransition-BF27WYg0.js → SettingEnableTransition-BeJMq6ny.js} +2 -2
  9. package/dist/{SettingHome-DWyDzFcn.js → SettingHome-yGmbxzJm.js} +3 -3
  10. package/dist/{SettingMenu-Dz0fbIa3.js → SettingMenu-yWnqZqmz.js} +4 -4
  11. package/dist/{SettingMode-AD_S9ZkO.js → SettingMode-B8fgUhvi.js} +1 -1
  12. package/dist/{SettingNavSearch-oWIPjQFc.js → SettingNavSearch--yQEXqEL.js} +3 -3
  13. package/dist/{SettingOther-ZlPSoHz3.js → SettingOther-CSs1_D9L.js} +3 -3
  14. package/dist/{SettingPage-BnwAMror.js → SettingPage-Pm7KblGG.js} +2 -2
  15. package/dist/{SettingTabbar-Vvz-1zTa.js → SettingTabbar-DM5hdlQf.js} +6 -6
  16. package/dist/{SettingThemes-CLhV2OOt.js → SettingThemes-DqygGgDK.js} +5 -5
  17. package/dist/{SettingToolbar-nCkGwG2H.js → SettingToolbar-BP-yWiKo.js} +3 -3
  18. package/dist/{SettingTopbar-DMujCz4n.js → SettingTopbar-Dd_SXy2e.js} +4 -4
  19. package/dist/{SettingWidthMode-7qsluuMR.js → SettingWidthMode-DOtJg5uB.js} +2 -2
  20. package/dist/built-in/devtools/index.d.ts +13 -0
  21. package/dist/built-in/index.d.ts +1 -0
  22. package/dist/built-in/layout-component/Layout.vue.d.ts +7 -1
  23. package/dist/built-in/layout-component/components/Topbar/index.vue.d.ts +1 -1
  24. package/dist/built-in/layout-component/composables/useLayoutVisible.d.ts +8 -0
  25. package/dist/built-in/layout-component/interface.d.ts +22 -0
  26. package/dist/features/stores/modules/settings.d.ts +6 -6
  27. package/dist/features/stores/modules/user.d.ts +14 -2
  28. package/dist/{index-D210UZpV.js → index--xVOZLmn.js} +1 -1
  29. package/dist/{index-DSNuCtw6.js → index-BSTB6EhQ.js} +1 -1
  30. package/dist/{index-BN5BYOEh.js → index-C88VclMA.js} +15618 -15440
  31. package/dist/{index-2Zh2rSjN.js → index-CnbD4YWA.js} +3 -3
  32. package/dist/{index-BaRQkDKe.js → index-D8kKHmiD.js} +1 -1
  33. package/dist/index-GSKGoyv_.js +4 -0
  34. package/dist/{index-DQnnCCLU.js → index-igxVB1m3.js} +4 -5
  35. package/dist/{index-XjjX4TvH.js → index-npW0xuOi.js} +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/{pick-BZcCK3Xe.js → pick-l2RrF3SE.js} +1 -1
  38. package/dist/{question-line-CfkciTFq.js → question-line-DCMVyZ3e.js} +2 -2
  39. package/dist/{right-Bfe2p1o0.js → right-aIVrXLnr.js} +4 -4
  40. package/dist/style.css +1 -1
  41. package/package.json +8 -8
  42. package/src/built-in/devtools/index.ts +292 -0
  43. package/src/built-in/index.ts +1 -0
  44. package/src/built-in/layout-component/Layout.vue +93 -9
  45. package/src/built-in/layout-component/components/Header/HeaderFullMenu/index.vue +8 -4
  46. package/src/built-in/layout-component/components/Header/index.vue +4 -11
  47. package/src/built-in/layout-component/components/Sidebar/MainSidebar.vue +3 -1
  48. package/src/built-in/layout-component/components/Sidebar/SubSidebar.vue +4 -21
  49. package/src/built-in/layout-component/components/Topbar/index.vue +4 -105
  50. package/src/built-in/layout-component/composables/useLayoutVisible.ts +87 -0
  51. package/src/built-in/layout-component/interface.ts +25 -0
  52. package/src/core/create.ts +1 -3
  53. package/src/core/request.ts +0 -1
  54. package/src/features/assets/styles/globals.css +2 -1
  55. package/src/features/composables/log.ts +3 -5
  56. package/src/features/stores/modules/keepAlive.ts +10 -14
  57. package/src/features/stores/modules/menu.ts +20 -13
  58. package/src/features/stores/modules/route.ts +3 -3
  59. package/src/features/stores/modules/settings.ts +2 -7
  60. package/src/features/stores/modules/user.ts +57 -2
  61. package/src/index.ts +3 -0
  62. package/types/vue-router.d.ts +9 -2
  63. package/dist/HeaderThinMenu-B6cpu5jx.js +0 -4
  64. package/dist/index-iASxH_cj.js +0 -4
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pubinfo/core",
3
3
  "type": "module",
4
- "version": "2.0.14",
4
+ "version": "2.1.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -31,9 +31,9 @@
31
31
  "pinia": "^3.0.3",
32
32
  "vue": "^3.5.17",
33
33
  "vue-router": "^4.5.1",
34
- "@pubinfo/config": "2.0.14",
35
- "@pubinfo/devtools": "2.0.14",
36
- "@pubinfo/vite": "2.0.14"
34
+ "@pubinfo/config": "2.1.0",
35
+ "@pubinfo/devtools": "2.1.0",
36
+ "@pubinfo/vite": "2.1.0"
37
37
  },
38
38
  "dependencies": {
39
39
  "@alova/adapter-axios": "^2.0.16",
@@ -47,7 +47,6 @@
47
47
  "@vueuse/integrations": "^13.5.0",
48
48
  "ant-design-vue": "^4.2.6",
49
49
  "axios": "^1.10.0",
50
- "consola": "^3.4.2",
51
50
  "floating-vue": "5.2.2",
52
51
  "hookable": "^5.5.3",
53
52
  "hotkeys-js": "^3.13.14",
@@ -64,7 +63,8 @@
64
63
  "sortablejs": "^1.15.6",
65
64
  "unctx": "^2.4.1",
66
65
  "vue-m-message": "^4.0.2",
67
- "zxcvbn": "^4.4.2"
66
+ "zxcvbn": "^4.4.2",
67
+ "@pubinfo/shared": "2.1.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@alova/mock": "^2.0.17",
@@ -92,8 +92,8 @@
92
92
  "vite-plugin-dts": "^4.5.4",
93
93
  "vue": "^3.5.17",
94
94
  "vue-router": "^4.5.1",
95
- "@pubinfo/config": "2.0.14",
96
- "@pubinfo/vite": "2.0.14"
95
+ "@pubinfo/config": "2.1.0",
96
+ "@pubinfo/vite": "2.1.0"
97
97
  },
98
98
  "scripts": {
99
99
  "dev": "vite build -w -m watch",
@@ -0,0 +1,292 @@
1
+ import type { ModuleOptions } from '@/core';
2
+ import { devtoolsStorageKey } from '@pubinfo/devtools/constants';
3
+ import { setupDevtoolsPanel } from '@pubinfo/devtools/panel';
4
+
5
+ /**
6
+ * 本模块将宿主应用与 @pubinfo/devtools 打通:
7
+ * - 在应用启动时注册 Devtools 面板(UnoCSS / Chrome / Server Router)。
8
+ * - 监听 HTTP 钩子,在请求打上 `meta.__devtools__` 时,
9
+ * 自动把响应快照写入 localStorage,便于面板读取与回放。
10
+ */
11
+
12
+ const DEVTOOLS_RAW_RESPONSE_KEY = devtoolsStorageKey.response;
13
+
14
+ const methodNameMap: Record<string, string> = {
15
+ GET: 'Get',
16
+ POST: 'Post',
17
+ PUT: 'Put',
18
+ PATCH: 'Patch',
19
+ DELETE: 'Delete',
20
+ HEAD: 'Head',
21
+ };
22
+
23
+ const requestMethodNames = Object.values(methodNameMap);
24
+
25
+ /**
26
+ * 粗略判断一个值是否为「请求实例」。
27
+ *
28
+ * 判定规则:对象上是否存在大小写首字母形式的方法(Get/Post/...)。
29
+ * 这是对 packages/core/src/core/request.ts 导出的请求封装的一个宽松契约检查。
30
+ */
31
+ function isRequestInstance(value: any) {
32
+ if (!value || typeof value !== 'object') {
33
+ return false;
34
+ }
35
+ return requestMethodNames.some(name => typeof (value as any)[name] === 'function');
36
+ }
37
+
38
+ /**
39
+ * 将任意 payload 转为可序列化的普通对象(若可能)。
40
+ *
41
+ * - 通过 JSON 序列化/反序列化去除响应对象上的循环引用、原型链等复杂结构。
42
+ * - 若序列化失败(比如含不可序列化值或循环引用),则返回原值。
43
+ */
44
+ function toPlainObject<T>(payload: T): T {
45
+ if (payload === undefined || payload === null) {
46
+ return payload;
47
+ }
48
+ try {
49
+ return JSON.parse(JSON.stringify(payload)) as T;
50
+ }
51
+ catch {
52
+ return payload;
53
+ }
54
+ }
55
+
56
+ interface DevtoolsRequestPayload {
57
+ moduleCandidates?: string[]
58
+ exportName?: string
59
+ method: string
60
+ args?: any[]
61
+ }
62
+
63
+ interface DevtoolsResponsePayload {
64
+ success: boolean
65
+ result?: any
66
+ error?: {
67
+ message: string
68
+ stack?: string
69
+ name?: string
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 将一次请求的响应快照持久化到 localStorage。
75
+ *
76
+ * - 仅在浏览器环境生效(SSR 时直接跳过)。
77
+ * - 存储失败(超配额/序列化失败)时静默忽略,避免影响应用行为。
78
+ *
79
+ * @param _method 原始请求方法(预留位,当前未使用)
80
+ * @param payload 响应对象或错误对象(尽量保持原貌)
81
+ */
82
+ function storeDevtoolsResponse(_method: any, payload: any) {
83
+ if (typeof localStorage === 'undefined') {
84
+ return;
85
+ }
86
+ try {
87
+ localStorage.setItem(DEVTOOLS_RAW_RESPONSE_KEY, JSON.stringify(payload));
88
+ }
89
+ catch {
90
+ // ignore serialization/storage failures
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 解析一组候选模块,尝试找到并返回「请求实例」。
96
+ *
97
+ * 查找顺序:
98
+ * 1) 指定的具名导出(若提供 exportName 且命中并为请求实例则返回)。
99
+ * 2) 默认导出(若存在且为请求实例)。
100
+ * 3) 其他任一导出成员中第一个满足 isRequestInstance 的值。
101
+ *
102
+ * 任一候选路径导入失败时会被忽略,继续尝试下一个。
103
+ */
104
+ async function resolveRequestInstance(
105
+ candidates: string[] = [],
106
+ exportName?: string,
107
+ ) {
108
+ for (const specifier of candidates) {
109
+ if (!specifier) {
110
+ continue;
111
+ }
112
+ try {
113
+ const module = await import(/* @vite-ignore */ specifier);
114
+ const namedExport = exportName ? module?.[exportName] : undefined;
115
+ if (namedExport && isRequestInstance(namedExport)) {
116
+ return namedExport;
117
+ }
118
+ const defaultExport = module?.default;
119
+ if (defaultExport && isRequestInstance(defaultExport)) {
120
+ return defaultExport;
121
+ }
122
+ for (const value of Object.values(module)) {
123
+ if (isRequestInstance(value)) {
124
+ return value;
125
+ }
126
+ }
127
+ }
128
+ catch {
129
+ // 忽略导入失败的候选路径,继续尝试下一个。
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+
135
+ /**
136
+ * 根据面板发来的 payload 执行一次 API 请求。
137
+ *
138
+ * 输入契约(payload):
139
+ * - moduleCandidates: string[] 可能导出请求实例的模块路径(可为绝对/相对/别名)。
140
+ * - exportName: string 可选,期望从模块中拿到的具名导出名。
141
+ * - method: string HTTP 方法(GET/POST/...)。
142
+ * - args: any[] 传递给请求方法的参数(如 path/query/body 等)。
143
+ *
144
+ * 行为:
145
+ * - 解析请求实例 → 获取对应的方法 → 调用并等待结果。
146
+ * - 若返回值带有 .send(),则优先调用 .send() 以与内部请求封装对齐。
147
+ * - 返回标准化的结果:{ success, result } 或 { success:false, error }。
148
+ */
149
+ async function executeDevtoolsRequest(payload: DevtoolsRequestPayload): Promise<DevtoolsResponsePayload> {
150
+ const response: DevtoolsResponsePayload = {
151
+ success: false,
152
+ };
153
+
154
+ try {
155
+ const method = (payload.method ?? '').toUpperCase();
156
+ const mappedMethod = methodNameMap[method];
157
+ if (!mappedMethod) {
158
+ throw new Error(`请求实例不支持 ${method} 方法`);
159
+ }
160
+
161
+ const requestInstance: any = await resolveRequestInstance(
162
+ payload.moduleCandidates,
163
+ payload.exportName,
164
+ );
165
+
166
+ if (!requestInstance) {
167
+ throw new TypeError('未找到匹配的请求实例,请确认 API 文件的 request 导入可被 devtools 解析。');
168
+ }
169
+
170
+ const requestMethod = requestInstance[mappedMethod];
171
+ if (typeof requestMethod !== 'function') {
172
+ throw new TypeError(`请求实例不支持 ${method} 方法`);
173
+ }
174
+
175
+ let methodResult = requestMethod.apply(requestInstance, payload.args ?? []);
176
+ if (methodResult && typeof methodResult.send === 'function') {
177
+ methodResult = await methodResult.send();
178
+ }
179
+ else {
180
+ methodResult = await methodResult;
181
+ }
182
+
183
+ response.success = true;
184
+ response.result = toPlainObject(methodResult);
185
+ }
186
+ catch (error) {
187
+ if (error && typeof error === 'object' && 'response' in (error as any) && (error as any).response) {
188
+ response.success = true;
189
+ response.result = toPlainObject((error as any).response);
190
+ }
191
+ else if (error && typeof error === 'object' && !(error instanceof Error)) {
192
+ response.success = true;
193
+ response.result = toPlainObject(error);
194
+ }
195
+ else {
196
+ response.success = false;
197
+ response.error = {
198
+ message: error instanceof Error ? error.message : String(error),
199
+ stack: error instanceof Error ? error.stack : undefined,
200
+ name: error instanceof Error ? error.name : undefined,
201
+ };
202
+ }
203
+ }
204
+
205
+ return response;
206
+ }
207
+
208
+ const MESSAGE_REQUEST_TYPE = 'pubinfo:devtools-request';
209
+ const MESSAGE_RESPONSE_TYPE = 'pubinfo:devtools-response';
210
+
211
+ let devtoolsBridgeInitialised = false;
212
+
213
+ /**
214
+ * 在宿主页面和 Devtools iframe 之间建立 message 通信桥。
215
+ *
216
+ * 协议:
217
+ * - request: { type: 'pubinfo:devtools-request', id, payload }
218
+ * - response: { type: 'pubinfo:devtools-response', id, ...result }
219
+ *
220
+ * 注意:
221
+ * - 仅初始化一次;SSR/非浏览器环境跳过。
222
+ * - postMessage 使用 event.origin → fallback '*',尽量遵循同源策略。
223
+ */
224
+ function setupDevtoolsRequestBridge() {
225
+ if (devtoolsBridgeInitialised || typeof window === 'undefined') {
226
+ return;
227
+ }
228
+ devtoolsBridgeInitialised = true;
229
+ window.addEventListener('message', async (event) => {
230
+ const data = (event as MessageEvent<any>).data;
231
+ if (!data || data.type !== MESSAGE_REQUEST_TYPE || !data.id) {
232
+ return;
233
+ }
234
+ const payload: DevtoolsRequestPayload = data.payload ?? {};
235
+ const responsePayload = await executeDevtoolsRequest(payload);
236
+ const message = {
237
+ type: MESSAGE_RESPONSE_TYPE,
238
+ id: data.id,
239
+ ...responsePayload,
240
+ };
241
+
242
+ const target = (event.source as Window | null) ?? window.parent ?? window;
243
+ const origin = (event.origin && event.origin !== 'null') ? event.origin : (window.location?.origin ?? '*');
244
+ try {
245
+ target.postMessage(message, origin);
246
+ }
247
+ catch {
248
+ target.postMessage(message, '*');
249
+ }
250
+ });
251
+ }
252
+
253
+ /**
254
+ * 内置 Devtools 模块。
255
+ *
256
+ * 注册时机:`enforce: 'post'`,确保在路由/请求等核心模块之后挂载。
257
+ *
258
+ * 行为:
259
+ * - setup:注册各类 devtools 面板。
260
+ * - setup:监听来自 Devtools iframe 的消息,响应接口调试请求。
261
+ * - hooks.http:response:当请求元信息中显式标记 `__devtools__` 时,
262
+ * 将响应(或错误响应)快照保存到 localStorage,供面板读取。
263
+ */
264
+ export function Devtools(): ModuleOptions {
265
+ return {
266
+ name: 'built-in:devtools',
267
+ enforce: 'post',
268
+ setup() {
269
+ setupDevtoolsPanel();
270
+ setupDevtoolsRequestBridge();
271
+ },
272
+ hooks: {
273
+ 'http:response': {
274
+ onSuccess(response, method) {
275
+ const methodMeta = (method as any)?.meta ?? (method as any)?.config?.meta;
276
+ if (methodMeta?.__devtools__) {
277
+ storeDevtoolsResponse(method, response);
278
+ }
279
+ return response;
280
+ },
281
+ onError(error, method) {
282
+ const methodMeta = (method as any)?.meta ?? (method as any)?.config?.meta;
283
+ if (methodMeta?.__devtools__) {
284
+ const payload = (error as any)?.response ?? error;
285
+ storeDevtoolsResponse(method, payload);
286
+ }
287
+ return error;
288
+ },
289
+ },
290
+ },
291
+ };
292
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './authentication';
2
2
  export * from './authorization';
3
+ export * from './devtools';
3
4
  export * from './layout-component';
4
5
  export * from './n-progress';
5
6
  export * from './pinia-plugin';
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import type { LayoutProps } from './interface';
3
+ import { useCssVar } from '@vueuse/core';
2
4
  import RiLogoutBoxLine from '~icons/ri/logout-box-line';
3
5
  import { useAuth } from '@/features/composables';
4
6
  import BackTop from './components/BackTop/index.vue';
@@ -6,31 +8,44 @@ import LayoutContent from './components/Content/index.vue';
6
8
  import Copyright from './components/Copyright/index.vue';
7
9
  import LayoutHeader from './components/Header/index.vue';
8
10
  import NotAllowed from './components/NotAllowed/index.vue';
11
+
9
12
  import SettingBar from './components/SettingBar/index.vue';
10
13
  import LayoutSidebar from './components/Sidebar/index.vue';
11
-
12
14
  import LayoutTopbar from './components/Topbar/index.vue';
13
15
  import { useContext } from './composables/useContext';
14
16
  import { useGetSidebarActualWidth } from './composables/useGetComputedStyle';
15
17
  import { useHotkey } from './composables/useHotkey';
18
+ import { createLayoutVisible, useLayoutVisible } from './composables/useLayoutVisible';
16
19
  import { useWatermark } from './composables/useWatermark';
17
20
 
18
21
  defineOptions({
19
22
  name: 'Layout',
20
23
  });
21
24
 
22
- const routeInfo = useRoute();
23
- const { auth } = useAuth();
25
+ const props = withDefaults(
26
+ defineProps<LayoutProps>(),
27
+ {
28
+ showHeader: true,
29
+ showSidebar: true,
30
+ showTabbar: true,
31
+ showToolbar: true,
32
+ },
33
+ );
24
34
 
25
- const isAuth = computed(() => {
26
- return routeInfo.matched.every(item => auth(item.meta.auth ?? ''));
27
- });
35
+ createLayoutVisible(props);
36
+ const { show } = useLayoutVisible(props);
28
37
 
38
+ const routeInfo = useRoute();
39
+ const { auth } = useAuth();
29
40
  const { settingsStore } = useContext();
30
41
  const { mainSidebarActualWidth, subSidebarActualWidth } = useGetSidebarActualWidth();
31
42
  useHotkey();
32
43
  useWatermark();
33
44
 
45
+ const isAuth = computed(() => {
46
+ return routeInfo.matched.every(item => auth(item.meta.auth ?? ''));
47
+ });
48
+
34
49
  watch(() => settingsStore.settings.menu.subMenuCollapse, (val) => {
35
50
  if (settingsStore.mode === 'mobile') {
36
51
  if (!val) {
@@ -49,6 +64,37 @@ watch(() => routeInfo.path, () => {
49
64
  });
50
65
  }
51
66
  });
67
+
68
+ const scrollTop = ref(0);
69
+ const scrollOnHide = ref(false);
70
+ const globalTabbarHeight = useCssVar('--g-tabbar-height', document.documentElement || document.body);
71
+ const globalToolBarHeight = useCssVar('--g-toolbar-height', document.documentElement || document.body);
72
+
73
+ const topbarHeight = computed(() => {
74
+ const tabbarHeight = show.value.tabbar
75
+ ? Number.parseInt(globalTabbarHeight.value!)
76
+ : 0;
77
+ const toolbarHeight = show.value.toolbar
78
+ ? Number.parseInt(globalToolBarHeight.value!)
79
+ : 0;
80
+ return tabbarHeight + toolbarHeight;
81
+ });
82
+
83
+ function onScroll() {
84
+ scrollTop.value = (document.documentElement || document.body).scrollTop;
85
+ }
86
+
87
+ onMounted(() => {
88
+ window.addEventListener('scroll', onScroll);
89
+ });
90
+
91
+ onUnmounted(() => {
92
+ window.removeEventListener('scroll', onScroll);
93
+ });
94
+
95
+ watch(scrollTop, (val, oldVal) => {
96
+ scrollOnHide.value = settingsStore.settings.topbar.mode === 'sticky' && val > oldVal && val > topbarHeight.value;
97
+ });
52
98
  </script>
53
99
 
54
100
  <template>
@@ -81,9 +127,22 @@ watch(() => routeInfo.path, () => {
81
127
  />
82
128
 
83
129
  <div class="main-container" :style="{ 'padding-bottom': routeInfo.meta.paddingBottom }">
84
- <slot name="topbar">
85
- <LayoutTopbar />
86
- </slot>
130
+ <div
131
+ class="topbar-container"
132
+ :class="{
133
+ 'has-tabbar': show.tabbar,
134
+ 'has-toolbar': show.toolbar,
135
+ [`topbar-${settingsStore.settings.topbar.mode}`]: true,
136
+ 'shadow': !settingsStore.settings.topbar.switchTabbarAndToolbar && scrollTop,
137
+ 'hide': scrollOnHide,
138
+ 'switch-tabbar-toolbar': settingsStore.settings.topbar.switchTabbarAndToolbar,
139
+ }"
140
+ data-fixed-calc-width
141
+ >
142
+ <slot name="topbar">
143
+ <LayoutTopbar />
144
+ </slot>
145
+ </div>
87
146
 
88
147
  <div class="main">
89
148
  <div
@@ -271,4 +330,29 @@ header:not(.header-leave-active) + .wrapper .main-container .topbar-container {
271
330
  header:not(.header-leave-active) + .wrapper .main-container .topbar-container :deep(.tools) {
272
331
  --at-apply: hidden;
273
332
  }
333
+
334
+ /* 顶部栏 */
335
+ .topbar-container {
336
+ --at-apply: absolute top-0 z-100 flex flex-col;
337
+ box-shadow: 0 1px 0 0 var(--g-border-color);
338
+ transition: width 0.3s, top 0.3s, transform 0.3s, box-shadow 0.3s;
339
+ }
340
+
341
+ .topbar-container.topbar-fixed,
342
+ .topbar-container.topbar-sticky {
343
+ --at-apply: fixed;
344
+ }
345
+
346
+ .topbar-container.topbar-fixed.shadow,
347
+ .topbar-container.topbar-sticky.shadow {
348
+ box-shadow: 0 10px 10px -10px var(--g-box-shadow-color);
349
+ }
350
+
351
+ .topbar-container.topbar-sticky.hide {
352
+ top: calc((var(--g-tabbar-height) + var(--g-toolbar-height)) * -1) !important;
353
+ }
354
+
355
+ .topbar-container.switch-tabbar-toolbar {
356
+ flex-direction: column-reverse;
357
+ }
274
358
  </style>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { Menu } from '#/menu';
2
+ import type { MenuData } from '@/features';
3
3
  import { useElementSize } from '@vueuse/core';
4
4
  import SolarWidget5BoldDuotone from '~icons/solar/widget-5-bold-duotone';
5
5
  import { PubinfoIcon } from '@/features/components';
@@ -35,7 +35,7 @@ const moreApple = computed(() => {
35
35
  return menuHideIndex.value === menuStore.allMenus.length - 1;
36
36
  });
37
37
 
38
- function moreApplyClick(index: number, item: Menu.recordMainRaw) {
38
+ function moreApplyClick(index: number, item: MenuData) {
39
39
  const menuIndex = index + menuHideIndex.value + 1;
40
40
  switchTo(menuIndex, item);
41
41
  }
@@ -75,6 +75,10 @@ function fitMenus() {
75
75
  return restrictedIndex;
76
76
  }
77
77
 
78
+ function showMenu(item: MenuData) {
79
+ return (item.children && item.children.length !== 0) || item?.meta?.isDev;
80
+ }
81
+
78
82
  onMounted(() => {
79
83
  watch(
80
84
  width,
@@ -101,13 +105,13 @@ onMounted(() => {
101
105
  invisible
102
106
  :class="{
103
107
  'active': index === menuStore.actived,
104
- 'px-1 py-2': settingsStore.settings.menu.isRounded,
108
+ 'px-1 py-2': showMenu(item) && settingsStore.settings.menu.isRounded,
105
109
  'is-dev': item.meta?.isDev,
106
110
  }"
107
111
  >
108
112
  <NotCursor :is-cursor="item.meta?.isDev" :dev-text="item.meta?.devText">
109
113
  <div
110
- v-if="(item.children && item.children.length !== 0) || item?.meta?.isDev"
114
+ v-if="showMenu(item)"
111
115
  class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 px-5 py-4 text-[var(--g-header-menu-color)] transition-all hover:(bg-[var(--g-header-menu-hover-bg)] text-[var(--g-header-menu-hover-color)])"
112
116
  :class="{
113
117
  'text-[var(--g-header-menu-active-color)]! bg-[var(--g-header-menu-active-bg)]!': index === menuStore.actived,
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { useContext } from '../../composables/useContext';
2
+ import { useLayoutVisible } from '../../composables/useLayoutVisible';
3
3
  import Logo from '../Logo/index.vue';
4
4
  import Tools from '../Tools/index.vue';
5
5
  import HeaderMenu from './HeaderMenu.vue';
@@ -17,19 +17,12 @@ withDefaults(
17
17
  },
18
18
  );
19
19
 
20
- const { settingsStore, routeStore } = useContext();
21
-
22
- const route = useRoute();
23
-
24
- // 在这里需要判断当前的菜单模式是否渲染 Header
25
- const showHeader = computed<boolean>(() => {
26
- return (routeStore.getRemoteAppRouteById(route)?.meta?.layout as any)?.header ?? true;
27
- });
20
+ const { show } = useLayoutVisible();
28
21
  </script>
29
22
 
30
23
  <template>
31
- <Transition v-if="showHeader" name="header">
32
- <header v-if="settingsStore.mode === 'pc' && ['head', 'only-head'].includes(settingsStore.settings.menu.menuMode)">
24
+ <Transition name="header">
25
+ <header v-if="show.header">
33
26
  <div class="header-container">
34
27
  <div class="main">
35
28
  <slot name="logo">
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { PubinfoIcon } from '@/features/components';
3
3
  import { useContext } from '../../composables/useContext';
4
+ import { useLayoutVisible } from '../../composables/useLayoutVisible';
4
5
  import { useMenu } from '../../composables/useMenu';
5
6
  import Logo from '../Logo/index.vue';
6
7
  import Menu from '../Menu/index.vue';
@@ -13,6 +14,7 @@ const route = useRoute();
13
14
 
14
15
  const { settingsStore, menuStore, generateTitle } = useContext();
15
16
  const { switchTo } = useMenu();
17
+ const { show } = useLayoutVisible();
16
18
 
17
19
  function iconName(isActive: boolean, icon?: string, activeIcon?: string) {
18
20
  if (isActive && activeIcon) {
@@ -25,7 +27,7 @@ function iconName(isActive: boolean, icon?: string, activeIcon?: string) {
25
27
 
26
28
  <template>
27
29
  <Transition name="main-sidebar">
28
- <div v-if="['side', 'only-side'].includes(settingsStore.settings.menu.menuMode) || (settingsStore.mode === 'mobile' && settingsStore.settings.menu.menuMode !== 'single')" class="main-sidebar-container">
30
+ <div v-if="show.mainSidebar" class="main-sidebar-container">
29
31
  <Logo :show-title="false" class="sidebar-logo" />
30
32
  <!-- 侧边栏模式(含主导航) -->
31
33
  <div
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useContext } from '../../composables/useContext';
3
+ import { useLayoutVisible } from '../../composables/useLayoutVisible';
3
4
  import Logo from '../Logo/index.vue';
4
5
  import Menu from '../Menu/index.vue';
5
6
 
@@ -18,37 +19,19 @@ withDefaults(
18
19
 
19
20
  const route = useRoute();
20
21
 
21
- const { settingsStore, menuStore, routeStore } = useContext();
22
+ const { settingsStore, menuStore } = useContext();
23
+ const { show } = useLayoutVisible();
22
24
 
23
25
  const sidebarScrollTop = ref(0);
24
26
 
25
27
  function onSidebarScroll(e: Event) {
26
28
  sidebarScrollTop.value = (e.target as HTMLElement).scrollTop;
27
29
  }
28
-
29
- const enableSidebar = computed(() => {
30
- return settingsStore.mode === 'mobile' || (
31
- ['side', 'head', 'single'].includes(settingsStore.settings.menu.menuMode)
32
- && menuStore.sidebarMenus.length !== 0
33
- && !(
34
- settingsStore.settings.menu.subMenuOnlyOneHide
35
- && menuStore.sidebarMenus.length === 1
36
- && (
37
- !menuStore.sidebarMenus[0].children
38
- || menuStore.sidebarMenus[0]?.children.every(item => item.meta?.sidebar === false)
39
- )
40
- )
41
- );
42
- });
43
-
44
- const showSidebar = computed<boolean>(() => {
45
- return (routeStore.getRemoteAppRouteById(route)?.meta?.layout as any)?.sidebar ?? true;
46
- });
47
30
  </script>
48
31
 
49
32
  <template>
50
33
  <div
51
- v-if="showSidebar ? showSidebar : enableSidebar"
34
+ v-if="show.subSidebar"
52
35
  class="sub-sidebar-container"
53
36
  :class="{
54
37
  'is-collapse': settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse,