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