搜索结果列表页面开发
This commit is contained in:
140
src/components/FileSearch/Modal.vue
Normal file
140
src/components/FileSearch/Modal.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<d-modal
|
||||
v-model="visible"
|
||||
class="file-search-modal"
|
||||
:show-close="false"
|
||||
:style="{ width: '800px', height: '400px' }"
|
||||
close-on-click-overlay
|
||||
@close="handleClose"
|
||||
>
|
||||
<d-input ref="searchInput" placeholder="请输入关键词" suffix="search" clearable @input="handleSearch" />
|
||||
<div class="file-search-modal-list mt-4" ref="fileListRef" v-loading="loading">
|
||||
<gc-option
|
||||
v-for="(item, index) in fileList"
|
||||
:key="item"
|
||||
class="file-search-modal-list-item"
|
||||
:class="{active: index === searchIndex}"
|
||||
@click="handleFileSelect(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</gc-option>
|
||||
</div>
|
||||
<template #header></template>
|
||||
<template #footer></template>
|
||||
</d-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
const props = defineProps<{
|
||||
list: Array<string>,
|
||||
loading?: boolean
|
||||
}>();
|
||||
|
||||
const fileList = ref([]); // 过滤后的文件列表
|
||||
watch(() => props.list, () => {
|
||||
fileList.value = props.list;
|
||||
});
|
||||
const fileFilter = (queryStr = '') => {
|
||||
queryStr = queryStr?.trim().toLowerCase();
|
||||
fileList.value = props.list.filter(item => item?.toLowerCase()?.includes(queryStr));
|
||||
};
|
||||
|
||||
const emit = defineEmits(['onSelect', 'onFocus']);
|
||||
// 前端加载全量文件,前端完成检索逻辑
|
||||
const handleFocus = async(forceLoad?: boolean) => {
|
||||
emit('onFocus', typeof forceLoad === 'boolean' ? forceLoad : false);
|
||||
};
|
||||
|
||||
const handleSearch = query => {
|
||||
resetSearchIndex();
|
||||
fileFilter(query);
|
||||
};
|
||||
|
||||
const resetFilter = () => {
|
||||
fileFilter();
|
||||
fileListRef.value?.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetFilter();
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleFileSelect = file => {
|
||||
handleClose();
|
||||
|
||||
emit('onSelect', file);
|
||||
};
|
||||
|
||||
const searchIndex = ref(0);
|
||||
const resetSearchIndex = () => {
|
||||
searchIndex.value = 0;
|
||||
};
|
||||
|
||||
const visible = ref(false);
|
||||
const searchInput = ref(null);
|
||||
const fileListRef = ref(null);
|
||||
|
||||
// 注册页面监听ctrl + p
|
||||
useEventListener(document, 'keydown', e => {
|
||||
if (!visible.value) {
|
||||
if (e?.key === 'p' && e.ctrlKey) {
|
||||
visible.value = true;
|
||||
resetSearchIndex();
|
||||
|
||||
// 打开modal触发onFocus加载数据
|
||||
handleFocus();
|
||||
|
||||
setTimeout(() => {
|
||||
searchInput.value?.focus();
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
} else {
|
||||
if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
||||
e.key === 'ArrowDown' ? (searchIndex.value++) : (searchIndex.value--);
|
||||
|
||||
if (searchIndex.value < 0) {
|
||||
resetSearchIndex();
|
||||
} else if (searchIndex.value >= fileList.value.length) {
|
||||
searchIndex.value = fileList.value.length - 1;
|
||||
}
|
||||
|
||||
fileListRef.value?.querySelector('.active')?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
} else if (e.key === 'Enter') {
|
||||
const curFile = fileList.value[searchIndex.value];
|
||||
curFile && handleFileSelect(curFile);
|
||||
|
||||
visible.value = false;
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-search-modal {
|
||||
width: 800px;
|
||||
min-height: 400px;
|
||||
&-list {
|
||||
min-height: 300px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
:deep(.devui-loading__mask) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
src/components/FileSearch/index.vue
Normal file
129
src/components/FileSearch/index.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<d-select
|
||||
ref="fileSearchRef"
|
||||
v-model="fileSearchKey"
|
||||
allow-clear
|
||||
:filter="fileFilter"
|
||||
:loading="loading"
|
||||
remote
|
||||
placeholder="搜索文件名、后缀ctrl+p"
|
||||
@focus="handleFocus"
|
||||
@toggle-change="handleToggleChange"
|
||||
>
|
||||
<div class="g-file-search-list" ref="fileListRef">
|
||||
<Option
|
||||
v-for="(item, index) in fileList"
|
||||
:key="item"
|
||||
class="g-file-search-list-item"
|
||||
:class="{active: searchIndex === index}"
|
||||
:value="item"
|
||||
@click="handleFileSelect(item)"
|
||||
>
|
||||
<div>{{ item }}</div>
|
||||
</Option>
|
||||
</div>
|
||||
<template #empty>
|
||||
<div v-if="loading" class="flex-center py-2 opacity-50">
|
||||
数据加载中...
|
||||
</div>
|
||||
<div v-else-if="!query && !fileList?.length" class="flex-center py-2 opacity-50">
|
||||
找不到相关记录
|
||||
</div>
|
||||
</template>
|
||||
</d-select>
|
||||
<SearchModal v-bind="$props" @onFocus="handleFocus" @onSelect="handleFileSelect" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { Option } from 'vue-devui/select';
|
||||
|
||||
import SearchModal from './Modal.vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
const props = defineProps<{
|
||||
list: Array<string>,
|
||||
loading?: boolean
|
||||
}>();
|
||||
|
||||
const fileSearchKey = ref('');
|
||||
const fileList = ref(props.list); // 过滤后的文件列表
|
||||
watch(() => props.list, () => {
|
||||
fileList.value = props.list;
|
||||
});
|
||||
const fileFilter = (queryStr = '') => {
|
||||
setSearchIndex(0);
|
||||
|
||||
queryStr = queryStr?.trim().toLowerCase();
|
||||
fileList.value = props.list.filter(item => item?.toLowerCase()?.includes(queryStr));
|
||||
};
|
||||
|
||||
const emit = defineEmits(['onSelect', 'onFocus']);
|
||||
const fileSearchRef = ref(null);
|
||||
const dropdown = ref(false);
|
||||
const handleFileSelect = file => {
|
||||
dropdown.value = false;
|
||||
fileSearchKey.value = '';
|
||||
fileSearchRef.value.blur();
|
||||
|
||||
setTimeout(() => {
|
||||
fileSearchRef.value.toggleChange(false);
|
||||
emit('onSelect', file);
|
||||
});
|
||||
};
|
||||
// 前端加载全量文件,前端完成检索逻辑
|
||||
const handleFocus = async(forceLoad?: boolean) => {
|
||||
dropdown.value = true;
|
||||
|
||||
emit('onFocus', typeof forceLoad === 'boolean' ? forceLoad : false);
|
||||
};
|
||||
const handleToggleChange = isShow => {
|
||||
dropdown.value = isShow;
|
||||
};
|
||||
|
||||
// 注册页面监听
|
||||
const searchIndex = ref(0);
|
||||
const setSearchIndex = index => {
|
||||
searchIndex.value = index;
|
||||
if (index >= fileList.value.length) {
|
||||
searchIndex.value = fileList.value.length - 1;
|
||||
}
|
||||
if (index < 0) {
|
||||
searchIndex.value = 0;
|
||||
}
|
||||
|
||||
fileListRef.value?.querySelector('.active')?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
};
|
||||
|
||||
const fileListRef = ref(null);
|
||||
useEventListener(document, 'keydown', e => {
|
||||
if (dropdown.value) {
|
||||
if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
||||
const index = e.key === 'ArrowDown' ? (searchIndex.value + 1) : (searchIndex.value - 1);
|
||||
setSearchIndex(index);
|
||||
|
||||
e.stopPropagation();
|
||||
} else if (e.key === 'Enter') {
|
||||
const curFile = fileList.value[searchIndex.value];
|
||||
curFile && handleFileSelect(curFile);
|
||||
|
||||
dropdown.value = false;
|
||||
setSearchIndex(0);
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.g-file-search-list {
|
||||
&-item {
|
||||
&.active {
|
||||
background-color: var(--color-border-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user