mirror of
https://github.com/sendevia/website.git
synced 2026-03-06 07:40:50 +08:00
feat(theme): refactor post data handling and improve date processing
This commit is contained in:
@@ -1,28 +1,88 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, Ref } from "vue";
|
||||
|
||||
export interface PostItem {
|
||||
url: string;
|
||||
type Data = {
|
||||
title: string;
|
||||
description?: string;
|
||||
date?: string;
|
||||
timestamp?: number;
|
||||
url: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
glob: (pattern: string, options?: any) => Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
export function useAllPosts(withContent = false) {
|
||||
const posts = ref<PostItem[]>([]);
|
||||
onMounted(async () => {
|
||||
// @ts-ignore
|
||||
const modules = import.meta.glob("../../../posts/*.md", { eager: true });
|
||||
posts.value = Object.entries(modules)
|
||||
.map(([path, mod]: any) => {
|
||||
const fm = mod?.frontmatter || {};
|
||||
return {
|
||||
url: "/posts/" + path.split("/").pop().replace(/\.md$/, ".html"),
|
||||
title: fm.title || mod.title || path.split("/").pop().replace(/\.md$/, ""),
|
||||
date: fm.date || "",
|
||||
content: withContent ? mod?.default?.toString() || "" : undefined,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
||||
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,
|
||||
};
|
||||
|
||||
return po;
|
||||
});
|
||||
return posts;
|
||||
|
||||
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 postsRef = ref<Data[]>(posts);
|
||||
|
||||
return asRef ? postsRef : postsRef.value;
|
||||
}
|
||||
|
||||
export type { Data as Post };
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useAllPosts } from "../composables/useAllPosts";
|
||||
import { computed } from "vue";
|
||||
|
||||
const posts = useAllPosts();
|
||||
const postsRef = useAllPosts(true);
|
||||
const postsList = computed(() => postsRef.value ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-all-posts">
|
||||
<h1>所有文章</h1>
|
||||
<div class="posts-card" v-for="post in posts" :key="post.url">
|
||||
<a :href="post.url">{{ post.title }}</a>
|
||||
<span v-if="post.date"> - {{ post.date }}</span>
|
||||
</div>
|
||||
<div class="page-allposts" aria-labelledby="all-posts-heading">
|
||||
<h1 id="allposts-heading">所有文章</h1>
|
||||
<section class="posts-list">
|
||||
<div class="posts-card" v-for="post in postsList" :key="post.url">
|
||||
<h2 class="post-title">
|
||||
<a :href="post.url">{{ post.title }}</a>
|
||||
</h2>
|
||||
<div class="post-meta">
|
||||
<span v-if="post.date" :datetime="post.date">{{ post.date }}</span>
|
||||
</div>
|
||||
<p v-if="post.description" class="post-desc">{{ post.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "../styles/mixin";
|
||||
|
||||
.page-all-posts {
|
||||
.page-allposts {
|
||||
grid-column: 1 / 13;
|
||||
|
||||
.posts-card a {
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useAllPosts } from "../composables/useAllPosts";
|
||||
import { useAllPosts, type Post } from "../composables/useAllPosts";
|
||||
|
||||
const posts = useAllPosts(true);
|
||||
const postsRef = useAllPosts(true);
|
||||
const query = ref("");
|
||||
const filteredPosts = computed(() => {
|
||||
if (!query.value) return [];
|
||||
const q = query.value.toLowerCase();
|
||||
return posts.value.filter((post) => post.title.toLowerCase().includes(q) || (post.content && post.content.toLowerCase().includes(q)));
|
||||
|
||||
const filteredPosts = computed<Post[]>(() => {
|
||||
const q = query.value.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
return (postsRef.value ?? []).filter((post) => {
|
||||
const inTitle = post.title?.toLowerCase().includes(q) ?? false;
|
||||
const inDesc = post.description?.toLowerCase().includes(q) ?? false;
|
||||
const inContent = post.content?.toLowerCase().includes(q) ?? false;
|
||||
const inDate = post.date?.toLowerCase().includes(q) ?? false;
|
||||
return inTitle || inDesc || inContent || inDate;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-search">
|
||||
<input v-model="query" placeholder="搜索文章..." class="search-input" />
|
||||
<div v-if="query">
|
||||
<div class="search-result" v-if="filteredPosts.length">
|
||||
<div v-for="post in filteredPosts" :key="post.url">
|
||||
<a :href="post.url">{{ post.title }}</a>
|
||||
<span v-if="post.date"> - {{ post.date }}</span>
|
||||
<main class="page-search" aria-labelledby="search-heading">
|
||||
<h1 id="search-heading" class="visually-hidden">搜索文章</h1>
|
||||
<input id="post-search" v-model="query" placeholder="搜索文章..." class="search-input" />
|
||||
<section>
|
||||
<h6 v-if="!query">请输入关键词以搜索文章。</h6>
|
||||
<div v-else>
|
||||
<div class="search-result" v-if="filteredPosts.length">
|
||||
<article v-for="post in filteredPosts" :key="post.url" class="search-item">
|
||||
<h2 class="result-title">
|
||||
<a :href="post.url">{{ post.title }}</a>
|
||||
</h2>
|
||||
<p v-if="post.date" :datetime="post.date">{{ post.date }}</p>
|
||||
<p v-if="post.description" class="result-desc">{{ post.description }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else>没有找到相关文章。</p>
|
||||
</div>
|
||||
<p v-else>没有找到相关文章。</p>
|
||||
</div>
|
||||
<p v-else>请输入关键词以搜索文章。</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user