feat:搜索新增常用菜单&移除常用菜单

This commit is contained in:
不做码农 2023-04-28 08:43:42 +08:00
parent 5e0cacb556
commit b9ad9522b1
8 changed files with 175 additions and 40 deletions

View File

@ -116,3 +116,15 @@
.el-menu--horizontal .el-sub-menu .el-sub-menu__icon-arrow { .el-menu--horizontal .el-sub-menu .el-sub-menu__icon-arrow {
right: calc(0px - var(--el-menu-base-level-padding)) !important; right: calc(0px - var(--el-menu-base-level-padding)) !important;
} }
// 弹出搜索框
.header-search-select {
.el-select-dropdown__item {
height: unset !important;
line-height: unset !important;
margin-bottom: 5px;
display: flex;
align-items: center;
justify-content: space-between;
}
}

View File

@ -11,7 +11,8 @@
filterable filterable
default-first-option default-first-option
remote remote
class="header_search_select" popper-class="header-search-select"
placement="bottom"
placeholder="菜单搜索支持标题、URL模糊查询" placeholder="菜单搜索支持标题、URL模糊查询"
@change="change"> @change="change">
<template #prefix> <template #prefix>
@ -20,8 +21,13 @@
</el-icon> </el-icon>
</template> </template>
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')"> <el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')">
<span style="float: left">{{ option.item.title.join(' > ') }}</span> <span style="float: left">
<span style="float: right; color: var(--el-text-color-secondary); font-size: 13px">{{ option.item.path }}</span> <div>
{{ option.item.title.join(' > ') }}
<div class="path">{{ option.item.path }}</div>
</div>
</span>
<span style="float: right" @click.stop="handleLove(option.item)"> <svg-icon color="#ccc" name="star" /></span>
</el-option> </el-option>
</el-select> </el-select>
</el-dialog> </el-dialog>
@ -33,7 +39,8 @@ import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi' import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate' import { isHttp } from '@/utils/validate'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
import { findItem, color16 } from '@/utils/ruoyi'
const { proxy } = getCurrentInstance()
const search = ref('') const search = ref('')
const options = ref([]) const options = ref([])
const searchPool = ref([]) const searchPool = ref([])
@ -108,12 +115,15 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
const data = { const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path, path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle] title: [...prefixTitle],
icon: 'menu',
menuTitle: ''
} }
if (r.meta && r.meta.title) { if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title] data.title = [...data.title, r.meta.title]
data.icon = r.meta.icon
data.menuTitle = r.meta.title
if (r.redirect !== 'noRedirect') { if (r.redirect !== 'noRedirect') {
// only push the routes with title // only push the routes with title
// special case: need to exclude parent router without redirect // special case: need to exclude parent router without redirect
@ -138,6 +148,28 @@ function querySearch(query) {
options.value = [] options.value = []
} }
} }
/**
* 添加快捷菜单
* @param {*} item
*/
function handleLove(item) {
var arraryObjectLocal = proxy.$cache.local.getJSON('commonlyUseMenu') || []
var len = 12
if (arraryObjectLocal.length >= len) {
proxy.$modal.msgError(`最多可添加${len}个常用菜单`)
return
}
let index = findItem(arraryObjectLocal, 'path', item.path)
if (index <= -1) {
arraryObjectLocal.push({ ...item, color: color16() })
proxy.$cache.local.setJSON('commonlyUseMenu', arraryObjectLocal)
proxy.$modal.msgSuccess('添加成功')
usePermissionStore().setCommonlyUsedRoutes()
} else {
proxy.$modal.msgError('该菜单已存在')
}
}
onMounted(() => { onMounted(() => {
searchPool.value = generateRoutes(routes.value) searchPool.value = generateRoutes(routes.value)
@ -178,18 +210,14 @@ watch(searchPool, (list) => {
} }
} }
.header_search_select {
height: 50px;
:deep(.el-input__wrapper) {
height: 50px;
}
}
.search-icon { .search-icon {
cursor: pointer; cursor: pointer;
font-size: 18px; font-size: 18px;
vertical-align: middle; vertical-align: middle;
} }
} }
.path {
color: #ccc;
font-size: 10px;
}
</style> </style>

View File

@ -31,9 +31,13 @@
<el-switch v-model="isDark" class="mt-2" inline-prompt /> <el-switch v-model="isDark" class="mt-2" inline-prompt />
</span> </span>
</div> --> </div> -->
<!-- <h3 class="drawer-title">
{{ $t('layout.themeColor') }}
</h3> -->
<div class="drawer-item"> <div class="drawer-item">
<span>{{ $t('layout.themeColor') }}</span> <span>{{ $t('layout.themeColor') }}</span>
<span class="comp-style"> <span class="comp-style quick-color-wrap">
<!-- <span :style="{ 'background-color': item }" v-for="item in predefineColors" @change="themeChange(item)"></span> -->
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" /> <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
</span> </span>
</div> </div>
@ -329,5 +333,16 @@ defineExpose({
float: right; float: right;
margin: -3px 8px 0px 0px; margin: -3px 8px 0px 0px;
} }
.quick-color-wrap {
display: flex;
align-items: center;
span {
width: 15px;
height: 15px;
margin-right: 10px;
cursor: pointer;
}
}
} }
</style> </style>

View File

@ -3,6 +3,7 @@ import { getRouters } from '@/api/system/menu'
import Layout from '@/layout/index' import Layout from '@/layout/index'
import ParentView from '@/components/ParentView' import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink' import InnerLink from '@/layout/components/InnerLink'
import cache from '@/plugins/cache'
// 匹配views里面所有的.vue文件 // 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue') const modules = import.meta.glob('./../../views/**/*.vue')
@ -12,7 +13,8 @@ const usePermissionStore = defineStore('permission', {
routes: [], routes: [],
defaultRoutes: [], defaultRoutes: [],
topbarRouters: [], topbarRouters: [],
sidebarRouters: [] sidebarRouters: [],
commonlyUsedRoutes: [] //常用路由
}), }),
actions: { actions: {
setRoutes(routes) { setRoutes(routes) {
@ -43,9 +45,23 @@ const usePermissionStore = defineStore('permission', {
this.setSidebarRouters(constantRoutes.concat(sidebarRoutes)) this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
this.setDefaultRoutes(sidebarRoutes) this.setDefaultRoutes(sidebarRoutes)
this.setTopbarRoutes(defaultRoutes) this.setTopbarRoutes(defaultRoutes)
this.setCommonlyUsedRoutes()
resolve(rewriteRoutes) resolve(rewriteRoutes)
}) })
}) })
},
// 设置常用路由
setCommonlyUsedRoutes() {
var arraryObjectLocal = cache.local.getJSON('commonlyUseMenu') || []
this.commonlyUsedRoutes = arraryObjectLocal
},
// 移除常用路由
removeCommonlyUsedRoutes(item) {
var routes = this.commonlyUsedRoutes
const fi = routes.findIndex((v) => v.path === item.path)
routes.splice(fi, 1)
cache.local.setJSON('commonlyUseMenu', routes)
} }
} }
}) })

View File

@ -84,7 +84,7 @@ service.interceptors.response.use(
} else if (message.includes('Request failed with status code 429')) { } else if (message.includes('Request failed with status code 429')) {
message = '请求过于频繁,请稍后再试' message = '请求过于频繁,请稍后再试'
} else if (message.includes('Request failed with status code')) { } else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常' message = '系统接口' + message.substr(message.length - 3) + '异常,请联系管理员'
} }
ElMessage({ ElMessage({
message: message, message: message,

View File

@ -257,3 +257,28 @@ export function isEmpty(obj) {
return false return false
} }
} }
/**
* 查找对象的唯一键值对比如id去判断是否存在某个数据中
* @param {*} arr 数组
* @param {*} key 对象键值名
* @param {*} val
* @returns
*/
export function findItem(arr, key, val) {
for (let i = 0; i < arr.length; i++) {
if (arr[i][key] == val) {
return i
}
}
return -1
}
export function color16() {
//十六进制颜色随机
const r = Math.floor(Math.random() * 256)
const g = Math.floor(Math.random() * 256)
const b = Math.floor(Math.random() * 256)
const color = `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`
return color
}

View File

@ -1,33 +1,60 @@
<template> <template>
<div class="tool-wrap"> <div class="tool-wrap">
<template v-for="item in menuList"> <template v-for="item in commonRouters">
<div class="tool-item" v-if="checkPermi(item)"> <div class="tool-item">
<span class="close-used" @click="removeRoute(item)" v-if="showRemove">
<el-icon><CloseBold /></el-icon>
</span>
<router-link :to="item.path"> <router-link :to="item.path">
<svg-icon :name="item.name" class-name="card-panel-icon mb10" :color="item.color" /> <svg-icon :name="item.icon" class-name="card-panel-icon mb10" :color="item.color" />
<div>{{ item.title }}</div> <div class="title">{{ item.menuTitle }}</div>
</router-link> </router-link>
</div> </div>
</template> </template>
<el-empty :image-size="80" v-if="commonRouters && commonRouters.length <= 0" />
</div> </div>
</template> </template>
<script setup> <script setup>
const { proxy } = getCurrentInstance() import usePermissionStore from '@/store/modules/permission'
const menuList = ref([
{ path: '/dashboard', title: '控制台', color: '#40c9c6', name: 'dashboard' },
{ path: '/tool/gen', title: '代码生成', color: '#40c9c6', name: 'code', perms: ['tool:gen:list'] },
{ path: '/tool/file', title: '文件存储', color: '#6A5ACD', name: 'upload', perms: ['tool:file:list'] },
// // { path: '/system/user', title: '', color: '#7FFF00', name: 'peoples' },
{ path: '/system/dict', title: '字典管理', color: '#B0E0E6', name: 'dict', perms: ['system:dict:list'] },
{ path: '/monitor/job', title: '定时任务', color: '#D2691E', name: 'job', perms: ['monitor:job:list'] },
{ path: '/system/log/operlog', title: '操作日志', color: '#D2691E', name: 'form', perms: ['monitor:operlog:list'] }
// { path: '/system/log/logininfor', title: '', color: '#D2691E', name: 'logininfor' }
])
function checkPermi(v) { const props = defineProps({
if (v && v.perms) { modelValue: {
return proxy.$auth.hasPermiOr(v.perms) type: Boolean,
default: false
} }
return true })
const showRemove = ref(false)
watch(
() => props.modelValue,
(val) => {
showRemove.value = val
},
{
immediate: true
}
)
const commonRouters = computed(() => usePermissionStore().commonlyUsedRoutes)
// const { proxy } = getCurrentInstance()
// const menuList = ref([
// { path: '/dashboard', title: '', color: '#40c9c6', name: 'dashboard' },
// { path: '/tool/gen', title: '', color: '#40c9c6', name: 'code', perms: ['tool:gen:list'] },
// { path: '/tool/file', title: '', color: '#6A5ACD', name: 'upload', perms: ['tool:file:list'] },
// // // { path: '/system/user', title: '', color: '#7FFF00', name: 'peoples' },
// { path: '/system/dict', title: '', color: '#B0E0E6', name: 'dict', perms: ['system:dict:list'] },
// { path: '/monitor/job', title: '', color: '#D2691E', name: 'job', perms: ['monitor:job:list'] },
// { path: '/system/log/operlog', title: '', color: '#D2691E', name: 'form', perms: ['monitor:operlog:list'] }
// // { path: '/system/log/logininfor', title: '', color: '#D2691E', name: 'logininfor' }
// ])
// function checkPermi(v) {
// if (v && v.perms) {
// return proxy.$auth.hasPermiOr(v.perms)
// }
// return true
// }
function removeRoute(item) {
usePermissionStore().removeCommonlyUsedRoutes(item)
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -40,11 +67,22 @@ function checkPermi(v) {
text-align: center; text-align: center;
width: 100px; width: 100px;
margin-bottom: 30px; margin-bottom: 30px;
position: relative;
.card-panel-icon { .card-panel-icon {
width: 30px; width: 25px;
height: 30px; height: 25px;
} }
.title {
color: #606266;
font-size: 13px;
}
}
.close-used {
position: absolute;
right: 0;
top: 0;
cursor: pointer;
} }
} }
</style> </style>

View File

@ -51,9 +51,10 @@
<el-card shadow="hover"> <el-card shadow="hover">
<template #header> <template #header>
<span><svg-icon name="tool" /> 常用功能</span> <span><svg-icon name="tool" /> 常用功能</span>
<el-button class="home-card-more" text @click="showEdit = !showEdit">{{ $t('btn.edit') }}</el-button>
</template> </template>
<div class="info"> <div class="info">
<el-scrollbar wrap-class="scrollbar-wrapper"> <CommonMenu></CommonMenu></el-scrollbar> <el-scrollbar wrap-class="scrollbar-wrapper"> <CommonMenu v-model="showEdit"></CommonMenu></el-scrollbar>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
@ -108,7 +109,7 @@ import dayjs from 'dayjs'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import useSocketStore from '@/store/modules/socket' import useSocketStore from '@/store/modules/socket'
const showEdit = ref(false)
const data = { const data = {
newVisitis: { newVisitis: {
expectedData: [100, 120, 161, 134, 105, 160, 165], expectedData: [100, 120, 161, 134, 105, 160, 165],