wms-antdvue/.svn/pristine/31/31e4c54600797da2b8945a1e7c119e520e48c0b6.svn-base
2024-11-07 16:33:03 +08:00

429 lines
10 KiB
Plaintext

<template>
<basicModal
@register="register"
:bodyStyle="getBodyStyle"
ref="modalRef"
wrapClassName="app-search"
>
<div class="app-search-card">
<div class="app-search-card-input">
<a-input
ref="searchInputRef"
v-model:value="searchKeyword"
:loading="loading"
allowClear
placeholder="请输入关键词搜索"
size="large"
@input="handleSearch"
>
<template #prefix>
<span v-if="loading"><LoadingOutlined /></span>
<span v-else><SearchOutlined /></span>
</template>
</a-input>
</div>
<div class="app-search-card-result" :class="{ 'search-result-dark': settingStore.darkTheme }">
<div v-if="!loading && !searchResult.length" class="no-result">
<a-empty v-if="!loading" :image="simpleImage" />
<a-spin v-else size="small" />
</div>
<div v-else-if="loading" class="no-result">
<a-spin size="small" />
</div>
<ul v-else class="result-ul">
<li
v-for="(item, index) in searchResult"
:key="item.key"
:class="{ 'result-ul-li-on': index === activeIndex }"
:data-index="index"
class="result-ul-li"
@click="handleEnter"
@mouseenter="handleMouseenter"
>
<a href="javascript:;">
<div class="result-ul-li-icon">
<InteractionOutlined />
</div>
<div class="result-ul-li-content"> {{ item.title }}</div>
<div class="result-ul-li-action">
<EnterOutlined />
</div>
</a>
</li>
</ul>
</div>
</div>
<template #footer>
<div class="app-search-card-footer">
<ul class="commands">
<li>
<span class="commands-icon">
<EnterOutlined />
</span>
<span>确认</span>
</li>
<li>
<span class="mr-2 commands-icon">
<ArrowUpOutlined />
</span>
<span class="commands-icon">
<ArrowDownOutlined />
</span>
<span>切换</span>
</li>
<li>
<span class="commands-icon">
<CloseOutlined />
</span>
<span>ESC关闭</span>
</li>
</ul>
</div>
</template>
</basicModal>
</template>
<script lang="ts" setup>
import type { Menu } from '@/router/types';
import { ref, unref, onBeforeMount, nextTick, computed } from 'vue';
import { basicModal, useModal } from '@/components/Modal';
import {
EnterOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
CloseOutlined,
InteractionOutlined,
LoadingOutlined,
SearchOutlined,
} from '@ant-design/icons-vue';
import { onKeyStroke, useDebounceFn } from '@vueuse/core';
import { getMenus } from '@/router/menus';
import { filter } from '@/utils/helper/treeHelper';
import { useGo } from '@/hooks/web/usePage';
import { Empty } from 'ant-design-vue';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
const loading = ref(false);
const searchInputRef = ref();
const searchKeyword = ref('');
const searchResult = ref<SearchResult[]>([]);
const activeIndex = ref(-1);
let menuList: Menu[] = [];
const go = useGo();
const settingStore = useProjectSettingStore();
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
interface SearchResult {
title: string;
path: string;
icon?: string;
key: string;
}
const [register, { openModal, closeModal }] = useModal({
title: '搜索页面',
width: 608,
wrapClassName: 'app-search',
keyboard: false,
});
const getBodyStyle = computed(() => {
return {
background: settingStore.darkTheme ? 'rgb(31,31,31)' : 'rgb(239, 239, 245)',
};
});
onBeforeMount(async () => {
const list = await getMenus();
menuList = list;
});
function show() {
openModal();
nextTick(() => {
searchInputRef.value.focus();
});
}
function hide() {
closeModal();
}
const handleSearch = useDebounceFn(search, 200);
function search() {
loading.value = true;
searchKeyword.value = searchKeyword.value.trim();
if (!searchKeyword.value) {
searchResult.value = [];
loading.value = false;
return;
}
const reg = createSearchReg(unref(searchKeyword));
const filterMenu = filter(menuList, (item) => {
return reg.test(item.title) && !item.meta?.hidden;
});
searchResult.value = handlerSearchResult(filterMenu, reg);
activeIndex.value = 0;
nextTick(() => {
loading.value = false;
});
}
function handlerSearchResult(filterMenu: Menu[], reg: RegExp, parent?: Menu) {
const ret: SearchResult[] = [];
filterMenu.forEach((item) => {
const { title, path, key, icon, children, meta } = item;
if (!meta?.hidden && reg.test(title) && !children?.length) {
ret.push({
title: parent?.title ? `${parent.title} > ${title}` : title,
path: path?path:parent?.path,
icon,
key,
});
}
if (!meta?.hideChildrenInMenu && Array.isArray(children) && children.length) {
ret.push(...handlerSearchResult(children as any, reg, item));
}
});
return ret;
}
// Translate special characters
function transform(c: string) {
const code: string[] = ['$', '(', ')', '*', '+', '.', '[', ']', '?', '\\', '^', '{', '}', '|'];
return code.includes(c) ? `\\${c}` : c;
}
function createSearchReg(key: string) {
const keys = [...key].map((item) => transform(item));
const str = ['', ...keys, ''].join('.*');
return new RegExp(str);
}
function handleMouseenter(e: any) {
const index = e.target.dataset.index;
activeIndex.value = Number(index);
}
function handleClose() {
searchResult.value = [];
searchKeyword.value = '';
hide();
}
async function handleEnter() {
if (!searchResult.value.length) {
return;
}
const result = unref(searchResult);
const index = unref(activeIndex);
if (result.length === 0 || index < 0) {
return;
}
const to = result[index];
handleClose();
await nextTick();
go(to.path);
}
// 按方向上键
function handleUp() {
if (!searchResult.value.length) return;
activeIndex.value--;
if (activeIndex.value < 0) {
activeIndex.value = searchResult.value.length - 1;
}
}
// 按方向下键
function handleDown() {
if (!searchResult.value.length) return;
activeIndex.value++;
if (activeIndex.value > searchResult.value.length - 1) {
activeIndex.value = 0;
}
}
// 回车搜索
onKeyStroke('Enter', handleEnter);
// 按方向上键
onKeyStroke('ArrowUp', handleUp);
// 按方向下键
onKeyStroke('ArrowDown', handleDown);
// 键盘 esc 取消弹窗
onKeyStroke('Escape', handleClose);
defineExpose({
show,
});
</script>
<style lang="less">
.app-search .ant-modal .ant-modal-content .ant-modal-footer {
padding: 2px 0;
}
</style>
<style lang="less" scoped>
.app-search {
position: fixed;
top: 60px;
left: 50%;
margin-left: -235px;
&-card {
width: 100%;
padding: 0;
background: var(--color);
box-shadow: var(--box-shadow);
&-input {
margin-top: 6px;
}
:deep(.n-card .n-card__footer) {
padding: 0;
}
&-result {
.no-result {
font-size: 0.9em;
margin: 0 auto;
padding: 36px 0;
text-align: center;
width: 80%;
p {
color: #969faf;
}
}
.result-ul {
list-style: none;
margin: 14px 0 0 0;
padding: 0;
:deep(.n-scrollbar .n-scrollbar-container .n-scrollbar-content) {
max-height: 640px;
}
&-li {
border-radius: 4px;
display: flex;
padding-bottom: 8px;
position: relative;
a {
display: flex;
align-items: center;
background: #fff;
border-radius: 4px;
padding: 0 12px;
width: 100%;
color: rgb(51, 54, 57);
border-bottom: 1px solid var(--border-color);
.n-icon {
color: #969faf;
}
}
&-content {
align-items: center;
color: var(--text-color);
display: flex;
flex-direction: row;
height: 56px;
padding-left: 6px;
padding-right: 12px;
flex: 1;
box-sizing: border-box;
}
&-icon {
font-size: 18px;
}
&-on {
a {
background-color: #2d8cf0!important;
color: #fff;
.n-icon {
color: #fff;
}
.result-ul-li-content {
color: #fff;
}
}
}
}
}
}
.search-result-dark {
.result-ul {
&-li {
a {
background: #282828;
color: #b3b3b3;
}
}
}
}
&-footer {
align-items: center;
background: var(--color);
border-radius: 0 0 8px 8px;
box-shadow: var(--box-shadow);
display: flex;
flex-shrink: 0;
height: 44px;
padding: 0 14px;
position: relative;
user-select: none;
width: 100%;
.commands {
display: flex;
align-items: center;
li {
display: flex;
align-items: center;
margin-right: 14px;
padding-left: 1px;
span {
color: #969faf;
}
}
&-icon {
align-items: center;
background: linear-gradient(-225deg, var(--color), var(--color));
border-radius: 2px;
box-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px rgba(30, 35, 90, 0.4);
display: flex;
height: 18px;
justify-content: center;
margin-right: 0.4em;
padding-bottom: 2px;
width: 20px;
}
}
}
}
}
.light-item-bg {
background: rgb(239, 239, 245);
}
</style>