|
@@ -1,15 +1,18 @@
|
|
|
<script setup lang="ts">
|
|
|
import { match } from "pinyin-pro";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
+import { getConfig } from "@/config";
|
|
|
import { useRouter } from "vue-router";
|
|
|
import SearchResult from "./SearchResult.vue";
|
|
|
import SearchFooter from "./SearchFooter.vue";
|
|
|
import { useNav } from "@/layout/hooks/useNav";
|
|
|
import { transformI18n } from "@/plugins/i18n";
|
|
|
-import { ref, computed, shallowRef } from "vue";
|
|
|
-import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
|
|
|
+import SearchHistory from "./SearchHistory.vue";
|
|
|
+import type { optionsItem, dragItem } from "../types";
|
|
|
+import { ref, computed, shallowRef, watch } from "vue";
|
|
|
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
|
|
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
|
|
+import { cloneDeep, isAllEmpty, storageLocal } from "@pureadmin/utils";
|
|
|
import Search from "@iconify-icons/ri/search-line";
|
|
|
|
|
|
interface Props {
|
|
@@ -24,16 +27,26 @@ interface Emits {
|
|
|
const { device } = useNav();
|
|
|
const emit = defineEmits<Emits>();
|
|
|
const props = withDefaults(defineProps<Props>(), {});
|
|
|
+
|
|
|
const router = useRouter();
|
|
|
const { locale } = useI18n();
|
|
|
|
|
|
+const HISTORY_TYPE = "history";
|
|
|
+const COLLECT_TYPE = "collect";
|
|
|
+const LOCALEHISTORYKEY = "menu-search-history";
|
|
|
+const LOCALECOLLECTKEY = "menu-search-collect";
|
|
|
+
|
|
|
const keyword = ref("");
|
|
|
-const scrollbarRef = ref();
|
|
|
const resultRef = ref();
|
|
|
+const historyRef = ref();
|
|
|
+const scrollbarRef = ref();
|
|
|
const activePath = ref("");
|
|
|
-const inputRef = ref<HTMLInputElement | null>(null);
|
|
|
+const historyPath = ref("");
|
|
|
const resultOptions = shallowRef([]);
|
|
|
+const historyOptions = shallowRef([]);
|
|
|
const handleSearch = useDebounceFn(search, 300);
|
|
|
+const historyNum = getConfig().MenuSearchHistory;
|
|
|
+const inputRef = ref<HTMLInputElement | null>(null);
|
|
|
|
|
|
/** 菜单树形结构 */
|
|
|
const menusData = computed(() => {
|
|
@@ -49,6 +62,36 @@ const show = computed({
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+watch(
|
|
|
+ () => props.value,
|
|
|
+ newValue => {
|
|
|
+ if (newValue) getHistory();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+const showSearchResult = computed(() => {
|
|
|
+ return keyword.value && resultOptions.value.length > 0;
|
|
|
+});
|
|
|
+
|
|
|
+const showSearchHistory = computed(() => {
|
|
|
+ return !keyword.value && historyOptions.value.length > 0;
|
|
|
+});
|
|
|
+
|
|
|
+const showEmpty = computed(() => {
|
|
|
+ return (
|
|
|
+ (!keyword.value && historyOptions.value.length === 0) ||
|
|
|
+ (keyword.value && resultOptions.value.length === 0)
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+function getStorageItem(key) {
|
|
|
+ return storageLocal().getItem<optionsItem[]>(key) || [];
|
|
|
+}
|
|
|
+
|
|
|
+function setStorageItem(key, value) {
|
|
|
+ storageLocal().setItem(key, value);
|
|
|
+}
|
|
|
+
|
|
|
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
|
|
|
function flatTree(arr) {
|
|
|
const res = [];
|
|
@@ -79,11 +122,8 @@ function search() {
|
|
|
))
|
|
|
: false
|
|
|
);
|
|
|
- if (resultOptions.value?.length > 0) {
|
|
|
- activePath.value = resultOptions.value[0].path;
|
|
|
- } else {
|
|
|
- activePath.value = "";
|
|
|
- }
|
|
|
+ activePath.value =
|
|
|
+ resultOptions.value?.length > 0 ? resultOptions.value[0].path : "";
|
|
|
}
|
|
|
|
|
|
function handleClose() {
|
|
@@ -91,54 +131,143 @@ function handleClose() {
|
|
|
/** 延时处理防止用户看到某些操作 */
|
|
|
setTimeout(() => {
|
|
|
resultOptions.value = [];
|
|
|
+ historyPath.value = "";
|
|
|
keyword.value = "";
|
|
|
}, 200);
|
|
|
}
|
|
|
|
|
|
function scrollTo(index) {
|
|
|
- const scrollTop = resultRef.value.handleScroll(index);
|
|
|
+ const ref = resultOptions.value.length ? resultRef.value : historyRef.value;
|
|
|
+ const scrollTop = ref.handleScroll(index);
|
|
|
scrollbarRef.value.setScrollTop(scrollTop);
|
|
|
}
|
|
|
|
|
|
-/** key up */
|
|
|
-function handleUp() {
|
|
|
- const { length } = resultOptions.value;
|
|
|
- if (length === 0) return;
|
|
|
- const index = resultOptions.value.findIndex(
|
|
|
- item => item.path === activePath.value
|
|
|
- );
|
|
|
- if (index === 0) {
|
|
|
- activePath.value = resultOptions.value[length - 1].path;
|
|
|
- scrollTo(resultOptions.value.length - 1);
|
|
|
+/** 获取当前选项和路径 */
|
|
|
+function getCurrentOptionsAndPath() {
|
|
|
+ const isResultOptions = resultOptions.value.length > 0;
|
|
|
+ const options = isResultOptions ? resultOptions.value : historyOptions.value;
|
|
|
+ const currentPath = isResultOptions ? activePath.value : historyPath.value;
|
|
|
+ return { options, currentPath, isResultOptions };
|
|
|
+}
|
|
|
+
|
|
|
+/** 更新路径并滚动到指定项 */
|
|
|
+function updatePathAndScroll(newIndex, isResultOptions) {
|
|
|
+ if (isResultOptions) {
|
|
|
+ activePath.value = resultOptions.value[newIndex].path;
|
|
|
} else {
|
|
|
- activePath.value = resultOptions.value[index - 1].path;
|
|
|
- scrollTo(index - 1);
|
|
|
+ historyPath.value = historyOptions.value[newIndex].path;
|
|
|
}
|
|
|
+ scrollTo(newIndex);
|
|
|
+}
|
|
|
+
|
|
|
+/** key up */
|
|
|
+function handleUp() {
|
|
|
+ const { options, currentPath, isResultOptions } = getCurrentOptionsAndPath();
|
|
|
+ if (options.length === 0) return;
|
|
|
+ const index = options.findIndex(item => item.path === currentPath);
|
|
|
+ const prevIndex = (index - 1 + options.length) % options.length;
|
|
|
+ updatePathAndScroll(prevIndex, isResultOptions);
|
|
|
}
|
|
|
|
|
|
/** key down */
|
|
|
function handleDown() {
|
|
|
- const { length } = resultOptions.value;
|
|
|
- if (length === 0) return;
|
|
|
- const index = resultOptions.value.findIndex(
|
|
|
- item => item.path === activePath.value
|
|
|
- );
|
|
|
- if (index + 1 === length) {
|
|
|
- activePath.value = resultOptions.value[0].path;
|
|
|
- } else {
|
|
|
- activePath.value = resultOptions.value[index + 1].path;
|
|
|
- }
|
|
|
- scrollTo(index + 1);
|
|
|
+ const { options, currentPath, isResultOptions } = getCurrentOptionsAndPath();
|
|
|
+ if (options.length === 0) return;
|
|
|
+ const index = options.findIndex(item => item.path === currentPath);
|
|
|
+ const nextIndex = (index + 1) % options.length;
|
|
|
+ updatePathAndScroll(nextIndex, isResultOptions);
|
|
|
}
|
|
|
|
|
|
/** key enter */
|
|
|
function handleEnter() {
|
|
|
- const { length } = resultOptions.value;
|
|
|
- if (length === 0 || activePath.value === "") return;
|
|
|
- router.push(activePath.value);
|
|
|
+ const { options, currentPath, isResultOptions } = getCurrentOptionsAndPath();
|
|
|
+ if (options.length === 0 || currentPath === "") return;
|
|
|
+ const index = options.findIndex(item => item.path === currentPath);
|
|
|
+ if (index === -1) return;
|
|
|
+ if (isResultOptions) {
|
|
|
+ saveHistory();
|
|
|
+ } else {
|
|
|
+ updateHistory();
|
|
|
+ }
|
|
|
+ router.push(options[index].path);
|
|
|
handleClose();
|
|
|
}
|
|
|
|
|
|
+/** 删除历史记录 */
|
|
|
+function handleDelete(item) {
|
|
|
+ const key = item.type === HISTORY_TYPE ? LOCALEHISTORYKEY : LOCALECOLLECTKEY;
|
|
|
+ let list = getStorageItem(key);
|
|
|
+ list = list.filter(listItem => listItem.path !== item.path);
|
|
|
+ setStorageItem(key, list);
|
|
|
+ getHistory();
|
|
|
+}
|
|
|
+
|
|
|
+/** 收藏历史记录 */
|
|
|
+function handleCollect(item) {
|
|
|
+ let searchHistoryList = getStorageItem(LOCALEHISTORYKEY);
|
|
|
+ let searchCollectList = getStorageItem(LOCALECOLLECTKEY);
|
|
|
+ searchHistoryList = searchHistoryList.filter(
|
|
|
+ historyItem => historyItem.path !== item.path
|
|
|
+ );
|
|
|
+ setStorageItem(LOCALEHISTORYKEY, searchHistoryList);
|
|
|
+ if (!searchCollectList.some(collectItem => collectItem.path === item.path)) {
|
|
|
+ searchCollectList.unshift({ ...item, type: COLLECT_TYPE });
|
|
|
+ setStorageItem(LOCALECOLLECTKEY, searchCollectList);
|
|
|
+ }
|
|
|
+ getHistory();
|
|
|
+}
|
|
|
+
|
|
|
+/** 存储搜索记录 */
|
|
|
+function saveHistory() {
|
|
|
+ const { path, meta } = resultOptions.value.find(
|
|
|
+ item => item.path === activePath.value
|
|
|
+ );
|
|
|
+ const searchHistoryList = getStorageItem(LOCALEHISTORYKEY);
|
|
|
+ const searchCollectList = getStorageItem(LOCALECOLLECTKEY);
|
|
|
+ const isCollected = searchCollectList.some(item => item.path === path);
|
|
|
+ const existingIndex = searchHistoryList.findIndex(item => item.path === path);
|
|
|
+ if (!isCollected) {
|
|
|
+ if (existingIndex !== -1) searchHistoryList.splice(existingIndex, 1);
|
|
|
+ if (searchHistoryList.length >= historyNum) searchHistoryList.pop();
|
|
|
+ searchHistoryList.unshift({ path, meta, type: HISTORY_TYPE });
|
|
|
+ storageLocal().setItem(LOCALEHISTORYKEY, searchHistoryList);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 更新存储的搜索记录 */
|
|
|
+function updateHistory() {
|
|
|
+ let searchHistoryList = getStorageItem(LOCALEHISTORYKEY);
|
|
|
+ const historyIndex = searchHistoryList.findIndex(
|
|
|
+ item => item.path === historyPath.value
|
|
|
+ );
|
|
|
+ if (historyIndex !== -1) {
|
|
|
+ const [historyItem] = searchHistoryList.splice(historyIndex, 1);
|
|
|
+ searchHistoryList.unshift(historyItem);
|
|
|
+ setStorageItem(LOCALEHISTORYKEY, searchHistoryList);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 获取本地历史记录 */
|
|
|
+function getHistory() {
|
|
|
+ const searchHistoryList = getStorageItem(LOCALEHISTORYKEY);
|
|
|
+ const searchCollectList = getStorageItem(LOCALECOLLECTKEY);
|
|
|
+ historyOptions.value = [...searchHistoryList, ...searchCollectList];
|
|
|
+ historyPath.value = historyOptions.value[0]?.path;
|
|
|
+}
|
|
|
+
|
|
|
+/** 拖拽改变收藏顺序 */
|
|
|
+function handleDrag(item: dragItem) {
|
|
|
+ const searchCollectList = getStorageItem(LOCALECOLLECTKEY);
|
|
|
+ const [reorderedItem] = searchCollectList.splice(item.oldIndex, 1);
|
|
|
+ searchCollectList.splice(item.newIndex, 0, reorderedItem);
|
|
|
+ storageLocal().setItem(LOCALECOLLECTKEY, searchCollectList);
|
|
|
+ historyOptions.value = [
|
|
|
+ ...getStorageItem(LOCALEHISTORYKEY),
|
|
|
+ ...getStorageItem(LOCALECOLLECTKEY)
|
|
|
+ ];
|
|
|
+ historyPath.value = reorderedItem.path;
|
|
|
+}
|
|
|
+
|
|
|
onKeyStroke("Enter", handleEnter);
|
|
|
onKeyStroke("ArrowUp", handleUp);
|
|
|
onKeyStroke("ArrowDown", handleDown);
|
|
@@ -174,14 +303,21 @@ onKeyStroke("ArrowDown", handleDown);
|
|
|
/>
|
|
|
</template>
|
|
|
</el-input>
|
|
|
- <div class="search-result-container">
|
|
|
+ <div class="search-content">
|
|
|
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
|
|
|
- <el-empty
|
|
|
- v-if="resultOptions.length === 0"
|
|
|
- description="暂无搜索结果"
|
|
|
+ <el-empty v-if="showEmpty" description="暂无搜索结果" />
|
|
|
+ <SearchHistory
|
|
|
+ v-if="showSearchHistory"
|
|
|
+ ref="historyRef"
|
|
|
+ v-model:value="historyPath"
|
|
|
+ :options="historyOptions"
|
|
|
+ @click="handleEnter"
|
|
|
+ @delete="handleDelete"
|
|
|
+ @collect="handleCollect"
|
|
|
+ @drag="handleDrag"
|
|
|
/>
|
|
|
<SearchResult
|
|
|
- v-else
|
|
|
+ v-if="showSearchResult"
|
|
|
ref="resultRef"
|
|
|
v-model:value="activePath"
|
|
|
:options="resultOptions"
|
|
@@ -196,7 +332,7 @@ onKeyStroke("ArrowDown", handleDown);
|
|
|
</template>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.search-result-container {
|
|
|
+.search-content {
|
|
|
margin-top: 12px;
|
|
|
}
|
|
|
</style>
|