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

feat: migrate post data management to Pinia store

This commit is contained in:
2025-12-05 00:10:20 +08:00
parent 2c3d531363
commit 42a446cfaf
4 changed files with 167 additions and 111 deletions

View File

@@ -1,35 +1,40 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import { storeToRefs } from "pinia";
import { useGlobalData } from "../composables/useGlobalData";
import { useGlobalScroll } from "../composables/useGlobalScroll";
import { useAllPosts, type Post } from "../composables/useAllPosts";
import { usePostStore, type PostData } from "../stores/posts";
import { useSearchStateStore } from "../stores/searchState";
import { useScreenWidthStore } from "../stores/screenWidth";
import { handleTabNavigation } from "../utils/tabNavigation";
const { frontmatter } = useGlobalData();
const { isScrolled } = useGlobalScroll({ threshold: 100 });
const searchStateStore = useSearchStateStore();
const screenWidthStore = useScreenWidthStore();
const postsStore = usePostStore();
const { posts } = storeToRefs(postsStore);
const isHome = computed(() => frontmatter.value.home === true);
const articlesRef = useAllPosts(true);
const query = ref("");
const appbar = ref<HTMLElement | null>(null);
const searchInput = ref<HTMLInputElement | null>(null);
const isTabFocusable = computed(() => !screenWidthStore.isAboveBreakpoint);
// 计算过滤后的文章
const filteredPosts = computed<Post[]>(() => {
// 计算过滤后的文章,使用 PostData 类型
const filteredPosts = computed<PostData[]>(() => {
const q = query.value.trim().toLowerCase();
if (!q || !articlesRef.value) return [];
if (!q || !posts.value.length) return [];
return articlesRef.value.filter((post) => {
const { title = "", description = "", content = "", date = "" } = post;
return posts.value.filter((post) => {
return (
title.toLowerCase().includes(q) ||
description.toLowerCase().includes(q) ||
content.toLowerCase().includes(q) ||
date.toLowerCase().includes(q)
post.title.includes(q) ||
post.description.includes(q) ||
post.date.includes(q) ||
post.tags.some((t) => t.includes(q)) ||
post.categories.some((t) => t.includes(q))
);
});
});

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed } from "vue";
import { useGlobalData } from "../composables/useGlobalData";
import { useAllPosts } from "../composables/useAllPosts";
import { usePostStore } from "../stores/posts";
const { page } = useGlobalData();
const articlesRef = useAllPosts(true);
const postsStore = usePostStore();
const postsRef = computed(() => postsStore.posts);
function normalize(u: string | undefined | null) {
if (!u) return "";
@@ -48,7 +49,7 @@ const currentCandidates = computed(() => {
});
const currentIndex = computed(() => {
const posts = articlesRef.value || [];
const posts = postsRef.value || [];
for (let i = 0; i < posts.length; i++) {
const post = posts[i];
@@ -68,14 +69,14 @@ const currentIndex = computed(() => {
});
const prev = computed(() => {
const posts = articlesRef.value || [];
const posts = postsRef.value || [];
const idx = currentIndex.value;
if (idx > 0) return posts[idx - 1];
return null;
});
const next = computed(() => {
const posts = articlesRef.value || [];
const posts = postsRef.value || [];
const idx = currentIndex.value;
if (idx >= 0 && idx < posts.length - 1) return posts[idx + 1];
return null;

View File

@@ -1,95 +0,0 @@
import { ref, Ref } from "vue";
type Data = {
title: string;
description?: string;
impression?: string;
date?: string;
timestamp?: number;
url: string;
content?: string;
};
declare global {
interface ImportMeta {
glob: (pattern: string, options?: any) => Record<string, any>;
}
}
const modules = import.meta.glob("../../../posts/**/*.md", { eager: true }) as Record<string, any>;
export function useAllPosts(asRef: true): Ref<Data[]>;
export function useAllPosts(asRef?: false): Data[];
export function useAllPosts(asRef = false) {
const posts = Object.keys(modules).map((filePath) => {
const mod: any = (modules as any)[filePath];
const frontmatter =
mod.frontmatter ||
mod.default?.frontmatter ||
mod.attributes ||
mod.default?.attributes ||
mod.__pageData?.frontmatter ||
mod.default?.__pageData?.frontmatter ||
{};
const rawDate = frontmatter.date ?? null;
let dateStr = "";
let timestamp: number | undefined = undefined;
if (rawDate != null) {
let d: Date | null = null;
if (rawDate instanceof Date) d = rawDate;
else if (typeof rawDate === "number") d = new Date(rawDate);
else if (typeof rawDate === "string") {
const parsed = Date.parse(rawDate);
if (!isNaN(parsed)) d = new Date(parsed);
else d = new Date(rawDate);
} else {
const parsed = Date.parse(String(rawDate));
if (!isNaN(parsed)) d = new Date(parsed);
}
if (d && !isNaN(d.getTime())) {
dateStr = d.toISOString().slice(0, 10);
timestamp = d.getTime();
}
}
const filename = filePath.split("/").pop() || filePath;
const name = filename.replace(/\.mdx?$/, "").replace(/\.md$/, "");
const url = `/posts/${encodeURIComponent(name)}.html`;
const content =
mod.excerpt ||
mod.excerpt?.text ||
mod.attributes?.excerpt ||
(typeof mod.default === "string" ? mod.default : undefined);
const po: Data = {
title: frontmatter.title || name,
description:
frontmatter.description || frontmatter.excerpt || (typeof content === "string" ? content.slice(0, 160) : "") || "",
date: dateStr,
timestamp,
url,
content: typeof content === "string" ? content : undefined,
impression: frontmatter.impression,
};
return po;
});
posts.sort((a, b) => {
if (a.timestamp && b.timestamp) return b.timestamp - a.timestamp;
if (a.timestamp) return -1;
if (b.timestamp) return 1;
return 0;
});
const articlesRef = ref<Data[]>(posts);
return asRef ? articlesRef : articlesRef.value;
}
export type { Data as Post };

View File

@@ -0,0 +1,145 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
export type PostData = {
id: string;
title: string;
url: string;
date: string;
timestamp: number;
description: string;
impression?: string;
tags: string[];
categories: string[];
};
declare global {
interface ImportMeta {
glob: (pattern: string, options?: any) => Record<string, any>;
}
}
const modules = import.meta.glob("../../../posts/**/*.md", { eager: true }) as Record<string, any>;
/**
* 生成唯一ID
*/
const generateHashId = (str: string): string => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash |= 0;
}
return Math.abs(hash).toString(36);
};
/**
* 核心解析逻辑
*/
const parseModule = (filePath: string, mod: any): PostData => {
// 优先获取 VitePress 编译后的 __pageData其次是 standard frontmatter
const pageData = mod.__pageData || {};
const frontmatter = pageData.frontmatter || mod.frontmatter || {};
// 日期处理
const rawDate = frontmatter.date || pageData.lastUpdated;
let dateStr = "";
let timestamp = 0;
if (rawDate) {
const d = new Date(rawDate);
if (!isNaN(d.getTime())) {
dateStr = d.toISOString().split("T")[0];
timestamp = d.getTime();
}
}
const filename = filePath.split("/").pop() || "";
const name = filename.replace(/\.mdx?$/, "");
const url = `/posts/${encodeURIComponent(name)}`;
// 摘要/内容提取
const content = frontmatter.description || pageData.description || mod.excerpt;
// 标签与分类处理
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : frontmatter.tags ? [frontmatter.tags] : [];
const categories = Array.isArray(frontmatter.categories)
? frontmatter.categories
: frontmatter.categories
? [frontmatter.categories]
: [];
return {
id: generateHashId(filePath),
title: frontmatter.title || pageData.title || name,
description: content || "",
impression: frontmatter.impression || "",
date: dateStr,
timestamp,
url,
tags,
categories,
};
};
/**
* 文章列表管理
*/
export const usePostStore = defineStore("posts", () => {
const _allPosts = Object.keys(modules)
.map((filePath) => parseModule(filePath, modules[filePath]))
.sort((a, b) => b.timestamp - a.timestamp);
const posts = ref<PostData[]>(_allPosts);
// 最新文章
const latestPosts = computed(() => posts.value.slice(0, 5));
// 获取所有唯一的 Tags
const allTags = computed(() => {
const tagSet = new Set<string>();
posts.value.forEach((p) => p.tags.forEach((t) => tagSet.add(t)));
return Array.from(tagSet);
});
// 获取所有唯一的 Categories
const allCategories = computed(() => {
const catSet = new Set<string>();
posts.value.forEach((p) => p.categories.forEach((c) => catSet.add(c)));
return Array.from(catSet);
});
// 根据 URL 获取文章
const getPostByUrl = (url: string) => {
// 移除 .html 后缀以防匹配失败
const target = url.replace(/\.html$/, "");
return posts.value.find((p) => p.url.replace(/\.html$/, "") === target);
};
// 根据 ID 获取文章
const getPostById = (id: string) => {
return posts.value.find((p) => p.id === id);
};
// 根据 Tag 获取文章列表
const getPostsByTag = (tag: string) => {
return posts.value.filter((p) => p.tags.includes(tag));
};
// 根据 Category 获取文章列表
const getPostsByCategory = (category: string) => {
return posts.value.filter((p) => p.categories.includes(category));
};
return {
posts,
latestPosts,
allTags,
allCategories,
getPostByUrl,
getPostById,
getPostsByTag,
getPostsByCategory,
};
});