Files
fe.ems.vue3/src/layouts/BasicLayout.vue
T

491 lines
14 KiB
Vue

<script setup lang="ts">
import {
ProLayout,
getMenuData,
clearMenuItem,
MenuDataItem,
} from 'antdv-pro-layout';
import RightContent from './components/RightContent.vue';
import Tabs from './components/Tabs.vue';
import GlobalMask from '@/components/GlobalMask/index.vue';
import ForcePasswdChange from '@/components/ForcePasswdChange/index.vue';
import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
import {
computed,
nextTick,
onMounted,
onUnmounted,
reactive,
watch,
} from 'vue';
import { useRouter } from 'vue-router';
import useLayoutStore from '@/store/modules/layout';
import useAppStore from '@/store/modules/app';
import useNeListStore from '@/store/modules/ne_list';
import useRouterStore from '@/store/modules/router';
import useTabsStore from '@/store/modules/tabs';
import useI18n from '@/hooks/useI18n';
import useAlarmStore from '@/store/modules/alarm';
import { getServerTime } from '@/api';
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getTimezoneOffset, parseDateUTCToStr } from '@/utils/date-utils';
import { parseUrlPath } from '@/plugins/file-static-url';
const { proConfig, waterMarkContent } = useLayoutStore();
const { t, currentLocale } = useI18n();
const routerStore = useRouterStore();
const neListStore = useNeListStore();
const tabsStore = useTabsStore();
const appStore = useAppStore();
const router = useRouter();
/**菜单面板 */
const layoutState = reactive({
collapsed: false, // 是否展开菜单面板
openKeys: ['/'], // 打开菜单key
selectedKeys: ['/index'], // 选中高亮菜单key
});
/**监听路由变化改变菜单面板选项 */
watch(
router.currentRoute,
v => {
const matched = v.matched.concat();
layoutState.openKeys = matched
.filter(r => r.path !== v.path)
.map(r => r.path);
layoutState.selectedKeys = matched
.filter(r => r.name !== 'Root')
.map(r => r.path);
// 路由地址含有内嵌地址标识又是隐藏菜单需要处理打开高亮菜单栏
if (v.path.includes(MENU_PATH_INLINE) && v.meta.hideInMenu) {
const idx = v.path.lastIndexOf(MENU_PATH_INLINE);
layoutState.openKeys = layoutState.selectedKeys.slice(0, -1);
layoutState.selectedKeys[matched.length - 1] = v.path.slice(0, idx);
}
},
{ immediate: true }
);
// 动态路由添加到菜单面板
// const rootRoute = router.getRoutes().find(r => r.name === 'Root');
// if (rootRoute) {
// const children = routerStore.setRootRouterData(rootRoute.children);
// const buildRouterData = routerStore.buildRouterData;
// if (buildRouterData.length > 0) {
// rootRoute.children = children.concat(buildRouterData);
// } else {
// rootRoute.children = children;
// }
// }
//
// const { menuData } = getMenuData(clearMenuItem(router.getRoutes()));
//
const menuData = computed(() => {
const rootRoute = router.getRoutes().find(r => r.name === 'Root');
if (rootRoute) {
const children = routerStore.setRootRouterData(rootRoute.children);
const buildRouterData = routerStore.buildRouterData;
if (buildRouterData.length > 0) {
rootRoute.children = children.concat(buildRouterData);
} else {
rootRoute.children = children;
}
}
const neTypes = neListStore.getNeSelectOtions.map(v => v.value);
let routes = clearMenuItem(router.getRoutes());
routes = routerStore.clearMenuItemByNeList(routes, neTypes);
const { menuData } = getMenuData(routes);
return menuData;
});
/**面包屑数据对象,排除根节点和首页不显示 */
const breadcrumb = computed(() => {
const matched = router.currentRoute.value.matched.concat();
// 菜单中隐藏子节点不显示面包屑
if (matched.length == 2) {
const hideChildInMenu = matched[0].meta?.hideChildInMenu || false;
if (hideChildInMenu) {
return [];
}
}
return matched
.filter(r => !['Root', 'Index'].includes(r.name as string))
.map(item => {
return {
path: item.path,
breadcrumbName: fnLocale(item),
};
});
});
/**
* 给页面组件设置路由名称
*
* 路由名称设为缓存key
* @param component 页面组件
* @param name 路由名称
*/
function fnComponentSetName(component: any, to: any) {
if (component && component.type) {
// 通过路由取最后匹配的,确认是缓存的才处理
const matched = to.matched.concat();
const lastRoute = matched[matched.length - 1];
if (!lastRoute.meta.cache) return component;
const routeName = lastRoute.name;
const routeDef = lastRoute.components.default;
// 有命名但不是跳转的路由文件
const __name = component.type.__name;
if (__name && __name !== routeName) {
routeDef.name = routeName;
routeDef.__name = routeName;
Reflect.set(component, 'type', routeDef);
return component;
}
}
return component;
}
// 清空导航栏标签
tabsStore.clear();
// LOGO地址
const logoUrl = computed(() => {
let url =
appStore.logoType === 'brand'
? parseUrlPath(appStore.filePathBrand)
: parseUrlPath(appStore.filePathIcon);
if (url.indexOf('{language}') === -1) {
return url;
}
// 语言参数替换
const local = currentLocale.value;
const lang = local.split('_')[0];
return url.replace('{language}', lang);
});
/**系统使用手册跳转 */
function fnClickHelpDoc(language?: string) {
const routeData = router.resolve({ name: 'HelpDoc' });
let href = routeData.href;
if (language) {
href = `${routeData.href}?language=${language}`;
}
window.open(href, '_blank');
}
/**
* 国际化翻译转换
*/
function fnLocale(m: MenuDataItem) {
if (!m.meta) return;
let title = m.meta?.title ?? '';
if (title.indexOf('router.') !== -1) {
title = t(title);
}
return title;
}
/**检查系统名称是否超出范围进行滚动 */
function fnCheckAppNameOverflow() {
const container: HTMLDivElement | null = document.querySelector('.app-name');
if (!container) return;
const text: HTMLDivElement | null = container.querySelector('.marquee');
if (!text) return;
if (text.offsetWidth > container.offsetWidth) {
text.classList.add('app-name_scrollable');
text.setAttribute('data-content', text.innerText);
} else {
text.classList.remove('app-name_scrollable');
}
}
watch(
() => appStore.appName,
() => nextTick(fnCheckAppNameOverflow)
);
//
onMounted(() => {
fnCheckAppNameOverflow();
fnGetServerTime();
useAlarmStore().fnGetActiveAlarmInfo();
});
// ==== 服务器时间显示 start
let serverTimeInterval: any = null;
let serverTime = 0;
let serverTimeZone = getTimezoneOffset();
let activeAlarmRefresh = 0;
// 获取服务器时间
function fnGetServerTime() {
getServerTime().then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
clearInterval(serverTimeInterval);
serverTimeInterval = null;
serverTime = res.data.timestamp;
serverTimeZone = res.data.timezone;
// 用DOM直接修改
const serverTimeDom = document.getElementById('serverTimeDom');
serverTimeInterval = setInterval(() => {
serverTime += 1000;
activeAlarmRefresh += 1;
if (serverTimeDom) {
serverTimeDom.innerText = parseDateUTCToStr(
serverTime,
serverTimeZone
);
} else {
clearInterval(serverTimeInterval);
serverTimeInterval = null;
}
if (activeAlarmRefresh === 5) {
useAlarmStore().fnGetActiveAlarmInfo();
activeAlarmRefresh = 0;
}
}, 1000);
}
});
}
// 监听可视改变
document.addEventListener('visibilitychange', function () {
if (document.visibilityState == 'hidden') {
//切离该页面时执行
clearInterval(serverTimeInterval);
serverTimeInterval = null;
}
if (document.visibilityState == 'visible') {
//切换到该页面时执行
clearInterval(serverTimeInterval);
serverTimeInterval = null;
fnGetServerTime();
useAlarmStore().fnGetActiveAlarmInfo();
}
});
onUnmounted(() => {
clearInterval(serverTimeInterval);
serverTimeInterval = null;
});
// ==== 服务器时间显示 end
</script>
<template>
<a-watermark :content="waterMarkContent" :z-index="100">
<ProLayout
v-model:collapsed="layoutState.collapsed"
v-model:selectedKeys="layoutState.selectedKeys"
v-model:openKeys="layoutState.openKeys"
:menu-data="menuData"
:breadcrumb="{ routes: breadcrumb }"
v-bind="proConfig"
:iconfont-url="scriptUrl"
:locale="fnLocale"
>
<!--插槽-菜单头-->
<template #menuHeaderRender>
<RouterLink :to="{ name: 'Index' }" :replace="true">
<template
v-if="
appStore.logoType === 'icon' ||
(proConfig.layout === 'side' && layoutState.collapsed)
"
>
<img
class="logo-icon"
:src="logoUrl"
:alt="appStore.appName"
:title="appStore.appName"
/>
<h1 class="app-name" :title="appStore.appName">
<span class="marquee app-name_scrollable">
{{ appStore.appName }}
</span>
</h1>
</template>
<template v-if="appStore.logoType === 'brand'">
<img
class="logo-brand"
:src="logoUrl"
:alt="appStore.appName"
:title="appStore.appName"
/>
</template>
</RouterLink>
</template>
<!--插槽-渲染顶部内容区域,仅布局side有效-->
<template #headerContentRender></template>
<!--插槽-渲染顶部内容右端区域-->
<template #headerContentRightRender>
<RightContent />
</template>
<!--插槽-导航标签项-->
<template #tabRender="{ width, fixedHeader, headerRender }">
<Tabs
:width="width"
:fixed-header="fixedHeader"
:header-render="headerRender"
/>
</template>
<!--插槽-页面路由导航面包屑-->
<template #breadcrumbRender="{ route, params, routes }">
<span v-if="routes.indexOf(route) === routes.length - 1">
{{ route.breadcrumbName }}
</span>
<RouterLink v-else :to="{ path: route.path, params }">
{{ route.breadcrumbName }}
</RouterLink>
</template>
<!--内容页面视图-->
<RouterView v-slot="{ Component, route }">
<transition name="slide-left" mode="out-in">
<KeepAlive :include="tabsStore.getCaches">
<component
:is="fnComponentSetName(Component, route)"
:key="route.path"
/>
</KeepAlive>
</transition>
</RouterView>
<!--插槽-内容底部-->
<template #footerRender="{ width }">
<footer class="footer">
<div class="footer-fixed" :style="{ width }">
<div style="flex: 1">
<span>{{ appStore.copyright }}</span>
</div>
<a-space direction="horizontal" :size="8">
<span id="serverTimeDom"></span>
<a-button
type="link"
:href="appStore.officialUrl"
target="_blank"
size="small"
v-perms:has="['system:setting:official']"
v-if="appStore.officialUrl !== '#'"
>
{{ t('loayouts.basic.officialUrl') }}
</a-button>
<a-button
type="link"
size="small"
v-perms:has="['system:setting:doc']"
@click="fnClickHelpDoc()"
>
{{ t('loayouts.basic.helpDoc') }}
</a-button>
</a-space>
</div>
</footer>
</template>
</ProLayout>
<!-- 全局遮罩 -->
<GlobalMask />
<!-- 强制密码修改 -->
<ForcePasswdChange />
</a-watermark>
</template>
<style lang="less" scoped>
.logo-icon {
height: 32px;
width: 32px;
vertical-align: middle;
border-style: none;
border-radius: 6.66px;
}
.logo-brand {
height: 48px;
width: 174px;
vertical-align: middle;
border-style: none;
margin-right: 16px;
}
.app-name {
overflow: hidden;
// text-overflow: ellipsis;
white-space: nowrap;
width: 148px;
> .app-name_scrollable {
padding-right: 12px;
display: inline-block;
animation: scrollable-animation linear 6s infinite both;
&::after {
content: attr(data-content);
position: absolute;
top: 0;
right: -100%;
transition: right 0.3s ease;
}
@keyframes scrollable-animation {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-100% - 12px));
}
}
}
}
.theme-light.theme-menu-light .app-name {
color: #141414 !important;
}
.theme-light.theme-menu-dark .app-name {
color: #fff !important;
}
.theme-dark.theme-menu-light .app-name {
color: #fff !important;
}
.theme-dark.theme-menu-dark .app-name {
color: #fff !important;
}
.footer {
z-index: 16;
margin: 0px;
width: auto;
margin-top: 32px;
&-fixed {
position: fixed;
bottom: 0;
right: 0;
width: auto;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
padding: 4px 16px;
background: #fff;
box-shadow: 0 1px 4px #0015291f;
transition: background 0.3s, width 0.2s;
height: 32px;
}
[data-theme='dark'] &-fixed {
background: #141414;
}
& #serverTimeDom {
color: inherit;
opacity: 0.85;
transition: all 0.3s;
}
}
</style>