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:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
78
.vitepress/theme/stores/searchState.ts
Normal file
78
.vitepress/theme/stores/searchState.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user