commit:升级到vue3,更新最近工作流技术栈,支持sa-token

This commit is contained in:
Jerry
2024-07-05 22:42:33 +08:00
parent bbcc608584
commit 565ecb6371
1751 changed files with 236790 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<el-breadcrumb-item :to="{ name: layoutStore.indexName }" :replace="true" @click="hadleClick">
<div class="breadcrumb-home">
<img src="@/assets/img/s-home.png" alt="" /><span>首页</span>
</div>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in layoutStore.currentMenuPath" :key="item.menuId">
{{ item.menuName }}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script lang="ts" setup>
import { useLayoutStore } from '@/store';
const layoutStore = useLayoutStore();
// 返回首页时,设置当前菜单为空
const hadleClick = () => {
layoutStore.setCurrentMenu(null);
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.breadcrumb-home {
display: flex;
align-items: center;
img {
position: relative;
top: -1px;
margin-right: 4px;
vertical-align: top;
}
span {
vertical-align: middle;
}
}
.el-breadcrumb__item {
display: flex;
align-items: center;
span {
color: #999;
}
:deep(.el-breadcrumb__inner) {
color: #999;
}
&:last-child :deep(.el-breadcrumb__inner) {
color: #333;
}
}
.app-breadcrumb.el-breadcrumb {
display: inline-block;
line-height: 60px;
margin-left: 25px;
.no-redirect {
color: #97a8be;
cursor: text;
}
img {
vertical-align: middle;
}
:deep(.el-breadcrumb__inner.is-link) {
color: #606266 !important;
font-weight: normal;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<div style="position: relative; height: 100%" class="sidebar-bg">
<multiColumn v-if="layoutStore.supportColumn" :menuList="layoutStore.menuList" />
<div
v-else
class="left-menu"
:class="layoutStore.collapsed ? 'collapse' : ''"
style="height: 100%; padding-top: 16px; padding-bottom: 16px"
>
<el-scrollbar wrap-class="scrollbar_dropdown__wrap" style="height: 100%">
<el-menu
mode="vertical"
active-text-color="#ffd04b"
text-color="#a4a5a7"
background-color="#2d3039"
:default-active="layoutStore.currentMenuId"
:unique-opened="true"
@select="selectMenuById"
:collapse="layoutStore.collapsed"
>
<template v-for="menu in layoutStore.menuList" :key="menu.menuId">
<sub-menu :menu="menu" />
</template>
</el-menu>
</el-scrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from '@/store';
import SubMenu from './SubMenu.vue';
import multiColumn from './multi-column.vue';
import { useSelectMenu } from './hooks';
const layoutStore = useLayoutStore();
const { selectMenuById } = useSelectMenu();
</script>
<style lang="scss">
.sidebar-title-text {
font-size: 18px;
font-weight: bold;
}
.left-menu .el-submenu__title {
height: 50px;
color: #a4a5a7;
line-height: 50px;
i {
color: #a4a5a7 !important;
}
.el-submenu__icon-arrow {
margin-top: -4px;
}
}
.sidebar-bg {
.el-menu-item {
color: #a4a5a7;
}
}
.collapse .is-active .el-submenu__title {
position: relative;
background-color: #43474e !important;
&::before {
position: absolute;
top: 0;
left: 0;
display: block;
width: 4px;
height: 100%;
background: $color-primary;
content: '';
}
}
.collapse .el-icon-arrow-right {
display: none;
}
.el-menu-item:hover,
.el-submenu__title:hover {
background-color: transparent !important;
}
.el-menu-item.is-active {
color: white !important;
background-color: #43474e !important;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
display: block;
width: 4px;
height: 100%;
background: $color-primary !important;
content: '';
}
i {
color: white !important;
}
}
.el-menu--vertical .el-submenu__title {
& > span {
display: inline !important;
}
}
.el-menu--vertical .el-menu.el-menu--popup {
padding: 16px 8px;
background-color: white !important;
.el-menu-item,
.el-submenu__title {
height: 40px;
padding: 0 3px !important;
color: #333;
line-height: 40px;
&::before {
display: none;
}
&:hover {
background-color: #f6f6f6 !important;
& > .multi-column-menu-popover {
display: block;
}
}
&.is-active {
color: $color-primary !important;
background-color: $color-primary-light-9 !important;
}
.el-submenu__icon-arrow {
margin-top: -5px;
color: #333;
}
}
.el-menu--vertical {
transform: translateX(6px);
}
}
.sidebar-bg .el-submenu {
.el-menu {
background-color: #1d1f24 !important;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div class="menu-wrapper">
<el-menu-item
ref="item"
:index="menu.menuId"
v-if="menu.children == null || menu.children.length == 0"
>
<template v-slot:title>
<orange-icon v-if="menu.icon" :icon="menu.icon" class="menu-icon" />
<span :style="getTextStyle(!menu.icon)">{{ menu.menuName }}</span>
</template>
<orange-icon
v-if="menu.icon && !isChild && store.collapsed"
:icon="menu.icon"
class="menu-icon"
/>
</el-menu-item>
<el-sub-menu v-else :index="menu.menuId">
<template v-slot:title>
<orange-icon v-if="menu.icon" :icon="menu.icon" class="menu-icon" />
<span :style="getTextStyle(!menu.icon)" v-show="!store.collapsed || isChild">{{
menu.menuName
}}</span>
</template>
<template v-for="child in menu.children" :key="child.menuId">
<sub-menu class="nest-menu" :menu="child" :isChild="true" />
</template>
</el-sub-menu>
</div>
</template>
<script setup lang="ts">
import { MenuItem } from '@/types/upms/menu';
import { useLayoutStore } from '@/store';
import OrangeIcon from '@/components/icons/index.vue';
interface IProps {
menu: MenuItem;
isChild?: boolean;
}
const store = useLayoutStore();
const props: IProps = defineProps<IProps>();
// const getIconStyle = (isShow: boolean) => {
// if (isShow && props.isChild) {
// return [{ 'margin-left': '13px' }];
// }
// };
const getTextStyle = (isShow: boolean) => {
if (isShow && props.isChild) {
return [{ 'padding-left': '13px' }];
}
};
</script>
<style scoped>
.menu-icon {
margin-right: 10px;
font-size: 18px;
}
.nest-menu :deep(.el-menu-item span:first-child),
.nest-menu :deep(.el-menu-item .menu-icon:first-child),
.nest-menu :deep(.el-submenu__title span:first-child) {
padding-left: 8px !important;
}
.nest-menu :deep(.el-submenu__title .menu-icon:first-child) {
margin-left: 8px !important;
}
</style>
@/types/upms/menu

View File

@@ -0,0 +1,117 @@
<template>
<div
ref="root"
class="tags-item"
:class="{ active: active }"
@mouseenter.prevent="mouseEnterHandle"
@mouseout.prevent="mouseLeaveHandle"
@mousemove.prevent="mouseMoveHandle"
>
<span class="title">{{ title }}</span>
<el-icon
ref="icon"
v-if="supportClose && (mouseEnter || active)"
style="margin-left: 6px; color: #999"
@mouseenter.prevent="iconMouseEnterHandle"
@mouseleave.prevent="iconMouseLeaveHandle"
@click.stop="onClose"
>
<Close v-if="!iconEnter" /><CircleCloseFilled class="hover-close" v-if="iconEnter" />
</el-icon>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'close'): void;
}>();
withDefaults(
defineProps<{
title: string;
supportClose?: boolean;
active: boolean;
}>(),
{
supportClose: true,
active: false,
},
);
const root = ref<HTMLElement>();
const icon = ref<ComponentPublicInstance>();
const mouseEnter = ref(false);
const iconEnter = ref(false);
const onClose = () => {
emit('close');
};
const mouseEnterHandle = () => {
//iconEnter.value = false;
mouseEnter.value = true;
};
// 解决鼠标移动过快mouseleave失效图标不能正确改变样式
const mouseMoveHandle = (e: MouseEvent) => {
if (['DIV', 'SPAN'].includes((e.target as HTMLElement).nodeName)) {
iconEnter.value = false;
}
};
const mouseLeaveHandle = (e: MouseEvent) => {
iconEnter.value = false;
//console.log(props.title, root.value?.contains(e.relatedTarget as Node), e);
if (e.type == 'mouseout' && root.value?.contains(e.relatedTarget as Node)) {
return;
}
mouseEnter.value = false;
};
const iconMouseEnterHandle = () => {
iconEnter.value = true;
};
const iconMouseLeaveHandle = () => {
iconEnter.value = false;
};
defineExpose({
root,
});
</script>
<style lang="scss" scoped>
.tags-item {
display: flex;
align-items: center;
height: 28px;
padding: 0 10px;
color: #999;
background: white;
border: 1px solid #e8e8e8;
border-radius: 3px;
box-sizing: border-box;
cursor: pointer;
}
.tags-item .title {
font-size: 12px;
}
.close {
display: none;
vertical-align: middle;
color: #999;
}
.tags-item.active .close {
display: inline-block;
margin-left: 6px;
}
.hover-close {
color: $color-text-secondary;
}
.tags-item:hover {
color: #333;
}
.tags-item.active {
color: #333;
}
.tags-item + .tags-item {
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<div class="tags-panel" ref="panel">
<el-icon class="arrow left"><el-icon-arrow-left @click="leftClick" /></el-icon>
<el-icon class="arrow right"><el-icon-arrow-right @click="rightClick" /></el-icon>
<div class="main-panel">
<div
class="scroll-box"
ref="scroll"
:style="{ transform: 'translateX(' + translateX + 'px)' }"
>
<TagItem
ref="home"
class="item"
title="主页"
:active="layoutStore.currentMenuId == null"
:supportClose="false"
@click="onTagItemClick(null)"
@contextmenu.prevent="openMenu(null, $event)"
/>
<TagItem
ref="items"
class="item"
v-for="item in layoutStore.tagList"
:key="item.menuId"
:title="item.menuName"
:active="item.menuId == layoutStore.currentMenuId"
@close="onTagItemClose(item)"
@click="onTagItemClick(item)"
@contextmenu.prevent="openMenu(item, $event)"
></TagItem>
</div>
</div>
<div
v-show="contextMenuVisible"
@click.stop="onMenuMaskClick"
@contextmenu="openMaskMenu"
style="
position: fixed;
top: 0;
left: 0;
z-index: 99999;
width: 100vw;
height: 100vh;
background: rgb(0 0 0 / 1%);
"
>
<ul
class="contextmenu"
style="z-index: 99999; background: white"
:style="{ left: left + 'px', top: top + 'px' }"
>
<li @click="closeSelectTag" :class="currentMenu == null ? 'disabled' : ''">关闭</li>
<li @click="closeOthersTags">关闭其他</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { useLayoutStore } from '@/store';
import { MenuItem } from '@/types/upms/menu';
import { T } from '@/types/generic';
import TagItem from './TagItem.vue';
const layoutStore = useLayoutStore();
const panel = ref();
const scroll = ref();
const home = ref();
const items = ref<T[]>([]);
const contextMenuVisible = ref(false);
const translateX = ref(0);
const left = ref(0);
const top = ref(0);
let currentMenu: MenuItem | null = null;
const onTagItemClick = (item: MenuItem | null) => {
layoutStore.setCurrentMenu(item);
};
const openMenu = (item: MenuItem | null, event: MouseEvent) => {
// console.log(item, event);
currentMenu = item;
contextMenuVisible.value = true;
left.value = event.clientX;
top.value = event.clientY;
};
const onMenuMaskClick = () => {
console.log('onMenuMaskClick');
contextMenuVisible.value = false;
};
const openMaskMenu = (e: MouseEvent) => {
// console.log('openMaskMenu');
e.preventDefault();
};
const closeSelectTag = () => {
if (currentMenu != null) {
layoutStore.removeTag(currentMenu.menuId);
}
};
const closeOthersTags = () => {
//console.log('closeOthersTags');
if (currentMenu != null) {
layoutStore.closeOtherTags(currentMenu.menuId);
} else {
layoutStore.clearAllTags();
}
};
const onTagItemClose = (item: MenuItem) => {
layoutStore.removeTag(item.menuId);
};
onMounted(() => {
initTagPanel(layoutStore.tagList);
scrollToMenu(layoutStore.currentMenu);
});
function initTagPanel(tagList: MenuItem[]) {
nextTick(() => {
let width = (home.value ? home.value.root.offsetWidth : 0) + 60 + tagList.length * 5;
//console.log('width', width);
items.value.forEach(row => {
//console.log(row);
width += row.root.offsetWidth + 5;
});
//console.log('width', width);
scroll.value.style.width = width + 'px';
const showArrow = width > panel.value.offsetWidth;
if (!showArrow) {
translateX.value = 0;
} else if (panel.value.offsetWidth - width >= translateX.value && translateX.value !== 0) {
translateX.value = panel.value.offsetWidth - width;
}
//console.log('xxx', translateX.value);
});
}
function scrollToMenu(menu: MenuItem) {
nextTick(() => {
layoutStore.tagList.forEach((row, index) => {
if (row.menuId === menu.menuId) {
let el = items.value[index].root;
if (-el.offsetLeft > translateX.value) {
translateX.value = -el.offsetLeft;
} else if (
el.offsetLeft + el.offsetWidth + 60 >
panel.value.offsetWidth - translateX.value
) {
translateX.value = panel.value.offsetWidth - el.offsetLeft - el.offsetWidth - 60;
}
}
});
});
}
watch(
() => layoutStore.currentMenu,
(menu: MenuItem) => {
scrollToMenu(menu);
},
);
watch(
() => layoutStore.tagList,
tagList => {
if (tagList && tagList.length) {
initTagPanel(tagList);
}
},
{ deep: true },
);
const leftClick = () => {
if (!items.value) return;
let x = 0;
for (let i = layoutStore.tagList.length - 1; i >= 0; i--) {
const el = items.value[i].root;
//console.log(el.innerText, layoutStore.tagList[i].menuName);
console.log(el.offsetLeft, translateX.value, -el.offsetLeft > translateX.value);
if (-el.offsetLeft > translateX.value) {
x = -el.offsetLeft;
break;
}
console.log('x', x);
}
if (x > 0) {
x = 0;
}
console.log('x', x);
translateX.value = x;
};
const rightClick = () => {
if (!items.value) return;
let x = translateX.value;
for (let i = 0; i < layoutStore.tagList.length; i++) {
const el = items.value[i].root;
if (el.offsetLeft + el.offsetWidth + 60 > panel.value.offsetWidth - translateX.value) {
x = panel.value.offsetWidth - el.offsetLeft - el.offsetWidth - 60;
break;
}
}
console.log('x', x, panel.value.offsetWidth, scroll.value.offsetWidth);
let max = panel.value.offsetWidth - scroll.value.offsetWidth - 60;
console.log('max', max);
if (x < max) {
x = max;
}
if (x > 0) x = 0;
translateX.value = x;
};
</script>
<style lang="scss" scoped>
.tags-panel {
width: 100px;
background-color: white;
flex: 1;
}
.main-panel {
position: relative;
overflow: hidden;
margin: 0 30px;
}
.scroll-box {
display: flex;
align-items: center;
overflow: hidden;
width: 100%;
height: 48px;
white-space: nowrap;
transition: 0.3s;
flex-wrap: nowrap;
}
.arrow {
z-index: 100;
width: 30px;
height: 48px;
font-size: 14px;
text-align: center;
color: #999;
line-height: 48px;
cursor: pointer;
box-sizing: border-box;
}
.arrow.left {
float: left;
}
.arrow.right {
float: right;
}
.contextmenu {
position: fixed;
z-index: 2;
padding: 5px 0;
margin: 0;
font-size: 12px;
color: #333;
border-radius: 5px;
box-shadow: 2px 2px 3px 0 rgb(0 0 0 / 30%);
list-style-type: none;
font-weight: 400;
}
.contextmenu li {
padding: 7px 16px;
margin: 0;
cursor: pointer;
}
.contextmenu li.disabled {
padding: 7px 16px;
margin: 0;
cursor: not-allowed;
}
.contextmenu li:hover {
background: #eee;
}
</style>
@/types/upms/menu

View File

@@ -0,0 +1,51 @@
import { SysMenuBindType } from '@/common/staticDict';
import { getToken } from '@/common/utils';
import { useLayoutStore } from '@/store';
import { findMenuItemById } from '@/store/utils';
import { MenuItem } from '@/types/upms/menu';
export const useSelectMenu = () => {
const layoutStore = useLayoutStore();
/**
* 选择菜单,跳转到目标页面
* 外链弹出新窗口
*
* @param menuId 菜单ID
*/
const selectMenuById = (menuId: string) => {
const menuItem: MenuItem | null = findMenuItemById(menuId, layoutStore.menuList);
if (menuItem) selectMenu(menuItem);
};
/**
* 选择菜单,跳转到目标页面
* 外链弹出新窗口
*
* @param menu 菜单项
*/
const selectMenu = (menu: MenuItem) => {
// TODO 外链暂时直接弹出新窗口,有其它规则时,可以从这里开始修改
if (
menu != null &&
menu.bindType === SysMenuBindType.THRID_URL &&
menu.targetUrl != null &&
menu.targetUrl !== ''
) {
const token = getToken();
let targetUrl = menu.targetUrl;
if (targetUrl.indexOf('?') === -1) {
targetUrl = targetUrl + '?';
}
targetUrl = targetUrl + 'token=' + token;
window.open(targetUrl);
return;
}
layoutStore.setCurrentMenu(menu);
};
return {
selectMenuById,
selectMenu,
};
};

View File

@@ -0,0 +1,153 @@
<template>
<ul class="multi-column-menu">
<template v-for="menu in menuList" :key="menu.menuId">
<el-popover
placement="right-start"
width="220"
trigger="hover"
:disabled="!menu.children || (menu.children || []).length === 0 || level >= 1"
:show-arrow="false"
>
<template v-slot:reference>
<li
@click="selectMenuItem(menu)"
:class="{ active: layoutStore.currentMenuId === menu.menuId }"
>
<div class="menu-name">
<orange-icon
v-if="menu.icon"
:icon="menu.icon"
style="margin-right: 5px; font-size: 18px"
/>
{{ menu.menuName }}
</div>
<el-icon v-if="menu.children && menu.children.length"><el-icon-arrow-right /></el-icon>
<div
class="multi-column-menu-popover"
:class="{ level2: level > 1 }"
v-if="level >= 1 && (menu.children || []).length"
>
<div class="popover-box">
<multiColumnMenu
:menuList="menu.children"
:key="column?.menuId + '-' + menu.menuId"
:level="2"
:column="column"
@select="select"
/>
</div>
</div>
</li>
</template>
<multiColumnMenu
v-if="(menu.children || []).length && level < 1"
:menuList="menu.children"
:level="level + 1"
:column="column"
@select="select"
/>
</el-popover>
</template>
</ul>
</template>
<script setup lang="ts">
import { ArrowRight as ElIconArrowRight } from '@element-plus/icons-vue';
import { MenuItem } from '@/types/upms/menu';
import { useLayoutStore } from '@/store';
import OrangeIcon from '@/components/icons/index.vue';
import { SysMenuType } from '@/common/staticDict';
import { useSelectMenu } from './hooks';
const emit = defineEmits<{
(e: 'select'): void;
}>();
const props = withDefaults(
defineProps<{
level: number;
menuList?: Array<MenuItem>;
column?: MenuItem;
}>(),
{
level: 0,
},
);
const layoutStore = useLayoutStore();
const { selectMenu } = useSelectMenu();
const selectMenuItem = (menu: MenuItem) => {
if (layoutStore.currentMenuId == menu.menuId || menu.menuType == SysMenuType.DIRECTORY) return;
// 单页面清空所有tags和cachePage
// if (!layoutStore.supportTags) {
// layoutStore.clearAllTags();
// }
if (props.column && props.column.menuId !== layoutStore.currentColumnId) {
layoutStore.setCurrentColumn(props.column);
}
nextTick(() => {
selectMenu(menu);
select();
});
};
const select = () => {
emit('select');
};
</script>
<style lang="scss">
.multi-column-menu {
width: 200px;
padding: 0 8px;
margin: 0;
list-style: none;
li {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
padding: 0 16px;
font-size: 14px;
color: #333;
border-radius: 4px;
cursor: pointer;
.menu-name {
display: flex;
align-items: center;
}
i {
color: #999;
}
&:hover {
background-color: #f6f6f6;
& > .multi-column-menu-popover {
display: block;
}
}
&.active {
color: $color-primary;
background-color: $color-primary-light-9;
}
}
}
.multi-column-menu-popover {
position: absolute;
top: 0;
left: 100%;
display: none;
padding-left: 16px;
&.level2 {
padding-left: 22px;
}
.popover-box {
padding: 12px;
background-color: white;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
}
}
</style>
@/types/upms/menu

View File

@@ -0,0 +1,143 @@
<template>
<div class="multi-column-wrap">
<el-scrollbar
wrap-class="scrollbar_dropdown__wrap"
style="width: 80px; height: calc(100vh - 60px)"
v-if="menuList && menuList.length"
>
<ul class="multi-column-list">
<template v-for="(menu, index) in menuList" :key="menu.menuId">
<el-popover
ref="popover"
placement="right-start"
width="220"
trigger="hover"
:disabled="
!menu.children || (menu.children || []).length === 0 || !layoutStore.collapsed
"
:show-arrow="false"
>
<template v-slot:reference>
<li
@click="onColumnChange(menu)"
:class="{ active: layoutStore.currentColumnId === menu.menuId }"
>
<orange-icon v-if="menu.icon" :icon="menu.icon" />
<p :title="menu.menuName.length > 4 ? menu.menuName : undefined">
{{ menu.menuName }}
</p>
</li>
</template>
<multiColumnMenu
v-if="(menu.children || []).length"
:menuList="menu.children"
:level="1"
@select="selectMenu(index)"
:column="menu"
/>
</el-popover>
</template>
</ul>
</el-scrollbar>
<el-scrollbar
v-if="children && children.length"
class="children-menu-scrollbar"
wrap-class="scrollbar_dropdown__wrap"
style="height: calc(100vh - 60px); background-color: white"
:scroll-x="false"
>
<div style="padding: 24px 0">
<multiColumnMenu
:menuList="children"
:level="0"
:key="layoutStore.currentColumnId"
:columnId="layoutStore.currentColumnId"
/>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { PopoverInstance } from 'element-plus';
import { MenuItem } from '@/types/upms/menu';
import { useLayoutStore } from '@/store';
import OrangeIcon from '@/components/icons/index.vue';
import multiColumnMenu from './multi-column-menu.vue';
defineProps<{ menuList: MenuItem[] }>();
const popover: Ref<PopoverInstance[]> = ref([]);
const children: Ref<MenuItem[]> = ref([]);
const layoutStore = useLayoutStore();
const onColumnChange = (column: MenuItem) => {
layoutStore.setCurrentColumn(column);
};
const selectMenu = (index: number) => {
// console.log('selectMenu', index);
// 自动隐藏弹出体,只在侧边栏折叠状态下才会触发
popover.value[index].hide();
};
watch(
() => layoutStore.currentColumn,
(newVal, oldVal) => {
if (newVal == oldVal) return;
children.value = newVal.children || [];
},
{ immediate: true },
);
</script>
<style lang="scss">
.multi-column-wrap {
display: flex;
min-width: 81px;
height: 100%;
border-right: 1px solid #e8e8e8;
.children-menu-scrollbar {
width: 0;
flex: 1;
.el-scrollbar__bar.is-horizontal {
display: none;
}
}
.multi-column-list {
width: 80px;
padding: 16px 0;
margin: 0;
text-align: center;
list-style: none;
li {
display: flex;
align-items: center;
height: 80px;
font-size: 14px !important;
color: #a4a5a7;
flex-direction: column;
justify-items: center;
cursor: pointer;
&.active,
&:hover {
color: #fff;
background-color: rgb(246 246 246 / 30%);
}
i {
margin-top: 14px;
font-size: 24px !important;
}
p {
overflow: hidden;
width: 100%;
padding: 0 10px;
margin: 12px 0 0;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
</style>
@/types/upms/menu

View File

@@ -0,0 +1,495 @@
<template>
<el-container :class="'container-' + defaultFormItemSize" :style="getMainStyle">
<el-container style="background-color: #f5f8f9">
<el-header
class="header"
style="padding: 0"
:class="layoutStore.supportColumn ? 'multi-column-header' : ''"
>
<div class="logo has-multi-column" v-if="layoutStore.supportColumn">
<img src="@/assets/img/logo_white.png" alt="" />
</div>
<div class="header-main">
<div
class="logo"
v-if="!layoutStore.supportColumn"
style="padding-left: 8px; margin-right: 8px"
>
<img src="@/assets/img/login_logo.png" alt="" />
</div>
<div class="title">{{ projectName }}</div>
<bread-crumb class="breadcrumb-container" />
<div class="header-menu" style="flex-grow: 1">
<el-dropdown trigger="click" style="margin-right: 14px" @command="handleMessage">
<el-badge
is-dot
:hidden="!messageCount.totalCount || messageCount.totalCount <= 0"
style="height: 20px; line-height: 20px; cursor: pointer"
>
<i class="online-icon icon-message" style="font-size: 16px; color: #333" />
</el-badge>
<template v-slot:dropdown>
<el-dropdown-menu style="min-width: 130px">
<el-dropdown-item class="user-dropdown-item" command="remindingMessage">
催办消息
<el-badge
:value="messageCount.remindingMessageCount"
:hidden="
!messageCount.remindingMessageCount ||
messageCount.remindingMessageCount <= 0
"
/>
</el-dropdown-item>
<el-dropdown-item class="user-dropdown-item" command="copyMessage">
抄送消息
<el-badge
:value="messageCount.copyMessageCount"
:hidden="!messageCount.copyMessageCount || messageCount.copyMessageCount <= 0"
/>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span class="line"></span>
<img :src="headerImg ? headerImg : defaultHeaderImg" class="header-img" />
<el-dropdown class="user-dropdown" trigger="click" @command="handleCommand">
<span class="el-dropdown-link"
>{{ (userInfo || {}).showName
}}<el-icon class="el-icon--right"><el-icon-arrow-down /></el-icon>
</span>
<template v-slot:dropdown>
<el-dropdown-menu>
<el-dropdown-item class="user-dropdown-item" command="modifyPassword"
>修改密码</el-dropdown-item
>
<el-dropdown-item class="user-dropdown-item" command="modifyHeadImage"
>修改头像</el-dropdown-item
>
<el-dropdown-item class="user-dropdown-item" command="logout"
>退出登录</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-header>
<el-main class="layout-main">
<el-aside
:width="
layoutStore.collapsed
? layoutStore.supportColumn
? '80px'
: '64px'
: layoutStore.supportColumn
? '280px'
: '204px'
"
class="sidebar"
>
<sidebar style="overflow: hidden"></sidebar>
</el-aside>
<div class="layout-content">
<div class="tag-wrap" v-if="layoutStore.supportTags">
<i
class="online-icon"
:class="layoutStore.collapsed ? 'icon-expand' : 'icon-unexpand'"
style="font-size: 16px; color: #333; cursor: pointer"
@click="() => layoutStore.toggleCollapsed()"
/>
<tag-panel />
</div>
<el-scrollbar wrap-class="scrollbar_dropdown__wrap" :style="getContextStyle">
<router-view
v-slot="{ Component }"
class="page-box"
style="overflow: hidden; margin: 16px"
:style="getRouterViewStyle"
>
<transition name="el-fade-in-linear" :appear="true">
<keep-alive :include="layoutStore.cachePages">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</el-scrollbar>
</div>
</el-main>
</el-container>
</el-container>
</template>
<script lang="ts">
export default {
name: 'Layout',
};
</script>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus';
import { useRouter, useRoute } from 'vue-router';
import defaultHeaderImg from '@/assets/img/default-header.jpg';
import { getToken, setToken } from '@/common/utils/index';
import { useLayoutStore, useLoginStore, useMessage } from '@/store';
import { SysMenuBindType, SysOnlineFormType } from '@/common/staticDict';
import LoginController from '@/api/system/LoginController';
import { MenuItem } from '@/types/upms/menu';
import { useUpload } from '@/common/hooks/useUpload';
import Sidebar from './components/Sidebar.vue';
import BreadCrumb from './components/BreadCrumb.vue';
import TagPanel from './components/TagPanel.vue';
const router = useRouter();
const route = useRoute();
const layoutStore = useLayoutStore();
const loginStore = useLoginStore();
const documentClientHeight = inject('documentClientHeight', ref(500));
const projectName = import.meta.env.VITE_PROJECT_NAME;
const { getUploadFileUrl } = useUpload();
const userInfo = loginStore.userInfo;
// TODO 切换菜单会触发该方法?
// 用户头像
const headerImg = computed(() => {
if (userInfo && userInfo.headImageUrl) {
let temp = getUploadFileUrl(userInfo.headImageUrl, {
filename: userInfo.headImageUrl.filename,
});
return temp;
} else {
return null;
}
});
// 消息数量
const messageStore = useMessage();
const messageCount = computed(() => {
return messageStore.messageCount || {};
});
// TODO 监听当前菜单变化,跳转到目标路由
watch(
() => layoutStore.currentMenu,
(newVal, oldVal) => {
//console.log('当前页发生变化', newVal, oldVal);
if (newVal != oldVal) {
jumpTo(newVal);
}
// else {
// if (route.name != layoutStore.indexName) {
// router.replace({
// name: layoutStore.indexName,
// });
// }
// }
},
);
// 路由跳转
function jumpTo(menuItem: MenuItem) {
if (menuItem != null) {
// 路由菜单
if (
menuItem.bindType === SysMenuBindType.ROUTER &&
menuItem.formRouterName != null &&
menuItem.formRouterName !== ''
) {
router.replace({
name: menuItem.formRouterName,
});
return;
}
// 在线表单菜单
if (
menuItem.bindType === SysMenuBindType.ONLINE_FORM &&
menuItem.onlineFormId != null &&
menuItem.onlineFormId !== ''
) {
router.replace({
name: 'onlineForm',
query: {
formId: menuItem.onlineFormId,
formType: SysOnlineFormType.QUERY,
},
});
return;
}
// 工单列表菜单
if (
menuItem.bindType === SysMenuBindType.WORK_ORDER &&
menuItem.onlineFormId != null &&
menuItem.onlineFormId !== '' &&
menuItem.onlineFlowEntryId != null &&
menuItem.onlineFlowEntryId !== ''
) {
router.replace({
name: 'onlineForm',
query: {
formId: menuItem.onlineFormId,
entryId: menuItem.onlineFlowEntryId,
formType: SysOnlineFormType.WORK_ORDER,
},
});
return;
}
// 报表菜单
if (
menuItem.bindType === SysMenuBindType.REPORT &&
menuItem.reportPageId != null &&
menuItem.reportPageId !== ''
) {
router.replace({
name: 'reportRender',
query: {
pageId: menuItem.reportPageId,
},
});
return;
}
// 外部链接
if (
menuItem.bindType === SysMenuBindType.THRID_URL &&
menuItem.targetUrl != null &&
menuItem.targetUrl !== ''
) {
let token = getToken();
let targetUrl = menuItem.targetUrl;
if (targetUrl.indexOf('?') === -1) {
targetUrl = targetUrl + '?';
}
targetUrl = targetUrl + 'token=' + token;
window.open(targetUrl);
return;
}
}
console.warn('没有匹配到目标路由');
if (route.name !== layoutStore.indexName) {
router.replace({
name: layoutStore.indexName,
});
}
}
const getMainStyle = computed(() => {
return [
{
height: documentClientHeight.value + 'px',
},
];
});
const getContextStyle = computed(() => {
return [
{
height: documentClientHeight.value - (layoutStore.supportTags ? 108 : 60) + 'px',
},
];
});
const getRouterViewStyle = computed(() => {
return [
{
'min-height': documentClientHeight.value - (layoutStore.supportTags ? 108 : 60) - 32 + 'px',
},
];
});
const handleMessage = (command: string) => {
router.push({
name: 'formMessage',
query: {
type: command,
},
});
};
const handleCommand = (command: string) => {
switch (command) {
case 'logout':
ElMessageBox.confirm('是否退出登录?', '', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
LoginController.logout()
.then(() => {
ElMessage({
type: 'success',
message: '退出成功',
});
setToken(null);
router.replace('/login');
})
.catch(e => {
console.log(e);
ElMessage({
type: 'error',
message: e,
});
});
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消退出',
});
});
break;
default:
ElMessage.warning(`click on item ${command}`);
break;
}
};
onMounted(() => {
messageStore.startMessage();
});
onBeforeUnmount(() => {
messageStore.stopMessage();
});
</script>
<style lang="scss">
//@import url('@/assets/style/element-variables.scss');
//$--color-primary: #f70;
.header {
z-index: 1;
.header-main {
display: flex;
align-items: center;
padding-left: 4px;
box-shadow: 0 2px 10px 1px rgb(65 64 133 / 10%);
flex: 1;
.title {
overflow: hidden;
width: 144px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.logo {
padding-left: 16px;
img {
width: 40px;
}
}
.has-multi-column {
display: flex;
justify-content: center;
align-items: center;
width: 80px;
height: 60px;
padding-left: 0;
background-color: $color-primary;
img {
width: 36px;
}
}
.title {
font-size: 22px;
font-weight: bold;
color: #434344;
}
}
.sidebar {
transition: 0.3s;
}
.el-menu {
background-color: #2d3039 !important;
}
.layout-main {
display: flex !important;
.layout-content {
width: 0;
flex: 1;
}
.tag-wrap {
display: flex;
align-items: center;
padding: 0 25px;
background-color: white;
.collapse-img {
padding: 4px;
margin-right: 12px;
background-color: #f6f7f9;
border-radius: 4px;
cursor: pointer;
img {
vertical-align: middle;
}
}
.collapse {
transform: rotate(180deg);
}
}
}
.message-popover {
padding: 5px !important;
}
.message-popover .el-table::before {
height: 0 !important;
}
.message-popover .el-table td {
border: none;
}
.header-menu {
align-items: center;
padding-right: 45px;
.header-img {
width: 31px !important;
height: 31px !important;
margin-right: 8px;
}
.line {
display: inline-block;
width: 1px;
height: 24px;
background-color: #e8e8e8;
vertical-align: middle;
}
}
.user-dropdown {
color: #333 !important;
.el-icon-arrow-down {
color: #333;
}
}
.code-generation-btn {
display: flex;
justify-content: center;
align-items: center;
width: 82px;
height: 32px;
padding: 0 !important;
margin-right: 20px !important;
color: $color-primary !important;
border-color: $color-primary !important;
span {
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
img {
margin-right: 4px;
}
}
}
.multi-column-header .header-main {
padding-left: 16px;
.title {
width: 185px;
}
}
.page-box {
display: flex !important;
flex-direction: column;
}
</style>