1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-05 23:32:45 +08:00

feat: refactor search state from composable to Pinia store

This commit is contained in:
2025-11-30 20:44:42 +08:00
parent 8cdca34922
commit d4bc468ed7
4 changed files with 117 additions and 71 deletions

View File

@@ -3,13 +3,13 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import { useGlobalData } from "../composables/useGlobalData";
import { useGlobalScroll } from "../composables/useGlobalScroll";
import { useAllPosts, type Post } from "../composables/useAllPosts";
import { useSearchState } from "../composables/useSearchState";
import { useSearchStateStore } from "../stores/searchState";
import { useScreenWidthStore } from "../stores/screenWidth";
import { handleTabNavigation } from "../utils/tabNavigation";
const { frontmatter } = useGlobalData();
const { isScrolled } = useGlobalScroll({ threshold: 100 });
const { isSearchActive, isSearchTyping, deactivateSearch, setSearchFocus, setSearchTyping } = useSearchState();
const searchStateStore = useSearchStateStore();
const screenWidthStore = useScreenWidthStore();
const isHome = computed(() => frontmatter.value.home === true);
const articlesRef = useAllPosts(true);
@@ -36,31 +36,31 @@ const filteredPosts = computed<Post[]>(() => {
// 处理输入框焦点 (仅激活,不处理关闭)
const handleFocus = () => {
if (!isSearchActive.value) {
isSearchActive.value = true;
if (!searchStateStore.isSearchActive) {
searchStateStore.activate();
}
setSearchFocus(true);
searchStateStore.setFocus(true);
};
// 处理输入框失焦 (只改变焦点状态,不关闭搜索,解决双击问题)
const handleBlur = () => {
setSearchFocus(false);
searchStateStore.setFocus(false);
};
// 处理输入
const handleInput = () => {
const hasContent = query.value.trim().length > 0;
setSearchTyping(hasContent);
if (hasContent && !isSearchActive.value) {
isSearchActive.value = true;
searchStateStore.setTyping(hasContent);
if (hasContent && !searchStateStore.isSearchActive) {
searchStateStore.activate();
}
};
// 清除搜索状态
const clearSearchState = () => {
query.value = "";
setSearchTyping(false);
deactivateSearch();
searchStateStore.setTyping(false);
searchStateStore.deactivate();
if (searchInput.value) {
searchInput.value.blur();
}
@@ -75,7 +75,7 @@ const handleResultClick = () => {
// 处理外部点击
const handleDocumentClick = (event: Event) => {
if (!isSearchActive.value) return;
if (!searchStateStore.isSearchActive) return;
const target = event.target as HTMLElement;
const isClickInsideInput = searchInput.value && searchInput.value.contains(target);
@@ -87,23 +87,26 @@ const handleDocumentClick = (event: Event) => {
};
// 监听状态自动聚焦
watch(isSearchActive, async (isActive) => {
if (isActive && searchInput.value) {
await nextTick();
setTimeout(() => {
searchInput.value?.focus();
}, 100);
} else if (!isActive) {
if (query.value !== "") {
query.value = "";
setSearchTyping(false);
watch(
() => searchStateStore.isSearchActive,
async (isActive) => {
if (isActive && searchInput.value) {
await nextTick();
setTimeout(() => {
searchInput.value?.focus();
}, 100);
} else if (!isActive) {
if (query.value !== "") {
query.value = "";
searchStateStore.setTyping(false);
}
}
}
});
);
// 键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (!isSearchActive.value) return;
if (!searchStateStore.isSearchActive) return;
const container = appbar.value;
const items = container?.querySelectorAll(".searchInput, .item") || null;
@@ -113,9 +116,14 @@ const handleKeydown = (event: KeyboardEvent) => {
clearSearchState();
}
if (event.key === "Tab" && query.value.trim() !== "") {
if (event.key === "Tab") {
event.preventDefault();
handleTabNavigation(container, items, event.shiftKey);
// 当搜索框没有内容时Tab 键取消搜索激活状态
if (query.value.trim() === "") {
searchStateStore.deactivate();
}
}
};
@@ -137,8 +145,8 @@ onUnmounted(() => {
:class="{
scroll: isScrolled,
homeLayout: isHome,
searching: isSearchActive,
typing: isSearchTyping,
searching: searchStateStore.isSearchActive,
typing: searchStateStore.isSearchTyping,
}"
:tabindex="isTabFocusable ? 0 : -1"
>

View File

@@ -2,11 +2,11 @@
import { computed } from "vue";
import { useGlobalData } from "../composables/useGlobalData";
import { useScreenWidthStore } from "../stores/screenWidth";
import { useSearchState } from "../composables/useSearchState";
import { useSearchStateStore } from "../stores/searchState";
const { page, theme } = useGlobalData();
const screenWidthStore = useScreenWidthStore();
const { isSearchActive, activateSearch, deactivateSearch } = useSearchState();
const searchStateStore = useSearchStateStore();
// 计算导航段落
const navSegment = computed(() => {
@@ -30,19 +30,14 @@ function isActive(link: string): boolean {
// 处理fab点击事件 - 切换搜索状态
function toggleSearch(event: MouseEvent) {
event.stopPropagation();
if (isSearchActive.value) {
deactivateSearch();
} else {
activateSearch();
}
searchStateStore.toggle();
}
</script>
<template>
<nav :class="screenWidthStore.isAboveBreakpoint ? 'rail' : 'bar'">
<button class="fab" @mousedown.prevent @click.stop="toggleSearch">
<span>{{ isSearchActive ? "close" : "search" }}</span>
<span>{{ searchStateStore.isSearchActive ? "close" : "search" }}</span>
</button>
<div class="destinations">
<div class="segment" v-for="item in navSegment" :key="item.link" :class="isActive(item.link) ? 'active' : 'inactive'">

View File

@@ -1,35 +0,0 @@
import { ref } from "vue";
const isSearchActive = ref(false);
const isSearchFocused = ref(false);
const isSearchTyping = ref(false);
export function useSearchState() {
const activateSearch = () => {
isSearchActive.value = true;
};
const deactivateSearch = () => {
isSearchActive.value = false;
isSearchFocused.value = false;
isSearchTyping.value = false;
};
const setSearchFocus = (focused: boolean) => {
isSearchFocused.value = focused;
};
const setSearchTyping = (typing: boolean) => {
isSearchTyping.value = typing;
};
return {
isSearchActive,
isSearchFocused,
isSearchTyping,
activateSearch,
deactivateSearch,
setSearchFocus,
setSearchTyping,
};
}

View File

@@ -0,0 +1,78 @@
import { defineStore } from "pinia";
import { ref, watch } from "vue";
/**
* 搜索状态管理
*/
export const useSearchStateStore = defineStore("searchState", () => {
// 响应式状态
const isSearchActive = ref<boolean>(false);
const isSearchFocused = ref<boolean>(false);
const isSearchTyping = ref<boolean>(false);
/**
* 激活搜索
*/
function activate() {
isSearchActive.value = true;
}
/**
* 停用搜索
*/
function deactivate() {
isSearchActive.value = false;
isSearchFocused.value = false;
isSearchTyping.value = false;
}
/**
* 设置搜索焦点状态
* @param focused - 是否获得焦点
*/
function setFocus(focused: boolean) {
isSearchFocused.value = focused;
// 当获得焦点时,自动激活搜索
if (focused && !isSearchActive.value) {
isSearchActive.value = true;
}
}
/**
* 设置搜索输入状态
* @param typing - 是否正在输入
*/
function setTyping(typing: boolean) {
isSearchTyping.value = typing;
}
/**
* 切换搜索状态
*/
function toggle() {
if (isSearchActive.value) {
deactivate();
} else {
activate();
}
}
// 监听状态变化
watch(isSearchFocused, () => {
isSearchActive.value = true;
});
return {
// 状态
isSearchActive,
isSearchFocused,
isSearchTyping,
// 方法
activate,
deactivate,
setFocus,
setTyping,
toggle,
};
});