搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View 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>

View 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>