diff --git a/package.json b/package.json index 83e93bce..5854b8d6 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "echarts": "4.1.0", "element-ui": "2.4.11", "file-saver": "1.3.8", + "fuse.js": "3.4.2", "js-cookie": "2.2.0", "jsonlint": "1.6.3", "jszip": "3.1.5", diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue new file mode 100644 index 00000000..ab0d556a --- /dev/null +++ b/src/components/HeaderSearch/index.vue @@ -0,0 +1,187 @@ +<template> + <div :class="{'show':show}" class="header-search"> + <svg-icon class-name="search-icon" icon-class="search" @click="click" /> + <el-select + ref="headerSearchSelect" + v-model="search" + :remote-method="querySearch" + filterable + default-first-option + remote + placeholder="Search" + class="header-search-select" + @change="change"> + <el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')"/> + </el-select> + </div> +</template> + +<script> +import Fuse from 'fuse.js' +import path from 'path' +import i18n from '@/lang' + +export default { + name: 'HeaderSearch', + data() { + return { + search: '', + options: [], + searchPool: [], + show: false, + fuse: undefined + } + }, + computed: { + routers() { + return this.$store.getters.permission_routers + }, + lang() { + return this.$store.getters.language + } + }, + watch: { + lang() { + this.searchPool = this.generateRouters(this.routers) + }, + routers() { + this.searchPool = this.generateRouters(this.routers) + }, + searchPool(list) { + this.initFuse(list) + }, + show(value) { + if (value) { + document.body.addEventListener('click', this.close) + } else { + document.body.removeEventListener('click', this.close) + } + } + }, + mounted() { + this.searchPool = this.generateRouters(this.routers) + }, + methods: { + click() { + this.show = !this.show + if (this.show) { + this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus() + } + }, + close() { + this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur() + this.options = [] + this.show = false + }, + change(val) { + this.$router.push(val.path) + this.search = '' + this.options = [] + this.$nextTick(() => { + this.show = false + }) + }, + initFuse(list) { + this.fuse = new Fuse(list, { + shouldSort: true, + threshold: 0.4, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [{ + name: 'title', + weight: 0.7 + }, { + name: 'path', + weight: 0.3 + }] + }) + }, + // Filter out the routes that can be displayed in the sidebar + // And generate the internationalized title + generateRouters(routers, basePath = '/', prefixTitle = []) { + let res = [] + + for (const router of routers) { + // skip hidden router + if (router.hidden) { continue } + + const data = { + path: path.resolve(basePath, router.path), + title: [...prefixTitle] + } + + if (router.meta && router.meta.title) { + // generate internationalized title + const i18ntitle = i18n.t(`route.${router.meta.title}`) + + data.title = [...data.title, i18ntitle] + + if (router.redirect !== 'noredirect') { + // only push the routes with title + // special case: need to exclude parent router without redirect + res.push(data) + } + } + + // recursive child routers + if (router.children) { + const tempRouters = this.generateRouters(router.children, data.path, data.title) + if (tempRouters.length >= 1) { + res = [...res, ...tempRouters] + } + } + } + return res + }, + querySearch(query) { + if (query !== '') { + this.options = this.fuse.search(query) + } else { + this.options = [] + } + } + } +} +</script> + +<style lang="scss" scoped> +.header-search { + font-size: 0 !important; + + .search-icon { + cursor: pointer; + font-size: 18px; + vertical-align: middle; + } + + .header-search-select { + font-size: 18px; + transition: width 0.2s; + width: 0; + overflow: hidden; + background: transparent; + border-radius: 0; + display: inline-block; + vertical-align: middle; + + /deep/ .el-input__inner { + border-radius: 0; + border: 0; + padding-left: 0; + padding-right: 0; + box-shadow: none !important; + border-bottom: 1px solid #d9d9d9; + vertical-align: middle; + } + } + + &.show { + .header-search-select { + width: 210px; + margin-left: 10px; + } + } +} +</style> diff --git a/src/icons/svg/search.svg b/src/icons/svg/search.svg new file mode 100644 index 00000000..84233dda --- /dev/null +++ b/src/icons/svg/search.svg @@ -0,0 +1 @@ +<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg> \ No newline at end of file diff --git a/src/views/layout/components/Navbar.vue b/src/views/layout/components/Navbar.vue index 018ef0f9..dcb09ec1 100644 --- a/src/views/layout/components/Navbar.vue +++ b/src/views/layout/components/Navbar.vue @@ -6,24 +6,26 @@ <div class="right-menu"> <template v-if="device!=='mobile'"> - <error-log class="errLog-container right-menu-item"/> + <search class="right-menu-item" /> + + <error-log class="errLog-container right-menu-item hover-effect"/> <el-tooltip :content="$t('navbar.screenfull')" effect="dark" placement="bottom"> - <screenfull class="right-menu-item"/> + <screenfull class="right-menu-item hover-effect"/> </el-tooltip> <el-tooltip :content="$t('navbar.size')" effect="dark" placement="bottom"> - <size-select class="right-menu-item"/> + <size-select class="right-menu-item hover-effect"/> </el-tooltip> - <lang-select class="right-menu-item"/> + <lang-select class="right-menu-item hover-effect"/> <el-tooltip :content="$t('navbar.theme')" effect="dark" placement="bottom"> - <theme-picker class="theme-picker right-menu-item"/> + <theme-picker class="right-menu-item hover-effect"/> </el-tooltip> </template> - <el-dropdown class="avatar-container right-menu-item" trigger="click"> + <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> <div class="avatar-wrapper"> <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar"> <i class="el-icon-caret-bottom"/> @@ -57,6 +59,7 @@ import Screenfull from '@/components/Screenfull' import SizeSelect from '@/components/SizeSelect' import LangSelect from '@/components/LangSelect' import ThemePicker from '@/components/ThemePicker' +import Search from '@/components/HeaderSearch' export default { components: { @@ -66,7 +69,8 @@ export default { Screenfull, SizeSelect, LangSelect, - ThemePicker + ThemePicker, + Search }, computed: { ...mapGetters([ @@ -100,6 +104,7 @@ export default { float: left; cursor: pointer; transition: background .3s; + &:hover { background: rgba(0, 0, 0, .025) } @@ -124,24 +129,30 @@ export default { } .right-menu-item { - cursor: pointer; display: inline-block; padding: 0 8px; height: 100%; - font-size: 20px; + font-size: 18px; color: #5a5e66; vertical-align: text-bottom; - transition: background .3s; - &:hover { - background: rgba(0, 0, 0, .025) + + &.hover-effect { + cursor: pointer; + transition: background .3s; + + &:hover { + background: rgba(0, 0, 0, .025) + } } } .avatar-container { margin-right: 30px; + .avatar-wrapper { margin-top: 5px; position: relative; + .user-avatar { cursor: pointer; width: 40px;