diff --git a/src/App.vue b/src/App.vue index c7ca19a..31f3ea5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,9 @@ import { useI18n } from 'vue-i18n' import { useDisplay } from 'vuetify' import { useUserStore } from './stores/user' import { useLocaleStore } from './stores/locale' +import { useMenuStore } from './stores/menu' +import { storeToRefs } from 'pinia' +import { useSearchHistory } from './composables/useSearchHistory' import type { LocaleCode } from './plugins/i18n' import Toast from './components/Toast.vue' @@ -13,8 +16,16 @@ const { t } = useI18n() const router = useRouter() const userStore = useUserStore() const localeStore = useLocaleStore() +const menuStore = useMenuStore() const localeMenuOpen = ref(false) +const { categoryLayers, layerActiveValues } = storeToRefs(menuStore) +const searchHistory = useSearchHistory() +const searchHistoryList = computed(() => searchHistory.list.value) +const searchExpanded = ref(false) +const searchKeyword = ref('') +const searchInputRef = ref<{ focus: () => void } | null>(null) + function chooseLocale(loc: LocaleCode) { localeStore.setLocale(loc) localeMenuOpen.value = false @@ -61,6 +72,39 @@ async function refreshUserData() { await userStore.fetchPositionsValue() } +function expandSearch() { + searchExpanded.value = true + nextTick(() => { + searchInputRef.value?.focus() + }) +} + +function collapseSearch() { + searchExpanded.value = false + searchKeyword.value = '' +} + +function onSearchBlur() { + setTimeout(() => { + collapseSearch() + }, 200) +} + +function onSearchSubmit() { + const kw = searchKeyword.value.trim() + if (kw) { + searchHistory.add(kw) + // 触发搜索事件的逻辑,可以利用 store 或者 router 跳转 + router.push({ path: '/', query: { q: kw } }) + } + collapseSearch() +} + +function selectHistoryItem(item: string) { + searchKeyword.value = item + onSearchSubmit() +} + onMounted(() => { refreshUserData() }) @@ -75,55 +119,109 @@ watch( - diff --git a/src/assets/styles/base.scss b/src/assets/styles/base.scss index 8d94214..f281d25 100644 --- a/src/assets/styles/base.scss +++ b/src/assets/styles/base.scss @@ -29,4 +29,9 @@ html, body { width: 0; height: 0; } +} + +.flex-column { + display: flex; + flex-direction: column; } \ No newline at end of file diff --git a/src/stores/menu.ts b/src/stores/menu.ts index c27177e..16698e5 100644 --- a/src/stores/menu.ts +++ b/src/stores/menu.ts @@ -35,10 +35,59 @@ export const useMenuStore = defineStore('menu', () => { return layers }) + /** 分类切换时的防抖状态,防止重复请求 */ + let categorySelectTimer: ReturnType | null = null + + /** 触发事件加载的回调函数(由 Home 页面注册) */ + const loadEventsCallback = ref<(() => void) | null>(null) + + /** 触发清除事件列表缓存的回调函数(由 Home 页面注册) */ + const clearCacheCallback = ref<(() => void) | null>(null) + + /** 分类选中时:若有 children 则展开下一层并默认选中第一个,并重新加载列表 */ + function onCategorySelect(layerIndex: number, selectedId: string) { + if (!selectedId || layerActiveValues.value[layerIndex] === selectedId) { + return + } + + if (categorySelectTimer) { + clearTimeout(categorySelectTimer) + } + + const nextValues = [...layerActiveValues.value] + nextValues[layerIndex] = selectedId + nextValues.length = layerIndex + 1 + + const layers = categoryLayers.value + const layer = layers[layerIndex] + const node = layer?.find((n) => n.id === selectedId) + const children = filterVisible(node?.children) + + const firstChild = children[0] + if (firstChild && layerIndex < 2) { + nextValues.push(firstChild.id) + } + layerActiveValues.value = nextValues + + if (clearCacheCallback.value) { + clearCacheCallback.value() + } + + categorySelectTimer = setTimeout(() => { + categorySelectTimer = null + if (loadEventsCallback.value) { + loadEventsCallback.value() + } + }, 100) + } + return { categoryTree, layerActiveValues, categoryLayers, - filterVisible + filterVisible, + onCategorySelect, + loadEventsCallback, + clearCacheCallback } }) diff --git a/src/views/Home.vue b/src/views/Home.vue index e3a23e6..9a5417d 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,66 +1,21 @@