444 lines
12 KiB
Vue
444 lines
12 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
ProLayout,
|
||
WaterMark,
|
||
getMenuData,
|
||
clearMenuItem,
|
||
type 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 { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
|
||
import {
|
||
computed,
|
||
reactive,
|
||
watch,
|
||
onMounted,
|
||
onUnmounted,
|
||
nextTick,
|
||
} from 'vue';
|
||
import useLayoutStore from '@/store/modules/layout';
|
||
import useRouterStore from '@/store/modules/router';
|
||
import useTabsStore from '@/store/modules/tabs';
|
||
import useAlarmStore from '@/store/modules/alarm';
|
||
import useAppStore from '@/store/modules/app';
|
||
import { useRouter } from 'vue-router';
|
||
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
|
||
const { proConfig, waterMarkContent } = useLayoutStore();
|
||
import useI18n from '@/hooks/useI18n';
|
||
import { getServerTime } from '@/api';
|
||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||
|
||
import { parseDateToStr } from '@/utils/date-utils';
|
||
import { parseUrlPath } from '@/plugins/file-static-url';
|
||
const { t, currentLocale } = useI18n();
|
||
const routerStore = useRouterStore();
|
||
const tabsStore = useTabsStore();
|
||
const appStore = useAppStore();
|
||
const router = useRouter();
|
||
|
||
/**菜单面板 */
|
||
let 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 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 serverTime = reactive({
|
||
timestamp: 0,
|
||
zone: 'UTC', // 时区 UTC
|
||
interval: null as any, // 定时器
|
||
});
|
||
|
||
// 获取服务器时间
|
||
function fnGetServerTime() {
|
||
getServerTime().then(res => {
|
||
if (res.code === RESULT_CODE_SUCCESS && res.data) {
|
||
const serverTimeDom = document.getElementById('serverTimeDom');
|
||
// 时区
|
||
const utcOffset = res.data.timeZone / 3600;
|
||
serverTime.zone = `UTC ${utcOffset}`;
|
||
// 时间戳
|
||
serverTime.timestamp = parseInt(res.data.timestamp);
|
||
serverTime.interval = setInterval(() => {
|
||
serverTime.timestamp += 1000;
|
||
// serverTimeStr.value = parseDateToStr(serverTime.timestamp);
|
||
// 用DOM直接修改
|
||
if (serverTimeDom) {
|
||
serverTimeDom.innerText = parseDateToStr(serverTime.timestamp);
|
||
}
|
||
}, 1000);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 监听可视改变
|
||
document.addEventListener('visibilitychange', function () {
|
||
if (document.visibilityState == 'hidden') {
|
||
//切离该页面时执行
|
||
clearInterval(serverTime.interval);
|
||
serverTime.interval = null;
|
||
}
|
||
if (document.visibilityState == 'visible') {
|
||
//切换到该页面时执行
|
||
clearInterval(serverTime.interval);
|
||
serverTime.interval = null;
|
||
fnGetServerTime();
|
||
useAlarmStore().fnGetActiveAlarmInfo();
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
clearInterval(serverTime.interval);
|
||
serverTime.interval = null;
|
||
});
|
||
// ==== 服务器时间显示 end
|
||
</script>
|
||
|
||
<template>
|
||
<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 />
|
||
</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));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
padding: 4px 16px;
|
||
background: #fff;
|
||
box-shadow: 0 1px 4px #0015291f;
|
||
transition: background 0.3s, width 0.2s;
|
||
height: 32px;
|
||
}
|
||
|
||
& #serverTimeDom {
|
||
color: inherit;
|
||
opacity: 0.85;
|
||
transition: all 0.3s;
|
||
}
|
||
}
|
||
</style>
|