mirror of
https://github.com/pure-admin/vue-pure-admin.git
synced 2025-06-07 08:57:19 +08:00
parent
b5645b19e1
commit
16122aec17
@ -22,6 +22,7 @@
|
|||||||
"CachingAsyncRoutes": false,
|
"CachingAsyncRoutes": false,
|
||||||
"TooltipEffect": "light",
|
"TooltipEffect": "light",
|
||||||
"ResponsiveStorageNameSpace": "responsive-",
|
"ResponsiveStorageNameSpace": "responsive-",
|
||||||
|
"MenuSearchHistory": 6,
|
||||||
"MapConfigure": {
|
"MapConfigure": {
|
||||||
"amapKey": "97b3248d1553172e81f168cf94ea667e",
|
"amapKey": "97b3248d1553172e81f168cf94ea667e",
|
||||||
"options": {
|
"options": {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
|
|
||||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
|
|
||||||
import { useNav } from "@/layout/hooks/useNav";
|
import { useNav } from "@/layout/hooks/useNav";
|
||||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
|
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
|
||||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||||
|
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
|
||||||
|
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ total: number }>(), {
|
const props = withDefaults(defineProps<{ total: number }>(), {
|
||||||
total: 0
|
total: 0
|
||||||
|
198
src/layout/components/search/components/SearchHistory.vue
Normal file
198
src/layout/components/search/components/SearchHistory.vue
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Sortable from "sortablejs";
|
||||||
|
import SearchHistoryItem from "./SearchHistoryItem.vue";
|
||||||
|
import type { optionsItem, dragItem, Props } from "../types";
|
||||||
|
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||||
|
import { useResizeObserver, isArray, delay } from "@pureadmin/utils";
|
||||||
|
import { ref, watch, nextTick, computed, getCurrentInstance } from "vue";
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: "update:value", val: string): void;
|
||||||
|
(e: "enter"): void;
|
||||||
|
(e: "collect", val: optionsItem): void;
|
||||||
|
(e: "delete", val: optionsItem): void;
|
||||||
|
(e: "drag", val: dragItem): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyRef = ref();
|
||||||
|
const innerHeight = ref();
|
||||||
|
/** 判断是否停止鼠标移入事件处理 */
|
||||||
|
const stopMouseEvent = ref(false);
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
const instance = getCurrentInstance()!;
|
||||||
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
|
const itemStyle = computed(() => {
|
||||||
|
return item => {
|
||||||
|
return {
|
||||||
|
background:
|
||||||
|
item?.path === active.value ? useEpThemeStoreHook().epThemeColor : "",
|
||||||
|
color: item.path === active.value ? "#fff" : "",
|
||||||
|
fontSize: item.path === active.value ? "16px" : "14px"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
color: useEpThemeStoreHook().epThemeColor,
|
||||||
|
fontWeight: 500
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const active = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val: string) {
|
||||||
|
emit("update:value", val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
newValue => {
|
||||||
|
if (newValue) {
|
||||||
|
if (stopMouseEvent.value) {
|
||||||
|
delay(100).then(() => (stopMouseEvent.value = false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const historyList = computed(() => {
|
||||||
|
return props.options.filter(item => item.type === "history");
|
||||||
|
});
|
||||||
|
|
||||||
|
const collectList = computed(() => {
|
||||||
|
return props.options.filter(item => item.type === "collect");
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleCollect(item) {
|
||||||
|
emit("collect", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(item) {
|
||||||
|
stopMouseEvent.value = true;
|
||||||
|
emit("delete", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 鼠标移入 */
|
||||||
|
async function handleMouse(item) {
|
||||||
|
if (!stopMouseEvent.value) active.value = item.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTo() {
|
||||||
|
emit("enter");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeResult() {
|
||||||
|
// el-scrollbar max-height="calc(90vh - 140px)"
|
||||||
|
innerHeight.value = window.innerHeight - window.innerHeight / 10 - 140;
|
||||||
|
}
|
||||||
|
|
||||||
|
useResizeObserver(historyRef, resizeResult);
|
||||||
|
|
||||||
|
function handleScroll(index: number) {
|
||||||
|
const curInstance = instance?.proxy?.$refs[`historyItemRef${index}`];
|
||||||
|
if (!curInstance) return 0;
|
||||||
|
const curRef = isArray(curInstance)
|
||||||
|
? (curInstance[0] as ElRef)
|
||||||
|
: (curInstance as ElRef);
|
||||||
|
const scrollTop = curRef.offsetTop + 128; // 128 两个history-item(56px+56px=112px)高度加上下margin(8px+8px=16px)
|
||||||
|
return scrollTop > innerHeight.value ? scrollTop - innerHeight.value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeIndex = (evt): void => {
|
||||||
|
emit("drag", { oldIndex: evt.oldIndex, newIndex: evt.newIndex });
|
||||||
|
};
|
||||||
|
|
||||||
|
let sortableInstance = null;
|
||||||
|
|
||||||
|
watch(
|
||||||
|
collectList,
|
||||||
|
val => {
|
||||||
|
if (val.length > 1) {
|
||||||
|
nextTick(() => {
|
||||||
|
const wrapper: HTMLElement =
|
||||||
|
document.querySelector(".collect-container");
|
||||||
|
if (!wrapper || sortableInstance) return;
|
||||||
|
sortableInstance = Sortable.create(wrapper, {
|
||||||
|
animation: 160,
|
||||||
|
onStart: event => {
|
||||||
|
event.item.style.cursor = "move";
|
||||||
|
},
|
||||||
|
onEnd: event => {
|
||||||
|
event.item.style.cursor = "pointer";
|
||||||
|
},
|
||||||
|
onUpdate: handleChangeIndex
|
||||||
|
});
|
||||||
|
resizeResult();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose({ handleScroll });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="historyRef" class="history">
|
||||||
|
<template v-if="historyList.length">
|
||||||
|
<div :style="titleStyle">搜索历史</div>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in historyList"
|
||||||
|
:key="item.path"
|
||||||
|
:ref="'historyItemRef' + index"
|
||||||
|
class="history-item dark:bg-[#1d1d1d]"
|
||||||
|
:style="itemStyle(item)"
|
||||||
|
@click="handleTo"
|
||||||
|
@mouseenter="handleMouse(item)"
|
||||||
|
>
|
||||||
|
<SearchHistoryItem
|
||||||
|
:item="item"
|
||||||
|
@delete-item="handleDelete"
|
||||||
|
@collect-item="handleCollect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="collectList.length">
|
||||||
|
<div :style="titleStyle">
|
||||||
|
收藏{{ collectList.length > 1 ? "(可拖拽排序)" : "" }}
|
||||||
|
</div>
|
||||||
|
<div class="collect-container">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in collectList"
|
||||||
|
:key="item.path"
|
||||||
|
:ref="'historyItemRef' + (index + historyList.length)"
|
||||||
|
class="history-item dark:bg-[#1d1d1d]"
|
||||||
|
:style="itemStyle(item)"
|
||||||
|
@click="handleTo"
|
||||||
|
@mouseenter="handleMouse(item)"
|
||||||
|
>
|
||||||
|
<SearchHistoryItem :item="item" @delete-item="handleDelete" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.history {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
padding: 14px;
|
||||||
|
margin: 8px auto 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0.1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: font-size 0.16s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { optionsItem } from "../types";
|
||||||
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import Star from "@iconify-icons/ep/star";
|
||||||
|
import Close from "@iconify-icons/ep/close";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: optionsItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: "collectItem", val: optionsItem): void;
|
||||||
|
(e: "deleteItem", val: optionsItem): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
|
function handleCollect(item) {
|
||||||
|
emit("collectItem", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(item) {
|
||||||
|
emit("deleteItem", item);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="useRenderIcon(item.meta?.icon)" />
|
||||||
|
<span class="history-item-title">
|
||||||
|
{{ transformI18n(item.meta?.title) }}
|
||||||
|
</span>
|
||||||
|
<IconifyIconOffline
|
||||||
|
v-show="item.type === 'history'"
|
||||||
|
:icon="Star"
|
||||||
|
class="w-[18px] h-[18px] mr-2 hover:text-[#d7d5d4]"
|
||||||
|
@click.stop="handleCollect(item)"
|
||||||
|
/>
|
||||||
|
<IconifyIconOffline
|
||||||
|
:icon="Close"
|
||||||
|
class="w-[18px] h-[18px] hover:text-[#d7d5d4] cursor-pointer"
|
||||||
|
@click.stop="handleDelete(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.history-item-title {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,15 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { match } from "pinyin-pro";
|
import { match } from "pinyin-pro";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { getConfig } from "@/config";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import SearchResult from "./SearchResult.vue";
|
import SearchResult from "./SearchResult.vue";
|
||||||
import SearchFooter from "./SearchFooter.vue";
|
import SearchFooter from "./SearchFooter.vue";
|
||||||
import { useNav } from "@/layout/hooks/useNav";
|
import { useNav } from "@/layout/hooks/useNav";
|
||||||
import { transformI18n } from "@/plugins/i18n";
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
import { ref, computed, shallowRef } from "vue";
|
import SearchHistory from "./SearchHistory.vue";
|
||||||
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
|
import type { optionsItem, dragItem } from "../types";
|
||||||
|
import { ref, computed, shallowRef, watch } from "vue";
|
||||||
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
|
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
|
||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||||
|
import { cloneDeep, isAllEmpty, storageLocal } from "@pureadmin/utils";
|
||||||
import Search from "@iconify-icons/ri/search-line";
|
import Search from "@iconify-icons/ri/search-line";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -24,16 +27,26 @@ interface Emits {
|
|||||||
const { device } = useNav();
|
const { device } = useNav();
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
const props = withDefaults(defineProps<Props>(), {});
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { locale } = useI18n();
|
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 keyword = ref("");
|
||||||
const scrollbarRef = ref();
|
|
||||||
const resultRef = ref();
|
const resultRef = ref();
|
||||||
|
const historyRef = ref();
|
||||||
|
const scrollbarRef = ref();
|
||||||
const activePath = ref("");
|
const activePath = ref("");
|
||||||
const inputRef = ref<HTMLInputElement | null>(null);
|
const historyPath = ref("");
|
||||||
const resultOptions = shallowRef([]);
|
const resultOptions = shallowRef([]);
|
||||||
|
const historyOptions = shallowRef([]);
|
||||||
const handleSearch = useDebounceFn(search, 300);
|
const handleSearch = useDebounceFn(search, 300);
|
||||||
|
const historyNum = getConfig().MenuSearchHistory;
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
/** 菜单树形结构 */
|
/** 菜单树形结构 */
|
||||||
const menusData = computed(() => {
|
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) {
|
function flatTree(arr) {
|
||||||
const res = [];
|
const res = [];
|
||||||
@ -79,11 +122,8 @@ function search() {
|
|||||||
))
|
))
|
||||||
: false
|
: false
|
||||||
);
|
);
|
||||||
if (resultOptions.value?.length > 0) {
|
activePath.value =
|
||||||
activePath.value = resultOptions.value[0].path;
|
resultOptions.value?.length > 0 ? resultOptions.value[0].path : "";
|
||||||
} else {
|
|
||||||
activePath.value = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
@ -91,54 +131,143 @@ function handleClose() {
|
|||||||
/** 延时处理防止用户看到某些操作 */
|
/** 延时处理防止用户看到某些操作 */
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resultOptions.value = [];
|
resultOptions.value = [];
|
||||||
|
historyPath.value = "";
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollTo(index) {
|
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);
|
scrollbarRef.value.setScrollTop(scrollTop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前选项和路径 */
|
||||||
|
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 {
|
||||||
|
historyPath.value = historyOptions.value[newIndex].path;
|
||||||
|
}
|
||||||
|
scrollTo(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/** key up */
|
/** key up */
|
||||||
function handleUp() {
|
function handleUp() {
|
||||||
const { length } = resultOptions.value;
|
const { options, currentPath, isResultOptions } = getCurrentOptionsAndPath();
|
||||||
if (length === 0) return;
|
if (options.length === 0) return;
|
||||||
const index = resultOptions.value.findIndex(
|
const index = options.findIndex(item => item.path === currentPath);
|
||||||
item => item.path === activePath.value
|
const prevIndex = (index - 1 + options.length) % options.length;
|
||||||
);
|
updatePathAndScroll(prevIndex, isResultOptions);
|
||||||
if (index === 0) {
|
|
||||||
activePath.value = resultOptions.value[length - 1].path;
|
|
||||||
scrollTo(resultOptions.value.length - 1);
|
|
||||||
} else {
|
|
||||||
activePath.value = resultOptions.value[index - 1].path;
|
|
||||||
scrollTo(index - 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** key down */
|
/** key down */
|
||||||
function handleDown() {
|
function handleDown() {
|
||||||
const { length } = resultOptions.value;
|
const { options, currentPath, isResultOptions } = getCurrentOptionsAndPath();
|
||||||
if (length === 0) return;
|
if (options.length === 0) return;
|
||||||
const index = resultOptions.value.findIndex(
|
const index = options.findIndex(item => item.path === currentPath);
|
||||||
item => item.path === activePath.value
|
const nextIndex = (index + 1) % options.length;
|
||||||
);
|
updatePathAndScroll(nextIndex, isResultOptions);
|
||||||
if (index + 1 === length) {
|
|
||||||
activePath.value = resultOptions.value[0].path;
|
|
||||||
} else {
|
|
||||||
activePath.value = resultOptions.value[index + 1].path;
|
|
||||||
}
|
|
||||||
scrollTo(index + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** key enter */
|
/** key enter */
|
||||||
function handleEnter() {
|
function handleEnter() {
|
||||||
const { length } = resultOptions.value;
|
const { options, currentPath, isResultOptions } = getCurrentOptionsAndPath();
|
||||||
if (length === 0 || activePath.value === "") return;
|
if (options.length === 0 || currentPath === "") return;
|
||||||
router.push(activePath.value);
|
const index = options.findIndex(item => item.path === currentPath);
|
||||||
|
if (index === -1) return;
|
||||||
|
if (isResultOptions) {
|
||||||
|
saveHistory();
|
||||||
|
} else {
|
||||||
|
updateHistory();
|
||||||
|
}
|
||||||
|
router.push(options[index].path);
|
||||||
handleClose();
|
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("Enter", handleEnter);
|
||||||
onKeyStroke("ArrowUp", handleUp);
|
onKeyStroke("ArrowUp", handleUp);
|
||||||
onKeyStroke("ArrowDown", handleDown);
|
onKeyStroke("ArrowDown", handleDown);
|
||||||
@ -174,14 +303,21 @@ onKeyStroke("ArrowDown", handleDown);
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div class="search-result-container">
|
<div class="search-content">
|
||||||
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
|
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
|
||||||
<el-empty
|
<el-empty v-if="showEmpty" description="暂无搜索结果" />
|
||||||
v-if="resultOptions.length === 0"
|
<SearchHistory
|
||||||
description="暂无搜索结果"
|
v-if="showSearchHistory"
|
||||||
|
ref="historyRef"
|
||||||
|
v-model:value="historyPath"
|
||||||
|
:options="historyOptions"
|
||||||
|
@click="handleEnter"
|
||||||
|
@delete="handleDelete"
|
||||||
|
@collect="handleCollect"
|
||||||
|
@drag="handleDrag"
|
||||||
/>
|
/>
|
||||||
<SearchResult
|
<SearchResult
|
||||||
v-else
|
v-if="showSearchResult"
|
||||||
ref="resultRef"
|
ref="resultRef"
|
||||||
v-model:value="activePath"
|
v-model:value="activePath"
|
||||||
:options="resultOptions"
|
:options="resultOptions"
|
||||||
@ -196,7 +332,7 @@ onKeyStroke("ArrowDown", handleDown);
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.search-result-container {
|
.search-content {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,24 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Props } from "../types";
|
||||||
import { transformI18n } from "@/plugins/i18n";
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
import { useResizeObserver } from "@pureadmin/utils";
|
import { useResizeObserver } from "@pureadmin/utils";
|
||||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
import { ref, computed, getCurrentInstance, onMounted } from "vue";
|
import { ref, computed, getCurrentInstance, onMounted } from "vue";
|
||||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||||
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
|
|
||||||
|
|
||||||
interface optionsItem {
|
|
||||||
path: string;
|
|
||||||
meta?: {
|
|
||||||
icon?: string;
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value: string;
|
|
||||||
options: Array<optionsItem>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "update:value", val: string): void;
|
(e: "update:value", val: string): void;
|
||||||
@ -27,9 +14,9 @@ interface Emits {
|
|||||||
|
|
||||||
const resultRef = ref();
|
const resultRef = ref();
|
||||||
const innerHeight = ref();
|
const innerHeight = ref();
|
||||||
const props = withDefaults(defineProps<Props>(), {});
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
const instance = getCurrentInstance()!;
|
const instance = getCurrentInstance()!;
|
||||||
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
const itemStyle = computed(() => {
|
const itemStyle = computed(() => {
|
||||||
return item => {
|
return item => {
|
||||||
@ -93,7 +80,7 @@ defineExpose({ handleScroll });
|
|||||||
@click="handleTo"
|
@click="handleTo"
|
||||||
@mouseenter="handleMouse(item)"
|
@mouseenter="handleMouse(item)"
|
||||||
>
|
>
|
||||||
<component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
|
<component :is="useRenderIcon(item.meta?.icon)" />
|
||||||
<span class="result-item-title">
|
<span class="result-item-title">
|
||||||
{{ transformI18n(item.meta?.title) }}
|
{{ transformI18n(item.meta?.title) }}
|
||||||
</span>
|
</span>
|
||||||
|
20
src/layout/components/search/types.ts
Normal file
20
src/layout/components/search/types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
interface optionsItem {
|
||||||
|
path: string;
|
||||||
|
type: "history" | "collect";
|
||||||
|
meta: {
|
||||||
|
icon?: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface dragItem {
|
||||||
|
oldIndex: number;
|
||||||
|
newIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
options: Array<optionsItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { optionsItem, dragItem, Props };
|
@ -85,6 +85,22 @@ class StorageProxy implements ProxyStorage {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取数据仓库中所有的key
|
||||||
|
*/
|
||||||
|
public async keys() {
|
||||||
|
return new Promise<string[]>((resolve, reject) => {
|
||||||
|
this.storage
|
||||||
|
.keys()
|
||||||
|
.then(keys => {
|
||||||
|
resolve(keys);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
2
types/global.d.ts
vendored
2
types/global.d.ts
vendored
@ -96,6 +96,7 @@ declare global {
|
|||||||
CachingAsyncRoutes?: boolean;
|
CachingAsyncRoutes?: boolean;
|
||||||
TooltipEffect?: Effect;
|
TooltipEffect?: Effect;
|
||||||
ResponsiveStorageNameSpace?: string;
|
ResponsiveStorageNameSpace?: string;
|
||||||
|
MenuSearchHistory?: number;
|
||||||
MapConfigure?: {
|
MapConfigure?: {
|
||||||
amapKey?: string;
|
amapKey?: string;
|
||||||
options: {
|
options: {
|
||||||
@ -131,6 +132,7 @@ declare global {
|
|||||||
overallStyle?: string;
|
overallStyle?: string;
|
||||||
showLogo?: boolean;
|
showLogo?: boolean;
|
||||||
showModel?: string;
|
showModel?: string;
|
||||||
|
menuSearchHistory?: number;
|
||||||
mapConfigure?: {
|
mapConfigure?: {
|
||||||
amapKey?: string;
|
amapKey?: string;
|
||||||
options: {
|
options: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user