Просмотр исходного кода

fix: 外链跳转处理
feat: 菜单全局搜索功能
fix: index 导致的样式问题

lanceJiang 1 год назад
Родитель
Сommit
ce923f2950

+ 6 - 6
src/layout/LayoutLeft/index.scss

@@ -5,10 +5,10 @@
 	.el-aside {
 		width: auto;
 		background-color: var(--el-menu-bg-color);
-		//border-right: 1px solid var(--el-aside-border-color);
-		box-shadow: 1px 0 2px var(--el-aside-border-color);
+		border-right: 1px solid var(--el-aside-border-color);
+		//box-shadow: 1px 0 2px var(--el-aside-border-color);
 		//box-shadow: 1px 0 4px -1px var(--el-aside-border-color);
-		z-index: $header_index + 1;
+		//z-index: $header_index + 1;
 		//box-shadow: 2px 0 8px #1d23290d;
 		.aside-box {
 			display: flex;
@@ -61,9 +61,9 @@
 		height: $header_height;
 		padding: 0;
 		background-color: var(--el-header-bg-color);
-		//border-bottom: 1px solid var(--el-header-border-color);
-		box-shadow: 0 1px 4px -1px var(--el-header-border-color);
-		z-index: $header_index;
+		border-bottom: 1px solid var(--el-header-border-color);
+		//box-shadow: 0 1px 4px -1px var(--el-header-border-color);
+		//z-index: $header_index;
 	}
 	.app-main {
 		min-height: 0;

+ 6 - 6
src/layout/LayoutLeftMix/index.scss

@@ -9,9 +9,9 @@
 		width: 70px;
 		height: 100%;
 		background-color: var(--el-menu-bg-color);
-		//border-right: 1px solid var(--el-aside-border-color);
-		box-shadow: 1px 0 2px var(--el-aside-border-color);
-		z-index: $header_index + 3;
+		border-right: 1px solid var(--el-aside-border-color);
+		//box-shadow: 1px 0 2px var(--el-aside-border-color);
+		//z-index: $header_index + 3;
 		.logo {
 			display: flex;
 			align-items: center;
@@ -89,9 +89,9 @@
 		height: 100%;
 		overflow: hidden;
 		background-color: var(--el-menu-bg-color);
-		//border-right: 1px solid var(--el-aside-border-color);
-		z-index: $header_index + 2;
-		box-shadow: 1px 0 2px var(--el-aside-border-color);
+		border-right: 1px solid var(--el-aside-border-color);
+		//z-index: $header_index + 2;
+		//box-shadow: 1px 0 2px var(--el-aside-border-color);
 		//box-shadow: 2px 0 8px #1d23290d;
 		//box-shadow: 1px 0 4px -1px var(--el-aside-border-color);
 		transition: width 0.3s ease;

+ 3 - 3
src/layout/LayoutTop/index.scss

@@ -10,9 +10,9 @@
 		height: $header_height;
 		padding: 0;
 		background-color: var(--el-header-bg-color);
-		//border-bottom: 1px solid var(--el-header-border-color);
-		box-shadow: 0 1px 4px -1px var(--el-header-border-color);
-		z-index: $header_index;
+		border-bottom: 1px solid var(--el-header-border-color);
+		//box-shadow: 0 1px 4px -1px var(--el-header-border-color);
+		//z-index: $header_index;
 		.logo {
 			display: flex;
 			align-items: center;

+ 2 - 1
src/layout/LayoutTop/index.vue

@@ -56,6 +56,7 @@ import SubMenu from '@/layout/components/Menu/SubMenu.vue'
 import MenuIcon from '@/layout/components/Menu/MenuIcon.vue'
 import useStore from '@/store'
 import { generateTitle } from '@/utils/i18n'
+import { isExternal } from '@/utils/validate.ts'
 
 const title = import.meta.env.VITE_APP_TITLE
 const { permission, setting, app } = useStore()
@@ -67,7 +68,7 @@ const menuList = computed(() => permission.showMenuList)
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string)
 
 const handleClickMenu = (subItem: Menu.MenuOptions) => {
-	if (subItem.meta.isLink) return window.open(subItem.meta.isLink, '_blank')
+	if (isExternal(subItem.path)) return window.open(subItem.path, '_blank')
 	router.push(subItem.path)
 }
 </script>

+ 4 - 4
src/layout/LayoutTopMix/index.scss

@@ -11,10 +11,10 @@
     //padding: 0 15px 0 0;
     padding: 0;
     background-color: var(--el-header-bg-color);
-		//border-bottom: 1px solid var(--el-header-border-color);
-		//box-shadow: 0 2px 8px var(--el-header-border-color);
-		box-shadow: 0 1px 4px -1px var(--el-header-border-color);
-		z-index: $header_index + 1;
+    border-bottom: 1px solid var(--el-header-border-color);
+    //box-shadow: 0 2px 8px var(--el-header-border-color);
+    //box-shadow: 0 1px 4px -1px var(--el-header-border-color);
+    //z-index: $header_index + 1;
     .header-lf {
       display: flex;
       align-items: center;

+ 4 - 9
src/layout/components/Header/ToolBarRight.vue

@@ -1,17 +1,14 @@
 <template>
 	<div class="tool-bar-right">
 		<div class="header-icon">
-			<!--			<AssemblySize />-->
-			<Screenfull />
+			<ScreenFull />
 			<el-tooltip content="布局大小" effect="dark" placement="bottom">
 				<SizeSelect class="right-menu-item le-hover-effect--bg" />
 			</el-tooltip>
 			<Language class="right-menu-item le-hover-effect--bg" />
-			<!--      todo...搜索 -->
-			<!--			<SearchMenu id="searchMenu" />-->
+			<SearchMenu />
 			<!--			<ThemeSetting id="themeSetting" />-->
 			<Message class="right-menu-item le-hover-effect--bg" />
-			<!--			<Fullscreen id="fullscreen" />-->
 		</div>
 		<Avatar />
 	</div>
@@ -19,12 +16,10 @@
 
 <script setup lang="ts">
 import { computed } from 'vue'
-// import AssemblySize from './components/AssemblySize.vue'
 import Language from './components/Language.vue'
-import Screenfull from '@/components/Screenfull/index.vue'
+import ScreenFull from '@/components/Screenfull/index.vue'
 import SizeSelect from '@/components/SizeSelect/index.vue'
-// import SearchMenu from './components/SearchMenu.vue'
-// import ThemeSetting from './components/ThemeSetting.vue'
+import SearchMenu from './components/SearchMenu.vue'
 import Message from './components/Message.vue'
 // import Fullscreen from './components/Fullscreen.vue'
 import Avatar from './components/Avatar.vue'

+ 25 - 25
src/layout/components/Header/components/Message.vue

@@ -1,35 +1,35 @@
 <template>
-	<div class="message">
-		<el-popover placement="bottom" :width="310" trigger="click">
-			<template #reference>
+	<el-popover placement="bottom" :width="310" trigger="click">
+		<template #reference>
+			<div class="right-menu-item le-hover-effect--bg">
 				<el-badge class="item" :value="total">
 					<i class="le-iconfont le-notice"></i>
 				</el-badge>
-			</template>
-			<el-tabs v-model="activeTab">
-				<el-tab-pane v-for="v of tabsConfig" :key="v.name" :name="v.name">
-					<template #label> {{ v.label }}({{ v.list.length }}) </template>
-					<template v-if="v.list.length">
-						<div class="message-list">
-							<div v-for="item of v.list" :key="item.id" class="message-item">
-								<!--<img src="" alt="" class="message-icon" />-->
-								<div class="message-content">
-									<div class="message-title">
-										<span class="txt text-overflow_ellipsis" :title="item.title">{{ item.title }}</span>
-										<span class="message-date">{{ item.createTime }}</span>
-									</div>
-									<span class="message-txt">{{ item.content }}</span>
+			</div>
+		</template>
+		<el-tabs v-model="activeTab">
+			<el-tab-pane v-for="v of tabsConfig" :key="v.name" :name="v.name">
+				<template #label> {{ v.label }}({{ v.list.length }}) </template>
+				<template v-if="v.list.length">
+					<div class="message-list">
+						<div v-for="item of v.list" :key="item.id" class="message-item">
+							<!--<img src="" alt="" class="message-icon" />-->
+							<div class="message-content">
+								<div class="message-title">
+									<span class="txt text-overflow_ellipsis" :title="item.title">{{ item.title }}</span>
+									<span class="message-date">{{ item.createTime }}</span>
 								</div>
+								<span class="message-txt">{{ item.content }}</span>
 							</div>
 						</div>
-					</template>
-					<template v-else>
-						<LeNoData />
-					</template>
-				</el-tab-pane>
-			</el-tabs>
-		</el-popover>
-	</div>
+					</div>
+				</template>
+				<template v-else>
+					<LeNoData />
+				</template>
+			</el-tab-pane>
+		</el-tabs>
+	</el-popover>
 </template>
 
 <script setup lang="ts">

+ 120 - 0
src/layout/components/Header/components/SearchMenu.vue

@@ -0,0 +1,120 @@
+<template>
+	<div class="search-menu-wrap">
+		<div class="right-menu-item le-hover-effect--bg" @click="handleOpen">
+			<LeIcon icon-class="le-search" />
+		</div>
+		<el-dialog v-model="isShowSearch" destroy-on-close :modal="false" :show-close="false" fullscreen @click="closeSearch">
+			<el-autocomplete
+				ref="menuInputRef"
+				v-model="searchMenu"
+				value-key="path"
+				placeholder="菜单搜索"
+				:fetch-suggestions="searchMenuList"
+				@select="handleClickMenu"
+				@click.stop
+			>
+				<template #prefix>
+					<LeIcon icon-class="le-search" />
+				</template>
+				<template #default="{ item }">
+					<MenuIcon v-if="item.meta?.icon" :icon-class="item.meta.icon" />
+					<span> {{ item.meta.local_title }} </span>
+				</template>
+			</el-autocomplete>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="SearchMenu">
+import { ref, computed, nextTick } from 'vue'
+import { AppRouteRecordRaw } from '@/router/types'
+import MenuIcon from '@/layout/components/Menu/MenuIcon.vue'
+import { useRouter } from 'vue-router'
+import { generateTitle } from '@/utils/i18n'
+import { isExternal } from '@/utils/validate'
+import useStore from '@/store'
+const router = useRouter()
+const { permission } = useStore()
+const menuList = computed(() => {
+	return permission.showMenuListFlat.map(v => {
+		return {
+			...v,
+			meta: {
+				...v.meta,
+				local_title: generateTitle(v.meta.title)
+			}
+		}
+	})
+})
+const searchMenuList = (queryString: string, cb: () => {}) => {
+	const _menuList = queryString ? menuList.value.filter(filterNodeMethod(queryString)) : menuList.value
+	cb(_menuList)
+}
+
+// 打开搜索框
+const isShowSearch = ref(false)
+const menuInputRef = ref()
+const searchMenu = ref('')
+const handleOpen = () => {
+	isShowSearch.value = true
+	nextTick(() => {
+		setTimeout(() => {
+			menuInputRef.value.focus()
+		})
+	})
+}
+
+// 搜索窗关闭
+const closeSearch = () => {
+	isShowSearch.value = false
+}
+
+// 筛选菜单
+const filterNodeMethod = (queryString: string) => {
+	return (restaurant: AppRouteRecordRaw) => {
+		return restaurant.meta.local_title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
+	}
+}
+
+// 点击菜单跳转
+const handleClickMenu = (menuItem: AppRouteRecordRaw | Record<string, any>) => {
+	searchMenu.value = ''
+	if (isExternal(menuItem.path)) window.open(menuItem.path, '_blank')
+	else router.push(menuItem.path)
+	closeSearch()
+}
+</script>
+<style scoped lang="scss">
+.search-menu-wrap {
+	height: 100%;
+}
+:deep(.el-dialog) {
+	background-color: rgb(0 0 0 / 50%);
+	border-radius: 0 !important;
+	box-shadow: unset !important;
+	.el-dialog__header {
+		border-bottom: none !important;
+	}
+}
+:deep(.el-autocomplete) {
+	position: absolute;
+	top: 100px;
+	left: 50%;
+	width: 550px;
+	transform: translateX(-50%);
+	.el-input__wrapper {
+		background-color: var(--el-bg-color);
+	}
+}
+.el-autocomplete__popper {
+	.el-icon {
+		position: relative;
+		top: 2px;
+		font-size: 16px;
+	}
+	span {
+		margin: 0 0 0 10px;
+		font-size: 14px;
+	}
+}
+</style>

+ 4 - 3
src/layout/components/Menu/SubMenu.vue

@@ -1,6 +1,6 @@
 <template>
 	<template v-for="subItem in menuList" :key="subItem.path">
-		<el-sub-menu v-if="subItem.children?.length" popperClass="layout-menu-popper-wrap" :index="subItem.path">
+		<el-sub-menu v-if="subItem.children?.length" popper-class="layout-menu-popper-wrap" :index="subItem.path">
 			<template #title>
 				<MenuIcon v-if="subItem.meta.icon" :icon-class="subItem.meta.icon" />
 				<!--				<el-icon v-if="subItem.meta.icon">
@@ -10,7 +10,7 @@
 			</template>
 			<SubMenu :menu-list="subItem.children" />
 		</el-sub-menu>
-		<el-menu-item v-else popperClass="layout-menu-popper-wrap" :index="subItem.path" @click="handleClickMenu(subItem)">
+		<el-menu-item v-else popper-class="layout-menu-popper-wrap" :index="subItem.path" @click="handleClickMenu(subItem)">
 			<MenuIcon v-if="subItem.meta.icon" :icon-class="subItem.meta.icon" />
 			<!--      <el-icon>
 				<component :is="subItem.meta.icon"></component>
@@ -26,12 +26,13 @@
 import { useRouter } from 'vue-router'
 import { generateTitle } from '@/utils/i18n'
 import MenuIcon from './MenuIcon.vue'
+import { isExternal } from '@/utils/validate'
 
 defineProps<{ menuList: Menu.MenuOptions[] }>()
 
 const router = useRouter()
 const handleClickMenu = (subItem: Menu.MenuOptions) => {
-	if (subItem.meta.isLink) return window.open(subItem.meta.isLink, '_blank')
+	if (isExternal(subItem.path)) return window.open(subItem.path, '_blank')
 	router.push(subItem.path)
 	// router.push({ name: subItem.name })
 	// router.push('/404')

+ 17 - 4
src/store/modules/permission.ts

@@ -8,7 +8,7 @@ import { getMenuList } from '@/api/system/menu'
 const modules = import.meta.glob('@/views/**/*.vue')
 export const Layout = () => import('@/layout/index.vue') // todo...
 // export const Layout = () => import('@/layout/index_old.vue.vue')
-// export const Test = () => import('@/layout/test.vue')
+// export const RouteView = () => import('@/layout/RouteView.vue')
 // export const Layout = () => import('@/layouts/index.vue')
 
 // const hasPermission = (roles: string[], route: AppRouteRecordRaw) => {
@@ -35,7 +35,7 @@ export const filterAsyncRoutes = (routes: AppRouteRecordRaw[], roles: string[])
 		// console.warn(tmp.component, 'tmp.component')
 		// 特殊Layout 配置 标识
 		/*if (!tmp.component) {
-			tmp.component = Test
+			tmp.component = RouteView
 		} else*/ if (!tmp.component || tmp.component === 'Layout') {
 			tmp.component = Layout
 		} else {
@@ -62,7 +62,8 @@ const getShowMenuList = (menuList: AppRouteRecordRaw[], parentPath = '') => {
 	return menuList.filter(item => {
 		// console.error(item, 'item')
 		item.meta = item.meta ? item.meta : {}
-		item.meta.icon = item.meta.icon || 'Menu'
+		// 默认icon
+		item.meta.icon = item.meta.icon || 'menu'
 		// path全链 重组
 		item.path = /\/.*/.test(item.path) ? item.path : `${parentPath}/${item.path}`
 		if (!item.meta.hidden) {
@@ -72,6 +73,14 @@ const getShowMenuList = (menuList: AppRouteRecordRaw[], parentPath = '') => {
 		return false
 	})
 }
+// 可展示的路由平铺
+const getMenuListFlat = (menuList: AppRouteRecordRaw[]) => {
+	const _menuList = JSON.parse(JSON.stringify(menuList))
+	return _menuList.flatMap((v: AppRouteRecordRaw) => {
+		const _children = v.children || []
+		return [v, ...getMenuListFlat(_children)]
+	})
+}
 
 const usePermissionStore = defineStore({
 	id: 'permission',
@@ -82,7 +91,7 @@ const usePermissionStore = defineStore({
 	}),
 	getters: {
 		// 有效的 菜单列表
-		showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify([...constantMenuList, ...state.menuList])))
+		showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify([...constantMenuList, ...state.menuList]))),
 		/*getShowMenuList([
 				...JSON.parse(JSON.stringify(state.menuList)),
 				// 测试
@@ -95,6 +104,10 @@ const usePermissionStore = defineStore({
 				}
 			])*/
 		// showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify(state.routes)))
+		// 菜单权限列表 ==> 扁平化之后的一维数组菜单,主要用来添加动态路由
+		showMenuListFlat() {
+			return getMenuListFlat(this.showMenuList)
+		}
 	},
 	actions: {
 		setRoutes(menuList: AppRouteRecordRaw[]) {