Skip to content

Commit 68a9203

Browse files
committed
feat&refactor: 菜单搜索支持键盘选择&悬浮主题背景
1 parent b71ecd8 commit 68a9203

File tree

2 files changed

+124
-55
lines changed

2 files changed

+124
-55
lines changed
Lines changed: 1 addition & 0 deletions
Loading

ruoyi-fastapi-frontend/src/components/HeaderSearch/index.vue

Lines changed: 123 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,49 @@
11
<template>
2-
<div :class="{'show':show}" class="header-search">
2+
<div class="header-search">
33
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
4-
<el-select
5-
ref="headerSearchSelect"
6-
v-model="search"
7-
:remote-method="querySearch"
8-
filterable
9-
default-first-option
10-
remote
11-
placeholder="Search"
12-
class="header-search-select"
13-
@change="change"
4+
<el-dialog
5+
:visible.sync="show"
6+
width="600px"
7+
@close="close"
8+
:show-close="false"
9+
append-to-body
1410
>
15-
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
16-
</el-select>
11+
<el-input
12+
v-model="search"
13+
ref="headerSearchSelectRef"
14+
size="large"
15+
@input="querySearch"
16+
prefix-icon="el-icon-search"
17+
placeholder="菜单搜索,支持标题、URL模糊查询"
18+
clearable
19+
@keyup.enter.native="selectActiveResult"
20+
@keydown.up.native="navigateResult('up')"
21+
@keydown.down.native="navigateResult('down')"
22+
>
23+
</el-input>
24+
<el-scrollbar wrap-class="right-scrollbar-wrapper">
25+
<div class="result-wrap">
26+
<div class="search-item" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1">
27+
<div class="left">
28+
<svg-icon class="menu-icon" :icon-class="item.icon" />
29+
</div>
30+
<div class="search-info" @click="change(item)">
31+
<div class="menu-title">
32+
{{ item.title.join(" / ") }}
33+
</div>
34+
<div class="menu-path">
35+
{{ item.path }}
36+
</div>
37+
</div>
38+
<svg-icon icon-class="enter" v-show="index === activeIndex"/>
39+
</div>
40+
</div>
41+
</el-scrollbar>
42+
</el-dialog>
1743
</div>
1844
</template>
1945

2046
<script>
21-
// fuse is a lightweight fuzzy-search module
22-
// make search results more in line with expectations
2347
import Fuse from 'fuse.js/dist/fuse.min.js'
2448
import path from 'path'
2549
import { isHttp } from '@/utils/validate'
@@ -31,11 +55,15 @@ export default {
3155
search: '',
3256
options: [],
3357
searchPool: [],
58+
activeIndex: -1,
3459
show: false,
3560
fuse: undefined
3661
}
3762
},
3863
computed: {
64+
theme() {
65+
return this.$store.state.settings.theme
66+
},
3967
routes() {
4068
return this.$store.getters.defaultRoutes
4169
}
@@ -46,13 +74,6 @@ export default {
4674
},
4775
searchPool(list) {
4876
this.initFuse(list)
49-
},
50-
show(value) {
51-
if (value) {
52-
document.body.addEventListener('click', this.close)
53-
} else {
54-
document.body.removeEventListener('click', this.close)
55-
}
5677
}
5778
},
5879
mounted() {
@@ -63,23 +84,26 @@ export default {
6384
this.show = !this.show
6485
if (this.show) {
6586
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
87+
this.options = this.searchPool
6688
}
6789
},
6890
close() {
6991
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
92+
this.search = ''
7093
this.options = []
7194
this.show = false
95+
this.activeIndex = -1
7296
},
7397
change(val) {
74-
const path = val.path;
75-
const query = val.query;
98+
const path = val.path
99+
const query = val.query
76100
if(isHttp(val.path)) {
77101
// http(s):// 路径新窗口打开
78-
const pindex = path.indexOf("http");
79-
window.open(path.substr(pindex, path.length), "_blank");
102+
const pindex = path.indexOf("http")
103+
window.open(path.substr(pindex, path.length), "_blank")
80104
} else {
81105
if (query) {
82-
this.$router.push({ path: path, query: JSON.parse(query) });
106+
this.$router.push({ path: path, query: JSON.parse(query) })
83107
} else {
84108
this.$router.push(path)
85109
}
@@ -117,11 +141,13 @@ export default {
117141
118142
const data = {
119143
path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path,
120-
title: [...prefixTitle]
144+
title: [...prefixTitle],
145+
icon: ''
121146
}
122147
123148
if (router.meta && router.meta.title) {
124149
data.title = [...data.title, router.meta.title]
150+
data.icon = router.meta.icon
125151
126152
if (router.redirect !== 'noRedirect') {
127153
// only push the routes with title
@@ -145,52 +171,94 @@ export default {
145171
return res
146172
},
147173
querySearch(query) {
174+
this.activeIndex = -1
148175
if (query !== '') {
149-
this.options = this.fuse.search(query)
176+
this.options = this.fuse.search(query).map((item) => item.item) ?? this.searchPool
150177
} else {
151-
this.options = []
178+
this.options = this.searchPool
179+
}
180+
},
181+
activeStyle(index) {
182+
if (index !== this.activeIndex) return {}
183+
return {
184+
"background-color": this.theme,
185+
"color": "#fff"
186+
}
187+
},
188+
navigateResult(direction) {
189+
if (direction === "up") {
190+
this.activeIndex = this.activeIndex <= 0 ? this.options.length - 1 : this.activeIndex - 1
191+
} else if (direction === "down") {
192+
this.activeIndex = this.activeIndex >= this.options.length - 1 ? 0 : this.activeIndex + 1
193+
}
194+
},
195+
selectActiveResult() {
196+
if (this.options.length > 0 && this.activeIndex >= 0) {
197+
this.change(this.options[this.activeIndex])
152198
}
153199
}
154200
}
155201
}
156202
</script>
157203
158-
<style lang="scss" scoped>
159-
.header-search {
160-
font-size: 0 !important;
204+
<style lang='scss' scoped>
205+
::v-deep {
206+
.el-dialog__header {
207+
padding: 0 !important;
208+
}
209+
}
161210
211+
.header-search {
162212
.search-icon {
163213
cursor: pointer;
164214
font-size: 18px;
165215
vertical-align: middle;
166216
}
217+
}
167218
168-
.header-search-select {
169-
font-size: 18px;
170-
transition: width 0.2s;
171-
width: 0;
172-
overflow: hidden;
173-
background: transparent;
174-
border-radius: 0;
175-
display: inline-block;
176-
vertical-align: middle;
219+
.result-wrap {
220+
height: 280px;
221+
margin: 6px 0;
222+
223+
.search-item {
224+
display: flex;
225+
height: 48px;
226+
align-items: center;
227+
padding-right: 10px;
177228
178-
::v-deep .el-input__inner {
179-
border-radius: 0;
180-
border: 0;
181-
padding-left: 0;
182-
padding-right: 0;
183-
box-shadow: none !important;
184-
border-bottom: 1px solid #d9d9d9;
185-
vertical-align: middle;
229+
.left {
230+
width: 60px;
231+
text-align: center;
232+
233+
.menu-icon {
234+
width: 18px;
235+
height: 18px;
236+
}
186237
}
187-
}
188238
189-
&.show {
190-
.header-search-select {
191-
width: 210px;
192-
margin-left: 10px;
239+
.search-info {
240+
padding-left: 5px;
241+
margin-top: 10px;
242+
width: 100%;
243+
display: flex;
244+
flex-direction: column;
245+
justify-content: flex-start;
246+
flex: 1;
247+
248+
.menu-title,
249+
.menu-path {
250+
height: 20px;
251+
}
252+
.menu-path {
253+
color: #ccc;
254+
font-size: 10px;
255+
}
193256
}
194257
}
258+
259+
.search-item:hover {
260+
cursor: pointer;
261+
}
195262
}
196263
</style>
264+

0 commit comments

Comments
 (0)