|
|
@@ -0,0 +1,740 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import type { MenuProps } from 'antdv-next';
|
|
|
+
|
|
|
+import { computed, h, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
|
+
|
|
|
+import { preferences } from '@vben/preferences';
|
|
|
+
|
|
|
+import {
|
|
|
+ curUserInfo,
|
|
|
+ type curUserInfoPayload,
|
|
|
+ leftMenuListFromB,
|
|
|
+} from '@/api/account';
|
|
|
+import defaultCompanyLogo from '@/assets/image/earth_18301626@2x.png';
|
|
|
+import defaultAvatar from '@/assets/image/user.png';
|
|
|
+import { resolveEnterpriseCodeFromLocation } from '@/router/guard';
|
|
|
+import { Dropdown, Menu } from 'antdv-next';
|
|
|
+
|
|
|
+import SelectLang from '#/components/select-lang.vue';
|
|
|
+import { $t } from '#/locales';
|
|
|
+
|
|
|
+const leftMenuItems = ref<MenuProps['items']>([]);
|
|
|
+const selectedKeys = ref<string[]>([]);
|
|
|
+const userMenuItems = computed<MenuProps['items']>(() => [
|
|
|
+ { key: 'ChangeInformation', label: $t('home.userMenu.changeInformation') },
|
|
|
+ { key: 'Logout', label: $t('home.userMenu.logout') },
|
|
|
+]);
|
|
|
+const openMenuKeys = ref<string[]>([]);
|
|
|
+const isMobileSidebarOpen = ref(false);
|
|
|
+const userInfo = ref<curUserInfoPayload>();
|
|
|
+const pageTitle = ref('');
|
|
|
+const pageCrumb = ref('');
|
|
|
+const iframeSrc = ref('');
|
|
|
+const menuMetaByKey = ref<
|
|
|
+ Record<string, { crumb: string; iframeSrc: string; title: string }>
|
|
|
+>({});
|
|
|
+const companyLogoSrc = ref('/Content/Images/company-logo.png');
|
|
|
+const avatarSrc = computed(() => {
|
|
|
+ const avatar = userInfo.value?.avatar;
|
|
|
+ return avatar ? `/File/Download?fileId=${avatar}` : defaultAvatar;
|
|
|
+});
|
|
|
+
|
|
|
+function handleCompanyLogoError() {
|
|
|
+ companyLogoSrc.value = defaultCompanyLogo;
|
|
|
+}
|
|
|
+
|
|
|
+type RawMenuNode = {
|
|
|
+ code?: string;
|
|
|
+ deleted?: boolean;
|
|
|
+ fullName?: string;
|
|
|
+ icon?: string;
|
|
|
+ iconClass?: string;
|
|
|
+ iconColor?: string;
|
|
|
+ id?: string;
|
|
|
+ isDeleted?: boolean;
|
|
|
+ link?: string;
|
|
|
+ name?: string;
|
|
|
+ subMenuList?: RawMenuNode[];
|
|
|
+};
|
|
|
+
|
|
|
+function buildMenuIframeSrc(link?: string, id?: string) {
|
|
|
+ const linkPart = String(link ?? '').trim();
|
|
|
+ if (!linkPart) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const idPart = String(id ?? '').trim();
|
|
|
+ const addMenuIdQuery = (url: string) => {
|
|
|
+ if (!idPart) {
|
|
|
+ return url;
|
|
|
+ }
|
|
|
+
|
|
|
+ const hashIndex = url.indexOf('#');
|
|
|
+ const pathAndQuery = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
|
+ const hashPart = hashIndex === -1 ? '' : url.slice(hashIndex);
|
|
|
+ const joiner = pathAndQuery.includes('?') ? '&' : '?';
|
|
|
+ return `${pathAndQuery}${joiner}menuId=${idPart}${hashPart}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ if (/^https?:\/\//i.test(linkPart)) {
|
|
|
+ return addMenuIdQuery(linkPart);
|
|
|
+ }
|
|
|
+
|
|
|
+ const normalizedPath = linkPart.startsWith('/') ? linkPart : `/${linkPart}`;
|
|
|
+ return addMenuIdQuery(normalizedPath);
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeMenuIconClass(iconClass?: string) {
|
|
|
+ if (!iconClass) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return iconClass
|
|
|
+ .replaceAll(',', ' ')
|
|
|
+ .split(' ')
|
|
|
+ .map((token) => token.trim())
|
|
|
+ .filter(Boolean);
|
|
|
+}
|
|
|
+
|
|
|
+function renderMenuLabel(node: RawMenuNode, fallbackText: string) {
|
|
|
+ const iconClass = node.iconClass || node.icon;
|
|
|
+ const text = node.name || node.fullName || fallbackText;
|
|
|
+ const normalizedIconClass = normalizeMenuIconClass(iconClass);
|
|
|
+
|
|
|
+ if (normalizedIconClass.length === 0) {
|
|
|
+ return text;
|
|
|
+ }
|
|
|
+
|
|
|
+ return h('span', { class: 'menu-node-label' }, [
|
|
|
+ h('i', {
|
|
|
+ class: ['menu-node-icon', ...normalizedIconClass],
|
|
|
+ }),
|
|
|
+ h('span', { class: 'menu-node-text' }, text),
|
|
|
+ ]);
|
|
|
+}
|
|
|
+
|
|
|
+function mapMenuItems(
|
|
|
+ nodes: RawMenuNode[],
|
|
|
+ firstLeafPathRef: string[],
|
|
|
+ parentPath: string[] = [],
|
|
|
+ parentLabelPath: string[] = [],
|
|
|
+ metaMap: Record<
|
|
|
+ string,
|
|
|
+ { crumb: string; iframeSrc: string; title: string }
|
|
|
+ > = {},
|
|
|
+): NonNullable<MenuProps['items']> {
|
|
|
+ return nodes
|
|
|
+ .filter((node) => !(node.deleted || node.isDeleted))
|
|
|
+ .map((node, index) => {
|
|
|
+ const key = node.id || node.code || `${node.name ?? 'menu'}-${index}`;
|
|
|
+ const labelText = node.name || node.fullName || key;
|
|
|
+ const currentPath = [...parentPath, key];
|
|
|
+ const currentLabelPath = [...parentLabelPath, labelText];
|
|
|
+ const topLevelTitle = currentLabelPath[0] ?? labelText;
|
|
|
+ metaMap[key] = {
|
|
|
+ crumb: currentLabelPath.join(' / '),
|
|
|
+ iframeSrc: buildMenuIframeSrc(node.link, node.id),
|
|
|
+ title: topLevelTitle,
|
|
|
+ };
|
|
|
+ const children = mapMenuItems(
|
|
|
+ node.subMenuList ?? [],
|
|
|
+ firstLeafPathRef,
|
|
|
+ currentPath,
|
|
|
+ currentLabelPath,
|
|
|
+ metaMap,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (children.length === 0 && firstLeafPathRef.length === 0) {
|
|
|
+ firstLeafPathRef.push(...currentPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ key,
|
|
|
+ label: renderMenuLabel(node, key),
|
|
|
+ children: children.length > 0 ? children : undefined,
|
|
|
+ };
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function applyMenuMeta(key: string) {
|
|
|
+ const meta = menuMetaByKey.value[key];
|
|
|
+ if (!meta) {
|
|
|
+ pageTitle.value = '';
|
|
|
+ pageCrumb.value = '';
|
|
|
+ iframeSrc.value = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ pageTitle.value = meta.title;
|
|
|
+ pageCrumb.value = meta.crumb;
|
|
|
+ iframeSrc.value = meta.iframeSrc;
|
|
|
+}
|
|
|
+
|
|
|
+async function loadLeftMenuFromB() {
|
|
|
+ const { result } = await leftMenuListFromB();
|
|
|
+ const firstLeafPath: string[] = [];
|
|
|
+ const nextMetaMap: Record<
|
|
|
+ string,
|
|
|
+ { crumb: string; iframeSrc: string; title: string }
|
|
|
+ > = {};
|
|
|
+ const items = mapMenuItems(
|
|
|
+ result?.subMenuList ?? [],
|
|
|
+ firstLeafPath,
|
|
|
+ [],
|
|
|
+ [],
|
|
|
+ nextMetaMap,
|
|
|
+ );
|
|
|
+
|
|
|
+ menuMetaByKey.value = nextMetaMap;
|
|
|
+
|
|
|
+ leftMenuItems.value = items;
|
|
|
+ if (firstLeafPath.length > 0) {
|
|
|
+ const defaultKey = firstLeafPath[firstLeafPath.length - 1]!;
|
|
|
+ selectedKeys.value = [defaultKey];
|
|
|
+ openMenuKeys.value = items
|
|
|
+ .filter(
|
|
|
+ (item) =>
|
|
|
+ item &&
|
|
|
+ (item as any).children &&
|
|
|
+ Array.isArray((item as any).children) &&
|
|
|
+ (item as any).children.length > 0,
|
|
|
+ )
|
|
|
+ .map((item) => String(item!.key));
|
|
|
+ applyMenuMeta(defaultKey);
|
|
|
+ } else {
|
|
|
+ selectedKeys.value = [];
|
|
|
+ openMenuKeys.value = [];
|
|
|
+ pageTitle.value = '';
|
|
|
+ pageCrumb.value = '';
|
|
|
+ iframeSrc.value = '';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function getCurUserInfo() {
|
|
|
+ const { result } = await curUserInfo();
|
|
|
+ userInfo.value = result;
|
|
|
+}
|
|
|
+
|
|
|
+let isRefreshingData = false;
|
|
|
+let refreshQueued = false;
|
|
|
+
|
|
|
+async function refreshHomeData() {
|
|
|
+ if (isRefreshingData) {
|
|
|
+ refreshQueued = true;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isRefreshingData = true;
|
|
|
+ do {
|
|
|
+ refreshQueued = false;
|
|
|
+ await getCurUserInfo();
|
|
|
+ await loadLeftMenuFromB();
|
|
|
+ } while (refreshQueued);
|
|
|
+ isRefreshingData = false;
|
|
|
+}
|
|
|
+
|
|
|
+function handleLocaleStorageChange(event: StorageEvent) {
|
|
|
+ if (!event.key?.endsWith('-preferences-locale')) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (event.newValue === event.oldValue) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ void refreshHomeData();
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await refreshHomeData();
|
|
|
+ window.addEventListener('storage', handleLocaleStorageChange);
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('storage', handleLocaleStorageChange);
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => preferences.app.locale,
|
|
|
+ (locale, previousLocale) => {
|
|
|
+ if (locale === previousLocale) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ void refreshHomeData();
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+function handleMenuOpenChange(keys: string[]) {
|
|
|
+ openMenuKeys.value = keys;
|
|
|
+}
|
|
|
+
|
|
|
+function handleLeftMenuClick({ key }: { key: string }) {
|
|
|
+ const normalizedKey = String(key);
|
|
|
+ selectedKeys.value = [normalizedKey];
|
|
|
+ applyMenuMeta(normalizedKey);
|
|
|
+}
|
|
|
+
|
|
|
+function openMobileSidebar() {
|
|
|
+ isMobileSidebarOpen.value = true;
|
|
|
+}
|
|
|
+
|
|
|
+function closeMobileSidebar() {
|
|
|
+ isMobileSidebarOpen.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+function handleUserMenuClick({ key }: { key: string }) {
|
|
|
+ if (key === 'ChangeInformation' && window.top && window.top !== window) {
|
|
|
+ window.top.location.href = '/user-profile';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (key === 'Logout') {
|
|
|
+ const enterpriseCode = resolveEnterpriseCodeFromLocation();
|
|
|
+ if (enterpriseCode) {
|
|
|
+ localStorage.removeItem(`token_${enterpriseCode}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const logoutUrl = `/Account/Logout?enterpriseCode=${enterpriseCode}`;
|
|
|
+ try {
|
|
|
+ if (window.top && window.top !== window) {
|
|
|
+ window.top.location.href = logoutUrl;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch {}
|
|
|
+
|
|
|
+ window.location.href = logoutUrl;
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="enterprise-page">
|
|
|
+ <div
|
|
|
+ :class="[{ show: isMobileSidebarOpen }]"
|
|
|
+ class="mobile-mask"
|
|
|
+ @click="closeMobileSidebar"
|
|
|
+ ></div>
|
|
|
+ <div class="enterprise-shell">
|
|
|
+ <aside
|
|
|
+ :class="[{ 'mobile-open': isMobileSidebarOpen }]"
|
|
|
+ class="left-panel"
|
|
|
+ >
|
|
|
+ <div class="brand-card">
|
|
|
+ <div class="logo-dot">
|
|
|
+ <img :src="companyLogoSrc" @error="handleCompanyLogoError" />
|
|
|
+ </div>
|
|
|
+ <div class="brand-text">{{ userInfo?.enterpriseName }}</div>
|
|
|
+ <button
|
|
|
+ class="mobile-close-btn"
|
|
|
+ type="button"
|
|
|
+ @click="closeMobileSidebar"
|
|
|
+ >
|
|
|
+ x
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="menu-scroll-wrap">
|
|
|
+ <Menu
|
|
|
+ :items="leftMenuItems"
|
|
|
+ :open-keys="openMenuKeys"
|
|
|
+ :selected-keys="selectedKeys"
|
|
|
+ class="enterprise-menu"
|
|
|
+ mode="inline"
|
|
|
+ @click="(info: any) => handleLeftMenuClick(info)"
|
|
|
+ @open-change="handleMenuOpenChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <section class="right-panel">
|
|
|
+ <header class="top-bar">
|
|
|
+ <div class="title-wrap">
|
|
|
+ <h1>{{ pageTitle }}</h1>
|
|
|
+ <div class="crumb">{{ pageCrumb }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="top-actions">
|
|
|
+ <button
|
|
|
+ class="mobile-toggle-btn"
|
|
|
+ type="button"
|
|
|
+ @click="openMobileSidebar"
|
|
|
+ >
|
|
|
+ Menu
|
|
|
+ </button>
|
|
|
+ <SelectLang />
|
|
|
+ <Dropdown
|
|
|
+ :menu="{
|
|
|
+ items: userMenuItems,
|
|
|
+ }"
|
|
|
+ placement="bottom"
|
|
|
+ @menu-click="(info: any) => handleUserMenuClick(info)"
|
|
|
+ >
|
|
|
+ <div class="user-avatar cursor-pointer">
|
|
|
+ <img :src="avatarSrc" alt="avatar" />
|
|
|
+ </div>
|
|
|
+ </Dropdown>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="content-placeholder">
|
|
|
+ <iframe
|
|
|
+ v-if="iframeSrc"
|
|
|
+ :src="iframeSrc"
|
|
|
+ border="0"
|
|
|
+ class="content-iframe"
|
|
|
+ ></iframe>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.enterprise-page {
|
|
|
+ height: 100vh;
|
|
|
+ min-height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+ background: radial-gradient(
|
|
|
+ circle at 78% 88%,
|
|
|
+ rgb(193 233 255 / 35%),
|
|
|
+ transparent 36%
|
|
|
+ ),
|
|
|
+ radial-gradient(circle at 70% 92%, rgb(255 216 199 / 30%), transparent 30%),
|
|
|
+ #f2f0f3;
|
|
|
+}
|
|
|
+
|
|
|
+.enterprise-shell {
|
|
|
+ display: flex;
|
|
|
+ height: 100vh;
|
|
|
+ min-height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f8f6f8;
|
|
|
+ box-shadow: 0 10px 30px rgb(60 34 51 / 8%);
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-mask {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 20;
|
|
|
+ pointer-events: none;
|
|
|
+ background: rgb(26 15 23 / 42%);
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-mask.show {
|
|
|
+ pointer-events: auto;
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.left-panel {
|
|
|
+ position: relative;
|
|
|
+ z-index: 30;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 256px;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 0;
|
|
|
+ padding: 24px 16px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f8f6f8;
|
|
|
+ border-right: none;
|
|
|
+}
|
|
|
+
|
|
|
+.menu-scroll-wrap {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ padding-right: 4px;
|
|
|
+ overflow-y: auto;
|
|
|
+ scrollbar-color: #cbb8c3 transparent;
|
|
|
+ scrollbar-width: thin;
|
|
|
+}
|
|
|
+
|
|
|
+.menu-scroll-wrap::-webkit-scrollbar {
|
|
|
+ width: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.menu-scroll-wrap::-webkit-scrollbar-thumb {
|
|
|
+ background-color: #cbb8c3;
|
|
|
+ border-radius: 999px;
|
|
|
+}
|
|
|
+
|
|
|
+.menu-scroll-wrap::-webkit-scrollbar-track {
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.brand-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ padding-bottom: 20px;
|
|
|
+ margin-bottom: 18px;
|
|
|
+ border-bottom: 1px solid #ece6ed;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-close-btn {
|
|
|
+ display: none;
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
+ margin-left: auto;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1;
|
|
|
+ color: #7a4860;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #d8cbd5;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.logo-dot {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 50%;
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.brand-text {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #4f4250;
|
|
|
+}
|
|
|
+
|
|
|
+.user-avatar {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu) {
|
|
|
+ background: transparent;
|
|
|
+ border-inline-end: none;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-item) {
|
|
|
+ height: auto;
|
|
|
+ padding: 9px 10px;
|
|
|
+ margin: 0 0 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.2;
|
|
|
+ color: #6f6771;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.menu-node-label) {
|
|
|
+ display: inline-flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.menu-node-icon) {
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1;
|
|
|
+ color: currentcolor;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-item-selected) {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ background: #8b1648;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-item-selected .menu-node-icon),
|
|
|
+:deep(.enterprise-menu .ant-menu-item-selected .menu-node-text) {
|
|
|
+ color: #fff !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-submenu-selected > .ant-menu-submenu-title) {
|
|
|
+ color: #5a4f5a;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-item-selected::after) {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-sub .ant-menu-item) {
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ant-menu-light.ant-menu-root.ant-menu-inline) {
|
|
|
+ border-inline-end: none;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ant-menu-light.ant-menu-inline .ant-menu-sub.ant-menu-inline) {
|
|
|
+ background: none;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-submenu-title) {
|
|
|
+ height: auto;
|
|
|
+ padding: 8px 10px;
|
|
|
+ margin: 0 0 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #5a4f5a;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.enterprise-menu .ant-menu-submenu-title:hover) {
|
|
|
+ color: #5a4f5a;
|
|
|
+}
|
|
|
+
|
|
|
+.right-panel {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 28px 32px;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 50px 0 0 50px;
|
|
|
+ box-shadow: 0 8px 20px rgb(95 67 84 / 10%);
|
|
|
+}
|
|
|
+
|
|
|
+.right-panel::before {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ z-index: 0;
|
|
|
+ content: '';
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.right-panel > * {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.top-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ border-bottom: 1px solid #f0ebf1;
|
|
|
+}
|
|
|
+
|
|
|
+.title-wrap {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: baseline;
|
|
|
+}
|
|
|
+
|
|
|
+.title-wrap h1 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 34px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #3e2b33;
|
|
|
+ letter-spacing: 0.2px;
|
|
|
+}
|
|
|
+
|
|
|
+.crumb {
|
|
|
+ margin-left: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8b808a;
|
|
|
+}
|
|
|
+
|
|
|
+.top-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.mobile-toggle-btn {
|
|
|
+ display: none;
|
|
|
+ height: 32px;
|
|
|
+ padding: 0 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6f4e60;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #dcced7;
|
|
|
+ border-radius: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.content-placeholder {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ margin-top: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.content-iframe {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.content-empty {
|
|
|
+ display: grid;
|
|
|
+ place-items: center;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #8b808a;
|
|
|
+}
|
|
|
+
|
|
|
+@media (width <= 960px) {
|
|
|
+ .enterprise-page {
|
|
|
+ padding: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .enterprise-shell {
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: calc(100vh - 20px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel {
|
|
|
+ position: fixed;
|
|
|
+ inset: 0 auto 0 0;
|
|
|
+ width: 280px;
|
|
|
+ border-right: 1px solid #efebf1;
|
|
|
+ transition: transform 0.24s ease;
|
|
|
+ transform: translateX(-100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .left-panel.mobile-open {
|
|
|
+ transform: translateX(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ .mobile-close-btn {
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right-panel {
|
|
|
+ padding: 20px 16px;
|
|
|
+ margin: 0;
|
|
|
+ border-radius: 0;
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .right-panel::before {
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .top-bar {
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mobile-toggle-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .title-wrap h1 {
|
|
|
+ font-size: 26px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|