hrp-ui-base 1.0.4 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hrp-ui-base",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "HRP 前端公共组件、工具方法和基础样式包",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -68,8 +68,10 @@
68
68
  "peerDependencies": {
69
69
  "@element-plus/icons-vue": "^2.3.1",
70
70
  "element-plus": "^2.10.7",
71
+ "pinia": "^3.0.3",
71
72
  "vant": "^4.9.0",
72
- "vue": "^3.5.19"
73
+ "vue": "^3.5.19",
74
+ "vue-router": "^4.0.0"
73
75
  },
74
76
  "dependencies": {
75
77
  "ali-oss": "^6.20.0",
@@ -0,0 +1,503 @@
1
+ <template>
2
+ <BaseLayout :isLeftMenu="isLeftMenu">
3
+ <template #header>
4
+ <SysHeader
5
+ :userConfig="userConfigData"
6
+ :menuList="systemMenuList"
7
+ :currentApplication="currentApplication"
8
+ :currentPath="currentPath"
9
+ :showSubmenuDrawer="!isLeftMenu"
10
+ :noticeCount="noticeStore.unReadCount"
11
+ :approvalCount="0"
12
+ :downloadActive="false"
13
+ :todoActive="false"
14
+ @menu-item-click="handleMenuItemClick"
15
+ @parent-click="handleParentClick"
16
+ @logo-click="goHomePage"
17
+ @collapse-change="handleCollapseChange"
18
+ @change-dark="handleChangeDark"
19
+ @change-follow-system="handleChangeFollowSystem"
20
+ @change-font-size="handleChangeFontSize"
21
+ @change-menu-position="handleChangeMenuPosition"
22
+ @download-click="handleDownloadClick"
23
+ @todo-click="handleTodoClick"
24
+ @message-navigate="handleMessageNavigate"
25
+ @message-download="handleMessageDownload"
26
+ @message-update-count="handleMessageUpdateCount"
27
+ @personal-sign="handlePersonalSign"
28
+ @clean-cache="handleCleanCache"
29
+ @logout="handleLogout"
30
+ />
31
+ </template>
32
+ <template #extra>
33
+ <PersonalizationGuideDialog
34
+ :visible="showNoviceGuide"
35
+ :menuPosition="configStore.userConfig.menuPosition"
36
+ :fontSize="configStore.userConfig.fontSize"
37
+ :dayOrNight="configStore.userConfig.dayOrNight"
38
+ :followSystem="configStore.userConfig.followSystem"
39
+ @update:visible="(val: boolean) => (showNoviceGuide = val)"
40
+ @preview="handleGuidePreview"
41
+ @finish="handleGuideFinish"
42
+ @skip="handleGuideSkip"
43
+ @restore="handleGuideRestore"
44
+ />
45
+ </template>
46
+ <template #side-menu>
47
+ <SideMenu
48
+ v-if="Array.isArray(leftMenuList) && leftMenuList.length > 0"
49
+ :menuList="leftMenuList"
50
+ menuPosition="left"
51
+ :currentPath="currentPath"
52
+ :navigationBar="configStore.userConfig.navigationBar"
53
+ @menu-item-click="handleMenuItemClick"
54
+ @parent-click="() => {}"
55
+ @collapse-change="handleCollapseChange"
56
+ />
57
+ </template>
58
+ <template #tabs>
59
+ <SysHeaderTabs
60
+ v-if="showTabs"
61
+ :tabs="tabsMenuList"
62
+ :activeTab="tabsMenuValue"
63
+ :homeUrl="options?.homeUrl || '/home'"
64
+ @tab-click="handleTabClick"
65
+ @tab-close="handleTabClose"
66
+ @tab-close-other="handleTabCloseOther"
67
+ @tab-close-all="handleTabCloseAll"
68
+ @tab-close-left="handleTabCloseLeft"
69
+ @tab-close-right="handleTabCloseRight"
70
+ />
71
+ </template>
72
+ <!-- 页面内容区域(默认插槽透传) -->
73
+ <slot></slot>
74
+ </BaseLayout>
75
+ </template>
76
+
77
+ <script lang="ts" setup>
78
+ import {
79
+ ref,
80
+ computed,
81
+ inject,
82
+ onMounted,
83
+ onUnmounted,
84
+ watch,
85
+ provide,
86
+ nextTick,
87
+ } from "vue";
88
+ import BaseLayout from "../base-layout/index.vue";
89
+ import SysHeader from "./SysHeader.vue";
90
+ import SysHeaderTabs from "./SysHeaderTabs.vue";
91
+ import SideMenu from "./sideMenu.vue";
92
+ import PersonalizationGuideDialog from "./personalization-guide-dialog.vue";
93
+ import { useLayoutConfigStore } from "./stores/useLayoutConfigStore";
94
+ import { useLayoutMenuStore } from "./stores/useLayoutMenuStore";
95
+ import { useLayoutTabsStore } from "./stores/useLayoutTabsStore";
96
+ import { useLayoutNoticeStore } from "./stores/useLayoutNoticeStore";
97
+ import { LAYOUT_ROUTER_KEY, LAYOUT_OPTIONS_KEY } from "./plugin";
98
+ import type { LayoutPluginOptions } from "./plugin";
99
+ import type { TabsOptions } from "./stores/useLayoutTabsStore";
100
+ import type HomeMenu from "../../api/bms/home/bo/HomeMenu";
101
+ import type NoticeVO from "../../api/notice/bo/NoticeVO";
102
+ import { ElNotification, ElMessage } from "element-plus";
103
+
104
+ defineOptions({ name: "LayoutContainer" });
105
+
106
+ const props = defineProps({
107
+ /** 是否显示 tabs 栏 */
108
+ showTabs: { type: Boolean, default: true },
109
+ });
110
+
111
+ // ====== 注入 ======
112
+ const router = inject<any>(LAYOUT_ROUTER_KEY);
113
+ const options = inject<LayoutPluginOptions>(LAYOUT_OPTIONS_KEY);
114
+
115
+ // ====== Stores ======
116
+ const configStore = useLayoutConfigStore();
117
+ const menuStore = useLayoutMenuStore();
118
+ const tabsStore = useLayoutTabsStore();
119
+ const noticeStore = useLayoutNoticeStore();
120
+
121
+ // ====== Computed ======
122
+ const menuPosition = computed(
123
+ () => configStore.userConfig.menuPosition || "top"
124
+ );
125
+ const isLeftMenu = computed(() => menuPosition.value === "left");
126
+
127
+ const leftMenuList = computed(() => {
128
+ const currentSystem =
129
+ menuStore.routerData[0]?.children?.[menuStore.currentSecondIndex] ||
130
+ menuStore.routerData[0]?.children?.[0];
131
+ return currentSystem?.children || [];
132
+ });
133
+
134
+ const systemMenuList = computed(() => {
135
+ const arr: HomeMenu[] = [];
136
+ for (let i = 0; i < menuStore.routerData.length; i++) {
137
+ arr.push(...(menuStore.routerData[i].children || []));
138
+ }
139
+ return arr;
140
+ });
141
+
142
+ const currentApplication = computed(() => menuStore.currentApplication);
143
+
144
+ const currentPath = computed(() => {
145
+ return router?.currentRoute?.value?.path || "";
146
+ });
147
+
148
+ const tabsMenuList = computed(() => tabsStore.tabsMenuList as any[]);
149
+ const tabsMenuValue = computed(() => tabsStore.tabsMenuValue);
150
+
151
+ const userConfigData = computed(() => ({
152
+ fontSize: configStore.userConfig.fontSize,
153
+ dayOrNight: configStore.userConfig.dayOrNight,
154
+ followSystem: configStore.userConfig.followSystem,
155
+ menuPosition: configStore.userConfig.menuPosition,
156
+ navigationBar: configStore.userConfig.navigationBar,
157
+ noviceGuide: configStore.userConfig.noviceGuide,
158
+ avatar: configStore.userConfig.avatar,
159
+ fileUploadType: configStore.userConfig.fileUploadType,
160
+ }));
161
+
162
+ // ====== 状态 ======
163
+ const showNoviceGuide = ref(false);
164
+ const updateNum = ref<number>(0);
165
+ const notificationRefs = ref<Map<string, any>>(new Map());
166
+
167
+ provide("updateNum", updateNum);
168
+
169
+ // ====== 菜单同步 ======
170
+ const syncSystemMenuByRoute = (path: string) => {
171
+ const systemList = menuStore.routerData[0]?.children || [];
172
+ for (let i = 0; i < systemList.length; i++) {
173
+ if (hasMenuPath(systemList[i], path)) {
174
+ if (i !== menuStore.currentSecondIndex) {
175
+ menuStore.changeCurrentSecondIndex(i, false);
176
+ }
177
+ return;
178
+ }
179
+ }
180
+ };
181
+
182
+ const hasMenuPath = (menu: HomeMenu, path: string): boolean => {
183
+ if (isSameMenuPath(menu.url, path)) return true;
184
+ return !!menu.children?.some((child: HomeMenu) => hasMenuPath(child, path));
185
+ };
186
+
187
+ const isSameMenuPath = (menuUrl?: string, currentPath?: string) => {
188
+ if (!menuUrl || !currentPath) return false;
189
+ return menuUrl.split("?")[0] === currentPath.split("?")[0];
190
+ };
191
+
192
+ // ====== SysHeader 事件处理 ======
193
+ const handleMenuItemClick = (item: HomeMenu) => {
194
+ if (item.url && currentPath.value !== item.url) {
195
+ router?.push({ path: item.url });
196
+ }
197
+ };
198
+
199
+ const handleParentClick = (_item: HomeMenu, index: number) => {
200
+ menuStore.changeCurrentSecondIndex(index, true, router);
201
+ };
202
+
203
+ const goHomePage = () => {
204
+ router?.replace({ path: options?.entryUrl || "/application-entry" });
205
+ };
206
+
207
+ const handleCollapseChange = (collapsed: boolean) => {
208
+ configStore.userConfig.navigationBar = collapsed;
209
+ configStore.changeUserConfig();
210
+ };
211
+
212
+ const handleChangeDark = (value: string, ref?: HTMLElement) => {
213
+ configStore.changeDarkAnimation(
214
+ value as "day" | "night",
215
+ ref
216
+ ? {
217
+ clientX: ref.getBoundingClientRect().x,
218
+ clientY: ref.getBoundingClientRect().y,
219
+ }
220
+ : undefined
221
+ );
222
+ };
223
+
224
+ const handleChangeFollowSystem = (value: boolean) => {
225
+ configStore.followSystemColorModel(value);
226
+ };
227
+
228
+ const handleChangeFontSize = (value: string) => {
229
+ configStore.changeFontSize(value as "small" | "medium" | "large");
230
+ };
231
+
232
+ const handleChangeMenuPosition = (value: string) => {
233
+ configStore.changeMenuPosition(value as "top" | "left");
234
+ };
235
+
236
+ const handleDownloadClick = () => {
237
+ router?.push({ path: "/bms/base-manage/base-information/export-center" });
238
+ };
239
+
240
+ const handleTodoClick = () => {
241
+ router?.push({ path: "/bms/base-manage/base-information/approval-center" });
242
+ };
243
+
244
+ const handleMessageNavigate = async (notice: NoticeVO) => {
245
+ await noticeStore.readNoticeInfo(notice.noticeId);
246
+ await noticeStore.getNoReadNoticeCount();
247
+ updateNum.value += 1;
248
+ if (notice.urlRoute.startsWith("/project/")) {
249
+ options?.onCrossAppNavigate?.(notice.urlRoute);
250
+ } else {
251
+ await router?.push(buildRouteLocation(notice.urlRoute));
252
+ }
253
+ };
254
+
255
+ const handleMessageDownload = async (notice: NoticeVO) => {
256
+ await noticeStore.readNoticeInfo(notice.noticeId);
257
+ await noticeStore.getNoReadNoticeCount();
258
+ updateNum.value += 1;
259
+ options?.onFileDownload?.(notice);
260
+ };
261
+
262
+ const handleMessageUpdateCount = async () => {
263
+ await noticeStore.getNoReadNoticeCount();
264
+ updateNum.value += 1;
265
+ };
266
+
267
+ const handlePersonalSign = () => {
268
+ // TODO: 打开签名弹窗
269
+ };
270
+
271
+ const handleCleanCache = () => {
272
+ sessionStorage.clear();
273
+ ElMessage.success("缓存清理成功");
274
+ };
275
+
276
+ const handleLogout = () => {
277
+ menuStore.doExitLogin();
278
+ };
279
+
280
+ // ====== Tabs 事件处理 ======
281
+ const handleTabClick = (tab: any) => {
282
+ if (tab.path !== currentPath.value) {
283
+ router?.push({ path: tab.path, query: tab.query });
284
+ }
285
+ };
286
+
287
+ const handleTabClose = (tab: any) => {
288
+ tabsStore.removeTabs(tab.path, router);
289
+ };
290
+
291
+ const handleTabCloseOther = (tab: any) => {
292
+ tabsStore.closeMultipleTab(tab.path);
293
+ if (currentPath.value !== tab.path) {
294
+ router?.push({ path: tab.path, query: tab.query });
295
+ }
296
+ };
297
+
298
+ const handleTabCloseAll = () => {
299
+ tabsStore.closeAllTabs(router);
300
+ };
301
+
302
+ const handleTabCloseLeft = (tab: any) => {
303
+ tabsStore.closeLeftTabs(tab.path, router);
304
+ };
305
+
306
+ const handleTabCloseRight = (tab: any) => {
307
+ tabsStore.closeRightTabs(tab.path, router);
308
+ };
309
+
310
+ // ====== 新手引导事件 ======
311
+ const handleGuidePreview = (config: {
312
+ menuPosition: string;
313
+ fontSize: string;
314
+ colorMode: string;
315
+ followSystem: boolean;
316
+ }) => {
317
+ configStore.changeFontSize(config.fontSize as "small" | "medium" | "large", true);
318
+ configStore.changeDark(config.colorMode as "day" | "night", true);
319
+ configStore.userConfig.menuPosition = config.menuPosition as "top" | "left";
320
+ };
321
+
322
+ const handleGuideFinish = (config: {
323
+ menuPosition: string;
324
+ fontSize: string;
325
+ colorMode: string;
326
+ }) => {
327
+ configStore.userConfig.menuPosition = config.menuPosition as "top" | "left";
328
+ configStore.changeFontSize(config.fontSize as "small" | "medium" | "large", true);
329
+ configStore.changeDark(config.colorMode as "day" | "night", true);
330
+ configStore.changeNoviceGuide(false);
331
+ showNoviceGuide.value = false;
332
+ };
333
+
334
+ const handleGuideSkip = () => {
335
+ configStore.changeNoviceGuide(false);
336
+ showNoviceGuide.value = false;
337
+ };
338
+
339
+ const handleGuideRestore = (config: {
340
+ menuPosition: string;
341
+ fontSize: string;
342
+ followSystem: boolean;
343
+ dayOrNight: string;
344
+ }) => {
345
+ configStore.userConfig.menuPosition = config.menuPosition as "top" | "left";
346
+ configStore.changeFontSize(config.fontSize as "small" | "medium" | "large", true);
347
+ configStore.followSystemColorModel(config.followSystem, true);
348
+ };
349
+
350
+ // ====== 通知弹窗 ======
351
+ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
352
+
353
+ const showNoticeNotification = async (notice: NoticeVO) => {
354
+ const spanId = `notice-action-${notice.noticeId}`;
355
+ const noticeId = notice.noticeId;
356
+
357
+ const closeNotification = () => {
358
+ const inst = notificationRefs.value.get(noticeId);
359
+ if (inst) {
360
+ inst.close();
361
+ notificationRefs.value.delete(noticeId);
362
+ }
363
+ };
364
+
365
+ const handleClick = async () => {
366
+ await noticeStore.readNoticeInfo(notice.noticeId);
367
+ await noticeStore.getNoReadNoticeCount();
368
+ updateNum.value += 1;
369
+ if (notice.noticeType === "file-notice") {
370
+ options?.onFileDownload?.(notice);
371
+ } else if (notice.urlRoute.startsWith("/project/")) {
372
+ options?.onCrossAppNavigate?.(notice.urlRoute);
373
+ } else {
374
+ await router?.push(buildRouteLocation(notice.urlRoute));
375
+ }
376
+ closeNotification();
377
+ };
378
+
379
+ const inst = ElNotification({
380
+ dangerouslyUseHTMLString: true,
381
+ title: notice.noticeTitle,
382
+ message: `<div>
383
+ <span style="display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;line-height:1.5em;max-height:7.5em">
384
+ ${notice.noticeContent}
385
+ </span>
386
+ </div>
387
+ <div style="text-align:right">
388
+ <span id="${spanId}" style="color:#409eff;border-bottom:1px solid #409eff;cursor:pointer">
389
+ ${notice.noticeType === "file-notice" ? "下载" : "查看详情"}
390
+ </span>
391
+ </div>`,
392
+ duration: 5000,
393
+ customClass: "notification-group",
394
+ offset: 40,
395
+ onClose: () => notificationRefs.value.delete(noticeId),
396
+ onClick: handleClick,
397
+ });
398
+
399
+ notificationRefs.value.set(noticeId, inst);
400
+
401
+ nextTick(() => {
402
+ const spanEl = document.getElementById(spanId);
403
+ if (spanEl) {
404
+ spanEl.addEventListener("click", (e) => {
405
+ e.stopPropagation();
406
+ handleClick();
407
+ });
408
+ }
409
+ });
410
+
411
+ await noticeStore.getNoReadNoticeCount();
412
+ updateNum.value += 1;
413
+ };
414
+
415
+ // ====== 辅助函数 ======
416
+ const buildRouteLocation = (urlRoute: string) => {
417
+ const [path, search] = urlRoute.split("?");
418
+ const query: Record<string, string> = {};
419
+ if (search) {
420
+ new URLSearchParams(search).forEach((value, key) => {
421
+ query[key] = value;
422
+ });
423
+ }
424
+ return { path: path || "/", query };
425
+ };
426
+
427
+ // ====== 生命周期 ======
428
+ onMounted(async () => {
429
+ // 初始化用户配置
430
+ configStore.initUserConfig();
431
+ await configStore.getUserConfig();
432
+
433
+ // 加载菜单
434
+ const routePath = router?.currentRoute?.value?.path || "";
435
+ await menuStore.getDomainAndHomeMenu(routePath, router);
436
+
437
+ // 恢复标签页
438
+ tabsStore.restoreTabsFromStorage();
439
+
440
+ // 新手引导
441
+ if (configStore.userConfig.noviceGuide) {
442
+ showNoviceGuide.value = true;
443
+ }
444
+
445
+ // 启动消息轮询
446
+ noticeStore.startPolling(async (notices) => {
447
+ for (let i = 0; i < notices.length; i++) {
448
+ showNoticeNotification(notices[i]);
449
+ if (i < notices.length - 1) {
450
+ await delay(100);
451
+ }
452
+ }
453
+ });
454
+ });
455
+
456
+ onUnmounted(() => {
457
+ noticeStore.stopPolling();
458
+ });
459
+
460
+ // 监听路由变化,添加标签页
461
+ watch(
462
+ () => router?.currentRoute?.value,
463
+ (toPath: any) => {
464
+ if (!toPath?.path || toPath.path === "/") return;
465
+ const params: TabsOptions = {
466
+ title:
467
+ (toPath.meta?.title as string) ||
468
+ (toPath.name as string) ||
469
+ "未命名页面",
470
+ path: toPath.path,
471
+ query: toPath.query as Record<string, any>,
472
+ name: toPath.name as string,
473
+ cachedCode: toPath.meta?.cachedCode as string,
474
+ };
475
+ tabsStore.addTabs(params);
476
+ },
477
+ { immediate: true, deep: true }
478
+ );
479
+
480
+ // 监听路由同步顶部菜单
481
+ watch(
482
+ [() => currentPath.value, () => menuStore.routerData],
483
+ ([routePath]) => {
484
+ if (routePath) syncSystemMenuByRoute(routePath);
485
+ },
486
+ { immediate: true, deep: true }
487
+ );
488
+ </script>
489
+
490
+ <style lang="scss">
491
+ .notification-group {
492
+ .el-notification__group {
493
+ position: static;
494
+ width: 100%;
495
+ }
496
+ .el-notification__content {
497
+ word-break: break-all;
498
+ }
499
+ }
500
+ .notification-group:hover {
501
+ cursor: pointer;
502
+ }
503
+ </style>
@@ -7,6 +7,7 @@ export { default as SideMenu } from './sideMenu.vue'
7
7
  export { default as SideMenuSonList } from './sideMenuSonList.vue'
8
8
  export { default as PersonalizationGuideDialog } from './personalization-guide-dialog.vue'
9
9
  export { default as MessageNotificationDrawer } from './message/message-notification-drawer.vue'
10
+ export { default as LayoutContainer } from './LayoutContainer.vue'
10
11
 
11
12
  // Sub-components
12
13
  export { default as DarkComponent } from './components/dark-component.vue'
@@ -20,5 +21,12 @@ export { default as AvatarComponent } from './components/avatar-component.vue'
20
21
  // Types
21
22
  export * from './types'
22
23
 
24
+ // Stores
25
+ export * from './stores'
26
+
27
+ // Plugin
28
+ export { createLayoutPlugin } from './plugin'
29
+ export type { LayoutPluginOptions } from './plugin'
30
+
23
31
  // Message dictionary
24
32
  export { NOTIFICATION_TYPE } from './message/message-dictionary'
@@ -0,0 +1,32 @@
1
+ import type { App } from "vue";
2
+ import type NoticeVO from "../../api/notice/bo/NoticeVO";
3
+
4
+ export interface LayoutPluginOptions {
5
+ /** Vue Router 实例 */
6
+ router: any;
7
+ /** 首页路径(默认 /home) */
8
+ homeUrl?: string;
9
+ /** 应用入口路径(默认 /application-entry) */
10
+ entryUrl?: string;
11
+ /** 文件下载处理函数(业务自定义) */
12
+ onFileDownload?: (notice: NoticeVO) => void;
13
+ /** PMS 跨应用跳转处理 */
14
+ onCrossAppNavigate?: (urlRoute: string) => void;
15
+ }
16
+
17
+ /** 全局注入 key */
18
+ export const LAYOUT_ROUTER_KEY = "__layout_router__";
19
+ export const LAYOUT_OPTIONS_KEY = "__layout_options__";
20
+
21
+ /**
22
+ * 创建布局插件
23
+ * SCM 在 main.ts 中使用:app.use(createLayoutPlugin({ router, ... }))
24
+ */
25
+ export function createLayoutPlugin(options: LayoutPluginOptions) {
26
+ return {
27
+ install(app: App) {
28
+ app.provide(LAYOUT_ROUTER_KEY, options.router);
29
+ app.provide(LAYOUT_OPTIONS_KEY, options);
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,5 @@
1
+ export { useLayoutConfigStore, UserConfigBo } from "./useLayoutConfigStore";
2
+ export { useLayoutMenuStore } from "./useLayoutMenuStore";
3
+ export { useLayoutTabsStore } from "./useLayoutTabsStore";
4
+ export type { TabsOptions } from "./useLayoutTabsStore";
5
+ export { useLayoutNoticeStore } from "./useLayoutNoticeStore";