Merge pull request #415 from jamebal/dev

feat: 搜索选项新增精准搜索
This commit is contained in:
jmal 2025-06-04 14:52:10 +08:00 committed by GitHub
commit 8e214abb40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 381 additions and 98 deletions

View File

@ -153,6 +153,14 @@ export default {
params
})
},
// 删除所有搜索记录🔍
removeAllSearchHistory: function(params) {
return request({
url: '/search/deleteAllSearchHistory',
method: 'delete',
params
})
},
// 搜索文件🔍
searchFile: function(params) {
return request({

View File

@ -454,8 +454,7 @@
},
loadSvg(svgName){
if(svgName){
let src = require(`@/icons/svg/${svgName}.svg`)
return `<svg class="tree-icon">${src.default.render()}<use xlink:href="#icon-${svgName}"/></svg>`
return `<svg class="tree-icon"><use xlink:href="#icon-${svgName}"/></svg>`
}
},
lazyLoad(event, data){

View File

@ -18,15 +18,57 @@
<span>个结果</span>
</div>
</div>
<div class="search-option-scope">
<div class="search-option-scope-title">
查找范围:
</div>
<div class="content-scope">
<div>
<el-checkbox
v-model="includeTagName"
class="search-option-checkbox"
@change="handleCommonChange"
>标签名称
</el-checkbox>
</div>
<div>
<el-checkbox
v-model="includeFileName"
class="search-option-checkbox"
@change="handleCommonChange"
>文件名称
</el-checkbox>
</div>
<div>
<el-checkbox
v-model="includeFileContent"
class="search-option-checkbox"
@change="handleCommonChange"
>文件内容
</el-checkbox>
</div>
</div>
</div>
<div class="file-search-option">
<div class="search-bar">
<div class="search-input-wrapper">
<div class="search-filters">
<div>
<el-checkbox
v-if="showExactSearch"
v-model="exactSearch"
class="search-option-checkbox"
@change="handleCommonChange"
>精准搜索
</el-checkbox>
</div>
<div>
<el-checkbox
v-model="searchOverall"
class="search-option-checkbox"
@change="handleSearchOverallChange"
@change="handleCommonChange"
>全盘搜索
</el-checkbox>
</div>
@ -34,7 +76,7 @@
<el-checkbox
v-model="searchMount"
class="search-option-checkbox"
@change="handleSearchMountChange"
@change="handleCommonChange"
>挂载目录
</el-checkbox>
</div>
@ -142,6 +184,13 @@
全盘搜索
</el-tag>
</div>
<div v-if="exactSearch">
<el-tag
closable
@close="resetExactSearch">
精准搜索
</el-tag>
</div>
<div v-if="searchMount">
<el-tag
closable
@ -217,10 +266,18 @@ export default {
fileTypeText: '全部文件',
searchMount: false,
searchOverall: false,
filterOption: this.emptyFilterOption
exactSearch: false,
filterOption: this.emptyFilterOption,
checkList: ['标签名称', '文件名称', '文件内容'],
includeTagName: true,
includeFileName: true,
includeFileContent: true,
}
},
computed: {
showExactSearch() {
return this.$store.getters.exactSearch
},
searchScope() {
const queryTagId = this.$route.query.tagId
const isRootPath = this.$route.query.path === '/';
@ -251,7 +308,7 @@ export default {
return ''
},
hasSearchConditions() {
return this.fileType !== 'all' || this.timeRange || this.hasSizeRange || this.searchMount || this.searchOverall
return this.fileType !== 'all' || this.timeRange || this.hasSizeRange || this.searchMount || this.searchOverall || this.exactSearch
},
emptyFilterOption() {
return {
@ -260,8 +317,12 @@ export default {
modifyEnd: null,
sizeMin: null,
sizeMax: null,
searchMount: false,
searchOverall: false,
searchMount: null,
searchOverall: null,
exactSearch: null,
includeTagName: true,
includeFileName: true,
includeFileContent: true
}
},
hasSizeRange () {
@ -306,13 +367,18 @@ export default {
modifyEnd: this.timeRange ? new Date(this.timeRange[1]).getTime() : null,
sizeMin: this.hasSizeRange ? this.sizeRange[0] : null,
sizeMax: this.hasSizeRange ? this.sizeRange[1] : null,
searchMount: this.searchMount,
searchOverall: this.searchOverall
searchMount: this.searchMount ? true : null,
searchOverall: this.searchOverall ? true : null,
exactSearch: this.exactSearch ? true : null,
includeTagName: this.includeTagName,
includeFileName: this.includeFileName,
includeFileContent: this.includeFileContent,
}
this.$emit('filter-change')
this.$emit('update:filter-option-param', this.filterOption)
this.$emit('update:has-search-conditions-param', this.hasSearchConditions)
localStorage.setItem('searchFilterOption', JSON.stringify(this.filterOption))
},
@ -347,6 +413,10 @@ export default {
// 设置搜索选项
this.searchMount = !!filterOption.searchMount
this.searchOverall = !!filterOption.searchOverall
this.exactSearch = !!filterOption.exactSearch
this.includeTagName = filterOption.includeTagName === undefined ? true : filterOption.includeTagName
this.includeFileName = filterOption.includeFileName === undefined ? true : filterOption.includeFileName
this.includeFileContent = filterOption.includeFileContent === undefined ? true : filterOption.includeFileContent
// 更新过滤条件
this.updateFilterOption()
@ -406,6 +476,11 @@ export default {
this.updateFilterOption()
},
resetExactSearch() {
this.exactSearch = false
this.updateFilterOption()
},
resetFileType() {
this.handleFileType('all')
},
@ -475,11 +550,7 @@ export default {
this.timeRange = [start, end]
this.updateFilterOption()
},
handleSearchMountChange() {
this.updateFilterOption()
},
handleSearchOverallChange() {
handleCommonChange() {
this.updateFilterOption()
}
}
@ -529,6 +600,33 @@ export default {
}
}
.search-option-scope {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
.search-option-scope-title {
flex: 1 1;
padding: 0 25px;
color: #303133;
font-weight: 500;
}
.content-scope {
flex: 1 1;
display: flex;
align-items: center;
width: 100%;
.search-option-checkbox {
padding: 8px 12px;
cursor: pointer;
}
}
}
.file-search-option {
display: flex;

View File

@ -161,16 +161,15 @@
ref="searchInput"
:class="searchInputClass"
v-show="showSearchButton"
:fetch-suggestions="queryRecentlySearchHistory"
:fetch-suggestions="fetchDynamicSuggestions"
hide-loading
:debounce="300"
:popper-append-to-body="false"
:placeholder="`搜索 ${cmdKey} P`"
v-model="searchFileName"
@keyup.enter.native="searchFileEnter(searchFileName)"
@keyup.enter.native="searchFileEnter"
@focus="searchInputFocus"
@blur="searchInputBlur"
@select="handleSelectSearchHistory"
@select="handleSelectSuggestion"
>
<template v-slot:suffix>
<div>
@ -201,15 +200,62 @@
<!-- 自定义提示项 -->
<template #default="{ item }" class="autocomplete-suggestion-custom">
<div class="suggestion-item">
<!-- 固定操作项:当前路径搜索 -->
<div v-if="item.sugType === 'action_current_path_search'" class="suggestion-item action-item">
<div class="suggestion-action-item-title">
<i class="el-icon-search"></i>
<div class="suggestion-action-item-text">
<span class="suggestion-item-keyword">{{ item.keyword }}</span>
<span class="suggestion-item-hint">当前路径 或按 Enter</span>
</div>
</div>
</div>
<!-- 固定操作项:全盘搜索 -->
<div v-else-if="item.sugType === 'action_global_search'" class="suggestion-item action-item">
<div class="suggestion-action-item-title">
<i class="el-icon-search"></i>
<div class="suggestion-action-item-text">
<span class="suggestion-item-keyword">{{ item.keyword }}</span>
<span class="suggestion-item-hint">全盘搜索</span>
</div>
</div>
</div>
<div v-else-if="item.sugType === 'clearAllHistory'" class="suggestion-item recently-item">
<span class="suggestion-item-text">最近搜索</span>
<div>
<el-button
round
type="text"
class="clear-search-history-btn"
@click.stop.prevent="clearAllSearchHistory($event)"
>清除所有</el-button>
</div>
</div>
<!-- 历史记录项 -->
<div v-else-if="item.sugType === 'history'" class="suggestion-item history-item">
<div class="suggestion-item-title">
<i class="el-icon-time"></i>
<span class="suggestion-item-text">{{ item.keyword }}</span>
</div>
<div>
<el-button round type="text" icon="el-icon-close" class="clear-search-history-btn" @click.native.prevent="clearSearchHistory(item, $event)"></el-button>
<el-button
round
type="text"
icon="el-icon-close"
class="clear-search-history-btn"
@click.stop.prevent="clearSearchHistoryForItem(item, $event)"
></el-button>
</div>
</div>
<!-- 默认显示 -->
<div v-else class="suggestion-item">
<span class="suggestion-item-text">{{ item.value }}</span>
</div>
</template>
</el-autocomplete>
@ -847,41 +893,41 @@
</template>
<script>
import SearchOption from '@/components/SearchOption/index.vue'
import DialogFileList from '@/components/ShowFile/DialogFileList.vue'
import FileClipboard from '@/components/ShowFile/FileClipboard.vue'
import store from '@/store'
import path from 'path'
import { fileOperations } from '@/utils/file-operations'
import { mapGetters, mapState } from 'vuex'
import { formatSize, formatTime } from '@/utils/number'
import { getElementToPageLeft } from '@/utils/dom'
import { suffix } from '@/utils/file-type'
import { formatExif,formatVideo } from '@/utils/media'
import api from '@/api/file-api'
import BreadcrumbFilePath from '@/components/Breadcrumb/BreadcrumbFilePath'
import IconFile from '@/components/Icon/IconFile'
import EmptyFile from '@/components/EmptyFile'
import SimTextPreview from '@/components/preview/SimTextPreview'
import ImageViewer from '@/components/preview/ImageViewer'
import VideoPreview from '@/components/preview/VideoPreview'
import AudioPreview from '@/components/preview/AudioPreview'
import ButtonUpload from '@/components/button/ButtonUpload'
import MessageDialog from '@/components/message/MessageDialog'
import EmptyFile from '@/components/EmptyFile'
import FileTree from '@/components/FileTree'
import '@/utils/directives.js'
import _ from "lodash";
import IconFile from '@/components/Icon/IconFile'
import MessageDialog from '@/components/message/MessageDialog'
import AudioPreview from '@/components/preview/AudioPreview'
import FileDetails from '@/components/preview/FileDetails.vue'
import IframePreview from '@/components/preview/IframePreview.vue'
import ImageViewer from '@/components/preview/ImageViewer'
import SimTextPreview from '@/components/preview/SimTextPreview'
import VideoPreview from '@/components/preview/VideoPreview'
import SearchOption from '@/components/SearchOption/index.vue'
import ShareDialog from '@/components/ShareDialog/index.vue'
import DialogFileList from '@/components/ShowFile/DialogFileList.vue'
import FileClipboard from '@/components/ShowFile/FileClipboard.vue'
import TagDialog from '@/components/TagDialog/index.vue'
import store from '@/store'
import { getElementToPageLeft } from '@/utils/dom'
import fileConfig from '@/utils/file-config'
import { fileOperations } from '@/utils/file-operations'
import { suffix } from '@/utils/file-type'
import { formatExif, formatVideo } from '@/utils/media'
import { formatSize, formatTime } from '@/utils/number'
import EditElement from '@/views/markdown/EditElement'
import IframePreview from '@/components/preview/IframePreview.vue'
import ShareDialog from '@/components/ShareDialog/index.vue'
import Clipboard from 'clipboard'
import TagDialog from '@/components/TagDialog/index.vue'
import FileDetails from "@/components/preview/FileDetails.vue";
import _ from 'lodash'
import path from 'path'
import { mapGetters, mapState } from 'vuex'
export default {
name: 'ShowFile',
@ -1178,7 +1224,9 @@ export default {
debounceSearch: null,// 搜索防抖
debounceGetFileList: null,// 获取文件列表防抖
filterOption: {}, // 搜索选项
searchHistoryList: [],// 搜索历史列表
searchSuggestions: [],// 搜索历史列表
_isSuggestionHandledBySelect: false,
_originalSearchFileNameBeforeSelect: '',
hasSearchFilterOption: false, // 是否有搜索选项
fileUsername: '', // 一般用于挂载文件
searchInputClass: 'search-input' // 搜索输入框样式
@ -2959,8 +3007,12 @@ export default {
getFileListEnter(onLoad) {
this.debounceGetFileList(onLoad)
},
searchFileEnter(key, onLoad) {
this.debounceSearch(key, onLoad)
searchFileEnter() {
if (this._isSuggestionHandledBySelect) {
return
}
this.debounceSearch(this.searchFileName)
this.$refs.searchInput.close()
},
searchFile(key, onLoad) {
if (key) {
@ -3021,6 +3073,10 @@ export default {
searchMount: this.filterOption.searchMount,
isMount: this.queryCondition.isMount,
searchOverall: this.filterOption.searchOverall,
exactSearch: this.filterOption.exactSearch,
includeTagName: this.filterOption.includeTagName,
includeFileName: this.filterOption.includeFileName,
includeFileContent: this.filterOption.includeFileContent,
}).then(res => {
this.loadData(res, onLoad)
this.listModeSearch = true
@ -4582,33 +4638,103 @@ export default {
},
showSearchOption() {
},
queryRecentlySearchHistory(queryString, cb) {
if (this.searchFileName) {
cb([])
return
async fetchDynamicSuggestions(queryString, cb) {
this._originalSearchFileNameBeforeSelect = queryString
this.searchSuggestions = []
const recentlySearch =
{
sugType: 'clearAllHistory',
keyword: '',
value: `__READONLY_DIVIDER_${Date.now()}__`,
id: 'recently_search_id'
}
if (!queryString) {
this.queryRecentlySearchHistory(queryString, recentlySearch, cb)
} else {
const fixedActionItems = [
{
sugType: 'action_current_path_search',
keyword: queryString,
value: queryString,
id: 'action_current_path_search_id'
},
{
sugType: 'action_global_search',
keyword: queryString,
value: queryString,
id: 'action_global_search_id'
},
]
this.searchSuggestions.push(...fixedActionItems)
this.queryRecentlySearchHistory(queryString, recentlySearch, cb)
}
},
queryRecentlySearchHistory(queryString = '', recentlySearch, cb) {
api.getRecentlySearchHistory({keyword: queryString}).then(res => {
this.searchHistoryList = res.data || []
cb(this.searchHistoryList)
const historyItems = res.data.map(item => ({
...item,
value: item.keyword,
sugType: 'history'
}))
if (historyItems.length > 0) {
this.searchSuggestions.push(recentlySearch)
}
this.searchSuggestions.push(...historyItems)
cb(this.searchSuggestions)
}).catch(error => {
console.error("Error fetching search history:", error)
this.searchHistoryList = []
cb([])
});
cb(this.searchSuggestions.length > 0 ? this.searchSuggestions : [])
})
},
handleSelectSearchHistory(item) {
this.searchFileName = item.keyword
this.filterOption = item
handleSelectSuggestion(item) {
this._isSuggestionHandledBySelect = true
if (item.id === "recently_search_id") {
this.$nextTick(() => {
if (this.searchFileName !== this._originalSearchFileNameBeforeSelect) {
this.searchFileName = this._originalSearchFileNameBeforeSelect
}
this._isSuggestionHandledBySelect = false
})
return
}
if ( item.sugType === 'history') {
this.searchFileName = item.keyword
}
if ( item.sugType === 'action_global_search') {
item.searchOverall = true
}
if ( item.sugType === 'action_current_path_search') {
item.searchOverall = false
}
this.debounceSearch(this.searchFileName, false)
this.$refs.searchOption.setFilterOption(item)
this.$nextTick(() => {
this._isSuggestionHandledBySelect = false
})
},
clearSearchHistory(itemToDelete, event) {
clearSearchHistoryForItem(itemToDelete, event) {
event.stopPropagation();
event.preventDefault();
const idToDelete = itemToDelete.id
api.removeSearchHistory({id: idToDelete}).then(() => {
const index = this.searchHistoryList.findIndex(item => item.id === idToDelete)
this.searchHistoryList.splice(index, 1)
const index = this.searchSuggestions.findIndex(item => item.id === idToDelete)
this.searchSuggestions.splice(index, 1)
})
},
clearAllSearchHistory(event) {
event.stopPropagation();
event.preventDefault();
this.$confirm('确定要清除所有搜索记录吗', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
api.removeAllSearchHistory({userId: this.$store.getters.userId}).then(() => {
this.searchSuggestions = this.searchSuggestions.filter(
item => item.sugType === 'action_current_path_search' || item.sugType === 'action_global_search'
);
})
}).catch(() => {
})
},
checkPreviewVisible() {
@ -4879,7 +5005,7 @@ export default {
}
.search-input {
width: 230px;
width: 250px;
transition: width 0.3s ease-in-out;
>>> .el-input--suffix .el-input__inner {
padding-right: 60px;
@ -4887,7 +5013,7 @@ export default {
}
.search-input-focus {
width: 230px;
width: 250px;
}
.search-close-btn {
@ -4915,37 +5041,80 @@ export default {
}
}
>>> .el-autocomplete-suggestion li {
padding: 0 5px 0 15px;
}
>>> .el-autocomplete-suggestion {
margin-top: 0 !important;
.suggestion-item {
display: flex;
justify-content: space-between;
align-items: center;
.suggestion-item-title {
display: flex;
gap: 10px;
justify-content: flex-start;
flex-direction: row;
align-items: center;
.suggestion-item-text {
width: 152px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.clear-search-history-btn {
padding: 8px;
.popper__arrow {
display: none;
}
&:hover {
li {
padding: 0 5px 0 15px;
}
.suggestion-item {
display: flex;
justify-content: space-between;
align-items: center;
.suggestion-item-title {
display: flex;
gap: 15px;
justify-content: flex-start;
flex-direction: row;
align-items: center;
.suggestion-item-text {
width: 168px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.clear-search-history-btn {
display: block;
padding: 8px;
display: none;
}
&:hover {
.clear-search-history-btn {
display: block;
}
}
}
.suggestion-item.action-item {
display: flex;
justify-content: space-between;
align-items: center;
.suggestion-action-item-title {
display: flex;
gap: 15px;
justify-content: flex-start;
flex-direction: row;
align-items: center;
height: 40px;
line-height: 40px;
.suggestion-action-item-text {
display: flex;
flex-wrap: wrap;
}
.suggestion-item-keyword {
line-height: normal;
color: #303133;
font-weight: 500;
margin-right: 10px;
}
.suggestion-item-hint {
color: #c8c9cc;
line-height: normal;
}
}
}
}

View File

@ -54,11 +54,11 @@
<el-form-item label="名称:">
<span>{{ file.name }}</span>
</el-form-item>
<!-- <el-form-item label="类型:">-->
<!-- <span>{{-->
<!-- file.isFolder ? '文件夹' : file.contentType-->
<!-- }}</span>-->
<!-- </el-form-item>-->
<el-form-item label="类型:">
<span>{{
file.isFolder ? '文件夹' : file.contentType
}}</span>
</el-form-item>
<div v-if="file.music">
<el-form-item label="🎵 歌手:">
<span>{{ file.music.singer }}</span>

View File

@ -13,6 +13,7 @@ const getters = {
userId: state => state.user.userId,
userInfo: state => state.user.userInfo,
menuList: state => state.user.menuList,
fileClipboard: state => state.fileClipboard
fileClipboard: state => state.fileClipboard,
exactSearch: state => state.user.exactSearch
}
export default getters

View File

@ -34,6 +34,7 @@ const getDefaultState = () => {
menuList: [],
netdiskName: store ? store.netdiskName : '',
netdiskLogo: store ? store.netdiskLogo : '',
exactSearch: store ? store.exactSearch : false,
newVersion: store ? store.newVersion : '',
iframePreviewConfig: {}
}
@ -78,6 +79,9 @@ const mutations = {
SET_NETDISK_LOGO:(state,netdiskLogo) => {
state.netdiskLogo = netdiskLogo;
},
SET_EXACT_SEARCH:(state,exactSearch) => {
state.exactSearch = exactSearch;
},
SET_NEW_VERSION:(state,newVersion) => {
state.newVersion = newVersion;
},
@ -156,7 +160,7 @@ const actions = {
// reject('Verification failed, please Login again.')
}
const { id, username, showName, avatar, netdiskName, netdiskLogo, newVersion, iframe} = data
const { id, username, showName, avatar, netdiskName, exactSearch, netdiskLogo, newVersion, iframe} = data
commit('SET_NAME', username)
commit('SET_SHOW_NAME', showName)
@ -165,6 +169,7 @@ const actions = {
commit('SET_USER_INFO', data)
commit('SET_NETDISK_NAME', netdiskName)
commit('SET_NETDISK_LOGO', netdiskLogo)
commit('SET_EXACT_SEARCH', exactSearch)
if (iframe) {
commit('SET_IFRAME_PREVIEW', JSON.parse(iframe))
} else {

View File

@ -29,6 +29,9 @@ body {
.el-message-box {
border-radius: $popperBorderRadius;
.el-button {
border-radius: 20px;
}
}
.el-message, .el-notification, .el-popover, .el-tooltip__popper, .el-dropdown-menu, .el-select-dropdown, .el-picker-panel, .el-picker-panel__body, .el-picker-panel__sidebar {