🍊
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
// 格式化文章列表
|
||||
const fmtArticleList = (articleList: any) => {
|
||||
// 按年份分类
|
||||
const groupedByYear = articleList.reduce((acc: any, item: any) => {
|
||||
const year = item.data.date.getFullYear();
|
||||
// 初始化
|
||||
!acc[year] && (acc[year] = []);
|
||||
acc[year].push(item.data);
|
||||
return acc;
|
||||
}, {});
|
||||
// 转换为目标格式
|
||||
return Object.keys(groupedByYear).map(year => ({ name: parseInt(year), data: groupedByYear[year] })).reverse();
|
||||
}
|
||||
|
||||
// 获取分类下的文章列表
|
||||
const getCategoriesList = async (categories: string) => {
|
||||
const posts = await getCollection("blog");
|
||||
const articleList = posts.filter((i: any) => i.data.categories == categories).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());;
|
||||
return fmtArticleList(articleList);
|
||||
}
|
||||
|
||||
// 获取标签下的文章列表
|
||||
const getTagsList = async (tags: string) => {
|
||||
const posts = await getCollection("blog");
|
||||
const articleList = posts.filter((i: any) => i.data.tags.includes(tags)).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());;
|
||||
return fmtArticleList(articleList);
|
||||
}
|
||||
|
||||
// 获取归档列表
|
||||
const getArchiveList = async () => {
|
||||
const posts = await getCollection("blog");
|
||||
const articleList = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());;
|
||||
return fmtArticleList(articleList);
|
||||
}
|
||||
|
||||
export { getCategoriesList, getTagsList, getArchiveList };
|
||||
@@ -0,0 +1,67 @@
|
||||
import path from 'path';
|
||||
import _fs from 'fs';
|
||||
const fs = _fs.promises;
|
||||
import { fileURLToPath } from 'url';
|
||||
// 获取当前模块的目录路径
|
||||
const __filename = fileURLToPath(import.meta.url); // 当前文件的绝对路径
|
||||
const __dirname = path.dirname(__filename); // 当前文件所在的目录
|
||||
|
||||
// 支持的图片扩展名
|
||||
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']);
|
||||
|
||||
/**
|
||||
* 获取目录中的所有图片文件
|
||||
* @param {string} dir - 图片目录路径
|
||||
* @returns {Promise<string[]>} - 图片文件名数组
|
||||
*/
|
||||
async function getImageFiles(dir: string) {
|
||||
try {
|
||||
const files = await fs.readdir(dir);
|
||||
return files.filter(file => SUPPORTED_EXTENSIONS.has(path.extname(file).toLowerCase()));
|
||||
} catch (error) {
|
||||
console.error(`无法读取目录 ${dir}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机打乱数组(Fisher-Yates 洗牌算法)
|
||||
* @param {any[]} array - 需要打乱的数组
|
||||
* @returns {any[]} - 打乱后的数组
|
||||
*/
|
||||
function shuffleArray(array: any[]) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个图片名称的无限迭代器
|
||||
* @param {string} dir - 图片目录路径
|
||||
* @returns {AsyncGenerator<string>} - 异步生成器
|
||||
*/
|
||||
async function* createImageIterator(dir: string) {
|
||||
let images = await getImageFiles(dir);
|
||||
if (images.length === 0) {
|
||||
throw new Error(`目录 ${dir} 中没有图片文件`);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
images = shuffleArray([...images]); // 打乱顺序
|
||||
for (const image of images) {
|
||||
yield image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const targetDir = path.resolve(__dirname, '../../public/assets/images/banner/'); // 目标目录
|
||||
const fileIter = createImageIterator(targetDir);
|
||||
const getCover = async (filename: string | null | undefined) => {
|
||||
if (filename) return filename;
|
||||
const { value } = await fileIter.next();
|
||||
return `/assets/images/banner/${value}`
|
||||
}
|
||||
|
||||
export default getCover;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const COMMITER_DATE_ISO_FORMAT = "%cI";
|
||||
|
||||
export default function getFileCreateTime(filePath: string, type: string): Date {
|
||||
try {
|
||||
const result = execSync(`git log --pretty=format:"${COMMITER_DATE_ISO_FORMAT}" ${filePath}`).toString();
|
||||
if (!result) {
|
||||
throw new Error('Git command failed to execute');
|
||||
}
|
||||
const commits = result.split(/\r?\n/g);
|
||||
return new Date(commits[type == 'create' ? commits.length - 1 : 0]);
|
||||
} catch (e) {
|
||||
// not committed yet
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getCollection } from "astro:content";
|
||||
// 获取封面图
|
||||
import getCover from "../utils/getCover";
|
||||
const posts = (await getCollection("blog")).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||
// 获取文章分类
|
||||
const getCategories = () => {
|
||||
const categoriesList = posts.reduce((acc: any, i: any) => {
|
||||
acc[i.data.categories] = (acc[i.data.categories] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.entries(categoriesList).map(([title, count]) => ({ title, count }));
|
||||
}
|
||||
|
||||
// 获取文章标签
|
||||
const getTags = () => {
|
||||
const frequencyMap = new Map();
|
||||
// 统计每个元素出现的频率
|
||||
posts.forEach(i => {
|
||||
i.data.tags.forEach(tag => frequencyMap.set(tag, (frequencyMap.get(tag) || 0) + 1));
|
||||
});
|
||||
const sortedArray = [...frequencyMap.entries()].sort((a, b) => b[1] - a[1]);
|
||||
return sortedArray.slice(0, 16).map(item => item[0]);
|
||||
}
|
||||
|
||||
// 获取推荐文章 (给文章添加 recommend: true 字段)
|
||||
const getRecommendArticles = () => {
|
||||
const recommendList = posts.filter(i => i.data.recommend).slice(0, 6);
|
||||
return (recommendList.length > 0 ? recommendList : posts).slice(0, 6).map(async i => ({ title: i.data.title, date: i.data.date, id: i.data.id, cover: await getCover(i.data.cover) }))
|
||||
};
|
||||
|
||||
export { getCategories, getTags, getRecommendArticles };
|
||||
@@ -0,0 +1,92 @@
|
||||
import dayjs from "dayjs";
|
||||
// 获取文章的描述
|
||||
const getDescription = (post: any, num: number = 150) => (post.rendered ? post.rendered.html.replace(/<[^>]+>/g, "").replace(/\s+/g, "") : post.body.replace(/\n/g, "").replace(/#/g, "")).slice(0, num) || '暂无简介'
|
||||
//处理时间
|
||||
const fmtTime = (time: any, fmt: string = 'MMMM D, YYYY') => dayjs(time).format(fmt)
|
||||
// 处理日期
|
||||
const fmtDate = (time: any) => {
|
||||
const past = dayjs(time);
|
||||
const now = dayjs();
|
||||
const duration = now.diff(past, 'seconds');
|
||||
if (duration < 60) {
|
||||
return `${duration} 秒前`;
|
||||
} else if (duration < 60 * 60) {
|
||||
const minutes = Math.floor(duration / 60);
|
||||
return `${minutes} 分钟前`;
|
||||
} else if (duration < 60 * 60 * 24) {
|
||||
const hours = Math.floor(duration / (60 * 60));
|
||||
return `${hours} 小时前`;
|
||||
} else if (duration < 60 * 60 * 24 * 30) {
|
||||
const days = Math.floor(duration / (60 * 60 * 24));
|
||||
return `${days} 天前`;
|
||||
} else if (duration < 60 * 60 * 24 * 30 * 12) {
|
||||
const months = Math.floor(duration / (60 * 60 * 24 * 30));
|
||||
return `${months} 个月前`;
|
||||
} else {
|
||||
const years = Math.floor(duration / (60 * 60 * 24 * 30 * 12));
|
||||
return `${years} 年前`;
|
||||
}
|
||||
}
|
||||
// 处理页码展示
|
||||
const fmtPage = (page: string | undefined) => page ? page.replace(/\//g, '') : null
|
||||
// 加载外部脚本
|
||||
const LoadScript = (src: string): Promise<HTMLScriptElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.onload = () => resolve(script);
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
// 加载外部CSS
|
||||
const LoadStyle = (href: string): Promise<HTMLLinkElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.type = "text/css";
|
||||
link.href = href;
|
||||
link.onload = () => resolve(link); // CSS 加载成功
|
||||
link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)); // CSS 加载失败
|
||||
document.head.appendChild(link); // 将 <link> 添加到文档中
|
||||
});
|
||||
}
|
||||
|
||||
// 请求封装
|
||||
const $GET = async (url: string, headers: Record<string, string> = {}): Promise<any> => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! Status: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
} catch (error) {
|
||||
console.error("GET request failed:", error);
|
||||
throw error; // 抛出错误以便调用者处理
|
||||
}
|
||||
};
|
||||
|
||||
const $POST = async (url: string, data: Record<string, any>, headers: Record<string, string> = {}): Promise<any> => {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { ...headers, },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! Status: ${res.status}`);
|
||||
}
|
||||
return res.json(); // 解析 JSON 数据
|
||||
} catch (error) {
|
||||
console.error("POST request failed:", error);
|
||||
throw error; // 抛出错误以便调用者处理
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export { $GET, $POST, getDescription, fmtTime, fmtDate, fmtPage, LoadScript, LoadStyle }
|
||||
@@ -0,0 +1,17 @@
|
||||
type RouterEventKey = "beforeCreate" | "created" | "beforeMount" | "mounted" | "afterMount";
|
||||
type RouterEventName = "astro:before-preparation" | "astro:after-preparation" | "astro:before-swap" | "astro:after-swap" | "astro:page-load";
|
||||
type EventHandler = (event: Event) => void;
|
||||
|
||||
// 路由事件映射
|
||||
const routerFn: Record<RouterEventKey, RouterEventName> = { beforeCreate: 'astro:before-preparation', created: 'astro:after-preparation', beforeMount: 'astro:before-swap', mounted: 'astro:after-swap', afterMount: 'astro:page-load' };
|
||||
|
||||
const updateRouter = (key: RouterEventKey, handler: EventHandler) => {
|
||||
const eventName = routerFn[key];
|
||||
if (!eventName) {
|
||||
throw new Error(`Invalid key ${key} for router update. Valid keys are: ${Object.keys(routerFn).join(", ")}.`);
|
||||
}
|
||||
document.removeEventListener(eventName, handler);
|
||||
document.addEventListener(eventName, handler);
|
||||
};
|
||||
|
||||
export default updateRouter;
|
||||
@@ -0,0 +1,30 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import * as cheerio from 'cheerio';
|
||||
|
||||
const setSearchJson = async (posts: any[]) => {
|
||||
const searchIndex = posts.map(i => {
|
||||
const $ = cheerio.load(`<body>${i.rendered.html}</body>`);
|
||||
return {
|
||||
title: i.data.title,
|
||||
url: `/article/${i.data.id}`,
|
||||
content: `${i.data.title} - ` + $('body').text().replace(/\n/g, '').replace(/<[^>]+>/g, '')
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
path.join(process.cwd(), 'dist', 'vh-search.json'),
|
||||
JSON.stringify(searchIndex)
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(process.cwd(), 'public', 'vh-search.json'),
|
||||
JSON.stringify(searchIndex)
|
||||
);
|
||||
console.log('\x1b[32m%s\x1b[0m', '搜索文件vh-search文件已生成 successfully');
|
||||
} catch (error) {
|
||||
console.error('Error writing search index file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export default setSearchJson;
|
||||
Reference in New Issue
Block a user