home.vue 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301
  1. <script setup lang="ts">
  2. import type { IContextMenuItem } from '@velofex-core/tabs-ui';
  3. import type { MenuProps } from 'antdv-next';
  4. import {
  5. computed,
  6. defineComponent,
  7. h,
  8. onBeforeUnmount,
  9. onMounted,
  10. ref,
  11. watch,
  12. } from 'vue';
  13. import {
  14. loadLocaleMessages,
  15. type SupportedLanguagesType,
  16. } from '@velofex/locales';
  17. import { preferences, updatePreferences } from '@velofex/preferences';
  18. import { VbenScrollbar } from '@velofex-core/shadcn-ui';
  19. import { TabsView } from '@velofex-core/tabs-ui';
  20. import {
  21. curUserInfo,
  22. type curUserInfoPayload,
  23. leftMenuListFromB,
  24. } from '@/api/account';
  25. import defaultCompanyLogo from '@/assets/image/earth_18301626@2x.png';
  26. import defaultAvatar from '@/assets/image/user.png';
  27. import { resolveEnterpriseCodeFromLocation } from '@/router/guard';
  28. import { Dropdown, Menu } from 'antdv-next';
  29. import HomeDashboardTab from '#/components/home-dashboard-tab.vue';
  30. import SelectLang from '#/components/select-lang.vue';
  31. import { $t } from '#/locales';
  32. const HOME_TAB_KEY = '__home__';
  33. const HomeTabIcon = defineComponent({
  34. name: 'HomeTabIcon',
  35. setup(_, { attrs }) {
  36. return () =>
  37. h('i', {
  38. ...attrs,
  39. class: ['home-tab-icon', 'icon-dashboard', attrs.class],
  40. });
  41. },
  42. });
  43. const leftMenuItems = ref<MenuProps['items']>([]);
  44. const selectedKeys = ref<string[]>([]);
  45. const userMenuItems = computed<MenuProps['items']>(() => [
  46. { key: 'Logout', label: $t('home.userMenu.logout') },
  47. ]);
  48. const openMenuKeys = ref<string[]>([]);
  49. const isMobileSidebarOpen = ref(false);
  50. const userInfo = ref<curUserInfoPayload>();
  51. const pageTitle = ref('');
  52. const pageCrumb = ref('');
  53. const menuMetaByKey = ref<
  54. Record<string, { crumb: string; iframeSrc: string; title: string }>
  55. >({});
  56. const iframeTabs = ref<
  57. Array<{ crumb: string; iframeSrc: string; key: string; title: string }>
  58. >([]);
  59. const activeTabKey = ref('');
  60. const homeRefreshStamp = ref(Date.now());
  61. const companyLogoSrc = ref('/Content/Images/company-logo.png');
  62. const avatarSrc = computed(() => {
  63. const avatar = userInfo.value?.avatarFileId;
  64. return avatar ? `/File/Download?fileId=${avatar}` : defaultAvatar;
  65. });
  66. const contentTabs = computed(() => {
  67. return iframeTabs.value.map((tab) => {
  68. return {
  69. fullPath: tab.key,
  70. meta: {
  71. icon: tab.key === HOME_TAB_KEY ? HomeTabIcon : undefined,
  72. tabClosable: tab.key !== HOME_TAB_KEY,
  73. title: tab.title,
  74. },
  75. name: tab.title,
  76. path: tab.key,
  77. } as any;
  78. });
  79. });
  80. const homeTabMeta = computed(() => ({
  81. crumb: $t('home.dashboard.crumb'),
  82. iframeSrc: '',
  83. icon: HomeTabIcon,
  84. key: HOME_TAB_KEY,
  85. title: $t('home.dashboard.tab'),
  86. }));
  87. function handleCompanyLogoError() {
  88. companyLogoSrc.value = defaultCompanyLogo;
  89. }
  90. type RawMenuNode = {
  91. code?: string;
  92. deleted?: boolean;
  93. fullName?: string;
  94. icon?: string;
  95. iconClass?: string;
  96. iconColor?: string;
  97. id?: string;
  98. isDeleted?: boolean;
  99. link?: string;
  100. name?: string;
  101. subMenuList?: RawMenuNode[];
  102. };
  103. function buildMenuIframeSrc(link?: string, id?: string) {
  104. const linkPart = String(link ?? '').trim();
  105. if (!linkPart) {
  106. return '';
  107. }
  108. const idPart = String(id ?? '').trim();
  109. const addMenuIdQuery = (url: string) => {
  110. if (!idPart) {
  111. return url;
  112. }
  113. const hashIndex = url.indexOf('#');
  114. const pathAndQuery = hashIndex === -1 ? url : url.slice(0, hashIndex);
  115. const hashPart = hashIndex === -1 ? '' : url.slice(hashIndex);
  116. const joiner = pathAndQuery.includes('?') ? '&' : '?';
  117. return `${pathAndQuery}${joiner}menuId=${idPart}${hashPart}`;
  118. };
  119. if (/^https?:\/\//i.test(linkPart)) {
  120. return addMenuIdQuery(linkPart);
  121. }
  122. const normalizedPath = linkPart.startsWith('/') ? linkPart : `/${linkPart}`;
  123. return addMenuIdQuery(normalizedPath);
  124. }
  125. function normalizeMenuIconClass(iconClass?: string) {
  126. if (!iconClass) {
  127. return [];
  128. }
  129. return iconClass
  130. .replaceAll(',', ' ')
  131. .split(' ')
  132. .map((token) => token.trim())
  133. .filter(Boolean);
  134. }
  135. function renderMenuLabel(node: RawMenuNode, fallbackText: string) {
  136. const iconClass = node.iconClass || node.icon;
  137. const text = node.name || node.fullName || fallbackText;
  138. const normalizedIconClass = normalizeMenuIconClass(iconClass);
  139. if (normalizedIconClass.length === 0) {
  140. return text;
  141. }
  142. return h('span', { class: 'menu-node-label' }, [
  143. h('i', {
  144. class: ['menu-node-icon', ...normalizedIconClass],
  145. }),
  146. h('span', { class: 'menu-node-text' }, text),
  147. ]);
  148. }
  149. function mapMenuItems(
  150. nodes: RawMenuNode[],
  151. firstLeafPathRef: string[],
  152. parentPath: string[] = [],
  153. parentLabelPath: string[] = [],
  154. metaMap: Record<
  155. string,
  156. { crumb: string; iframeSrc: string; title: string }
  157. > = {},
  158. ): NonNullable<MenuProps['items']> {
  159. return nodes
  160. .filter((node) => !(node.deleted || node.isDeleted))
  161. .map((node, index) => {
  162. const key = node.id || node.code || `${node.name ?? 'menu'}-${index}`;
  163. const labelText = node.name || node.fullName || key;
  164. const currentPath = [...parentPath, key];
  165. const currentLabelPath = [...parentLabelPath, labelText];
  166. metaMap[key] = {
  167. crumb: currentLabelPath.join(' / '),
  168. iframeSrc: buildMenuIframeSrc(node.link, node.id),
  169. title: labelText,
  170. };
  171. const children = mapMenuItems(
  172. node.subMenuList ?? [],
  173. firstLeafPathRef,
  174. currentPath,
  175. currentLabelPath,
  176. metaMap,
  177. );
  178. if (children.length === 0 && firstLeafPathRef.length === 0) {
  179. firstLeafPathRef.push(...currentPath);
  180. }
  181. return {
  182. key,
  183. label: renderMenuLabel(node, key),
  184. children: children.length > 0 ? children : undefined,
  185. };
  186. });
  187. }
  188. function applyMenuMeta(key: string) {
  189. const meta = menuMetaByKey.value[key];
  190. if (!meta) {
  191. pageTitle.value = '';
  192. pageCrumb.value = '';
  193. return;
  194. }
  195. pageTitle.value = meta.title;
  196. pageCrumb.value = meta.crumb;
  197. }
  198. function clearActiveTab() {
  199. activeTabKey.value = '';
  200. selectedKeys.value = [];
  201. pageTitle.value = '';
  202. pageCrumb.value = '';
  203. }
  204. function ensureHomeTab() {
  205. const nextHomeTab = homeTabMeta.value;
  206. const index = iframeTabs.value.findIndex((tab) => tab.key === HOME_TAB_KEY);
  207. if (index === -1) {
  208. iframeTabs.value.unshift(nextHomeTab);
  209. return;
  210. }
  211. iframeTabs.value.splice(index, 1, nextHomeTab);
  212. }
  213. function setActiveTab(key: string) {
  214. activeTabKey.value = key;
  215. if (key === HOME_TAB_KEY) {
  216. selectedKeys.value = [];
  217. pageTitle.value = homeTabMeta.value.title;
  218. pageCrumb.value = homeTabMeta.value.crumb;
  219. return;
  220. }
  221. selectedKeys.value = [key];
  222. applyMenuMeta(key);
  223. }
  224. function openIframeTab(key: string) {
  225. const meta = menuMetaByKey.value[key];
  226. if (!meta?.iframeSrc) {
  227. return;
  228. }
  229. const nextTab = {
  230. crumb: meta.crumb,
  231. iframeSrc: meta.iframeSrc,
  232. key,
  233. title: meta.title,
  234. };
  235. const existingIndex = iframeTabs.value.findIndex((tab) => tab.key === key);
  236. if (existingIndex === -1) {
  237. iframeTabs.value.push(nextTab);
  238. } else {
  239. iframeTabs.value.splice(existingIndex, 1, nextTab);
  240. }
  241. setActiveTab(key);
  242. }
  243. async function loadLeftMenuFromB() {
  244. const { result } = await leftMenuListFromB();
  245. const firstLeafPath: string[] = [];
  246. const nextMetaMap: Record<
  247. string,
  248. { crumb: string; iframeSrc: string; title: string }
  249. > = {};
  250. const items = mapMenuItems(
  251. result?.subMenuList ?? [],
  252. firstLeafPath,
  253. [],
  254. [],
  255. nextMetaMap,
  256. );
  257. menuMetaByKey.value = nextMetaMap;
  258. iframeTabs.value = iframeTabs.value.flatMap((tab) => {
  259. if (tab.key === HOME_TAB_KEY) {
  260. return [homeTabMeta.value];
  261. }
  262. const meta = nextMetaMap[tab.key];
  263. if (!meta?.iframeSrc) {
  264. return [];
  265. }
  266. return [
  267. {
  268. crumb: meta.crumb,
  269. iframeSrc: meta.iframeSrc,
  270. key: tab.key,
  271. title: meta.title,
  272. },
  273. ];
  274. });
  275. ensureHomeTab();
  276. leftMenuItems.value = items;
  277. if (firstLeafPath.length > 0) {
  278. openMenuKeys.value = items
  279. .filter(
  280. (item) =>
  281. item &&
  282. (item as any).children &&
  283. Array.isArray((item as any).children) &&
  284. (item as any).children.length > 0,
  285. )
  286. .map((item) => String(item!.key));
  287. if (activeTabKey.value && nextMetaMap[activeTabKey.value]?.iframeSrc) {
  288. setActiveTab(activeTabKey.value);
  289. } else if (iframeTabs.value.length > 0) {
  290. setActiveTab(iframeTabs.value[0]!.key);
  291. }
  292. } else {
  293. openMenuKeys.value = [];
  294. ensureHomeTab();
  295. setActiveTab(HOME_TAB_KEY);
  296. }
  297. }
  298. async function getCurUserInfo() {
  299. const { result } = await curUserInfo();
  300. userInfo.value = result;
  301. }
  302. let isRefreshingData = false;
  303. let refreshQueued = false;
  304. let isApplyingLocaleFromUserInfo = false;
  305. let hasInitializedLanguageFromUserInfo = false;
  306. function resolveLocaleByUserLanguage(
  307. language?: string,
  308. ): null | SupportedLanguagesType {
  309. const value = String(language ?? '').trim();
  310. if (value === 'en' || value === 'en-US') {
  311. return 'en-US';
  312. }
  313. if (value === 'zh-CN' || value === 'zh') {
  314. return 'zh-CN';
  315. }
  316. return null;
  317. }
  318. async function refreshHomeData() {
  319. if (isRefreshingData) {
  320. refreshQueued = true;
  321. return;
  322. }
  323. isRefreshingData = true;
  324. do {
  325. refreshQueued = false;
  326. await getCurUserInfo();
  327. if (!hasInitializedLanguageFromUserInfo) {
  328. const localeFromUserInfo = resolveLocaleByUserLanguage(
  329. userInfo.value?.language,
  330. );
  331. if (localeFromUserInfo && localeFromUserInfo !== preferences.app.locale) {
  332. isApplyingLocaleFromUserInfo = true;
  333. try {
  334. updatePreferences({
  335. app: {
  336. locale: localeFromUserInfo,
  337. },
  338. });
  339. await loadLocaleMessages(localeFromUserInfo);
  340. } finally {
  341. isApplyingLocaleFromUserInfo = false;
  342. }
  343. }
  344. hasInitializedLanguageFromUserInfo = true;
  345. }
  346. await loadLeftMenuFromB();
  347. } while (refreshQueued);
  348. isRefreshingData = false;
  349. }
  350. function handleLocaleStorageChange(event: StorageEvent) {
  351. if (!event.key?.endsWith('-preferences-locale')) {
  352. return;
  353. }
  354. if (event.newValue === event.oldValue) {
  355. return;
  356. }
  357. void refreshHomeData();
  358. }
  359. onMounted(async () => {
  360. await refreshHomeData();
  361. window.addEventListener('storage', handleLocaleStorageChange);
  362. });
  363. onBeforeUnmount(() => {
  364. window.removeEventListener('storage', handleLocaleStorageChange);
  365. });
  366. watch(
  367. () => preferences.app.locale,
  368. (locale, previousLocale) => {
  369. if (locale === previousLocale || isApplyingLocaleFromUserInfo) {
  370. return;
  371. }
  372. void refreshHomeData();
  373. },
  374. );
  375. function handleMenuOpenChange(keys: string[]) {
  376. openMenuKeys.value = keys;
  377. }
  378. function handleLeftMenuClick({ key }: { key: string }) {
  379. const normalizedKey = String(key);
  380. openIframeTab(normalizedKey);
  381. closeMobileSidebar();
  382. }
  383. function findMenuPathByKey(
  384. items: MenuProps['items'],
  385. targetKey: string,
  386. parentPath: string[] = [],
  387. ): null | string[] {
  388. for (const item of items ?? []) {
  389. if (!item) {
  390. continue;
  391. }
  392. const currentKey = String((item as any).key ?? '');
  393. if (!currentKey) {
  394. continue;
  395. }
  396. const currentPath = [...parentPath, currentKey];
  397. if (currentKey === targetKey) {
  398. return currentPath;
  399. }
  400. const childItems = (item as any).children as MenuProps['items'];
  401. if (!childItems || childItems.length === 0) {
  402. continue;
  403. }
  404. const matchedPath = findMenuPathByKey(childItems, targetKey, currentPath);
  405. if (matchedPath) {
  406. return matchedPath;
  407. }
  408. }
  409. return null;
  410. }
  411. function handleDashboardMenuOpen(key: string) {
  412. const normalizedKey = String(key ?? '');
  413. if (!normalizedKey) {
  414. return;
  415. }
  416. if (!menuMetaByKey.value[normalizedKey]?.iframeSrc) {
  417. return;
  418. }
  419. const menuPath = findMenuPathByKey(leftMenuItems.value, normalizedKey);
  420. if (menuPath && menuPath.length > 1) {
  421. const parentKeys = menuPath.slice(0, -1);
  422. openMenuKeys.value = [...new Set([...openMenuKeys.value, ...parentKeys])];
  423. }
  424. openIframeTab(normalizedKey);
  425. closeMobileSidebar();
  426. }
  427. function handleTabChange(key: string) {
  428. const normalizedKey = String(key);
  429. if (normalizedKey !== HOME_TAB_KEY && !menuMetaByKey.value[normalizedKey]) {
  430. return;
  431. }
  432. setActiveTab(normalizedKey);
  433. }
  434. function handleTabClose(key: string) {
  435. const normalizedKey = String(key);
  436. if (normalizedKey === HOME_TAB_KEY) {
  437. return;
  438. }
  439. const currentIndex = iframeTabs.value.findIndex(
  440. (tab) => tab.key === normalizedKey,
  441. );
  442. if (currentIndex === -1) {
  443. return;
  444. }
  445. iframeTabs.value.splice(currentIndex, 1);
  446. if (activeTabKey.value !== normalizedKey) {
  447. return;
  448. }
  449. const nextActiveTab =
  450. iframeTabs.value[currentIndex] ??
  451. iframeTabs.value[currentIndex - 1] ??
  452. null;
  453. if (nextActiveTab) {
  454. setActiveTab(nextActiveTab.key);
  455. return;
  456. }
  457. clearActiveTab();
  458. }
  459. function handleSortTabs(oldIndex: number, newIndex: number) {
  460. if (oldIndex === newIndex) {
  461. return;
  462. }
  463. const nextTabs = [...iframeTabs.value];
  464. const movedTabs = nextTabs.splice(oldIndex, 1);
  465. const movedTab = movedTabs[0];
  466. if (!movedTab) {
  467. return;
  468. }
  469. nextTabs.splice(newIndex, 0, movedTab);
  470. iframeTabs.value = nextTabs;
  471. }
  472. function addTabRefreshStamp(url: string) {
  473. const rawUrl = String(url ?? '');
  474. if (!rawUrl) {
  475. return rawUrl;
  476. }
  477. const hashIndex = rawUrl.indexOf('#');
  478. const pathAndQuery = hashIndex === -1 ? rawUrl : rawUrl.slice(0, hashIndex);
  479. const hashPart = hashIndex === -1 ? '' : rawUrl.slice(hashIndex);
  480. const [pathPart, queryPart = ''] = pathAndQuery.split('?');
  481. const params = new URLSearchParams(queryPart);
  482. params.set('__tab_refresh', String(Date.now()));
  483. const nextQuery = params.toString();
  484. return `${pathPart}${nextQuery ? `?${nextQuery}` : ''}${hashPart}`;
  485. }
  486. function refreshTabByKey(key: string) {
  487. const normalizedKey = String(key);
  488. if (normalizedKey === HOME_TAB_KEY) {
  489. homeRefreshStamp.value = Date.now();
  490. setActiveTab(HOME_TAB_KEY);
  491. return;
  492. }
  493. const index = iframeTabs.value.findIndex((tab) => tab.key === normalizedKey);
  494. if (index === -1) {
  495. return;
  496. }
  497. const targetTab = iframeTabs.value[index];
  498. if (!targetTab) {
  499. return;
  500. }
  501. iframeTabs.value.splice(index, 1, {
  502. ...targetTab,
  503. iframeSrc: addTabRefreshStamp(targetTab.iframeSrc),
  504. });
  505. setActiveTab(normalizedKey);
  506. }
  507. function closeOtherTabsByKey(key: string) {
  508. const normalizedKey = String(key);
  509. const targetTab = iframeTabs.value.find((tab) => tab.key === normalizedKey);
  510. if (!targetTab) {
  511. return;
  512. }
  513. iframeTabs.value =
  514. normalizedKey === HOME_TAB_KEY
  515. ? [homeTabMeta.value]
  516. : [homeTabMeta.value, targetTab];
  517. setActiveTab(normalizedKey);
  518. }
  519. function closeAllTabs() {
  520. iframeTabs.value = [homeTabMeta.value];
  521. setActiveTab(HOME_TAB_KEY);
  522. }
  523. function createTabContextMenus(tab: { key?: string }) {
  524. const tabKey = String(tab?.key ?? '');
  525. const hasTabs = iframeTabs.value.length > 0;
  526. const isCurrentTabRefreshable =
  527. hasTabs && tabKey && iframeTabs.value.some((item) => item.key === tabKey);
  528. const isHomeTab = tabKey === HOME_TAB_KEY;
  529. const isCurrentTabClosable =
  530. hasTabs &&
  531. !isHomeTab &&
  532. tabKey &&
  533. iframeTabs.value.some((item) => item.key === tabKey);
  534. const menus: IContextMenuItem[] = [
  535. {
  536. disabled: !isCurrentTabRefreshable,
  537. handler: () => {
  538. refreshTabByKey(tabKey);
  539. },
  540. key: 'refresh-current',
  541. text: '刷新当前',
  542. },
  543. {
  544. disabled: !isCurrentTabClosable,
  545. handler: () => {
  546. handleTabClose(tabKey);
  547. },
  548. key: 'close-current',
  549. text: '关闭当前',
  550. },
  551. {
  552. disabled: !tabKey || iframeTabs.value.length <= 1,
  553. handler: () => {
  554. closeOtherTabsByKey(tabKey);
  555. },
  556. key: 'close-other',
  557. text: '关闭其他',
  558. },
  559. {
  560. disabled: !hasTabs,
  561. handler: closeAllTabs,
  562. key: 'close-all',
  563. text: '关闭全部',
  564. },
  565. ];
  566. return menus;
  567. }
  568. function openMobileSidebar() {
  569. isMobileSidebarOpen.value = true;
  570. }
  571. function closeMobileSidebar() {
  572. isMobileSidebarOpen.value = false;
  573. }
  574. function handleUserMenuClick({ key }: { key: string }) {
  575. if (key === 'Logout') {
  576. const enterpriseCode = resolveEnterpriseCodeFromLocation();
  577. const logoutUrl = `/Account/Logout?enterpriseCode=${enterpriseCode}`;
  578. try {
  579. if (window.top && window.top !== window) {
  580. window.top.location.href = logoutUrl;
  581. return;
  582. }
  583. } catch {}
  584. window.location.href = logoutUrl;
  585. }
  586. }
  587. function handleIframeLoad(event: Event) {
  588. const iframe = event.target as HTMLIFrameElement | null;
  589. if (!iframe) {
  590. return;
  591. }
  592. try {
  593. const doc = iframe.contentDocument;
  594. if (!doc) {
  595. return;
  596. }
  597. doc.documentElement.style.background = 'transparent';
  598. if (doc.body) {
  599. doc.body.style.background = 'transparent';
  600. }
  601. } catch {}
  602. }
  603. </script>
  604. <template>
  605. <div class="enterprise-page">
  606. <div
  607. :class="[{ show: isMobileSidebarOpen }]"
  608. class="mobile-mask"
  609. @click="closeMobileSidebar"
  610. ></div>
  611. <div class="enterprise-shell">
  612. <aside
  613. :class="[{ 'mobile-open': isMobileSidebarOpen }]"
  614. class="left-panel"
  615. >
  616. <div class="brand-card">
  617. <div class="logo-dot">
  618. <img :src="companyLogoSrc" @error="handleCompanyLogoError" />
  619. </div>
  620. <div class="brand-text">{{ userInfo?.enterpriseName }}</div>
  621. <button
  622. class="mobile-close-btn"
  623. type="button"
  624. @click="closeMobileSidebar"
  625. >
  626. x
  627. </button>
  628. </div>
  629. <!-- <div class="menu-scroll-wrap"> -->
  630. <VbenScrollbar
  631. :shadow-bottom="false"
  632. :shadow-top="false"
  633. class="h-full"
  634. horizontal
  635. scroll-bar-class="z-10 hidden "
  636. shadow
  637. shadow-left
  638. shadow-right
  639. >
  640. <Menu
  641. :items="leftMenuItems"
  642. :open-keys="openMenuKeys"
  643. :selected-keys="selectedKeys"
  644. class="enterprise-menu"
  645. mode="inline"
  646. @click="(info: any) => handleLeftMenuClick(info)"
  647. @open-change="handleMenuOpenChange"
  648. />
  649. <!-- </div> -->
  650. </VbenScrollbar>
  651. </aside>
  652. <section class="right-panel">
  653. <header class="top-bar">
  654. <div class="title-wrap">
  655. <div class="crumb">{{ pageCrumb }}</div>
  656. </div>
  657. <div class="top-actions">
  658. <button
  659. class="mobile-toggle-btn"
  660. type="button"
  661. @click="openMobileSidebar"
  662. >
  663. Menu
  664. </button>
  665. <SelectLang />
  666. <Dropdown
  667. :menu="{
  668. items: userMenuItems,
  669. }"
  670. placement="bottom"
  671. @menu-click="(info: any) => handleUserMenuClick(info)"
  672. >
  673. <div class="user-avatar cursor-pointer">
  674. <img :src="avatarSrc" alt="avatar" />
  675. </div>
  676. </Dropdown>
  677. </div>
  678. </header>
  679. <div
  680. :class="{ 'home-active': activeTabKey === HOME_TAB_KEY }"
  681. class="content-placeholder"
  682. >
  683. <template v-if="iframeTabs.length > 0">
  684. <div class="content-tabs-wrap">
  685. <TabsView
  686. :active="activeTabKey"
  687. :context-menus="createTabContextMenus"
  688. :draggable="preferences.tabbar.draggable"
  689. :show-icon="true"
  690. :style-type="preferences.tabbar.styleType"
  691. :tabs="contentTabs"
  692. :wheelable="preferences.tabbar.wheelable"
  693. @close="handleTabClose"
  694. @sort-tabs="handleSortTabs"
  695. @update:active="handleTabChange"
  696. />
  697. </div>
  698. <div
  699. :class="{ 'home-active': activeTabKey === HOME_TAB_KEY }"
  700. class="content-iframe-stack"
  701. >
  702. <HomeDashboardTab
  703. v-show="activeTabKey === HOME_TAB_KEY"
  704. :key="homeRefreshStamp"
  705. :is-super-admin="userInfo?.isSuperAdmin"
  706. class="h-full"
  707. @open-menu="handleDashboardMenuOpen"
  708. />
  709. <iframe
  710. v-for="tab in iframeTabs"
  711. v-show="tab.key === activeTabKey && tab.key !== HOME_TAB_KEY"
  712. :key="tab.key"
  713. :src="tab.iframeSrc"
  714. border="0"
  715. class="content-iframe"
  716. @load="handleIframeLoad"
  717. ></iframe>
  718. </div>
  719. </template>
  720. <div v-else class="content-empty">暂无可显示内容</div>
  721. </div>
  722. </section>
  723. </div>
  724. </div>
  725. </template>
  726. <style scoped lang="scss">
  727. @media (width <= 960px) {
  728. .enterprise-page {
  729. padding: 10px;
  730. }
  731. .enterprise-shell {
  732. flex-direction: column;
  733. min-height: calc(100vh - 20px);
  734. }
  735. .left-panel {
  736. position: fixed;
  737. inset: 0 auto 0 0;
  738. width: 280px;
  739. border-right: 1px solid #efebf1;
  740. transition: transform 0.24s ease;
  741. transform: translateX(-100%);
  742. }
  743. .left-panel.mobile-open {
  744. transform: translateX(0);
  745. }
  746. .mobile-close-btn {
  747. display: block;
  748. }
  749. .right-panel {
  750. padding: 20px 16px;
  751. margin: 0;
  752. border-radius: 0;
  753. box-shadow: none;
  754. }
  755. .right-panel::before {
  756. border-radius: 0;
  757. }
  758. .top-bar {
  759. flex-direction: column;
  760. gap: 12px;
  761. align-items: flex-start;
  762. }
  763. .mobile-toggle-btn {
  764. display: inline-flex;
  765. align-items: center;
  766. justify-content: center;
  767. }
  768. }
  769. .enterprise-page {
  770. height: 100vh;
  771. min-height: 100vh;
  772. overflow: hidden;
  773. // background: radial-gradient(
  774. // circle at 78% 88%,
  775. // rgb(193 233 255 / 35%),
  776. // transparent 36%
  777. // ),
  778. // radial-gradient(circle at 70% 92%, rgb(255 216 199 / 30%), transparent 30%),
  779. // #f2f0f3;
  780. background: url('@/assets/image/background.png') no-repeat;
  781. background-size: cover;
  782. }
  783. .enterprise-shell {
  784. display: flex;
  785. height: 100vh;
  786. min-height: 100vh;
  787. overflow: hidden;
  788. // background: linear-gradient(180deg, #f8f6f8 0%, #fdf2f7 100%);
  789. box-shadow: 0 10px 30px rgb(60 34 51 / 8%);
  790. }
  791. .mobile-mask {
  792. position: fixed;
  793. inset: 0;
  794. z-index: 20;
  795. pointer-events: none;
  796. background: rgb(26 15 23 / 42%);
  797. opacity: 0;
  798. transition: opacity 0.2s ease;
  799. }
  800. .mobile-mask.show {
  801. pointer-events: auto;
  802. opacity: 1;
  803. }
  804. .left-panel {
  805. position: relative;
  806. z-index: 30;
  807. display: flex;
  808. flex-direction: column;
  809. width: 256px;
  810. height: 100%;
  811. min-height: 0;
  812. padding: 24px 16px;
  813. overflow: hidden;
  814. // background: linear-gradient(180deg, #f8f6f8 0%, #fdf2f7 100%);
  815. border-right: none;
  816. }
  817. .menu-scroll-wrap {
  818. flex: 1;
  819. min-height: 0;
  820. padding-right: 4px;
  821. overflow-y: auto;
  822. scrollbar-color: #cbb8c3 transparent;
  823. scrollbar-width: thin;
  824. }
  825. .menu-scroll-wrap::-webkit-scrollbar {
  826. width: 3px;
  827. }
  828. .menu-scroll-wrap::-webkit-scrollbar-thumb {
  829. background-color: #cbb8c3;
  830. border-radius: 999px;
  831. }
  832. .menu-scroll-wrap::-webkit-scrollbar-track {
  833. background: transparent;
  834. }
  835. .brand-card {
  836. display: flex;
  837. flex-direction: column;
  838. gap: 12px;
  839. align-items: center;
  840. padding-bottom: 20px;
  841. margin-bottom: 18px;
  842. border-bottom: 1px solid #ece6ed;
  843. }
  844. .mobile-close-btn {
  845. display: none;
  846. width: 28px;
  847. height: 28px;
  848. margin-left: auto;
  849. font-size: 12px;
  850. line-height: 1;
  851. color: #7a4860;
  852. background: #fff;
  853. border: 1px solid #d8cbd5;
  854. border-radius: 6px;
  855. }
  856. .logo-dot {
  857. display: grid;
  858. place-items: center;
  859. width: 30px;
  860. height: 30px;
  861. border-radius: 50%;
  862. img {
  863. width: 100%;
  864. height: 100%;
  865. object-fit: contain;
  866. }
  867. }
  868. .brand-text {
  869. font-size: 14px;
  870. font-weight: 600;
  871. color: #4f4250;
  872. }
  873. .user-avatar {
  874. display: grid;
  875. place-items: center;
  876. width: 32px;
  877. height: 32px;
  878. border-radius: 50%;
  879. img {
  880. width: 100%;
  881. height: 100%;
  882. object-fit: cover;
  883. }
  884. }
  885. :deep(.enterprise-menu) {
  886. background: transparent;
  887. border-inline-end: none;
  888. }
  889. :deep(.enterprise-menu .ant-menu-item) {
  890. height: auto;
  891. padding: 9px 10px;
  892. margin: 0 0 6px;
  893. font-size: 13px;
  894. line-height: 1.2;
  895. color: #6f6771;
  896. border-radius: 8px;
  897. }
  898. :deep(.menu-node-label) {
  899. display: inline-flex;
  900. gap: 8px;
  901. align-items: center;
  902. font-size: 16px;
  903. }
  904. :deep(.menu-node-icon) {
  905. font-size: 14px;
  906. line-height: 1;
  907. color: currentcolor;
  908. }
  909. :deep(.enterprise-menu .ant-menu-item-selected) {
  910. font-weight: 600;
  911. color: #fff;
  912. background: #8b1648;
  913. }
  914. :deep(.enterprise-menu .ant-menu-item-selected .menu-node-icon),
  915. :deep(.enterprise-menu .ant-menu-item-selected .menu-node-text) {
  916. color: #fff !important;
  917. }
  918. :deep(.enterprise-menu .ant-menu-submenu-selected > .ant-menu-submenu-title) {
  919. color: #5a4f5a;
  920. background: transparent;
  921. }
  922. :deep(.enterprise-menu .ant-menu-item-selected::after) {
  923. display: none;
  924. }
  925. :deep(.enterprise-menu .ant-menu-sub .ant-menu-item) {
  926. font-size: 12px;
  927. }
  928. :deep(.ant-menu-light.ant-menu-root.ant-menu-inline) {
  929. border-inline-end: none;
  930. }
  931. :deep(.ant-menu-light.ant-menu-inline .ant-menu-sub.ant-menu-inline) {
  932. background: none;
  933. }
  934. :deep(.enterprise-menu .ant-menu-submenu-title) {
  935. height: auto;
  936. padding: 8px 10px;
  937. margin: 0 0 6px;
  938. font-size: 13px;
  939. font-weight: 600;
  940. color: #5a4f5a;
  941. border-radius: 8px;
  942. }
  943. :deep(.enterprise-menu .ant-menu-submenu-title:hover) {
  944. color: #5a4f5a;
  945. }
  946. :deep(.content-tabs-wrap .vben-tabs-content) {
  947. height: 100%;
  948. }
  949. :deep(.content-tabs-wrap .tabs-chrome) {
  950. height: 100%;
  951. padding-right: 4px;
  952. }
  953. :deep(.content-tabs-wrap .tabs-chrome__item-main) {
  954. min-width: 72px;
  955. }
  956. :deep(.content-tabs-wrap [data-radix-scroll-area-viewport]) {
  957. overflow-y: hidden !important;
  958. scrollbar-width: none;
  959. }
  960. :deep(.content-tabs-wrap [data-radix-scroll-area-viewport]::-webkit-scrollbar) {
  961. height: 0;
  962. }
  963. :deep(.content-tabs-wrap [data-orientation='horizontal'].hidden) {
  964. display: flex !important;
  965. }
  966. :deep(.content-tabs-wrap [data-orientation='horizontal']) {
  967. height: 6px;
  968. padding: 0 2px;
  969. opacity: 0;
  970. transition: opacity 0.2s ease;
  971. }
  972. :deep(
  973. .content-tabs-wrap [data-orientation='horizontal'][data-state='visible']
  974. ) {
  975. opacity: 1;
  976. }
  977. // :deep(.content-tabs-wrap [data-orientation='horizontal'] .bg-border) {
  978. // background: rgb(139 22 72 / 50%) !important;
  979. // }
  980. :deep(.content-tabs-wrap .tabs-chrome__background-content) {
  981. border-radius: 20px !important;
  982. }
  983. :deep(
  984. .tabs-chrome__item:not(.dragging):hover:not(.is-active)
  985. .tabs-chrome__background-content
  986. ) {
  987. background-color: #c4a7b6 !important;
  988. }
  989. :deep(.content-tabs-wrap .is-active) {
  990. .tabs-chrome__background-content {
  991. background: #7e0040;
  992. }
  993. }
  994. :deep(.content-tabs-wrap .is-active .tabs-chrome__extra svg) {
  995. stroke: #fff;
  996. &:hover {
  997. background: transparent;
  998. }
  999. }
  1000. :deep(.content-tabs-wrap .is-active .text-sm) {
  1001. color: #fff !important;
  1002. }
  1003. :deep(.group.is-active .tabs-chrome__item-main) {
  1004. color: #fff;
  1005. }
  1006. :deep(.content-tabs-wrap .is-active .tabs-chrome__background-before),
  1007. :deep(.content-tabs-wrap .is-active .tabs-chrome__background-after) {
  1008. display: none;
  1009. }
  1010. :deep(.tabs-chrome__item:last-child) {
  1011. margin-right: 0;
  1012. }
  1013. :deep(.content-tabs-wrap .home-tab-icon) {
  1014. display: inline-flex;
  1015. align-items: center;
  1016. justify-content: center;
  1017. line-height: 1;
  1018. // color: #111;
  1019. vertical-align: middle;
  1020. }
  1021. // :deep(.content-tabs-wrap .is-active .home-tab-icon) {
  1022. // color: #111 !important;
  1023. // }
  1024. :deep(.overflow-y-hidden) {
  1025. overflow: hidden;
  1026. }
  1027. .right-panel {
  1028. position: relative;
  1029. display: flex;
  1030. flex: 1;
  1031. flex-direction: column;
  1032. padding: 12px 28px 32px;
  1033. overflow: hidden;
  1034. border-radius: 50px 0 0 50px;
  1035. }
  1036. .right-panel::before {
  1037. position: absolute;
  1038. inset: 0;
  1039. z-index: 0;
  1040. content: '';
  1041. background-color: #fff;
  1042. background-image: url('@/assets/image/bg.png');
  1043. background-repeat: no-repeat;
  1044. background-position: center;
  1045. background-size: cover;
  1046. }
  1047. .right-panel > * {
  1048. position: relative;
  1049. z-index: 1;
  1050. }
  1051. .top-bar {
  1052. display: flex;
  1053. align-items: center;
  1054. justify-content: space-between;
  1055. // padding-bottom: 16px;
  1056. // border-bottom: 1px solid #f0ebf1;
  1057. }
  1058. .title-wrap {
  1059. display: flex;
  1060. flex-direction: row;
  1061. align-items: baseline;
  1062. }
  1063. .crumb {
  1064. margin-left: 8px;
  1065. font-size: 14px;
  1066. color: #462424;
  1067. }
  1068. .top-actions {
  1069. display: flex;
  1070. gap: 10px;
  1071. align-items: center;
  1072. }
  1073. .mobile-toggle-btn {
  1074. display: none;
  1075. height: 32px;
  1076. padding: 0 12px;
  1077. font-size: 12px;
  1078. color: #6f4e60;
  1079. background: #fff;
  1080. border: 1px solid #dcced7;
  1081. border-radius: 16px;
  1082. }
  1083. .content-placeholder {
  1084. display: flex;
  1085. flex: 1;
  1086. flex-direction: column;
  1087. min-height: 0;
  1088. margin-top: 12px;
  1089. overflow: hidden;
  1090. background: transparent;
  1091. }
  1092. .content-placeholder.home-active {
  1093. overflow: visible;
  1094. }
  1095. .content-tabs-wrap {
  1096. flex-shrink: 0;
  1097. height: 42px;
  1098. padding: 4px 0;
  1099. overflow: hidden;
  1100. background: linear-gradient(180deg, #fcfafc 0%, #f7f3f6 100%);
  1101. border-bottom: 1px solid #f0ebf1;
  1102. }
  1103. .content-iframe-stack {
  1104. position: relative;
  1105. flex: 1;
  1106. min-height: 0;
  1107. iframe {
  1108. border-radius: 0 0 0 50px;
  1109. }
  1110. }
  1111. .content-iframe {
  1112. display: block;
  1113. width: 100%;
  1114. height: 100%;
  1115. background: transparent;
  1116. border: 0;
  1117. }
  1118. .content-empty {
  1119. display: grid;
  1120. place-items: center;
  1121. width: 100%;
  1122. height: 100%;
  1123. font-size: 14px;
  1124. color: #8b808a;
  1125. }
  1126. </style>