【更新】优化前端搜索组源码及个人中心优化

pull/162/MERGE
xiaonuobase 2023-09-09 01:01:01 +08:00
parent 1a828f8378
commit cad24eb2a8
7 changed files with 367 additions and 448 deletions

View File

@ -1,58 +0,0 @@
import { mapState, mapActions } from 'pinia'
import hotkeys from 'hotkeys-js'
import { searchStore } from '@/store'
export default {
mounted() {
// 绑定搜索功能快捷键 [ 打开 ]
hotkeys(this.searchHotkey.open, (event) => {
event.preventDefault()
this.searchPanelOpen()
})
// 绑定搜索功能快捷键 [ 关闭 ]
hotkeys(this.searchHotkey.close, (event) => {
event.preventDefault()
this.searchPanelClose()
})
},
beforeDestroy() {
hotkeys.unbind(this.searchHotkey.open)
hotkeys.unbind(this.searchHotkey.close)
},
computed: {
...mapState(searchStore, {
searchActive: (state) => state.active,
searchHotkey: (state) => state.hotkey
})
},
methods: {
...mapActions(searchStore, ['toggleActive', 'setActive']),
// 接收点击搜索按钮
handleSearchClick() {
this.toggleActive()
if (this.searchActive) {
setTimeout(() => {
if (this.$refs.panelSearch) {
this.$refs.panelSearch.focus()
}
}, 300)
}
},
searchPanelOpen() {
if (!this.searchActive) {
this.setActive(true)
setTimeout(() => {
if (this.$refs.panelSearch) {
this.$refs.panelSearch.focus()
}
}, 300)
}
},
// 关闭搜索面板
searchPanelClose() {
if (this.searchActive) {
this.setActive(false)
}
}
}
}

View File

@ -1,210 +1,262 @@
<template> <template>
<div @keyup.up="handleKeyUp" @keyup.down="handleKeyDown" @keyup.enter="handleKeyEnter" @click.self="handlePanelClick"> <div class="search panel-item" @click="searchPanelOpen">
<a-input <search-outlined />
ref="input"
v-model="searchText"
class="search-box"
style="width: 100%"
allowClear
placeholder="搜索页面(支持拼音检索)"
@change="querySearch"
>
<template #prefix>
<search-outlined />
</template>
</a-input>
<a-card
:body-style="{ padding: '0 0' }"
hoverable
@mouseenter="onCardIn"
@mouseleave="onCardOut"
@keypress.up="handleKeyUp"
@keypress.down="handleKeyDown"
style="margin: 10px 0"
>
<div ref="cardList" class="search-card beauty-scroll">
<a-list size="small" :data-source="resultsList">
<template #renderItem="{ item, index }">
<a-list-item
@click="handleSelect(item.fullPath)"
@mouseover="onCardItemHover(index)"
:class="{ active: index === cardIndex }"
style="padding-right: 10px"
>
<template #actions>
<a>
<enter-outlined />
</a>
</template>
<a-list-item-meta :description="item.fullName">
<template #title>
<a>{{ item.name }}</a>
</template>
<template #avatar>
<a-avatar style="color: var(--text-color); background-color: transparent" :type="item.icon">
<template #icon>
<component :is="item.icon" />
</template>
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</div>
</a-card>
<div class="search-tips">
<span class="key">S</span>
<span class="tips">打开搜索面板</span>
<span class="key">
<arrow-up-outlined />
</span>
<span class="key">
<arrow-down-outlined />
</span>
<span class="tips">选择</span>
<span class="key">
<enter-outlined />
</span>
<span class="tips">确认</span>
<span class="key left">Esc</span>
<span class="tips">关闭</span>
</div>
</div> </div>
<xn-form-container
title="搜索"
:visible="searchActive"
:closable="false"
:footer="null"
:width="600"
destroyOnClose
dialogClass="searchModal"
:bodyStyle="{ maxHeight: '520px', overflow: 'auto', padding: '14px' }"
@close="searchPanelClose"
>
<div
@keyup.up="handleKeyUp"
@keyup.down="handleKeyDown"
@keyup.enter="handleKeyEnter"
@click.self="handlePanelClick"
>
<a-input
ref="inputRef"
v-model="searchText"
class="search-box"
style="width: 100%"
allowClear
placeholder="搜索页面(支持拼音检索)"
@change="querySearch"
>
<template #prefix>
<search-outlined />
</template>
</a-input>
<a-card
:body-style="{ padding: '0 0' }"
hoverable
@mouseenter="onCardIn"
@mouseleave="onCardOut"
@keypress.up="handleKeyUp"
@keypress.down="handleKeyDown"
style="margin: 10px 0"
>
<div ref="cardListRef" class="search-card beauty-scroll">
<a-list size="small" :data-source="resultsList">
<template #renderItem="{ item, index }">
<a-list-item
@click="handleSelect(item.fullPath)"
@mouseover="onCardItemHover(index)"
:class="{ active: index === cardIndex }"
style="padding-right: 10px"
>
<template #actions>
<a>
<enter-outlined />
</a>
</template>
<a-list-item-meta :description="item.fullName">
<template #title>
<a>{{ item.name }}</a>
</template>
<template #avatar>
<a-avatar style="color: var(--text-color); background-color: transparent" :type="item.icon">
<template #icon>
<component :is="item.icon" />
</template>
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</div>
</a-card>
<div class="search-tips">
<span class="key">S</span>
<span class="tips">打开搜索面板</span>
<span class="key">
<arrow-up-outlined />
</span>
<span class="key">
<arrow-down-outlined />
</span>
<span class="tips">选择</span>
<span class="key">
<enter-outlined />
</span>
<span class="tips">确认</span>
<span class="key left">Esc</span>
<span class="tips">关闭</span>
</div>
</div>
</xn-form-container>
</template> </template>
<script> <script setup>
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { mapState } from 'pinia'
import { searchStore } from '@/store' import { searchStore } from '@/store'
import { useRouter, useRoute } from 'vue-router'
import hotkeys from 'hotkeys-js'
export default { const route = useRoute()
data() { const router = useRouter()
return { const searchText = ref('')
searchText: '', const cardIndex = ref(0)
cardIndex: 0, const results = ref([])
results: [] const search = searchStore()
} const inputRef = ref()
}, const cardListRef = ref()
computed: { const setActive = search.setActive
...mapState(searchStore, ['pool']), const toggleActive = search.toggleActive
// const pool = computed(() => {
resultsList() { return search.pool
return this.results.length === 0 || this.searchText === '' ? this.pool : this.results })
}, const searchActive = computed(() => {
// pool fuse return search.active
fuse() { })
return new Fuse(this.pool, { const searchHotkey = computed(() => {
shouldSort: true, // return search.hotkey
threshold: 0.6, // })
location: 0, // const mixinSearch = computed(() => {
distance: 100, // return mixinSearch
minMatchCharLength: 1, // })
keys: ['name', 'namePinyin', 'namePinyinFirst'] //
}) const resultsList = computed(() => {
} return results.value.length === 0 || searchText.value === '' ? pool.value : results.value
}, })
methods: { // pool fuse
// const fuse = computed(() => {
querySearch(e) { return new Fuse(pool.value, {
let queryString = e.target.value || '' shouldSort: true, //
const results = queryString && this.fuse.search(queryString).map((e) => e.item) threshold: 0.6, //
this.searchText = queryString location: 0, //
this.results = results distance: 100, //
}, minMatchCharLength: 1, //
// keys: ['name', 'namePinyin', 'namePinyinFirst']
focus() { })
this.searchText = '' })
setTimeout(() => { //
if (this.$refs.input) { const querySearch = (e) => {
this.$refs.input.focus() let queryString = e.target.value || ''
} const result = queryString && fuse.value.search(queryString).map((e) => e.item)
// searchText.value = queryString
this.searchText = '' results.value = result
this.results = [] }
}, 300) //
}, const focus = () => {
handleKeyEnter() { searchText.value = ''
let idx = this.cardIndex setTimeout(() => {
if (this.resultsList[idx]) { if (inputRef.value) {
this.handleSelect(this.resultsList[idx].fullPath) inputRef.value.focus()
}
},
handleKeyUp() {
this.handleKeyUpOrDown(true)
},
handleKeyDown() {
this.handleKeyUpOrDown(false)
},
handleKeyUpOrDown(up) {
let len = this.resultsList.length - 1
let idx = this.cardIndex
if (up) {
//
if (idx > 0) {
idx--
} else {
idx = len
}
} else {
//
if (idx < len) {
idx++
} else {
idx = 0
}
}
this.cardIndex = idx
if (this.$refs.cardList.getElementsByClassName('ant-list-item')[idx]) {
this.$refs.cardList.scrollTop = this.$refs.cardList.getElementsByClassName('ant-list-item')[idx].offsetTop
} else {
this.$refs.cardList.scrollTop = 0
}
},
onCardIn() {
this.$refs.input.activated = false
this.$refs.input.blur()
},
onCardOut() {
this.cardIndex = -1
},
onCardItemHover(index) {
this.cardIndex = index
},
//
handleSelect(path) {
//
if (path === this.$route.path) {
this.handleEsc()
return
}
this.$router.push({ path })
this.handleEsc()
},
//
closeSuggestion() {
if (this.$refs.input.activated) {
this.results = []
this.$refs.input.activated = false
}
},
//
handlePanelClick(e) {
if ('INPUT' !== e.target.tagName) {
this.handleEsc()
}
},
//
async handleEsc() {
this.closeSuggestion()
await this.$nextTick()
this.$emit('close')
} }
//
searchText.value = ''
results.value = []
}, 300)
}
const handleKeyEnter = () => {
let idx = cardIndex.value
if (resultsList.value[idx]) {
handleSelect(resultsList.value[idx].fullPath)
} }
} }
const handleKeyUp = () => {
handleKeyUpOrDown(true)
}
const handleKeyDown = () => {
handleKeyUpOrDown(false)
}
const handleKeyUpOrDown = (up) => {
let len = resultsList.value.length - 1
let idx = cardIndex.value
if (up) {
//
if (idx > 0) {
idx--
} else {
idx = len
}
} else {
//
if (idx < len) {
idx++
} else {
idx = 0
}
}
cardIndex.value = idx
if (cardListRef.value.getElementsByClassName('ant-list-item')[idx]) {
cardListRef.value.scrollTop = cardListRef.value.getElementsByClassName('ant-list-item')[idx].offsetTop
} else {
cardListRef.value.scrollTop = 0
}
}
const onCardIn = () => {
inputRef.value.activated = false
inputRef.value.blur()
}
const onCardOut = () => {
cardIndex.value = -1
}
const onCardItemHover = (index) => {
cardIndex.value = index
}
//
const handleSelect = (path) => {
//
if (path === route.path) {
searchPanelClose()
return
}
router.push({ path })
searchPanelClose()
}
//
const handlePanelClick = (e) => {
if ('INPUT' !== e.target.tagName) {
searchPanelClose()
}
}
//
const searchPanelOpen = () => {
if (!searchActive.value) {
setActive(true)
setTimeout(() => {
if (inputRef.value) {
inputRef.value.focus()
}
}, 300)
}
}
//
const searchPanelClose = () => {
if (searchActive.value) {
setActive(false)
}
results.value = []
if (inputRef.value.activated) {
inputRef.value.activated = false
}
}
onMounted(() => {
// [ ]
hotkeys(searchHotkey.value.open, (event) => {
event.preventDefault()
searchPanelOpen()
})
// [ ]
hotkeys(searchHotkey.value.close, (event) => {
event.preventDefault()
searchPanelClose()
})
})
//
hotkeys.unbind(searchHotkey.value.open)
hotkeys.unbind(searchHotkey.value.close)
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -249,7 +301,7 @@
} }
} }
.search-card { .search-card {
height: 220px; height: 380px;
overflow: hidden; overflow: hidden;
overflow-y: scroll; overflow-y: scroll;
} }

View File

@ -1,36 +1,34 @@
<template> <template>
<div class="d2-panel-search-item" :class="hoverMode ? 'can-hover' : ''" flex> <div class="d2-panel-search-item" :class="props.hoverMode ? 'can-hover' : ''" flex>
<div class="d2-panel-search-item__icon" flex-box="0"> <div class="d2-panel-search-item__icon" flex-box="0">
<div class="d2-panel-search-item__icon-box" flex="main:center cross:center"> <div class="d2-panel-search-item__icon-box" flex="main:center cross:center">
<a-icon v-if="item.icon" :type="item.icon" /> <a-icon v-if="props.item.icon" :type="props.item.icon" />
<a-icon v-else type="menu" /> <a-icon v-else type="menu" />
</div> </div>
</div> </div>
<div class="d2-panel-search-item__info" flex-box="1" flex="dir:top"> <div class="d2-panel-search-item__info" flex-box="1" flex="dir:top">
<div class="d2-panel-search-item__info-title" flex-box="1" flex="cross:center"> <div class="d2-panel-search-item__info-title" flex-box="1" flex="cross:center">
<span>{{ item.title }}</span> <span>{{ props.item.title }}</span>
</div> </div>
<div class="d2-panel-search-item__info-fullTitle" flex-box="0"> <div class="d2-panel-search-item__info-fullTitle" flex-box="0">
<span>{{ item.fullTitle }}</span> <span>{{ props.item.fullTitle }}</span>
</div> </div>
<div class="d2-panel-search-item__info-path" flex-box="0"> <div class="d2-panel-search-item__info-path" flex-box="0">
<span>{{ item.path }}</span> <span>{{ props.item.path }}</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
props: { item: {
item: { default: () => ({})
default: () => ({}) },
}, hoverMode: {
hoverMode: { default: false
default: false
}
} }
} })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,41 +0,0 @@
<template>
<a-modal
v-model:visible="visible"
title="修改密码"
:mask-closable="false"
:width="800"
:destroy-on-close="true"
@ok="handleOk"
@cancel="handleCancel"
>
</a-modal>
</template>
<script>
export default {
data() {
return {
visible: false
}
},
methods: {
//
showUpdPwdModal(value) {
this.visible = true
},
// icon
radioGroupChange(e) {
this.iconItemDefault = e.target.value
},
//
handleOk() {
this.visible = false
this.$emit('updPwdCallBack')
},
handleCancel() {
this.visible = false
this.$emit('updPwdCallBack')
}
}
}
</script>

View File

@ -1,12 +1,11 @@
<template> <template>
<div class="user-bar"> <div class="user-bar">
<div v-if="!ismobile" class="search panel-item hidden-sm-and-down" @click="handleSearchClick"> <!-- 搜索面板 -->
<search-outlined /> <panel-search v-if="!isMobile" />
</div> <div v-if="!isMobile" class="screen panel-item hidden-sm-and-down" @click="fullscreen">
<div v-if="!ismobile" class="screen panel-item hidden-sm-and-down" @click="fullscreen">
<fullscreen-outlined /> <fullscreen-outlined />
</div> </div>
<devUserMessage /> <dev-user-message />
<a-dropdown class="user panel-item"> <a-dropdown class="user panel-item">
<div class="user-avatar"> <div class="user-avatar">
<a-avatar :src="userInfo.avatar" /> <a-avatar :src="userInfo.avatar" />
@ -30,7 +29,7 @@
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<a-dropdown v-if="!ismobile" class="panel-item"> <a-dropdown v-if="!isMobile" class="panel-item">
<global-outlined /> <global-outlined />
<template #overlay> <template #overlay>
<a-menu :selected-keys="lang"> <a-menu :selected-keys="lang">
@ -43,7 +42,7 @@
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<div v-if="setDeawer === 'true'" class="setting panel-item" @click="openSetting"> <div v-if="setDrawer === 'true'" class="setting panel-item" @click="openSetting">
<layout-outlined /> <layout-outlined />
</div> </div>
</div> </div>
@ -52,141 +51,109 @@
<a-drawer v-model:visible="settingDialog" :closable="false" width="300"> <a-drawer v-model:visible="settingDialog" :closable="false" width="300">
<setting /> <setting />
</a-drawer> </a-drawer>
<!-- 搜索面板 -->
<xn-form-container
title="搜索"
:visible="searchActive"
:closable="false"
:footer="null"
:width="600"
destroyOnClose
dialogClass="searchModal"
:bodyStyle="{ maxHeight: '520px', overflow: 'auto', padding: '14px' }"
@close="searchPanelClose"
>
<panel-search ref="panelSearch" @close="searchPanelClose" />
</xn-form-container>
</template> </template>
<script> <script setup name="layoutUserBar">
import { createVNode } from 'vue' import { createVNode } from 'vue'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue' import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import screenfull from 'screenfull' import { Modal } from 'ant-design-vue'
import i18n from '@/locales/index'
import screenFull from 'screenfull'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import setting from './setting.vue' import Setting from './setting.vue'
import router from '@/router' import router from '@/router'
import tool from '@/utils/tool' import tool from '@/utils/tool'
import config from '@/config/index'
import loginApi from '@/api/auth/loginApi' import loginApi from '@/api/auth/loginApi'
import devUserMessage from './message.vue' import DevUserMessage from './message.vue'
import panelSearch from './panel-search/index.vue' import PanelSearch from './panel-search/index.vue'
import mixinSearch from './mixins/search'
import { mapState } from 'pinia'
import { globalStore } from '@/store' import { globalStore } from '@/store'
export default {
components: {
setting,
devUserMessage,
panelSearch
},
mixins: [mixinSearch],
data() {
return {
lang: [],
settingDialog: false,
userName: '',
userNameF: '',
setDeawer: import.meta.env.VITE_SET_DRAWER
}
},
computed: {
...mapState(globalStore, ['ismobile', 'userInfo'])
},
created() { const lang = ref(new Array(tool.data.get('APP_LANG') || config.LANG))
// const settingDialog = ref(false)
this.lang = new Array(this.$TOOL.data.get('APP_LANG') || this.$CONFIG.LANG) const setDrawer = ref(import.meta.env.VITE_SET_DRAWER)
this.userName = this.userInfo?.userName || '' const store = globalStore()
this.userNameF = this.userName.substring(0, 1) const isMobile = computed(() => {
}, return store.ismobile
methods: { })
// const userInfo = computed(() => {
handleUser(key) { return store.userInfo
if (key === 'uc') { })
router.push({ path: '/usercenter' }) const userName = ref(userInfo.value?.userName || '')
}
if (key === 'clearCache') { //
this.$confirm({ const handleUser = (key) => {
title: '提示', if (key === 'uc') {
content: '确认清理所有缓存?', router.push({ path: '/usercenter' })
icon: createVNode(ExclamationCircleOutlined), }
maskClosable: false, if (key === 'clearCache') {
okText: '确定', Modal.confirm({
cancelText: '取消', title: '提示',
onOk() { content: '确认清理所有缓存?',
message.loading('正在清理中...', 1) icon: createVNode(ExclamationCircleOutlined),
maskClosable: false,
okText: '确定',
cancelText: '取消',
onOk() {
message.loading('正在清理中...', 1)
tool.data.clear()
setTimeout(() => {
router.replace({ path: '/login' })
location.reload()
}, 100)
},
onCancel() {}
})
}
if (key === 'outLogin') {
Modal.confirm({
title: '提示',
content: '确认退出当前用户?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: false,
onOk() {
// token
const token = tool.data.get('TOKEN')
const param = {
token: token
}
message.loading('退出中...', 1)
loginApi
.logout(param)
.then(() => {
//
tool.data.remove('TOKEN')
tool.data.remove('USER_INFO')
tool.data.remove('MENU')
tool.data.remove('PERMISSIONS')
router.replace({ path: '/login' })
})
.catch(() => {
tool.data.clear() tool.data.clear()
setTimeout(() => { router.replace({ path: '/login' })
router.replace({ path: '/login' }) location.reload()
location.reload() })
}, 100) },
}, onCancel() {}
onCancel() {} })
}) }
} }
if (key === 'outLogin') { //
this.$confirm({ const handleIn18 = (key) => {
title: '提示', lang.value = []
content: '确认退出当前用户?', lang.value.push(key)
icon: createVNode(ExclamationCircleOutlined), i18n.locale = key
maskClosable: false, tool.data.set('APP_LANG', key)
onOk() { }
// token //
const token = tool.data.get('TOKEN') const openSetting = () => {
const param = { settingDialog.value = true
token: token }
} //
message.loading('退出中...', 1) const fullscreen = () => {
loginApi const element = document.documentElement
.logout(param) if (screenFull.isEnabled) {
.then(() => { screenFull.toggle(element)
// message.c
//
tool.data.remove('TOKEN')
tool.data.remove('USER_INFO')
tool.data.remove('MENU')
tool.data.remove('PERMISSIONS')
router.replace({ path: '/login' })
})
.catch(() => {
tool.data.clear()
router.replace({ path: '/login' })
location.reload()
})
},
onCancel() {}
})
}
},
//
handleIn18(key) {
this.lang = []
this.lang.push(key)
this.$i18n.locale = key
this.$TOOL.data.set('APP_LANG', key)
},
//
openSetting() {
this.settingDialog = true
},
//
fullscreen() {
const element = document.documentElement
if (screenfull.isEnabled) {
screenfull.toggle(element)
}
},
//
fullSearch() {}
} }
} }
</script> </script>

View File

@ -77,7 +77,9 @@
const global_store = globalStore() const global_store = globalStore()
const userInfo = ref(tool.data.get('USER_INFO')) const userInfo = computed(() => {
return global_store.userInfo
})
const cropUpload = ref() const cropUpload = ref()
const avatarLoading = ref(false) const avatarLoading = ref(false)
const uploadLogo = () => { const uploadLogo = () => {

View File

@ -39,15 +39,14 @@
import { required } from '@/utils/formRules' import { required } from '@/utils/formRules'
import userCenterApi from '@/api/sys/userCenterApi' import userCenterApi from '@/api/sys/userCenterApi'
import tool from '@/utils/tool' import tool from '@/utils/tool'
import { cloneDeep } from 'lodash-es'
import { globalStore } from '@/store' import { globalStore } from '@/store'
const store = globalStore() const store = globalStore()
const formRef = ref() const formRef = ref()
//
const userInfo = tool.data.get('USER_INFO')
let formData = ref({}) let formData = ref({})
formData.value = userInfo formData.value = cloneDeep(store.userInfo)
const submitLoading = ref(false) const submitLoading = ref(false)
// //
const formRules = { const formRules = {
@ -64,7 +63,7 @@
userCenterApi.userUpdateUserInfo(formData.value).then(() => { userCenterApi.userUpdateUserInfo(formData.value).then(() => {
submitLoading.value = false submitLoading.value = false
// //
store.setUserInfo(formData.value) store.setUserInfo(cloneDeep(formData.value))
tool.data.set('USER_INFO', formData.value) tool.data.set('USER_INFO', formData.value)
}) })
}) })