feat: 指令菜单功能 (#1407)
* feat: 指令菜单功能 * 增加快捷指令功能 * 废弃jwt结构StandardClaims替换为RegisteredClaims #1409 * 调整快捷指令登出操作 --------- Co-authored-by: krank <emosick@qq.com>
This commit is contained in:
committed by
GitHub
co-authored by
krank
parent
6ad7ccdd6e
commit
5f0402043a
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
width="30%"
|
||||
class="overlay"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<input v-model="searchInput" class="quick-input" placeholder="请输入你需要快捷到达的功能">
|
||||
</template>
|
||||
|
||||
<div v-for="(option,index) in options" :key="index">
|
||||
<div v-if="option.children.length" class="quick-title">{{ option.label }}</div>
|
||||
<div v-for="(item,key) in option.children" :key="index+'-'+key" class="quick-item" @click="item.func">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="close">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CommandMenu',
|
||||
}
|
||||
</script>
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const routerStore = useRouterStore()
|
||||
const dialogVisible = ref(false)
|
||||
const searchInput = ref('')
|
||||
const options = reactive([])
|
||||
const deepMenus = (menus) => {
|
||||
const arr = []
|
||||
menus.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
arr.push(...deepMenus(menu.children))
|
||||
} else {
|
||||
if (menu.meta.title && menu.meta.title.indexOf(searchInput.value) > -1) {
|
||||
arr.push({
|
||||
label: menu.meta.title,
|
||||
func: () => changeRouter(menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
const addQuickMenu = () => {
|
||||
const option = {
|
||||
label: '跳转',
|
||||
children: []
|
||||
}
|
||||
const menus = deepMenus(routerStore.asyncRouters[0].children)
|
||||
option.children.push(...menus)
|
||||
options.push(option)
|
||||
}
|
||||
|
||||
const addQuickOption = () => {
|
||||
const option = {
|
||||
label: '操作',
|
||||
children: []
|
||||
}
|
||||
const quickArr = [
|
||||
{
|
||||
label: '亮色主题',
|
||||
func: () => changeMode('light')
|
||||
}, {
|
||||
label: '暗色主题',
|
||||
func: () => changeMode('dark')
|
||||
}, {
|
||||
label: '退出登录',
|
||||
func: () => userStore.LoginOut()
|
||||
}
|
||||
]
|
||||
option.children.push(...quickArr.filter(item => item.label.indexOf(searchInput.value) > -1))
|
||||
options.push(option)
|
||||
}
|
||||
|
||||
addQuickMenu()
|
||||
addQuickOption()
|
||||
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const changeRouter = (e) => {
|
||||
const name = e.name
|
||||
if (name.indexOf('http:') > -1 || name.indexOf('https:') > -1) {
|
||||
window.open(name)
|
||||
return
|
||||
}
|
||||
router.push({ name: name })
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const changeMode = (e) => {
|
||||
if (e === null) {
|
||||
userStore.changeSideMode('dark')
|
||||
return
|
||||
}
|
||||
userStore.changeSideMode(e)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
watch(searchInput, () => {
|
||||
options.length = 0
|
||||
addQuickMenu()
|
||||
addQuickOption()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
border-radius: 4px;
|
||||
.el-dialog__header{
|
||||
padding:0 !important;
|
||||
margin-right:0 !important;
|
||||
}
|
||||
.el-dialog__body{
|
||||
padding: 12px !important;
|
||||
height: 50vh;
|
||||
overflow: auto !important;
|
||||
}
|
||||
.quick-title{
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
.quick-input{
|
||||
color: #666;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border:none;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.quick-item{
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -67,6 +67,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
})
|
||||
await router.replace({ name: userInfo.value.authority.defaultRouter })
|
||||
loadingInstance.value.close()
|
||||
|
||||
const isWin = ref(/windows/i.test(navigator.userAgent))
|
||||
if (isWin.value) {
|
||||
window.localStorage.setItem('osType', 'WIN')
|
||||
} else {
|
||||
window.localStorage.setItem('osType', 'MAC')
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -62,6 +62,16 @@
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
<el-dropdown-item icon="avatar">
|
||||
<div class="command-box" style="display: flex" @click="handleCommand">
|
||||
<div>指令菜单</div>
|
||||
<div style="margin-left: 8px">
|
||||
<span class="button">{{ first }}</span>
|
||||
+
|
||||
<span class="button">K</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="avatar" @click="toPerson">个人信息</el-dropdown-item>
|
||||
<el-dropdown-item icon="reading-lamp" @click="userStore.LoginOut">登 出</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -96,6 +106,7 @@
|
||||
</router-view>
|
||||
<BottomInfo />
|
||||
<setting />
|
||||
<CommandMenu ref="command"/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
@@ -114,6 +125,7 @@ import HistoryComponent from '@/view/layout/aside/historyComponent/history.vue'
|
||||
import Search from '@/view/layout/search/search.vue'
|
||||
import BottomInfo from '@/view/layout/bottomInfo/bottomInfo.vue'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import CommandMenu from '@/components/commandMenu/index.vue'
|
||||
import Setting from './setting/index.vue'
|
||||
import { setUserAuthority } from '@/api/user'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
@@ -131,8 +143,26 @@ const isCollapse = ref(false)
|
||||
const isSider = ref(true)
|
||||
const isMobile = ref(false)
|
||||
|
||||
const first = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const initPage = () => {
|
||||
const screenWidth = document.body.clientWidth
|
||||
// 判断当前用户的操作系统
|
||||
if(window.localStorage.getItem('osType') === 'WIN') {
|
||||
first.value = 'Ctrl'
|
||||
} else {
|
||||
first.value = '⌘'
|
||||
}
|
||||
// 当用户同时按下ctrl和k键的时候
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.ctrlKey && e.key === 'k') {
|
||||
// 阻止浏览器默认事件
|
||||
e.preventDefault();
|
||||
handleCommand()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
const screenWidth = document.body.clientWidth
|
||||
if (screenWidth < 1000) {
|
||||
isMobile.value = true
|
||||
isSider.value = false
|
||||
@@ -150,6 +180,11 @@ const initPage = () => {
|
||||
|
||||
initPage()
|
||||
|
||||
const command = ref()
|
||||
const handleCommand = () => {
|
||||
command.value.open()
|
||||
}
|
||||
|
||||
const loadingFlag = ref(false)
|
||||
onMounted(() => {
|
||||
// 挂载一些通用的事件
|
||||
@@ -246,4 +281,20 @@ const changeShadow = () => {
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/style/mobile.scss';
|
||||
.button {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: rgb(250,250,250);
|
||||
width: 25px!important;
|
||||
padding: 4px 8px !important;
|
||||
border: 1px solid #eaeaea;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
:deep .el-overlay {
|
||||
background-color: hsla(0,0%,100%,.9) !important;
|
||||
}
|
||||
.command-box{
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +1,20 @@
|
||||
<template>
|
||||
<div class="search-component">
|
||||
<div v-if="show" class="transition-box">
|
||||
<el-select
|
||||
ref="searchInput"
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
@blur="hiddenSearch"
|
||||
@change="changeRouter"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in routerStore.routerList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<div class="gvaIcon gvaIcon-refresh" :class="[reload ? 'reloading' : '']" @click="handleReload" />
|
||||
</div>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<Screenfull class="search-icon" :style="{cursor:'pointer'}" />
|
||||
</div>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<div class="service gvaIcon-customer-service" @click="toService" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<div class="gvaIcon gvaIcon-refresh" :class="[reload ? 'reloading' : '']" @click="handleReload" />
|
||||
</div>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<div class="gvaIcon gvaIcon-search" @click="showSearch" />
|
||||
</div>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<Screenfull class="search-icon" :style="{cursor:'pointer'}" />
|
||||
</div>
|
||||
<div
|
||||
class="user-box"
|
||||
>
|
||||
<div class="service gvaIcon-customer-service" @click="toService" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,35 +27,7 @@ export default {
|
||||
<script setup>
|
||||
import Screenfull from '@/view/layout/screenfull/index.vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
const changeRouter = (e) => {
|
||||
if (e.indexOf('http:') > -1 || e.indexOf('https:') > -1) {
|
||||
window.open(e)
|
||||
return
|
||||
}
|
||||
router.push({ name: e })
|
||||
}
|
||||
|
||||
const show = ref(false)
|
||||
const hiddenSearch = async() => {
|
||||
setTimeout(() => {
|
||||
show.value = false
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const searchInput = ref(null)
|
||||
const showSearch = async() => {
|
||||
show.value = true
|
||||
await nextTick()
|
||||
searchInput.value.focus()
|
||||
}
|
||||
import { ref } from 'vue'
|
||||
|
||||
const reload = ref(false)
|
||||
const handleReload = () => {
|
||||
@@ -117,7 +66,6 @@ const toService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.reloading{
|
||||
animation:turn 0.5s linear infinite;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user